Compare commits

...
Author SHA1 Message Date
koka 40b655517e Add living caverns and connected water terraces for alpha.9
Build Sanctuary / build (push) Canceled after 0s
2026-09-09 02:18:35 +02:00
koka bf84b37ea7 Record alpha.8 publication and verified Prism synchronization
Build Sanctuary / build (push) Canceled after 0s
2026-09-09 00:31:21 +02:00
koka 80e78e0421 Add rifts, river basins and stratified depths for alpha.8
Build Sanctuary / build (push) Canceled after 0s
2026-09-09 00:29:34 +02:00
koka d50d47e149 Add woodland canopies, sheltered dark oaks and exposed starter ores
Build Sanctuary / build (push) Canceled after 0s
2026-09-08 23:11:51 +02:00
koka f1e48fa969 Add layered meadows, rocky shores and lower wall springs
Build Sanctuary / build (push) Canceled after 0s
2026-09-08 19:16:45 +02:00
koka f621f921a1 Add natural shores, rock springs and temperate island biomes
Build Sanctuary / build (push) Canceled after 0s
2026-09-08 16:10:11 +02:00
224 changed files with 30044 additions and 90 deletions
+119
View File
@@ -1,5 +1,124 @@
# Changelog
## 0.1.0-alpha.9 — 2026-09-09
- Cavités luxuriantes éclairées par leur végétation, secteurs à spéléothèmes
et géodes daméthyste recherchés sous l’île, sur des supports naturels.
- Province de soufre limitée dans les profondeurs ; geysers utilisant
les blocs et conditions de fonctionnement de Minecraft 26.3-pre-2.
- Forêts et rivière de surface conservées ; recherche de lacs plus profonds
et volumineux et de groupes facultatifs de bassins en terrasses reliés par
des cascades. Les « rizières » désignent le relief aquatique, sans culture
de riz ni mécanique dascenseur ajoutée au joueur.
- Nouvelle clé `sanctuary:sanctuary_cavern`, réservée aux nouveaux mondes.
Les générations jusqu’à lalpha.8 restent séparées ; aucune sauvegarde
existante nest convertie ou régénérée.
Validation : au moins dix tests requis réussis sur chacune des graines `0`,
`42` et `8675309`, avec 1 800 ticks de fluides ; `check build assemblePack`
réussi. Les preuves cumulées comprennent deux bassins en terrasses reliés,
un geyser dormant puis éruptif, des baies lumineuses, des spéléothèmes et de
laméthyste bourgeonnante. Les mesures et leurs périmètres figurent dans
[Validation](docs/testing.md). Publication et synchronisations Prism restent
suivies séparément. Aucun site ni stock nest garanti sur chaque graine.
## 0.1.0-alpha.8 — 2026-09-09
- Forêts de surface conservées, avec de nouvelles failles courbes qui retirent
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).
Publication immuable et canal packwiz vérifiés, avec deux synchronisations
dans un dossier neuf puis deux dans la même instance Prism ; sauvegardes et
réglages conservés.
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,
avec bouleaux, clairières fleuries et bois plus secs.
- Vrais chênes noirs, champignons, podzol et mycélium sur les corniches intérieures
humides ; mousse plus ponctuelle et rares poches de jungle/bambou.
- Un emplacement facultatif pour un arbre remarquable (cerisier favorisé,
épicéa, jungle, acacia, palétuvier ou chêne pâle), sans terrain ajouté.
- Nappes de pierre, andésite, diorite et granite sur les affleurements ; dépôts
épais de sable, gravier et argile associés aux bassins. Rares matières de
soufre dans les profondeurs, sans grands bassins souterrains.
- Petits filons de charbon, fer et cuivre autorisés à affleurer, avec des essais
sur les roches de surface et les corniches pour les rendre repérables.
- Nouvelle clé `sanctuary:sanctuary_woodland`, avec formes, hauteur, ciel et
plans deau conservés. Créer un **nouveau monde Sanctuary** ; les sauvegardes
alpha.6 et antérieures conservent leur génération.
Validation : dix tests serveur réussis sur chacune des graines `0`, `42` et
`8675309`, arbres et minerais réellement observés, 1 800 ticks de fluides,
et `check build assemblePack` réussi.
## 0.1.0-alpha.6 — 2026-09-08
- Palette tempérée plus douce, avec des couleurs communes pour lherbe, le
feuillage et leau. Dappled Forest quitte l’île initiale et reste réservé
aux futurs continents ; les sauvegardes alpha.5 le conservent.
- Prairies fleuries et affleurements rocheux plus étendus, avec une répartition
des biomes selon laltitude. Les corniches inférieures peuvent porter de la
mousse, des fougères, des fleurs et des petits arbustes.
- Sources recherchées aussi dans les parois des strates inférieures. Les
cascades peuvent rejoindre un palier ou descendre dans le vide.
- Retrait des courts ruisseaux à niveau constant : l’île conserve ses étangs
et petits lacs. La grande rivière avec source, chute et bassin reste à
développer ; elle nest pas activée dans cette livraison.
- Nouvelle clé `sanctuary:sanctuary_layered`, avec les anciennes générations
conservées. Créer un **nouveau monde Sanctuary** pour cet incrément.
Validation : dix tests serveur réussis sur chacune des graines `0`, `42` et
`8675309`, avec 1 800 ticks de fluides, contrôles des corniches et respect des
modifications ultérieures aux sources. `check build assemblePack` réussi.
## 0.1.0-alpha.5 — 2026-09-08
- Plages plus larges et irrégulières, en nappes de sable, gravier et roche, avec
des transitions herbeuses. Dépôts de 3 à 5 blocs au lieu dune seule couche.
- Étangs et lacs moins creusés, rives variées et petites terrasses ; la canne
à sucre reste soumise à ses conditions vanilla.
- Quelques sources sortent de niches rocheuses naturelles à différentes
altitudes. Leurs cascades peuvent descendre dans le vide et servir à circuler
verticalement ; seuls les blocs sources sont posés par la génération.
- Petite réserve de lave dans une niche rocheuse accessible lorsque le relief
fournit un fond, des parois et une voûte adaptés.
- Mosaïque de cinq biomes tempérés, dont Dappled Forest de Minecraft 26.3.
- Petits filons de charbon, fer, cuivre, or, redstone, lapis et diamant à des
altitudes adaptées à l’île. Aucun quota ajouté après comptage des ressources.
- Nouvelle clé de génération `sanctuary:sanctuary_natural` ; anciennes versions
de terrain et dhydrologie conservées pour les sauvegardes existantes.
Créer un **nouveau monde Sanctuary** pour tester cette génération. La hauteur
et le ciel validés en alpha.3/4 sont conservés. Laccès à lEnd, les expansions
et les grandes rivières en pente restent à développer.
Validation : dix tests serveur réussis sur chacune des graines 0, 42 et 8675309,
avec 1 800 ticks réels de fluides. Les bassins et les réserves de lave restent
stables ; les neuf cascades atteignent Y=0. `check build assemblePack` et les
contrôles des couches de sédiments, des anciens générateurs et du pack réussissent.
## 0.1.0-alpha.4 — 2026-09-08
- Première hydrologie de surface : étangs, petits lacs et cours deau calmes
+56 -8
View File
@@ -17,27 +17,75 @@ conserve les intentions ; le [backlog](docs/backlog.md) prépare les premiers
tickets. Les systèmes d'expansion, d'économie, de progression et les dimensions
décrits dans la vision ne sont pas encore implémentés.
## Premier incrément
## Socle livré jusqu’à lalpha.8
- Mod `sanctuary` indépendant et pack Sanctuary construit avec packwiz.
- Preset **Sanctuary** sélectionnable à la création d'un monde, avec une île
flottante finie, un terrain naturel et un extérieur vide.
- Forêt de départ avec végétation, minerais et animaux vanilla. Les formes de
terrain reprennent l'approche du générateur 26.2, adaptée à l'API 26.3.
- Î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. 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,
dessous sculpté et nuages au-dessus du terrain. Les anciennes sauvegardes
conservent leurs paramètres de génération.
- Première hydrologie de surface : étangs, petits lacs et cours deau calmes,
avec des berges de sable et de la canne à sucre lorsque le terrain sy prête.
- É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.
- 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 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
le terrain : le spawn cherche un emplacement naturel. Le catalogue
TerraMix, les continents déverrouillables, les grandes rivières et cascades, les ruines et
TerraMix, les continents déverrouillables, les océans, les ruines et
les autres fonctionnalités restent à construire par tickets.
La génération alpha.8, `sanctuary:sanctuary_rift`, passe les contrôles moteur
sur les trois graines de référence, avec 1 800 ticks de fluides. Ces résultats
ne valident pas par avance lincrément suivant. Les observations et leurs
limites figurent dans [Validation](docs/testing.md).
## Alpha.9 : cavités vivantes et terrasses deau
Lincrément ajoute des cavités luxuriantes et lumineuses sous l’île, des
secteurs à spéléothèmes, des géodes daméthyste et des poches de soufre moins
envahissantes, avec de vrais geysers de soufre lorsque leurs conditions sont
réunies. Les forêts de surface sont conservées ; la recherche de lacs plus
profonds et volumineux accompagne des groupes facultatifs de bassins naturels
étagés. Leurs cascades relient les paliers et permettent de circuler par la nage.
Limage des « rizières » décrit ce paysage deau en terrasses : aucune culture
de riz nest ajoutée.
**La validation moteur alpha.9 passe sur les trois graines de référence.**
Les preuves de terrasses, geysers et décor souterrain sont cumulées sur ces
trois mondes, sans imposer chaque élément à chacun. La distribution est
vérifiée séparément, selon le guide packwiz. Le
nouveau réglage `sanctuary:sanctuary_cavern` nécessite de créer un **nouveau
monde Sanctuary**. Les anciennes générations jusqu’à lalpha.8 gardent leurs
paramètres ; aucune sauvegarde nest régénérée. Les cavités restent dans
lOverworld, sans créer la dimension des cavernes prévue par la vision.
Dappled Forest reste réservé aux futurs continents et présent dans les
anciennes sauvegardes alpha.5. Aucun lac, groupe de terrasses, geyser ou stock
de ressources nest garanti sur chaque graine. Voir le ticket local
[WG-10](docs/backlog.md).
Le premier champ de recherche climatique est défini : nord froid, sud chaud,
ouest sec et est humide, avec leurs combinaisons diagonales. Un outil de
développement relève les ressources après génération ; le système d'expansion
@@ -60,7 +108,7 @@ sera une mise à jour explicite, avec vérification des API et des sauvegardes.
| Fabric API | 0.160.0+26.3 |
| Fabric Loom | 1.17.20 |
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
| Sanctuary / pack | 0.1.0-alpha.4 |
| Sanctuary / pack | 0.1.0-alpha.9 |
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
uniquement la bibliothèque standard et repère aussi une installation Python
@@ -87,7 +135,7 @@ décrits dans [Validation](docs/testing.md).
Résultats :
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.4.jar` : mod à installer avec
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.9.jar` : mod à installer avec
Fabric API sur la version Minecraft indiquée.
- `build/packwiz/` : pack de développement complet, avec le mod construit et
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
+9 -3
View File
@@ -7,9 +7,15 @@ provenance détaillée et le périmètre du portage.
Minecraft appartient à Mojang Studios / Microsoft. Sanctuary est un projet
communautaire indépendant. Le dépôt ne redistribue pas le jeu.
Le biome de départ reprend la configuration de la forêt vanilla 26.3-pre-2,
adaptée pour une île finie. Les références de blocs, végétation et minerais
sont résolues par Minecraft ; leurs textures et modèles ne sont pas inclus.
Les biomes de départ reprennent les configurations de forêt, plaine, forêt de
bouleaux, forêt fleurie et Dappled Forest de Minecraft 26.3-pre-2, adaptées à
une île finie. Les variantes alpha.6 réutilisent les végétations vanilla pour
les prairies fleuries et les bosquets ; Dappled Forest reste disponible pour
les sauvegardes alpha.5. Lalpha.7 réemploie les arbres vanilla (dont chêne noir,
cerisier et essences rares), les champignons, le bambou et les blocs de soufre
de cette version, avec des conditions propres à l’île. Les règles des petits minerais sont adaptées à partir des
features vanilla de cette même version. Les références de blocs, végétation et
minerais sont résolues par Minecraft ; leurs textures et modèles ne sont pas inclus.
Fabric Loader : Apache-2.0. Fabric API : Apache-2.0. Fabric Loom et le wrapper
Gradle : Apache-2.0. Ces projets conservent leurs auteurs, notices et licences.
+173 -5
View File
@@ -10,7 +10,10 @@ 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 est livré en alpha.8, avec mesures moteur et mise à jour Prism vérifiées.
WG-10 dispose de mesures moteur alpha.9 ; la distribution reste suivie séparément.
Résultat visé : un client et un serveur Fabric compatibles peuvent ouvrir un monde Sanctuary, plusieurs joueurs y arrivent sur une même île sûre et les chunks extérieurs restent vides. La génération est reproductible et ses limites sont documentées.
@@ -118,13 +121,33 @@ Ce jalon fournit un outil de développement du terrain. L'interface d'expansion
- Pour l'île initiale tempérée et légèrement humide : privilégier petits étangs,
lacs de surface, rivière et berges propices à la canne à sucre, avec quelques
plages plus sèches. Pas de grands bassins souterrains remplis d'eau.
- L'eau ne se répand pas de manière incontrôlée dans le vide lors du chargement et des mises à jour de blocs du scénario testé.
- Les étangs et les lacs retiennent leur eau après les mises à jour de blocs.
Des sources rocheuses déclarées peuvent former des cascades jusquau vide ;
leurs écoulements restent attribuables à ces sources, sans inondation globale.
- Les profils du dessous et les bords du continent sont inspectés visuellement depuis les airs.
- La génération est mesurée sur une emprise et une machine indiquées ; les valeurs observées sont consignées, sans annoncer un objectif de performance non mesuré.
**Incrément île initiale — alpha.4 :** première hydrologie de surface, documentée
dans [Génération](worldgen.md). Ce travail prépare les eaux des continents sans
clore WG-06 : océans, grandes rivières et cascades restent à développer.
**Incrément île initiale — alpha.4 :** première hydrologie de surface retenue,
conservée pour les sauvegardes de cette version.
**Incrément île initiale — alpha.5 :** plages plus larges,
dépôts de trois à cinq couches sur support naturel, excavation limitée,
rives variées et sources rocheuses pouvant former des cascades jusque dans le
vide. Le contrat et lisolation des anciennes générations sont décrits dans
[Génération](worldgen.md). Ce travail prépare les eaux des continents sans clore
WG-06 : océans, grandes rivières et ouverture des continents restent à développer.
**Incrément île initiale — alpha.6 :** retrait des courts ruisseaux, conservation
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.
**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
prototype alpha.6 nont fourni aucun parcours retenu sur les graines de référence ;
ce prototype nest pas distribué. Ne pas imposer de rivière si le relief ne sy prête pas.
### WG-07 — Introduire un premier biome distinct et une structure
@@ -140,6 +163,151 @@ clore WG-06 : océans, grandes rivières et cascades restent à développer.
- La reprise du catalogue TerraMix natif d'Another World 26.2 est évaluée séparément ; « plus de cent biomes » reste une ambition tant que son adaptation n'est pas vérifiée.
- Les limites des futures zones Lost Cities et des structures uniques sont identifiées sans imposer leur livraison dans ce ticket.
**Incrément île initiale — alpha.5 :** cinq variantes
tempérées à dominante forestière, dont Dappled Forest vanilla 26.3, et petits
filons de sept minerais à des altitudes adaptées. Leurs stocks sont observés
après génération. Cela ne clôture pas WG-07 : aucune structure de référence ni
reprise du catalogue TerraMix nest encore fournie. La survie initiale doit
permettre de produire et progresser au-delà du bois et de la pierre ; laccès
à toute la progression Minecraft nest pas encore garanti pour chaque seed.
**Ticket palette — alpha.6 :** réserver les biomes très contrastés de la 26.3,
dont Dappled Forest, aux futurs continents. L’île combine prairies fleuries,
bosquets et zones rocheuses, avec des teintes cohérentes et une végétation
effective sur les corniches inférieures. Cela ne livre pas encore les continents.
### WG-08 — Forêts de survie et minerais visibles — alpha.7
**Branche :** `codex/woodland-canopy`.
**But :** conserver les formes rocheuses appréciées en alpha.6 tout en donnant
à l’île de vraies forêts de surface et des ressources de départ repérables.
**Critères dacceptation :**
- Chênes majoritaires parmi les arbres de surface observés, bouleaux secondaires,
avec des arbres au cœur de l’île et des clairières fleuries.
- Chênes noirs réels et champignons sur des corniches intérieures naturelles ;
jungle/bambou et matières de soufre rares et localisés.
- Un éventuel arbre remarquable, choisi parmi les essences rares, sans ajout
de terrain ni obligation de fournir chaque essence par graine.
- Affleurements de pierre, andésite, diorite et granite ; sable et gravier
associés aux eaux, avec des couches soutenues.
- Charbon, fer et cuivre visibles au contact de lair sur la roche, avec des
petits filons sans quota corrigé après génération.
- Démonstration dans le moteur, maintien des eaux après 1 800 ticks et
conservation des anciennes générations. Publication packwiz et mise à jour
de linstance Prism existante en conservant ses données personnelles.
**Vérification alpha.7 :** les trois graines de référence passent les tests
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. Lalpha.8 est publiée sur le canal stable et la même instance Prism
est synchronisée deux fois, avec sauvegardes et réglages conservés. Le ticket
est livré ; lappréciation visuelle et l’équilibrage en partie restent à
éprouver. Ce ticket local nest pas une issue distante publiée.
### WG-10 — Cavités luxuriantes et terrasses deau — alpha.9
**Branche :** `codex/lush-caverns-and-water-terraces`.
**But :** rendre les cavités sous Sanctuary plus vivantes et lumineuses,
diversifier leurs ressources et créer des eaux plus volumineuses, sans
remplacer les forêts et la rivière de surface appréciées en alpha.8.
**Critères dacceptation :**
- Des cavités luxuriantes portent une végétation réellement lumineuse et
enracinée dans les surfaces et plafonds existants. Les observations doivent
distinguer un biome déclaré, ses blocs décoratifs et leur lumière réelle.
- Des secteurs à spéléothèmes et des géodes daméthyste sont recherchés dans
la roche compatible ; leurs supports, leurs volumes et leur accessibilité
sont contrôlés dans les vrais chunks, sans géodes suspendues dans le vide.
- Le soufre occupe moins de roche quen alpha.8, sur un périmètre de comparaison
explicite. Les geysers éventuels utilisent le soufre actif et les conditions
exactes de Minecraft 26.3-pre-2 ; leur fonctionnement doit être démontré,
sans confondre un bloc jaune avec un geyser actif.
- Les forêts, la rivière et les bassins de surface restent présents lorsque
le relief le permet. Les lacs plus profonds et volumineux sont mesurés en
blocs deau réels, avec fond et parois retenus après simulation.
- Des groupes facultatifs de deux bassins naturels en terrasses
peuvent relier plusieurs niveaux par des cascades déclarées. Chaque palier
doit avoir ses supports et son propre volume retenu ; une cascade isolée
ou des plans deau sans liaison ne valident pas un groupe de terrasses.
Le second bassin peut avoir une sortie terminale vers les roches ou le vide,
sans créer un troisième palier.
- Ces cascades offrent un passage vertical selon les règles de nage de
Minecraft. « Rizières » désigne seulement leur disposition paysagère :
aucune culture de riz ni téléportation nentre dans ce ticket.
- Les sorties deau sont contrôlées après décoration et après 1 800 ticks,
y compris aux frontières de chunks. Leur finition initiale doit préserver
les aménagements du joueur aux rechargements suivants.
- Le nouveau réglage `sanctuary:sanctuary_cavern` sapplique aux nouveaux
mondes. Les identifiants et traitements alpha.1 à alpha.8 restent séparés ;
aucune sauvegarde personnelle nest modifiée par les essais.
- Les graines `0`, `42` et `8675309` disposent de mesures identifiées par
version. Les sites absents sont signalés ; aucun quota de ressources ni
bassin de secours ne remplit un relief incompatible.
- Après validation : `check build assemblePack`, artefacts immuables vérifiés,
deux synchronisations isolées puis deux dans la même instance Prism,
sauvegardes et réglages conservés.
**Validation moteur au 9 septembre 2026 :** les trois graines de référence
passent les contrôles finaux. Les preuves cumulées démontrent un groupe de
deux paliers reliés, un geyser actif et les décors souterrains demandés ; les
absences locales et périmètres de mesure figurent dans [Validation](testing.md).
Le build est réussi. La publication et la synchronisation de la même instance
Prism restent vérifiées lors de la distribution. Ce ticket local nest pas
une issue distante publiée ; lessai visuel et l’équilibrage restent ouverts.
## Réserve de thèmes futurs
Ces thèmes servent à retrouver la vision, pas à demander leur implémentation immédiate. On en extrait un ticket seulement lorsqu'il devient utile au prochain incrément jouable.
+39 -6
View File
@@ -30,18 +30,35 @@ de sélection des biomes feront partie du ticket de génération des continents.
Les ressources et cultures futures, notamment celles d'It's Alive !, pourront
ainsi demander l'exploration de plusieurs climats.
L'île initiale utilise encore un biome de départ spécifique. Une température
ou une humidité du biome ne constitue pas à elle seule une carte climatique
directionnelle : cette carte et la sélection des biomes des continents restent
des travaux distincts.
Lalpha.5 proposait cinq biomes tempérés dont Dappled Forest. Lalpha.6 réserve
ce dernier aux futurs continents et donne à l’île une palette plus douce de
prairies, prairies fleuries, bosquets, corniches moussues et zones rocheuses.
La répartition tient compte de laltitude, y compris dans les strates inférieures.
La variation locale et lhumidité dépendent de la graine ; elles ne
constituent pas encore le champ de recherche directionnel des continents.
Lalpha.7 densifie les forêts de chênes et de bouleaux en surface et donne
aux corniches intérieures humides des chênes noirs et des champignons.
Des champignons vanilla peuvent aussi se rencontrer en surface. Les
clairières fleuries et les affleurements restent présents. Les essences rares
et un éventuel arbre remarquable dépendent du terrain ; chaque graine nest
pas tenue de fournir tous les bois. Les minerais de départ peuvent affleurer,
avec des petits filons et sans correction des stocks après comptage.
## Eau de surface et exploration
L'île de départ doit pouvoir accueillir naturellement des étangs et lacs de
surface, des berges propices à la canne à sucre et, lorsque le relief s'y prête,
des rivières. L'eau s'inscrit dans les creux et les chemins du terrain existant.
Les rivières flottantes et les cascades doivent tenir compte de la continuité
de leur lit et du vide sous les îles.
Les bassins gardent un fond et des berges fermés. Une grande rivière reste
facultative : elle doit suivre le relief et former un parcours lisible avec
source, chute et bassin. Ses exutoires peuvent laisser couler leau dans le vide.
Sans parcours suffisamment long, l’île conserve seulement ses petits bassins.
Lalpha.6 livre cette variante à bassins seuls ; la grande rivière reste à développer.
Lalpha.5 ajoute séparément quelques sources sortant dune niche rocheuse
naturelle : leurs cascades peuvent descendre dans le vide et servir à remonter
verticalement. Cet écoulement est volontaire ; il ne doit pas entraîner des
fuites sur toutes les rives.
Le contrat ne demande ni un bassin souterrain imposé pour obtenir un stock
d'eau, ni une cuvette ajoutée systématiquement à une position fixe. Une surface
@@ -76,6 +93,22 @@ rapports de continents devront conserver leur propre graine, version, emprise
et état de génération, afin d'additionner seulement des régions connues et
disjointes lorsque le besoin économique sera défini.
## Ressources de départ — depuis lalpha.5
L’île vise les premières étapes de survie, les outils, lenchantement et la
redstone. Les sept minerais (charbon, fer, cuivre, or, redstone, lapis et diamant)
utilisent des petites veines à des altitudes compatibles avec le terrain relevé.
Le charbon et le fer ne reprennent plus leurs fréquences vanilla beaucoup trop
généreuses dans cette géométrie. Une petite réserve de lave peut apparaître dans une niche rocheuse accessible
pour les premières productions et laccès au Nether. Le bois, la pierre, les animaux et les cultures
renouvelables conservent leur fonctionnement Minecraft : ce réglage des minerais
ne constitue pas une interdiction des grandes constructions.
Les placements restent probabilistes, sans quota de rattrapage après comptage.
Une graine peut donc manquer dune ressource ; la présence de tout le parcours
Minecraft nest pas garantie. Les structures donnant accès à lEnd et leur lien
avec lexpansion restent à concevoir.
## Relevé de développement disponible
`ResourceSurveyDiagnostics`, dans `src/gametest`, lit les chunks au statut
+48 -3
View File
@@ -15,11 +15,33 @@ de l'ancien pack 26.2.
## Livrer une mise à jour
Lalpha.9 est en préparation dans les sources. Les exemples de commandes
ci-dessous ciblent cette prochaine version ; ils ne signifient pas quelle
est publiée. Le canal reste sur la dernière livraison vérifiée jusqu’à la
validation du build, des essais moteur et des nouveaux artefacts.
Dernière livraison vérifiée le 9 septembre 2026 :
[Sanctuary 0.1.0-alpha.8](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.8),
depuis le commit source `80e78e0`. Le JAR, le `.mrpack` et le ZIP damorçage
Prism sont publiés et leurs téléchargements publics vérifiés. Le canal stable
pointe sur cette version. SHA-256 du JAR :
```text
67c4bee0f1366b724b4b3912618e3e5c1488f6b80d9b35535f31936721e0bcaa
```
Le véritable installateur packwiz a réussi deux fois dans un dossier de
développement neuf, puis deux fois dans linstance Sanctuary Beta existante.
Le second passage laisse les fichiers gérés identiques. Un seul JAR Sanctuary
alpha.8 est actif ; les 161 fichiers personnels et réglages suivis conservent
leurs hashes. Lancien JAR et les manifestes locaux sont sauvegardés hors de
`mods/`. Aucun monde existant na été ouvert, converti ou régénéré.
Après les vérifications, le commit et le push de la branche du ticket, le script
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.4.mrpack
python3 scripts/publish_pack.py --notes-file chemin/vers/notes.md --asset build/Sanctuary-0.1.0-alpha.9.mrpack
```
Les options sont facultatives. Ce script **publie** sur le Git configuré dans
@@ -38,7 +60,7 @@ La procédure complète, également utilisable manuellement :
3. Préparer les métadonnées de la livraison, avec l'URL exacte du futur JAR :
```sh
python3 scripts/pack.py release https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.4/sanctuary-0.1.0-alpha.4.jar
python3 scripts/pack.py release https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.9/sanctuary-0.1.0-alpha.9.jar
```
4. Pousser le commit source vérifié. Créer une release Gitea correspondant à ce
@@ -117,10 +139,33 @@ La hauteur de 384 blocs, le dessous sculpté et le ciel de l'alpha.3 se testent
dans un **nouveau monde** avec le preset **Sanctuary**. Les anciens mondes gardent
leurs identifiants de génération et leurs hauteurs. Lhydrologie de lalpha.4
nécessite elle aussi un nouveau monde Sanctuary ; la mise à jour de linstance
najoute pas deau aux sauvegardes existantes. Les détails des versions de génération sont dans
najoute pas deau aux sauvegardes existantes. Les nouvelles plages, cascades,
biomes et petits filons de lalpha.5 utilisent à leur tour un identifiant
séparé : créer un nouveau monde Sanctuary pour les découvrir. Les détails des versions de génération sont dans
[Génération](worldgen.md). Les changements de version Minecraft ou de format
de sauvegarde demandent leur propre ticket et contrat de migration.
Lalpha.6 utilise à son tour `sanctuary:sanctuary_layered` pour ses biomes par
altitude et sa nouvelle hydrologie. Créer un nouveau monde Sanctuary ; les
sauvegardes alpha.5 conservent notamment leur palette avec Dappled Forest.
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.
Lalpha.9 prépare `sanctuary:sanctuary_cavern` pour les cavités luxuriantes,
les géodes, les geysers et les terrasses deau. Un **nouveau monde Sanctuary**
sera nécessaire pour essayer cet incrément après sa validation. Les mondes
alpha.8 conservent `sanctuary:sanctuary_rift` ; leur contenu nest pas retouché
par une mise à jour de la même instance Prism. Les anciennes sauvegardes et
les réglages restent à conserver lors des deux synchronisations de livraison.
Références officielles : [installation packwiz](https://packwiz.infra.link/tutorials/installing/packwiz-installer/),
[commandes Prism](https://prismlauncher.org/wiki/help-pages/custom-commands/),
[bootstrap v0.0.3](https://github.com/packwiz/packwiz-installer-bootstrap/releases/tag/v0.0.3).
+565 -21
View File
@@ -33,6 +33,12 @@ graine, limites de profondeur et dexcavation, support naturel, parois, ruisse
sur plateau compatible et partition indépendante de lordre des chunks. Les
scénarios vides, percés ou trop minces ne reçoivent aucun bassin de secours.
Les tâches `:sanctuary:naturalHydrologySmoke` et `:sanctuary:starterLavaSmoke`
complètent ces contrôles pour lalpha.5 : dépôts de trois à cinq couches sur
support naturel, terrasses peu creusées, niches de source, parcours de cascade
et petite réserve de lave avec accès sec. Elles vérifient le déterminisme et
labsence de remplissage de secours lorsque la géométrie ne convient pas.
## Tests dans le moteur Minecraft
`runGameTest` démarre le serveur de test headless officiel de Fabric dans
@@ -58,7 +64,9 @@ 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 validés pour lalpha.9 et les résultats historiques
propres à lalpha.8 figurent plus bas :
1. Le vrai générateur Sanctuary est chargé et le spawn collectif repose sur une
surface déjà présente dans la densité naturelle, avec un sol plein de 3×3
@@ -74,16 +82,18 @@ Les neuf tests Sanctuary vérifient :
5. Le nouveau monde et son bruit utilisent bien 384 blocs, à partir de Y=0,
tandis que les anciens paramètres restent à 256 blocs. Les nuages sont
au-dessus de l'île, à Y=352,33.
6. Les chunks complets inspectés gardent une marge vide sous l'île, à Y=0..15.
6. Aucun plancher solide napparaît sous lîle, à Y=0..15. Dans les générations
alpha.5 à alpha.7, seules les eaux des cascades déclarées peuvent traverser cette marge.
7. Le relevé après génération compte exactement tous les états des blocs des
chunks demandés, avec des totaux cohérents et une complétude explicite.
Voir [Expansion et ressources](expansion.md) pour le mode complet facultatif.
8. Le plan hydrologique se retrouve dans les vrais chunks après décoration :
eau retenue, fonds naturels sur trois blocs, parois continues, ouverture au
ciel, raccords entre chunks et canne à sucre pouvant survivre. Après des
mises à jour réelles de fluides, chaque bloc deau doit rester dans ce plan.
9. Les paramètres et le biome des anciennes sauvegardes nactivent ni le
traitement hydrologique ni la nouvelle décoration des berges.
profils épais de sédiments, bassins retenus, raccords entre chunks, canne à
sucre valide, niches rocheuses et descente réelle des cascades. La palette
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 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
@@ -129,20 +139,26 @@ et plantes déjà générés, et comparent tous les blocs deau observés au v
annoncé. Leur carte montre les vrais blocs de surface ; le feuillage et les
troncs sont masqués pour lire les berges. Il ne sagit pas dune capture du jeu.
Les chunks de l’île sont maintenus actifs pendant le test. Chaque position
deau doit être dans un chunk où les blocs sont effectivement simulés. Un petit
témoin deau temporaire, enfermé en hauteur dans un de ces mêmes chunks, doit
réellement couler ; il est ensuite entièrement retiré. Les sources des sites
reçoivent elles aussi des ticks, puis la rétention est revérifiée au tick 200.
Attendre près de la grille éloignée du framework ne remplacerait pas ce contrôle.
Pour lalpha.5, les chunks de l’île sont maintenus actifs, puis le test attend
`ServerLevel.isPositionTickingWithEntitiesLoaded` sur chaque chunk de fluide.
Ce prédicat, réellement utilisé par le planificateur des ticks en 26.3, exige
à la fois le statut de simulation et le chargement des entités. `FULL` et
`shouldTickBlocksAt` seuls ne prouvent pas que les ticks programmés sexécutent.
Un témoin deau temporaire est vérifié dans chaque chunk de source et de lave,
puis retiré entièrement. La simulation des sites dure **1 800 ticks** après cette
activation. La graine 0 doit notamment démontrer une cascade qui atteint `Y=0`.
Les fichiers `surface-hydrology-seed-<graine>-generated.json/.png` et
`surface-hydrology-seed-<graine>-after-200-ticks.json/.png` sont écrits dans
`mods/sanctuary/build/run/gameTest/diagnostics/`. Ils décrivent les sites et leur
nature, leurs coordonnées, le volume deau réel, la canne à sucre, les raccords
entre chunks, les berges et les bornes inspectées. Le nombre de blocs deau
nest pas une mesure de débit. Un résultat sans site `STREAM` ne valide pas
une rivière, même sil contient des lacs.
Les fichiers `natural-hydrology-seed-<graine>-generated.json/.png` et
`natural-hydrology-seed-<graine>-after-1800-ticks.json/.png` sont écrits dans
`mods/sanctuary/build/run/gameTest/diagnostics/`, avec des profils PNG des
cascades. Ils décrivent les sites, dépôts, fluides, biomes et ressources des
chunks inspectés, ainsi que la graine et la version. Ce relevé de ressources
reste local à ces chunks et ne représente pas le stock total de l’île.
Le diagnostic alpha.4 est conservé séparément, avec ses fichiers
`surface-hydrology-…-after-200-ticks` et ses eaux entièrement retenues.
Le nombre de blocs deau ne mesure pas un débit ; un résultat sans site
`STREAM` ne valide pas une rivière, même sil contient des lacs.
Pour itérer sur leau sans recalculer les trois grandes cartes de densité :
@@ -151,7 +167,8 @@ Pour itérer sur leau sans recalculer les trois grandes cartes de densité :
```
Ces options désactivent seulement les exports de densité et le relevé de
ressources. Elles gardent les assertions de densité et tous les tests de
ressources général (neuf chunks ou île entière). Le diagnostic hydrologique
garde ses propres comptes locaux. Les options gardent les assertions de densité et tous les tests de
comportement, y compris les cartes et contrôles hydrologiques. Le parcours
standard conserve les exports complets de densité pour la graine 0.
@@ -173,6 +190,533 @@ Pour cet essai visuel en monde de développement créatif, comparer la même vue
Y=63 ni assombrissement d'altitude au passage de Y=32. Les brouillards de l'eau,
de la météo et des effets d'aveuglement ou d'obscurité doivent rester présents.
## Contrôles et validation alpha.9 — 9 septembre 2026
Les mondes neufs des graines `0`, `42` et `8675309` utilisent les paramètres
de production `sanctuary:sanctuary_cavern`. Chaque journal termine au moins
dix tests requis et sa tâche Gradle sans échec, avec 1 800 ticks de fluides.
Les sources naturelles deau et de lave, les débordements et les geysers
démarrent uniquement avec leurs ticks de production. Le test programme
seulement un témoin artificiel indépendant pour vérifier que les ticks
fonctionnent ; il ne réveille pas les sources naturelles.
Le build complet `check build assemblePack` réussit en 6m 24s,
avec 15 tâches de tests purs de comportement.
Les rapports sont postérieurs au gel du code final ; les résultats de
lalpha.8 et les essais alpha.9 intermédiaires ne servent pas de preuve ici.
La présence dun groupe de terrasses connecté, dun geyser réellement actif,
de baies lumineuses, de spéléothèmes et daméthyste bourgeonnante est vérifiée
sur lensemble des trois graines. Chaque élément nest pas imposé à chaque
graine ; leurs absences locales restent visibles dans les tableaux.
| Graine | Chunks FULL | Failles / témoins | Chênes / bouleaux supérieurs | Chênes noirs inférieurs | Gros champignons | Petits sous plafond | Minerais dehors : charbon / fer / cuivre |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `0` | 251 | 3 / 3 | 321 / 251 | 18 | 14 | 211 | 506 / 178 / 147 |
| `42` | 246 | 2 / 2 | 362 / 177 | 11 | 8 | 69 | 617 / 269 / 193 |
| `8675309` | 242 | 3 / 3 | 248 / 125 | 14 | 11 | 131 | 761 / 317 / 328 |
Les arbres sont des groupes de pieds de troncs sur sol naturel, avec feuilles
observées ; les gros champignons ont une tige et un chapeau connectés.
| Graine | Blocs à baies lumineuses | Blocs émissifs | Spéléothèmes pointus | Améthyste bourgeonnante | Blocs daméthyste | Geysers dormants + éruptifs | Sources deau de geyser |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `0` | 363 | 391 | 501 | 214 | 818 | 1 | 1 |
| `42` | 280 | 308 | 592 | 210 | 945 | 4 | 4 |
| `8675309` | 446 | 476 | 380 | 141 | 504 | 5 | 5 |
Le décor souterrain est compté sur les blocs initiaux réellement générés.
Ses témoins sont revérifiés après les fluides : support, conservation de
laméthyste bourgeonnante et lumière du moteur pour les blocs émissifs.
Ces nombres comptent des blocs, pas des cavités ni des géodes distinctes.
Laméthyste bourgeonnante est reliée à une cavité naturelle par un parcours
dair vérifié avant et après les ticks ; les JSON donnent la position de
chaque raccord et les exemples de blocs souterrains, limités à 64 témoins.
Les geysers observés possèdent la vraie entité de bloc `potent_sulfur`,
du magma dessous et une source deau retenue au-dessus. Un même geyser
doit passer par les états dormant et éruptif pendant les ticks, sans
activation artificielle par le test.
| Graine | Niveaux Y des bassins | Colonnes deau par bassin | Descente depuis la bouche | Nouveaux blocs écoulés accessibles | Deux paliers connectés |
| --- | --- | --- | --- | --- | --- |
| `0` | 148 / 140 | 53 / 76 | 148 | 1 230 | Oui, après ticks |
| `0` | 177 / 163 | 169 / 47 | 177 | 3 370 | Oui, après ticks |
| `42` | 129 / 114 | 116 / 53 | 129 | 181 | Oui, après ticks |
| `42` | 112 / 107 | 105 / 161 | 112 | 354 | Oui, après ticks |
| `8675309` | 154 / 149 | 128 / 149 | 7 | 7 | Oui, après ticks |
| `8675309` | 153 / 112 | 87 / 58 | 153 | 871 | Oui, après ticks |
Chaque groupe réunit deux bassins retenus à des niveaux différents. Le
diagnostic suit leau réelle depuis chaque bouche, horizontalement ou en
descendant, sans mouvement vers le haut, jusquau bassin cible ;
un exutoire terminal sans bassin cible nest pas compté comme cette liaison.
La descente est mesurée sur ce parcours propre à la bouche. Les nouveaux
blocs accessibles en sont les eaux écoulées qui n’étaient ni retenues dans
les bassins ni présentes avant la simulation. Les exutoires terminaux
facultatifs doivent descendre dau moins douze blocs réels.
Les terrasses sont limitées à trois blocs de profondeur et cinq blocs
dincision ; les lacs, à huit blocs de profondeur et douze blocs dincision.
Pour les terrasses, le diagnostic exige aussi quatre blocs dair naturel
au-dessus de chaque colonne excavée, en plus des fonds et supports conservés.
| Graine | Longueur rivière | Déplacement | Niveau Y | Colonnes deau | Colonnes des bassins terminaux | Sources liées |
| --- | --- | --- | --- | --- | --- | --- |
| `0` | 409,8 | 199,7 | 243 | 2 863 | 240 / 241 | 1 |
| `42` | 247,8 | 171,0 | 247 | 1 712 | 254 / 247 | 1 |
| `8675309` | 340,7 | 251,3 | 239 | 2 247 | 265 / 244 | 1 |
La longueur additionne les déplacements du tracé, diagonales comprises. Les
bassins terminaux et larrivée de leur cascade sont contrôlés après les ticks.
| Graine | Colonnes du lac | Blocs deau réels | Profondeur retenue maximale |
| --- | --- | --- | --- |
| `0` | 728 | 3 746 | 8 |
| `42` | 722 | 3 027 | 8 |
| `8675309` | 992 | 4 978 | 8 |
Chaque ligne décrit un site classé `LAKE`, après simulation. Les étangs,
bassins de rivière et terrasses sont consignés dans leurs propres catégories.
| Graine | Eau retenue, geysers inclus | Dont sources de geyser | Eau hors retenues | Eau totale | Lave totale | Sources de lave | Coulées | Accès poche de lave |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `0` | 9 845 | 1 | 4 971 | 14 816 | 144 | 20 | 2 | `(96, 100, -45)` |
| `42` | 7 267 | 4 | 903 | 8 170 | 144 | 20 | 2 | `(67, 133, -108)` |
| `8675309` | 10 268 | 5 | 1 230 | 11 498 | 144 | 20 | 2 | `(-93, 156, -21)` |
Ces volumes sont mesurés après 1 800 ticks actifs ; ils ne mesurent pas un
débit et ne signifient pas que les coulées ont atteint leur état final.
Leau retenue correspond au volume attendu après décoration et comprend
les sources propres aux geysers. Les composantes extérieures sont rattachées
aux sources rocheuses ou aux débordements déclarés, avec leurs raccords
réels. La lave des coulées est distinguée de la poche couverte.
| Graine | Sols de mousse | Sols de podzol | Sols de mycélium |
| --- | --- | --- | --- |
| `0` | 13 175 | 10 936 | 5 074 |
| `42` | 4 897 | 3 712 | 2 200 |
| `8675309` | 10 058 | 6 714 | 3 293 |
Les stocks suivants portent sur les seuls chunks inspectés, avant les
ticks de fluides. Les variantes ordinaires et dardoise de chaque minerai
sont additionnées. Ce relevé ne couvre pas toute l’île et ne déclenche
jamais de remplissage pour atteindre un quota.
| Minerai | Graine 0 | Graine 42 | Graine 8675309 |
| --- | --- | --- | --- |
| Charbon | 7 858 | 6 788 | 4 992 |
| Fer | 3 224 | 2 700 | 2 172 |
| Cuivre | 2 439 | 2 132 | 1 621 |
| Or | 930 | 687 | 507 |
| Redstone | 924 | 631 | 523 |
| Lapis | 278 | 279 | 137 |
| Diamant | 162 | 86 | 88 |
| Émeraude | 64 | 41 | 42 |
| Matière | Graine 0 | Graine 42 | Graine 8675309 |
| --- | --- | --- | --- |
| Tuf | 657 813 | 491 736 | 239 434 |
| Cobblestone | 285 441 | 172 282 | 162 791 |
| Cobblestone moussue | 593 694 | 420 042 | 229 846 |
| Boue compactée | 168 698 | 167 276 | 158 922 |
| Ardoise des abîmes | 1 443 679 | 901 821 | 858 181 |
| Roche noire | 539 269 | 275 994 | 324 101 |
| Basalte | 605 828 | 390 889 | 307 652 |
| Soufre | 87 811 | 57 838 | 82 209 |
| Cinabre | 11 797 | 13 597 | 20 095 |
La sélection des chunks suit les sites deau et d’écologie ; elle nest pas
un échantillon représentatif de toute l’île. Ces stocks ne démontrent pas
laccessibilité de chaque bloc et ne prouvent pas, seuls, une réduction du
soufre par rapport à une autre version sur un périmètre différent.
Le tableau des minerais extérieurs sélectionne charbon, fer et cuivre ; les
JSON gardent les autres familles effectivement observées avec une face dair.
| Graine | Réentrée source deau | Réentrée source de lave | Réentrée débordement |
| --- | --- | --- | --- |
| `0` | 1 vérifiée(s) | 1 vérifiée(s) | 1 vérifiée(s) |
| `42` | 1 vérifiée(s) | 1 vérifiée(s) | 1 vérifiée(s) |
| `8675309` | 1 vérifiée(s) | 1 vérifiée(s) | 1 vérifiée(s) |
Après consommation des marqueurs, les rappels du post-traitement préservent
les modifications témoins apportées aux bouches ; les blocs sont restaurés
ensuite. Les 176 fichiers historiques suivis hors raccordement
conservent leur SHA-256. Aucun monde personnel nest ouvert par ces tests.
Preuves locales ignorées : `build/alpha9-check-build-final.log` (build et graine 0),
`build/alpha9-seed42-final.log`, `build/alpha9-seed8675309-final.log` et les rapports
`cavern-seed-<graine>-after-1800-ticks.json` et `-postprocessing-reentry.json`
sous `mods/sanctuary/build/run/gameTest/diagnostics/`.
`build/report-alpha9-validation.py` extrait les mesures et vérifie les preuves
cumulées, le gel des sources et les journaux. Le hash final du JAR est lu
dans `build/alpha9-jar-sha256.txt`, sans valeur codée dans lextracteur.
Ces résultats ne garantissent aucune géode, terrasse, rivière, geyser ou
ressource sur chaque graine. Lappréciation visuelle et l’équilibrage en
partie restent ouverts. La publication et les deux synchronisations de
la même instance Prism sont vérifiées séparément lors de la distribution.
## Contrôles et validation alpha.8 — 9 septembre 2026
Les trois essais utilisent des mondes neufs et le preset de production
`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.
La distribution est également vérifiée : deux synchronisations packwiz dans
un dossier de développement neuf, puis deux dans la même instance Prism.
Le second passage ne change aucun fichier géré ; un seul JAR Sanctuary
alpha.8 reste actif. Les SHA-256 des 161 fichiers personnels et réglages
suivis sont identiques avant et après. La livraison et le hash du JAR figurent
dans [Distribution Prism](packwiz.md).
## Contrôles et validation alpha.7 — 8 septembre 2026
`WoodlandDiagnostics` reprend les preuves hydrologiques et ajoute les forêts,
les champignons sous plafond, les roches exposées et les minerais visibles.
Les arbres sont comptés par pieds de troncs sur le sol naturel, avec au moins
trois bûches verticales et des feuilles réellement observées. Le chêne noir
requiert un pied principal de 2×2 et trois couches de support par pied ; ses
branches retombantes ne comptent pas comme de nouveaux arbres. Podzol et
mycélium sont des sols explicites : le tag `minecraft:dirt` de cette version
ne les contient pas.
Les essais utilisent des mondes neufs, le vrai preset de production et
Minecraft 26.3-pre-2 / Java 25 / Fabric Loader 0.19.5. Les dix tests requis
passent sur chacune des trois graines. `check build assemblePack` passe en
3 min 41 s sur la graine 0, avec les huit tests purs de forme, hydrologie,
lave, écologie et nouveaux bois. Les exports de densité et le relevé général
facultatifs sont désactivés ; les contrôles de densité restent actifs.
| Graine | Chunks FULL inspectés | Chênes de surface | Bouleaux de surface | Chênes à rayon ≤128 | Chênes noirs inférieurs | Minerais exposés dehors : charbon / fer / cuivre |
| --- | ---: | ---: | ---: | ---: | ---: | --- |
| `0` | 167 | 292 | 263 | 218 | 30 | 4 / 19 / 5 |
| `42` | 154 | 313 | 125 | 250 | 13 | 8 / 3 / 13 |
| `8675309` | 167 | 336 | 99 | 272 | 36 | 7 / 6 / 9 |
Ces nombres décrivent un échantillon borné : les eaux et leur voisinage, des
cibles écologiques et lemplacement de larbre remarquable. Ce nest pas un
inventaire complet de l’île ni un pourcentage représentatif de toutes ses
surfaces. Les sols sont mesurés sous les canopées. Les minerais recensés dans
la dernière colonne ont une face dair réelle, ouverte vers le ciel ou vers
une paroi dégagée sur douze blocs ; les faces dans de petites cavités sont
comptées séparément. Les trois roches présentent des nappes contiguës exposées.
La graine 42 démontre un cerisier remarquable en `(-88, 251, -88)`, avec son
tronc, ses feuilles et 21 bûches dans le volume prévu. Le site sélectionné sur
la graine 0 ne produit pas darbre rare : lespace et les conditions après
décoration restent déterminants. Aucun terrain nest ajouté pour le forcer.
Les autres essences rares et le soufre ne sont pas garantis sur chaque graine.
Les eaux sont vérifiées après décoration puis pendant 1 800 ticks réels dans
des chunks actifs, avec témoins indépendants du fonctionnement des fluides.
Les bassins et les dix-huit sources de chaque niche de lave sont conservés ;
les cascades restent reliées à leurs sources et dans lemprise observée.
Les pieds des arbres et les champignons observés gardent leur support. Les
sédiments sont comparés à leur matériau prévu sur toutes leurs couches ; une
roche peut être remplacée seulement par les roches, minerais ou dépôts de
terre effectivement configurés. Les berges de sable et gravier sont comptées
sur les vrais blocs, y compris par profondeur.
Les anciens générateurs restent isolés : les 77 fichiers historiques suivis
gardent leur SHA-256. La réentrée au post-traitement après consommation des
marqueurs respecte une modification ultérieure de bouche de source sur les
trois graines. Aucun monde de joueur nest ouvert par ces tests.
Preuves locales ignorées par Git : `build/alpha7-build.log`,
`build/alpha7-seed42.log`, `build/alpha7-seed8675309.log`, et les fichiers
`woodland-seed-<seed>-<phase>.json`, `-canopy.png`, `-lower-terraces.png` et
profils de sources dans `mods/sanctuary/build/run/gameTest/diagnostics/`.
Les cartes montrent des lectures de blocs ; lappréciation du paysage dans
le client reste à faire lors du prochain essai.
## Contrôles alpha.6
`layeredHydrologySmoke` vérifie les bassins sans ruisseau de secours, les
supports des dépôts, leur déterminisme et les sources sur les faces des
strates inférieures. Les reliefs vides, trop minces ou percés ne sont pas comblés.
`layeredEcologySmoke` vérifie les sols et le dégagement des corniches,
lexclusion du sommet et du vide, ainsi que la variation verticale des biomes.
`LayeredHydrologyDiagnostics` inspecte les nouveaux paramètres et refuse
Dappled Forest dans leur palette. Il vérifie les couleurs communes, les biomes
des surfaces hautes et basses, la végétation réelle et les surfaces rocheuses.
Les plantes vanilla sur une corniche mince sont relevées séparément : elles
ne constituent pas une preuve des trois supports exigés par la nouvelle
décoration moussue. Leurs coordonnées et leurs supports sont exportés.
Les tickets de toute lemprise sont posés avant le chargement des chunks `FULL`.
Le test appelle ensuite `ServerLevel.waitForEntities(chunk, 0)` sur cette même
emprise : le serveur GameTest accélère ses ticks et un délai en ticks seul ne
prouve pas que les lectures asynchrones dentités sont terminées. Le prédicat
`isPositionTickingWithEntitiesLoaded` et les témoins d’écoulement restent requis.
Le diagnostic simule 1 800 ticks dans les chunks actifs. Les bassins doivent
retenir leur eau. Les écoulements extérieurs doivent être reliés à une source
rocheuse déclarée par des connexions deau réelles, sans eau orpheline ni voisin
horizontal en dehors de lemprise inspectée. Les seuls sites de surface sont
des étangs et des lacs ; aucune rivière nest activée dans cette livraison.
Les ouvertures des sources sont vérifiées après toute la décoration ; une
source bouchée par du lichen ne peut pas satisfaire le contrôle de descente.
Après ces mesures, un test de réentrée pose temporairement un lichen à une
bouche dont les marqueurs de génération ont été consommés. Un nouvel appel au
post-traitement doit le conserver ; le test restaure ensuite le bloc dorigine.
La preuve est dans `layered-hydrology-seed-<seed>-postprocessing-reentry.json`.
Les cartes `layered-hydrology-seed-<seed>-<phase>*.png` montrent les surfaces,
les corniches et les profils de sources. Les JSON associés contiennent les
résultats et leur emprise. Ces lectures de blocs ne remplacent pas lexamen
du paysage dans le client.
## Validation alpha.6 — 8 septembre 2026
Les dix tests serveur passent sur chacune des graines `0`, `42` et `8675309`,
dans des mondes neufs Minecraft 26.3-pre-2 avec Java 25, sur Apple M1 à 8 Go.
Chaque emprise est réellement active pendant 1 800 ticks mesurés. La commande
finale `check build assemblePack`, avec les exports de densité et le relevé
ressources général facultatifs désactivés, passe en **3 min 30 s** sur la graine 0.
Les six tests de forme, hydrologie, lave et écologie passent également.
| Graine | Spawn naturel | Eau des bassins | Eau des cascades | Canne | Lave | Chunks FULL | Plantes sur corniches épaisses |
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
| `0` | `(0, 252, 0)` | 1 533 | 414 | 20 | 18 | 151 | 11 021 |
| `42` | `(0, 249, 0)` | 1 376 | 913 | 4 | 18 | 149 | 6 334 |
| `8675309` | `(0, 247, 0)` | 1 590 | 2 350 | 10 | 18 | 160 | 9 532 |
Ces nombres décrivent uniquement les chunks inspectés, après décoration et
simulation. Les plantes incluent la végétation vanilla et la nouvelle décoration,
sur des corniches avec trois supports naturels ; les plantes de corniches
plus minces sont comptées séparément. Les cinq variantes de biome sont observées
sur les surfaces inférieures, avec respectivement cinq, trois et deux changements
verticaux contrôlés. Les surfaces inspectées comportent 5 805, 4 765 et 10 045
colonnes de pierre au sommet ; il ne sagit pas dun quota pour l’île entière.
Sur la graine 0, les sources `(-112, 103, 0)` et `(-108, 227, -96)` atteignent
Y=0 ; celle de `(-110, 148, -46)` rejoint un palier à Y=130. La première traverse
une frontière de chunks. Sur la graine 42, la source `(-62, 151, -82)` dégage
correctement son ouverture autrefois obstruée par du lichen et descend de
49 blocs ; les deux autres descendent de 103 et 236 blocs. Les trois cascades
de la graine 8675309 atteignent Y=0. Tous les écoulements observés sont reliés
à leur source, sans eau orpheline ni dépassement horizontal de lemprise.
Les bassins conservent leur eau ; les sédiments et leurs supports résistent aux
ticks de gravité. Les trois niches de lave gardent leurs 18 sources, leur coque
et leur accès sec. Le test de réentrée après consommation des marqueurs réussit
sur les trois graines : une modification ultérieure de la bouche reste intacte.
Les 52 fichiers historiques de génération suivis pour cette livraison gardent
leur SHA-256 davant modification.
Les cartes de surface, de corniches et les profils de cascade ont été examinés.
Ce sont des lectures de blocs ; le rendu dans le client reste à apprécier en
jeu. La grande rivière traversante nest pas activée : cette livraison adopte
les petits bassins et les sources de paroi. Les ressources restent observées
après génération, sans garantie de toute la progression Minecraft pour chaque graine.
## Validation alpha.5 — 8 septembre 2026
Les essais utilisent Minecraft 26.3-pre-2, Java 25 et le preset de production
sur des mondes neufs, sur Apple M1 à 8 Go. Le diagnostic attend les chunks
réellement simulables avant de mesurer 1 800 ticks de fluides.
Les dix tests serveur réussissent sur chacune des graines `0`, `42` et
`8675309`. La vérification finale `check build assemblePack`, avec les cartes de
densité et le relevé général facultatifs désactivés, réussit en **3 min 11 s**
sur la graine 0. Les contrôles ciblés de ressources, de biomes et de fluides
restent actifs. Les tâches de forme, dhydrologie et de lave réussissent aussi.
| Graine | Spawn naturel | Eau des bassins | Eau des cascades | Canne à sucre | Lave | Chunks FULL inspectés |
| --- | --- | ---: | ---: | ---: | ---: | ---: |
| `0` | `(0, 252, 0)` | 1 930 | 1 649 | 52 | 18 | 189 |
| `42` | `(0, 249, 0)` | 1 859 | 1 058 | 25 | 18 | 177 |
| `8675309` | `(0, 247, 0)` | 1 830 | 3 960 | 16 | 18 | 195 |
Sur la graine 0, les trois sources partent de `(-108, 227, -96)`,
`(-26, 203, -141)` et `(-82, 176, 141)` et atteignent toutes **Y=0**. Les trois
sources des graines 42 et 8675309 atteignent également Y=0. Les bassins conservent leur
volume ; la niche de lave conserve 18 sources et son accès praticable, sans
incendie détecté. Le cours deau reste continu à travers les frontières de chunks.
Les dépôts ont trois à cinq couches et deux supports naturels en dessous.
La rive sèche la plus éloignée de leau atteint une distance de Manhattan de
20 blocs sur la graine 0, 28 sur la graine 42 et 27 sur la graine 8675309. Les cartes montrent des plages
larges et asymétriques, des bandes de gravier et des interruptions par la roche
ou lherbe. Quelques petites poches restent anguleuses à l’échelle du bloc.
Les profils montrent les cascades réelles ; ces lectures de blocs ne remplacent
pas lappréciation du relief et des couleurs dans le client.
Les cinq biomes sont vérifiés dans des chunks réels. Dans les **189 chunks
inspectés de la graine 0**, le relevé compte 754 minerais de charbon, 464 de fer,
197 de cuivre, 215 dor, 210 de redstone, 23 de lapis et 14 de diamant. Ce sont
des blocs présents, pas des rendements de minage ni un total pour l’île entière.
Le bois, la pierre et les ressources renouvelables nont pas de plafond de
construction ajouté. La progression complète et l’équilibrage coopératif restent
à éprouver en partie.
Le calcul initial du plan hydrologique prend 23 623 ms dans la vérification
finale de la graine 0 et 24 475 ms dans lessai de la graine 42 ; la recherche
de lave ajoute respectivement 1 075 ms et 1 568 ms. Le plan est mis en cache par état aléatoire de monde,
sans charger de chunks voisins. Ces durées sont distinctes du chargement des
chunks de diagnostic.
Le premier essai court a révélé un défaut du banc de test : un chunk FULL muni
dun ticket pouvait encore attendre ses entités et ne pas exécuter ses ticks de
fluide. Le contrôle utilise désormais le prédicat exact du moteur et un témoin
indépendant par chunk de source. Un autre essai a rencontré trois blocs de chêne couché vanilla au niveau
supérieur dun étang. Le diagnostic conserve cette décoration et distingue le
volume deau prévu des sources restantes : seuls des troncs horizontaux de
surface jouxtant une source, avec de leau dessous ou le fond peu profond
plein, sont acceptés. Leur position et leur état exacts, lappui ou leau sous
le bois, les parois et le reste des sources sont vérifiés
après simulation. Le contrôle de continuité du cours deau reste strict.
Le dernier essai de la graine 8675309 conserve les 1 830 sources prévues sans
tronc dans l’étang ; le décor vanilla peut varier avec lordre de génération
des chunks, tandis que le plan Sanctuary reste identique.
Deux arbres sur des paliers de la troisième graine dévient aussi les cascades.
Le trajet sur densité nue ne prévoit pas ces feuilles. Le diagnostic final
compare cette prévision aux blocs réels, puis vérifie les composantes deau
par connexions de face : toute composante hors bassin doit rejoindre une source
déclarée. Aucune eau ne doit toucher un chunk horizontal non inspecté. Lemprise
reste celle prévue initialement avec son halo dun chunk, sans élargissement
après observation des écoulements. Sur la graine 8675309, les trois composantes
comptent 2 445, 1 152 et 363 blocs deau après simulation ; toutes rejoignent
leur source et restent entièrement dans les chunks inspectés.
Les anciennes classes et ressources de terrain et dhydrologie sont également
comparées octet par octet avec lalpha.4.
Journaux : `build/alpha5-build.log`, `build/alpha5-seed42-final.log` et
`build/alpha5-seed8675309-provenance.log`. Les exports JSON/PNG sont dans le dossier
`diagnostics` décrit plus haut. Le dernier contrôle par composantes deau est
exécuté sur les graines 8675309 et 0 ; lessai réussi de la graine 42 utilisait
encore le contrôle plus restrictif du trajet prévu sur densité nue. Le code de
génération est identique entre ces essais.
## Validation alpha.4 — 8 septembre 2026
Les tests serveur utilisent le preset de production et des mondes neufs. Sur
+67 -4
View File
@@ -51,12 +51,75 @@ Les intégrations communautaires envisagées comprennent Fabric API, Sodium, Iri
L'île de Sanctuary est le point de départ commun. Elle flotte dans un vide qui ne doit pas être rempli par la génération habituelle du monde. Les continents suivants sont de vastes terres suspendues, créées progressivement selon une taille et une direction maîtrisées.
L'île initiale doit être **tempérée et légèrement humide**, avec quelques zones
plus sèches, notamment des plages. Une rivière et de petits étangs ou lacs de
surface peuvent accueillir de la canne à sucre. Les grands bassins souterrains
remplis d'eau ne sont pas retenus pour cette île. Le terrain doit laisser voir un
dessous sculpté et dégagé, dans un monde de **384 blocs de hauteur**, avec des
plus sèches, notamment de larges plages et dépôts irréguliers de sable, gravier,
pierre et terre enherbée. De petits étangs ou lacs de surface
peuvent accueillir de la canne à sucre. Lorsque le relief le permet, une grande
rivière suit la forme de l’île, depuis une source jusqu’à une cascade et un
bassin, avec des rives de sable, de gravier et dargile. Ce parcours doit former
un lieu reconnaissable ; il reste facultatif. À défaut, l’île conserve ses
petits bassins, sans imposer de courts ruisseaux. Le terrain est traité près de la surface,
avec des dépôts épais et des rives variées. De rares sources sortent de niches
rocheuses naturelles et leurs cascades peuvent descendre jusque dans le vide ;
les bassins restent retenus et ne doivent pas provoquer une inondation globale.
Une rivière peut avoir des échappements et des chutes déclarés. Les eaux
intérieures doivent rester localisées et retenues, sans aquifère qui remplit
globalement les cavités. Des bassins naturels étagés, évoquant des rizières,
peuvent se relier par des cascades et offrir des passages verticaux par la nage.
Cette image ne prévoit aucune culture de riz dans le mod Sanctuary.
Le terrain doit laisser voir un dessous sculpté et dégagé, dans un monde de
**384 blocs de hauteur**, avec des
nuages relevés et sans disque noir artificiel apparaissant sous un seuil d'altitude.
Lambiance de départ doit rester douce et tempérée, avec de véritables forêts
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, 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 et rester
ponctuelles, avec de vrais geysers actifs lorsque les conditions sy prêtent.
Quelques sources profondes peuvent former des coulées de lave ; elles ne doivent pas
transformer les profondeurs en grands bassins remplis. L’émeraude rejoint les
minerais à rechercher, avec lor, le diamant, la redstone et le lapis.
Les bois dominants sont le chêne, le chêne noir et le bouleau : chênes et
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.
Les cavités sous l’île doivent aussi accueillir des ambiances luxuriantes
avec de la végétation lumineuse, des secteurs à stalactites et stalagmites et
des géodes daméthyste. Leur décor doit utiliser les sols et plafonds du relief,
sans reconstruire l’île ni déposer de structures flottantes sans support.
Lalpha.9 introduit ces ambiances et recherche des lacs plus profonds et
volumineux ; les résultats et leurs limites sont documentés dans les contrôles moteur. Ces cavités appartiennent à
lOverworld de Sanctuary et ne remplacent pas le futur projet de dimension
des cavernes.
L’île doit offrir les ressources nécessaires aux premières étapes de Minecraft,
en quantités modestes pour pousser à explorer les expansions lors des grands
projets. Les minerais doivent être trouvables aux altitudes du terrain. Les
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. 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
la carte avec le nord en haut : **nord froid, sud chaud, ouest sec et est humide**.
+519 -23
View File
@@ -5,6 +5,12 @@ 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.9 utilise les paramètres `sanctuary:sanctuary_cavern`, pour de nouveaux
mondes uniquement. Sa validation moteur passe sur les trois graines de référence. Lalpha.8 conserve
`sanctuary:sanctuary_rift` et ses résultats sur trois graines avec 1 800 ticks
de fluides ; ils ne valident pas la génération v9. Les observations de chaque
version restent séparées dans [Validation](testing.md).
## Créer un monde
En solo : créer un **nouveau** monde et choisir le type de monde **Sanctuary**
@@ -22,43 +28,290 @@ La seed est libre. Modifier `level-type` ne convertit pas un monde existant :
utiliser un nouveau `level-name` pour tester une autre génération. Les fichiers
de test et les sauvegardes personnelles doivent rester distincts.
## Ce qui est généré
## Socle de génération conservé
Les points suivants décrivent le socle livré jusqu’à lalpha.8. Les évolutions
alpha.9 et leur état de validation sont précisés dans la section v9 ci-dessous.
- Le générateur reste celui de Minecraft, `minecraft:noise`.
- La forme reprend le champ de bruit flottant actif en 26.2 : `old_blended_noise`
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 v4 creuse ensuite localement les lits de 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.
La côte réelle apparaît plus tôt, selon la densité du terrain et la seed.
Il reste du vide permanent au-delà de **288 blocs**, sans répétition d'îles.
Aucun terrain ne réapparaît au-delà de **288 blocs**, sans répétition d'îles.
Les arbres peuvent dépasser la côte rocheuse de quelques blocs.
- Les nouveaux mondes utilisent **384 blocs de hauteur**, de `Y=0` à `Y=383`,
pour la dimension et le générateur. La roche disparaît avant leurs limites.
Il n'y a ni plancher de bedrock,
ni mer globale, ni reprise du terrain à distance. Le Nether et l'End restent
ceux de Minecraft.
- `sanctuary:starter_forest_hydrology` reprend végétation, minerais et animaux de la forêt
vanilla 26.3. Ses blocs sont exclusivement vanilla. C'est un biome distinct
pour ne pas hériter des tags de structures vanilla et de leurs apparitions
possibles dans le vide.
- Les carvers, géodes, donjons, lacs de lave et sources sont retirés de cette
forêt initiale. Les minerais remplacent la roche existante et les arbres ont
besoin de sol. Aucun mod de biomes externe n'est nécessaire.
- Les nouveaux mondes utilisent une palette tempérée de forêts de chênes et de
bouleaux, de clairières fleuries et de zones rocheuses. Les strates intérieures
portent des chênes noirs et des champignons sur leurs corniches naturelles.
Dappled Forest est réservé aux futurs continents. Leur végétation et leurs animaux proviennent
de Minecraft 26.3-pre-2. Les nouveaux identifiants ne reprennent pas les tags
de structures vanilla, afin de ne pas placer leurs structures dans le vide.
- Jusqu’à lalpha.8, les carvers, géodes, donjons, grands lacs de lave et sources
vanilla sont retirés. Lalpha.9 ajoute une intégration bornée de géodes et
de geysers, sans réactiver lensemble de la décoration souterraine vanilla.
Leau de Sanctuary suit son propre plan de surface et de niches
rocheuses. Les arbres et les petits filons ont besoin du terrain existant.
Aucun mod de biomes externe nest nécessaire.
Lhydrologie v4 fournit les premières eaux de surface décrites ci-dessous. La
répartition complète des ressources de survie et la variété de biomes restent
à développer. Les minerais vanilla conservent leurs plages daltitude ;
leur abondance n'est donc pas encore équilibrée pour une île flottante.
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.
## Cavités et terrasses deau — génération v9
Le nouveau réglage est `sanctuary:sanctuary_cavern`, avec le biome source
`sanctuary:cavern_island` et la règle de matière `sanctuary:cavern_island`.
La densité réutilise `sanctuary:final_density_rift` sans changer cet identifiant
historique. Ses nouvelles données et traitements
doivent rester distincts de `sanctuary:sanctuary_rift` et de toutes les
générations précédentes. Le preset sélectionne la nouvelle génération à la
création ; une mise à jour du JAR ne convertit pas une sauvegarde ni ne
régénère ses chunks. Les cavités restent dans lOverworld sur 384 blocs de
hauteur ; la dimension des cavernes décrite dans la vision nest pas ajoutée.
### Ambiances et ressources
Le ticket conserve les forêts de surface et ajoute des cavités luxuriantes
éclairées par leur végétation, des secteurs à spéléothèmes et des géodes
daméthyste sur des emplacements compatibles. Les sols, plafonds, coquilles
et ouvertures doivent être observés après la décoration réelle : une valeur
de biome ne prouve ni le décor ni son éclairage. Lobjectif est dutiliser
les volumes naturels de l’île, sans remplir ses trous pour accueillir une
structure.
Les dix biomes ont leurs propres identifiants : `cavern_oak_forest`,
`cavern_birch_forest`, `cavern_clearing`, `cavern_dry_woodland`,
`cavern_rocky_heath`, `cavern_dark_grove`, `cavern_bamboo_grove`,
`cavern_sulfur_depths`, `cavern_lush_caves` et `cavern_dripstone_caves`, dans
le namespace `sanctuary`. Les deux nouvelles ambiances luxuriantes et à
spéléothèmes sont limitées à Y<184 par la palette ; leur décoration recherche
des sols entre Y=40 et Y=182, avec trois blocs naturels de support et un
plafond situé de 6 à 48 blocs au-dessus.
Dans les cavités luxuriantes, la mousse remplace le sol compatible et de
petites plantes laccompagnent. Les vraies lianes `cave_vines` et
`cave_vines_plant` pendent sur 3 à 8 blocs ; leurs baies émettent de la lumière
et restent récoltables. Ce décor najoute pas deau. Les spéléothèmes utilisent
les blocs `pointed_dripstone` orientés, sur un appui de `dripstone_block`
remplaçant de la roche existante, avec une longueur de 1 à 4 blocs selon
lespace disponible.
La géode utilise une configuration vanilla compacte avec améthyste,
améthyste bourgeonnante, calcite et basalte lisse. Son enveloppe doit tenir
dans la roche préexistante et un passage de deux blocs de haut la relie à
la cavité, en préservant laméthyste bourgeonnante et les cristaux attachés.
Un chunk sur huit est candidat ; au plus une pose réussie est retenue par
chunk candidat. Cela ne garantit pas de géode sur une graine.
Le soufre devient admissible dans une bande climatique plus étroite sous
Y=176. La réduction de cette sélection peut être testée indépendamment du
terrain ; seule une comparaison des vrais blocs sur un périmètre commun
mesure sa diminution dans l’île. Un geyser éventuel creuse un seul bloc de
sol pour sa source deau, avec `potent_sulfur` dessous et `magma_block` encore
en dessous. Le fond, les quatre parois et la hauteur libre sont contrôlés.
Le cycle du geyser reste celui de Minecraft ; ce décor est distinct des
bassins hydrologiques et son eau doit être comptée séparément.
Leur présence, leur activité, l’éclairage et la cohabitation avec les eaux
et la végétation sont contrôlés dans les mondes de test. Les preuves de ces
éléments sont cumulées sur les trois graines. Un site incompatible peut
être rejeté ; aucun geyser ni stock nest garanti pour chaque graine.
### Lacs et bassins étagés
Le plan hydrologique conserve la recherche de rivière de
lalpha.8 et vise de plus grands lacs de surface. Les cibles actuelles sont
de lordre de **700 à 1 100 colonnes** par grand lac, jusqu’à **7 à 8 blocs**
de profondeur intérieure et une incision limitée à **12 blocs**. Ce sont des
bornes de recherche ; les dimensions réelles sont relevées séparément, sans quota deau à
remplir. Les fonds, les parois et les dépôts exigent du terrain naturel.
Les terrasses sont recherchées dans des cavités entre **Y=56 et Y=208**,
avec deux paliers par groupe et jusqu’à deux groupes facultatifs.
Un palier vise environ **40 à 180 colonnes**, une profondeur de **1 à 3 blocs**
et une incision dau plus **5 blocs**. Le fond doit conserver trois couches
de sédiments et deux couches de support naturel ; le plafond et lespace
disponible sont vérifiés. Les mesures moteur précisent les sites effectivement retenus.
La recherche suit le choix de pente de leau vanilla : parmi les passages
libres, elle privilégie ceux qui rejoignent la chute la plus proche. Un
bassin inférieur doit intercepter ce trajet réel, sans détour creusé pour
le rejoindre. Les sorties entre paliers sont déclarées et les cascades sont formées par les
ticks de fluides Minecraft. Le premier débordement doit atteindre le second
bassin. Celui-ci peut avoir un exutoire terminal qui s’écoule sur les roches
ou vers le vide, sans troisième bassin ajouté. Le générateur ne doit ni peindre une colonne
deau jusquau vide ni colmater un trou pour retenir un bassin. Les volumes
retenus et les liaisons réelles sont vérifiés séparément après décoration
et simulation, y compris aux frontières de chunks. Les cascades peuvent
servir à monter ou descendre selon la nage vanilla ; aucune mécanique
dascenseur supplémentaire nest prévue. « Rizières » décrit seulement
laspect du paysage en terrasses, sans ajouter une culture de riz.
**Validation moteur au 9 septembre 2026 :** les trois graines de référence
passent les contrôles après 1 800 ticks, avec preuves cumulées des terrasses
connectées, geysers actifs, baies lumineuses, spéléothèmes et améthyste
bourgeonnante. Le build complet réussit. Les résultats et leurs limites sont
consignés dans [Validation](testing.md). Aucune présence universelle nest
garantie ; la distribution est vérifiée séparément selon le guide packwiz.
## Failles, géologie et ressources — génération v8
Les nouveaux mondes utilisent `sanctuary:sanctuary_rift`, la densité
`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
@@ -119,8 +372,9 @@ ce gradient inférieur. Une nouvelle érosion travaille sur une transition de
deux bruits de sculpture existants. Elle laisse les lobes naturels se poursuivre
vers le bas, puis les amincit à différentes hauteurs ; elle n'ajoute ni socle ni
surface garantie. La transition n'affecte plus le terrain à partir de `Y=144`.
Une marge vide protège le bas de la dimension ; le contrôle des chunks terminés
vérifie que la tranche `Y=0..15` ne contient aucun bloc.
Une marge vide protège le bas de la dimension : aucun plancher rocheux ne doit
apparaître dans `Y=0..15`. À partir de lalpha.5, seules les eaux descendantes de
sources explicitement déclarées peuvent traverser cette marge.
Les champs optionnels du codec `vertical_offset`, `height` et `underside` valent
respectivement `0`, `256` et un bruit nul par défaut. Seule la nouvelle fonction
@@ -174,9 +428,187 @@ plans deau ; il ne simule pas encore une pente vers laval ou un débit. Le
tracés peuvent passer par les diagonales, mais leur eau reste connectée de
bloc en bloc. Lexcavation est limitée à six blocs, leau à quatre blocs de
profondeur, avec un fond plein sur trois blocs d’épaisseur, dont deux blocs naturels sous
le sédiment. Les grandes rivières en pente, cascades, océans, aquifères,
climats multiples et continents restent des incréments distincts.
**Créer un nouveau monde Sanctuary pour tester cette hydrologie.**
le sédiment. Ce contrat alpha.4 reste celui de ses sauvegardes. Les sources en cascade sont
ajoutées séparément dans la génération v5 ; les grandes rivières en pente, océans,
aquifères, climats multiples et continents restent des incréments distincts.
## Plages, sources et mosaïque tempérée — génération v5
Limplémentation **0.1.0-alpha.5** sélectionne la nouvelle clé de paramètres
`sanctuary:sanctuary_natural` (`SanctuarySpawn.NATURAL_SETTINGS`). Elle conserve
la densité relevée, les 384 blocs de hauteur et le ciel de lalpha.3. La classe
`NaturalHydrology` et son application sont séparées de `SurfaceHydrology` et
`HydrologyRuntime` : le code, les paramètres `sanctuary:sanctuary_hydrology` et
le biome `sanctuary:starter_forest_hydrology` de lalpha.4 restent inchangés.
Les clés alpha.2 et alpha.3 restent également disponibles avec leur comportement
antérieur. **Ces changements concernent uniquement les nouveaux mondes** : aucune
conversion de sauvegarde, régénération de chunk ou expansion nest effectuée.
### Dépôts et eau de surface
Le plan recherche des étangs, de petits lacs et un cours deau calme dans le
relief naturel. Lexcavation maximale passe à **quatre blocs**, pour une eau de
**trois blocs de profondeur** au plus. Les bassins gardent un fond et des parois
naturels pleins ; aucune digue nest ajoutée et aucune chambre souterraine nest
remplie. Le cours deau reste à niveau constant : il ne modélise pas un débit
ni une pente continue vers laval.
Les plages sont des nappes irrégulières qui suivent les faibles variations du
relief autour des plans deau. Elles peuvent s’étendre sur plusieurs blocs,
se rapprocher dune autre poche deau et retrouver progressivement lherbe ou
la pierre. Les matériaux varient entre sable, gravier, pierre et terre enherbée ;
largile peut apparaître dans les fonds. Le traitement dune terrasse sèche
retire au plus **deux blocs** du relief existant.
Chaque dépôt remplace **trois à cinq couches** de terrain déjà plein et garde
**deux couches naturelles de support** en dessous. Ce support permet notamment
au sable et au gravier de rester en place après les mises à jour de gravité.
Les veines de gravier, disques de sédiments et magma sous-marin vanilla ne sont
pas réintroduits dans ces nouveaux biomes. Les autres décorations rocheuses et
les minerais peuvent encore remplacer la pierre ; ils ne remplacent pas les
couches de sable, gravier ou argile. Les rives compatibles peuvent porter de la
canne à sucre après la végétation. Les troncs couchés vanilla peuvent occuper
quelques blocs de la surface dun étang ; les contrôles distinguent cette
décoration de leau restante et vérifient leur fond ou leau sous les troncs,
les sources voisines et la rétention du bassin.
### Sources rocheuses et cascades
Le plan peut retenir jusqu’à **trois sources** dans des niches rocheuses
naturelles, séparées des dépôts modifiés. Une source remplace un seul bloc de
roche, sous un toit, sur un fond et entre trois côtés déjà pleins. Son unique
sortie horizontale est naturellement ouverte sur une chute dau moins huit
blocs. Le générateur ne construit ni pylône, ni corniche, ni tunnel pour obtenir
ce résultat.
Seul le bloc source est généré. Les ticks de fluide de Minecraft forment la
cascade et ses éventuels passages sur des paliers. Ces écoulements peuvent
descendre **jusque dans le vide**, conformément au comportement demandé. Cette
exception concerne les sources déclarées. Le trajet calculé dans la roche nue
est une prévision : les arbres générés ensuite peuvent dévier une chute. Les
diagnostics vérifient donc les connexions réelles de chaque bloc deau vers une
source, dans lemprise des chunks inspectés et de leur halo. Ils échouent si de
leau touche une frontière non inspectée ou si un bassin perd sa coque. Les
déviations par rapport à la prévision restent mesurées dans le rapport. Le
comportement des fluides reste celui de Minecraft, y compris lorsque les joueurs
modifient une rive.
### Petite réserve de lave
`StarterLavaDeposit` recherche au plus une niche de **18 blocs sources** dans
la roche existante, accessible par un passage sec de deux blocs de haut depuis
une corniche naturelle. Le fond garde trois blocs pleins, les parois deux et
la couverture huit ; ces supports sont vérifiés avant les écritures. La niche
reste éloignée des eaux et de leurs parcours. Elle fournit de la lave prélevable
pour les premières productions et un portail du Nether. Si aucun site ne
convient, le plan reste vide : aucun bassin artificiel nest ajouté dans le vide.
Les tests vérifient laccès praticable, la rétention de la lave et labsence
dincendie après simulation.
### Biomes et minerais
Le biome source `sanctuary:temperate_island` utilise deux champs de bruit
initialisés par la seed pour répartir cinq variantes horizontales. Une rive et
la roche sous elle restent dans la même variante :
| Identifiant | Variante |
| --- | --- |
| `sanctuary:temperate_forest` | Forêt dominante |
| `sanctuary:temperate_plains` | Clairières et plaines tempérées |
| `sanctuary:temperate_birch_forest` | Forêt de bouleaux |
| `sanctuary:temperate_flower_forest` | Forêt fleurie |
| `sanctuary:temperate_dappled_forest` | Dappled Forest, peupliers et végétation associée |
Dappled Forest est un biome vanilla introduit dans
[Minecraft 26.3 Snapshot 1](https://www.minecraft.net/en-us/article/minecraft-26-3-snapshot-1).
Cette variante reprend ses données de **26.3-pre-2**, avec une humidité légèrement
relevée pour Sanctuary. Elle najoute ni dépendance externe ni structure de camp.
La palette tempérée ne constitue pas encore le catalogue TerraMix ni le champ
climatique des futurs continents.
Les nouveaux biomes remplacent les filons vanilla de métaux par de petites
features `sanctuary:island_ore_*`, à des altitudes adaptées à l’île relevée :
| Ressource | Taille du filon | Tentatives par chunk | Plage Y |
| --- | --- | --- | --- |
| Charbon | 6 | 2 | 96288 |
| Fer | 4 | 2 | 88248 |
| Cuivre | 4 | 1 | 96248 |
| Or | 4 | 1 | 104224 |
| Redstone | 4 | 1 | 96224 |
| Lapis | 3 | Une tentative, chance 1/2 | 112232 |
| Diamant | 3 | Une tentative, chance 1/4 | 104208 |
Ces paramètres décrivent des tentatives, pas des quantités promises : le filon
doit rencontrer la roche existante, et sa forme dépend du hasard de génération.
La sélection des biomes et des eaux ne cherche aucun stock cible. Un relevé
partiel reste un relevé partiel ; la présence des sept ressources sur une seed
de référence ne garantit pas leur présence sur toutes les seeds.
La validation de lalpha.5 est suivie dans [Validation](testing.md). Ses diagnostics
sont conçus pour inspecter les blocs décorés, les profils des dépôts, les
jointures de chunks, les biomes et les ressources, puis simuler réellement les
fluides dans les chunks des sites. Les cartes et profils PNG montrent des
lectures de blocs, pas un rendu Minecraft. La descente des
cascades est contrôlée après activation effective des chunks qui les simulent.
## Prairies et corniches — génération v6
Lalpha.6 utilise `sanctuary:sanctuary_layered` et le biome source
`sanctuary:layered_island`. Les formes et le ciel restent ceux de la génération
relevée. Les anciennes classes, biomes et règles de matériaux restent disponibles
pour les sauvegardes alpha.2 à alpha.5 ; seul le preset de création choisit v6.
### Une palette qui varie aussi en hauteur
Les cinq variantes partagent leurs teintes dherbe et de feuillage. Les champs
de climat liés à la seed déforment de larges bandes daltitude, autour de Y154
et Y214 : prairies et prairies fleuries sur le dessus, bosquets et corniches
moussues plus bas. Des zones rocheuses traversent ces bandes et remplacent la
couverture de terre par de la pierre sur le relief existant. Les prairies gardent
quelques arbres épars et leur végétation vanilla. Dappled Forest nappartient
plus à cette palette ; son utilisation sur de futurs continents reste à développer.
| Identifiant | Ambiance |
| --- | --- |
| `sanctuary:layered_meadow` | Prairie tempérée |
| `sanctuary:layered_flower_meadow` | Prairie fleurie |
| `sanctuary:layered_lower_grove` | Bosquet inférieur |
| `sanctuary:layered_mossy_terraces` | Corniches moussues |
| `sanctuary:layered_rocky_terraces` | Affleurements rocheux |
`LowerTerraceVegetationFeature` parcourt aussi les sols naturels sous les
surplombs, au moins huit blocs sous une surface supérieure. Elle demande trois
blocs pleins de support et deux blocs libres au-dessus. Elle peut remplacer la
surface existante par de la mousse et planter fougères, fleurs ou petits arbustes
lorsque leurs règles de survie le permettent. Les cellules hydrologiques, les
sources, les parcours deau prévus et les supports de lave sont protégés.
Il ny a ni corniche construite dans lair ni ajout d’éclairage artificiel.
### Bassins et sources de paroi
`LayeredHydrology` conserve les petits bassins et les dépôts épais, avec une
plus grande présence de pierre. Le court `STREAM` à niveau constant de lalpha.5
nest pas repris. Cette version retient uniquement les étangs et les petits
lacs ; elle ne cherche pas de grande rivière. Le parcours traversant avec
source, cascade et bassin reste un ticket distinct, à démontrer sur le relief
Minecraft réel. Aucun chenal de secours nest creusé.
La profondeur reste limitée à trois blocs et lincision à quatre. Le plan
vérifie le fond et les rives avant dappliquer ses écritures, dans les nouveaux
chunks seulement. Le sable, le gravier, largile et la roche suivent les rives.
Des niches de source sont également recherchées dans les strates inférieures,
et pas seulement près de la surface la plus haute. Leurs chutes sont produites
par les ticks de Minecraft à partir dun seul bloc source. Le post-traitement
des nouveaux chunks libère uniquement leur ouverture, déjà vide dans la densité
naturelle, pour quun lichen ou une plante ne bouche pas la source. Les positions
sont marquées dans la file vanilla pendant la génération, puis consommées une
seule fois. Un rechargement ne dégage pas une bouche modifiée par un joueur.
Aucune chute nest préremplie et aucun nouveau passage nest creusé dans la roche.
Les parcours, les écoulements et la végétation réellement observés, ainsi que
les limites du modèle, sont consignés dans [Validation](testing.md).
## Apparition commune
@@ -187,7 +619,8 @@ l'absence d'une telle surface, elle retient une colonne de sol praticable ; si
aucun emplacement naturel sûr n'existe, la création signale la seed concernée
plutôt que d'ajouter une plateforme. Elle ne s'applique qu'à l'Overworld
utilisant les paramètres `sanctuary:sanctuary`, `sanctuary:sanctuary_384` ou
`sanctuary:sanctuary_hydrology`.
`sanctuary:sanctuary_hydrology`, `sanctuary:sanctuary_natural` ou
`sanctuary:sanctuary_layered`.
Elle enregistre le spawn partagé dans la sauvegarde et ne le réinitialise pas
au redémarrage : les modifications administratives de `/setworldspawn` restent
conservées. Les lits et la dispersion habituelle du spawn suivent encore les
@@ -266,3 +699,66 @@ chunks sauvegardés.
La version de génération et les emprises doivent être gelées avant les premières
sauvegardes destinées à durer. Ce prototype ne promet pas encore de migration
automatique des mondes entre deux versions de terrain.
## Forêts, corniches humides et affleurements — génération v7
Lalpha.7 crée les nouveaux mondes avec `sanctuary:sanctuary_woodland` et
`sanctuary:woodland_island`. La densité `sanctuary:final_density_384`, les
formes rocheuses, la hauteur et le ciel restent identiques. Les plans des
bassins, sources et petites niches de lave reprennent les algorithmes alpha.6.
Les anciens paramètres, biomes et features sont conservés ; cette livraison
ne modifie pas les mondes existants.
La palette comporte huit biomes :
| Identifiant | Rôle |
| --- | --- |
| `sanctuary:woodland_oak_forest` | Forêt de chênes dominante, bouleaux secondaires |
| `sanctuary:woodland_birch_forest` | Forêt de bouleaux complémentaire |
| `sanctuary:woodland_clearing` | Clairière fleurie |
| `sanctuary:woodland_dry_woodland` | Bois sec plus clairsemé |
| `sanctuary:woodland_rocky_heath` | Masses rocheuses exposées |
| `sanctuary:woodland_dark_grove` | Corniches humides à chênes noirs et champignons |
| `sanctuary:woodland_bamboo_grove` | Rare intérieur à jungle et bambou |
| `sanctuary:woodland_sulfur_depths` | Rare poche profonde de soufre sur roche existante |
Une altitude élevée ne transforme plus automatiquement une forêt en prairie
ou en roche. Les forêts utilisent les véritables arbres vanilla avec leurs
conditions de croissance ; les affleurements sont des patches horizontaux
et des variations de matériau sur le terrain existant.
`woodland_groves` place de vrais chênes noirs sur des sols naturels de 2×2,
avec trois blocs de support et une place suffisante pour le tronc et la
canopée sous le plafond. Des champignons accompagnent les sols de podzol et
de mycélium, avec quelques touches de mousse. La jungle et le bambou restent
limités aux rares poches qui leur conviennent.
Un seul emplacement remarquable est recherché par graine, avec une essence
choisie parmi cerisier (favorisé), épicéa, jungle, acacia, palétuvier et chêne
pâle. Il ne produit pas de terrain : si le site ou les conditions de larbre
vanilla ne conviennent pas, il ne pousse pas. Ce nest pas une garantie de
chaque essence sur chaque île. Aucun cœur de Creaking nest ajouté par larbre
pâle utilisé ici.
Les affleurements mêlent des nappes cohérentes de pierre, andésite, diorite et
granite. Les berges conservent leur excavation limitée et leurs dépôts de
3 à 5 blocs, avec des patches de sable, gravier, argile et roche. Les plantations de `woodland_groves` évitent les sources, leurs exutoires
et les niches de lave. Les décorations vanilla conservent leurs placements ;
le traitement unique après génération peut rouvrir la seule bouche de source
déclarée si une plante lobstrue. Le soufre
reste une matière rare des profondeurs ; les grands bassins et sources
vanilla du biome Sulfur Caves ne sont pas activés.
Les anciens petits filons souterrains de charbon, fer et cuivre étaient peu
susceptibles datteindre les surfaces hautes ; leur réglage écartait aussi une
partie des blocs voisins de lair. Les nouvelles features `woodland_ore_*`
utilisent des tailles vanilla limitées (`size=6` pour le charbon, `size=5`
pour fer/cuivre), sans garantir ce nombre de blocs produits par filon,
autorisent cette exposition et ajoutent des essais aléatoires sur les roches
de surface et les corniches rencontrées depuis lair. Elles remplacent
seulement les roches naturelles reconnues par Minecraft, dont andésite,
diorite et granite. Les autres minerais et les anciens générateurs gardent
leurs réglages ; aucune quantité cible nest réinjectée après mesure.
La version et les graines de validation sont consignées dans [Validation](testing.md).
+2 -2
View File
@@ -8,6 +8,6 @@ loader_version=0.19.5
loom_version=1.17.20
fabric_api_version=0.160.0+26.3
mod_version=0.1.0-alpha.4
pack_version=0.1.0-alpha.4
mod_version=0.1.0-alpha.9
pack_version=0.1.0-alpha.9
maven_group=fr.koka.sanctuary
+114
View File
@@ -89,3 +89,117 @@ tasks.named('check') { dependsOn('hydrologySmoke') }
// src/test contains a JavaExec assertion harness; engine tests use Fabric's gametest source set.
tasks.named('test') { failOnNoDiscoveredTests = false }
tasks.register('naturalHydrologySmoke', JavaExec) {
group = 'verification'
description = 'Check broad shores, supported sediments and natural rock springs.'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.NaturalHydrologySmoke'
}
tasks.named('check') { dependsOn('naturalHydrologySmoke') }
tasks.register('starterLavaSmoke', JavaExec) {
group = 'verification'
description = 'Check a small contained lava niche in naturally supported rock.'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.StarterLavaDepositSmoke'
}
tasks.named('check') { dependsOn('starterLavaSmoke') }
// Alpha.6 has separate contracts so saved alpha.5 worlds keep their generation.
tasks.register('layeredHydrologySmoke', JavaExec) {
group = 'verification'
description = 'Check retained basins, broad rock shores and lower wall springs.'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.LayeredHydrologySmoke'
}
tasks.named('check') { dependsOn('layeredHydrologySmoke') }
tasks.register('layeredEcologySmoke', JavaExec) {
group = 'verification'
description = 'Check altitude biome variation and naturally supported lower terraces.'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.LayeredEcologySmoke'
}
tasks.named('check') { dependsOn('layeredEcologySmoke') }
// Alpha.7 keeps previous biome palettes and generators unchanged.
tasks.register('woodlandPaletteSmoke', JavaExec) {
group = 'verification'
description = 'Check surface forest coverage and bounded rare interior biomes.'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.WoodlandPaletteSmoke'
}
tasks.register('woodlandGroveSmoke', JavaExec) {
group = 'verification'
description = 'Check natural tree supports and canopy space without creating terrain.'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
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') }
tasks.register('cavernPaletteSmoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.CavernPaletteSmoke'
}
tasks.named('check') { dependsOn('cavernPaletteSmoke') }
tasks.register('cavernHydrologySmoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.CavernHydrologySmoke'
}
tasks.named('check') { dependsOn('cavernHydrologySmoke') }
tasks.register('cavernLavaSmoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.CavernLavaSmoke'
}
tasks.named('check') { dependsOn('cavernLavaSmoke') }
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,13 @@
package fr.koka.sanctuary.gametest;
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 fr.koka.sanctuary.worldgen.CavernHydrologyRuntime;
import net.minecraft.tags.FluidTags;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import fr.koka.sanctuary.worldgen.IslandShape;
import fr.koka.sanctuary.worldgen.MainIslandDensity;
import fr.koka.sanctuary.SanctuaryMod;
@@ -23,14 +30,26 @@ import net.minecraft.world.level.storage.LevelData;
public final class SanctuaryWorldGameTests {
public static LevelData.RespawnData initialSpawn;
@GameTest(maxTicks = 400)
@GameTest(maxTicks = 3000)
public void generatedSurfaceWaterRemainsContained(GameTestHelper helper) throws java.io.IOException {
HydrologyDiagnostics.start(helper);
var generator = (NoiseBasedChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
if (CavernHydrologyRuntime.enabled(generator)) CavernDiagnostics.start(helper);
else if (RiftHydrologyRuntime.enabled(generator)) RiftDiagnostics.start(helper);
else if (WoodlandHydrologyRuntime.enabled(generator)) WoodlandDiagnostics.start(helper);
else if (LayeredHydrologyRuntime.enabled(generator)) LayeredHydrologyDiagnostics.start(helper);
else if (NaturalHydrologyRuntime.enabled(generator)) NaturalHydrologyDiagnostics.start(helper);
else HydrologyDiagnostics.start(helper);
}
@GameTest(maxTicks = 200)
public void hydrologyDoesNotActivateForSavedLegacySettings(GameTestHelper helper) {
HydrologyDiagnostics.verifyLegacyIsolation(helper);
var generator = (NoiseBasedChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
if (CavernHydrologyRuntime.enabled(generator)) CavernDiagnostics.verifyLegacyIsolation(helper);
else if (RiftHydrologyRuntime.enabled(generator)) RiftDiagnostics.verifyLegacyIsolation(helper);
else if (WoodlandHydrologyRuntime.enabled(generator)) WoodlandDiagnostics.verifyLegacyIsolation(helper);
else if (LayeredHydrologyRuntime.enabled(generator)) LayeredHydrologyDiagnostics.verifyLegacyIsolation(helper);
else if (NaturalHydrologyRuntime.enabled(generator)) NaturalHydrologyDiagnostics.verifyLegacyIsolation(helper);
else HydrologyDiagnostics.verifyLegacyIsolation(helper);
helper.succeed();
}
@@ -104,8 +123,49 @@ public final class SanctuaryWorldGameTests {
for (int[] coordinate : new int[][]{{0, 0}, {-8, 0}, {8, 0}, {0, -8}, {0, 8}}) {
ChunkAccess chunk = helper.getLevel().getChunkSource().getChunk(
coordinate[0], coordinate[1], ChunkStatus.FULL, true);
helper.assertTrue(chunk != null && chunk.getSection(0).hasOnlyAir(),
"The fully generated island must leave Y0..15 empty, without a floor cut at the dimension bound");
helper.assertTrue(chunk != null, "The lower-margin test needs a fully generated chunk");
if (chunk.getSection(0).hasOnlyAir()) continue;
var generator = (NoiseBasedChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
helper.assertTrue(NaturalHydrologyRuntime.enabled(generator) || LayeredHydrologyRuntime.enabled(generator)
|| WoodlandHydrologyRuntime.enabled(generator) || RiftHydrologyRuntime.enabled(generator) || CavernHydrologyRuntime.enabled(generator),
"The legacy island must leave its lower margin entirely empty");
for (int x = chunk.getPos().getMinBlockX(); x <= chunk.getPos().getMaxBlockX(); x++) {
for (int z = chunk.getPos().getMinBlockZ(); z <= chunk.getPos().getMaxBlockZ(); z++) {
for (int y = 0; y < 16; y++) {
BlockPos p = new BlockPos(x, y, z);
var state = chunk.getBlockState(p);
if (CavernHydrologyRuntime.enabled(generator)) {
boolean water = state.getFluidState().is(FluidTags.WATER)
&& CavernDiagnostics.isWithinSpringInspectionFootprint(CavernHydrologyRuntime.cavernPlan(helper.getLevel()), x, z);
final int bx = x, bz = z;
boolean lava = state.getFluidState().is(FluidTags.LAVA)
&& CavernHydrologyRuntime.lavaPlan(helper.getLevel()).flowBounds().stream().anyMatch(b ->
bx >= (Math.floorDiv(b.minX(),16) - 1) * 16 && bx <= (Math.floorDiv(b.maxX(),16) + 2) * 16 - 1
&& bz >= (Math.floorDiv(b.minZ(),16) - 1) * 16 && bz <= (Math.floorDiv(b.maxZ(),16) + 2) * 16 - 1);
helper.assertTrue(state.isAir() || water || lava, "Lower margin must remain void except declared fluid falls: " + p);
continue;
}
if (RiftHydrologyRuntime.enabled(generator)) {
boolean water = state.getFluidState().is(FluidTags.WATER)
&& RiftDiagnostics.isWithinSpringInspectionFootprint(RiftHydrologyRuntime.riftPlan(helper.getLevel()), x, z);
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)
: LayeredHydrologyRuntime.enabled(generator)
? LayeredHydrologyDiagnostics.isWithinSpringInspectionFootprint(LayeredHydrologyRuntime.layeredPlan(helper.getLevel()), x, z)
: NaturalHydrologyDiagnostics.isWithinSpringInspectionFootprint(NaturalHydrologyRuntime.naturalPlan(helper.getLevel()), x, z)),
"The lower margin has no artificial floor; water must lie in the finite spring inspection footprint, with provenance checked by the full fluid diagnostic: " + p);
}
}
}
}
helper.succeed();
}
@@ -1,6 +1,22 @@
package fr.koka.sanctuary;
import fr.koka.sanctuary.worldgen.CavernIslandBiomeSource;
import fr.koka.sanctuary.worldgen.CavernGrovesFeature;
import fr.koka.sanctuary.worldgen.CavernDecorationsFeature;
import fr.koka.sanctuary.worldgen.CavernShoreSugarCaneFeature;
import fr.koka.sanctuary.worldgen.RiftIslandDensity;
import fr.koka.sanctuary.worldgen.RiftIslandBiomeSource;
import fr.koka.sanctuary.worldgen.RiftGrovesFeature;
import fr.koka.sanctuary.worldgen.RiftShoreSugarCaneFeature;
import fr.koka.sanctuary.worldgen.MainIslandDensity;
import fr.koka.sanctuary.worldgen.WoodlandIslandBiomeSource;
import fr.koka.sanctuary.worldgen.WoodlandShoreSugarCaneFeature;
import fr.koka.sanctuary.worldgen.WoodlandGrovesFeature;
import fr.koka.sanctuary.worldgen.NaturalShoreSugarCaneFeature;
import fr.koka.sanctuary.worldgen.TemperateIslandBiomeSource;
import fr.koka.sanctuary.worldgen.LayeredIslandBiomeSource;
import fr.koka.sanctuary.worldgen.LayeredShoreSugarCaneFeature;
import fr.koka.sanctuary.worldgen.LowerTerraceVegetationFeature;
import fr.koka.sanctuary.worldgen.ShoreSugarCaneFeature;
import net.fabricmc.api.ModInitializer;
import net.minecraft.core.Registry;
@@ -21,6 +37,22 @@ public final class SanctuaryMod implements ModInitializer {
public void onInitialize() {
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("main_island"), MainIslandDensity.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("shore_sugar_cane"), ShoreSugarCaneFeature.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("natural_shore_sugar_cane"), NaturalShoreSugarCaneFeature.CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("temperate_island"), TemperateIslandBiomeSource.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("layered_shore_sugar_cane"), LayeredShoreSugarCaneFeature.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("lower_terrace_vegetation"), LowerTerraceVegetationFeature.CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("layered_island"), LayeredIslandBiomeSource.CODEC);
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);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("cavern_island"), CavernIslandBiomeSource.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("cavern_groves"), CavernGrovesFeature.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("cavern_decorations"), CavernDecorationsFeature.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("cavern_shore_sugar_cane"), CavernShoreSugarCaneFeature.CODEC);
LOGGER.info("Sanctuary initialized: the Sanctuary world preset is available.");
}
}
@@ -0,0 +1,29 @@
package fr.koka.sanctuary.mixin;
import fr.koka.sanctuary.worldgen.CavernSpringOutlets;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.chunk.LevelChunk;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/** Finish only pending generation markers; completed saved chunks are never repaired on reload. */
@Mixin(LevelChunk.class)
public abstract class CavernSpringOutletMixin {
@Unique
private CavernSpringOutlets.Pending sanctuary$pendingCavernOutlets = CavernSpringOutlets.Pending.EMPTY;
@Inject(method = "postProcessGeneration", at = @At("HEAD"))
private void sanctuary$captureCavernOutlets(ServerLevel level, CallbackInfo callback) {
sanctuary$pendingCavernOutlets = CavernSpringOutlets.capturePending(level, (LevelChunk) (Object) this);
}
@Inject(method = "postProcessGeneration", at = @At("TAIL"))
private void sanctuary$finishCavernOutlets(ServerLevel level, CallbackInfo callback) {
var pending = sanctuary$pendingCavernOutlets;
sanctuary$pendingCavernOutlets = CavernSpringOutlets.Pending.EMPTY;
CavernSpringOutlets.finish(level, (LevelChunk) (Object) this, pending);
}
}
@@ -0,0 +1,29 @@
package fr.koka.sanctuary.mixin;
import fr.koka.sanctuary.worldgen.LayeredSpringOutlets;
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 LayeredSpringOutletMixin {
@Unique
private LayeredSpringOutlets.Pending sanctuary$pendingSpringOutlets = LayeredSpringOutlets.Pending.EMPTY;
@Inject(method = "postProcessGeneration", at = @At("HEAD"))
private void sanctuary$captureSpringOutlets(ServerLevel level, CallbackInfo callback) {
sanctuary$pendingSpringOutlets = LayeredSpringOutlets.capturePending(level, (LevelChunk) (Object) this);
}
@Inject(method = "postProcessGeneration", at = @At("TAIL"))
private void sanctuary$finishSpringOutlets(ServerLevel level, CallbackInfo callback) {
var pending = sanctuary$pendingSpringOutlets;
sanctuary$pendingSpringOutlets = LayeredSpringOutlets.Pending.EMPTY;
LayeredSpringOutlets.finish(level, (LevelChunk) (Object) this, pending);
}
}
@@ -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);
}
}
@@ -1,6 +1,11 @@
package fr.koka.sanctuary.mixin;
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 fr.koka.sanctuary.worldgen.CavernHydrologyRuntime;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import net.minecraft.core.Holder;
@@ -25,7 +30,32 @@ public abstract class SurfaceHydrologyMixin {
StructureManager structures, BiomeManager biomeManager, WorldGenRegion region,
Set<Holder<Biome>> biomes, CallbackInfoReturnable<CompletableFuture<ChunkAccess>> callback) {
var generator = (NoiseBasedChunkGenerator) (Object) this;
if (HydrologyRuntime.enabled(generator)) {
if (CavernHydrologyRuntime.enabled(generator)) {
callback.setReturnValue(callback.getReturnValue().thenApply(generated -> {
CavernHydrologyRuntime.apply(generator, randomState, generated);
return generated;
}));
} else if (RiftHydrologyRuntime.enabled(generator)) {
callback.setReturnValue(callback.getReturnValue().thenApply(generated -> {
RiftHydrologyRuntime.apply(generator, randomState, generated);
return generated;
}));
} else if (WoodlandHydrologyRuntime.enabled(generator)) {
callback.setReturnValue(callback.getReturnValue().thenApply(generated -> {
WoodlandHydrologyRuntime.apply(generator, randomState, generated);
return generated;
}));
} else if (LayeredHydrologyRuntime.enabled(generator)) {
callback.setReturnValue(callback.getReturnValue().thenApply(generated -> {
LayeredHydrologyRuntime.apply(generator, randomState, generated);
return generated;
}));
} else if (NaturalHydrologyRuntime.enabled(generator)) {
callback.setReturnValue(callback.getReturnValue().thenApply(generated -> {
NaturalHydrologyRuntime.apply(generator, randomState, generated);
return generated;
}));
} else if (HydrologyRuntime.enabled(generator)) {
callback.setReturnValue(callback.getReturnValue().thenApply(generated -> {
HydrologyRuntime.apply(generator, randomState, generated);
return generated;
@@ -0,0 +1,29 @@
package fr.koka.sanctuary.mixin;
import fr.koka.sanctuary.worldgen.WoodlandSpringOutlets;
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 WoodlandSpringOutletMixin {
@Unique
private WoodlandSpringOutlets.Pending sanctuary$pendingWoodlandOutlets = WoodlandSpringOutlets.Pending.EMPTY;
@Inject(method = "postProcessGeneration", at = @At("HEAD"))
private void sanctuary$captureWoodlandOutlets(ServerLevel level, CallbackInfo callback) {
sanctuary$pendingWoodlandOutlets = WoodlandSpringOutlets.capturePending(level, (LevelChunk) (Object) this);
}
@Inject(method = "postProcessGeneration", at = @At("TAIL"))
private void sanctuary$finishWoodlandOutlets(ServerLevel level, CallbackInfo callback) {
var pending = sanctuary$pendingWoodlandOutlets;
sanctuary$pendingWoodlandOutlets = WoodlandSpringOutlets.Pending.EMPTY;
WoodlandSpringOutlets.finish(level, (LevelChunk) (Object) this, pending);
}
}
@@ -0,0 +1,34 @@
package fr.koka.sanctuary.worldgen;
/** Alpha.9 keeps the upper woodland climate and specializes sheltered lower layers. */
public final class CavernBiomePalette {
public enum Kind {
OAK_FOREST, BIRCH_FOREST, CLEARING, DRY_WOODLAND, ROCKY_HEATH,
DARK_GROVE, BAMBOO_GROVE, SULFUR_DEPTHS, LUSH_CAVES, DRIPSTONE_CAVES
}
private CavernBiomePalette() {}
public static Kind select(int blockY, float variation, float moisture) {
double elevation = blockY - Math.clamp(variation, -1.0F, 1.0F) * 16
- Math.clamp(moisture, -1.0F, 1.0F) * 10;
// Small climate pockets replace alpha.8's broad sulfur province. Neither
// mineral pockets nor cave vegetation can take over the upper forest.
if (blockY < 176 && elevation < 168 && variation > 0.22F && variation < 0.42F
&& moisture > -0.42F && moisture < -0.12F) return Kind.SULFUR_DEPTHS;
if (variation > 0.48F) return Kind.ROCKY_HEATH;
if (blockY < 184 && elevation < 180) {
if (moisture > 0.05F && variation > -0.32F) return Kind.LUSH_CAVES;
if (moisture < -0.18F && variation < 0.22F) return Kind.DRIPSTONE_CAVES;
}
if (elevation < 214) {
if (variation < -0.45F && moisture > 0.40F) return Kind.BAMBOO_GROVE;
double shelteredMoisture = moisture + Math.min(0.55, (214 - elevation) / 160);
if (shelteredMoisture > -0.08) return Kind.DARK_GROVE;
}
if (moisture < -0.55F && variation > -0.25F) return Kind.DRY_WOODLAND;
if (Math.abs(variation) < 0.09F && moisture < 0.35F) return Kind.CLEARING;
if (variation < -0.33F) return Kind.BIRCH_FOREST;
return Kind.OAK_FOREST;
}
}
@@ -0,0 +1,296 @@
package fr.koka.sanctuary.worldgen;
import com.mojang.serialization.MapCodec;
import fr.koka.sanctuary.SanctuaryMod;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.tags.BlockTags;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.AmethystClusterBlock;
import net.minecraft.world.level.block.CaveVines;
import net.minecraft.world.level.block.GrowingPlantHeadBlock;
import net.minecraft.world.level.block.PotentSulfurBlock;
import net.minecraft.world.level.block.SpeleothemBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.PotentSulfurState;
import net.minecraft.world.level.block.state.properties.SpeleothemThickness;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.feature.Feature;
import net.minecraft.world.level.levelgen.feature.GeodeFeature;
/** Decorations in existing lower cavities only. All writes belong to the owner's chunk. */
public final class CavernDecorationsFeature implements Feature {
public static final MapCodec<CavernDecorationsFeature> CODEC = MapCodec.unit(CavernDecorationsFeature::new);
private static final ResourceKey<Feature> GEODE = ResourceKey.create(Registries.FEATURE, SanctuaryMod.id("cavern_amethyst_geode"));
private static final Direction[] SIDES = {Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST};
public enum Kind { LUSH, DRIPSTONE, SULFUR, GEODE }
public record Candidate(Kind kind, BlockPos floor, BlockPos ceiling) {
public Candidate { floor = floor.immutable(); ceiling = ceiling.immutable(); }
}
@Override public MapCodec<CavernDecorationsFeature> codec() { return CODEC; }
/** Pure, bounded probes for development diagnostics; no chunk loads or successful-placement promise. */
public static List<Candidate> candidates(NoiseBasedChunkGenerator generator, RandomState state, int chunkX, int chunkZ) {
if (!CavernHydrologyRuntime.enabled(generator) || Math.abs(chunkX) > 17 || Math.abs(chunkZ) > 17) return List.of();
var density = generator.generatorSettings().value().noiseRouter().finalDensity();
WoodlandGroveGeometry.Sampler sampler = (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z);
var biomes = generator.getBiomeSource().createUncachedResolver(state);
List<Candidate> found = new ArrayList<>();
for (int[] offset : new int[][]{{4, 4}, {4, 12}, {12, 4}, {12, 12}, {8, 8}}) {
int x = chunkX * 16 + offset[0], z = chunkZ * 16 + offset[1], floors = 0;
for (int y = 182; y >= 40; y--) {
if (sampler.sample(x, y, z) <= 0 || sampler.sample(x, y + 1, z) > 0
|| sampler.sample(x, y + 2, z) > 0 || sampler.sample(x, y - 1, z) <= 0
|| sampler.sample(x, y - 2, z) <= 0) continue;
int roof = y + 3;
while (roof <= Math.min(230, y + 48) && sampler.sample(x, roof, z) <= 0) roof++;
if (roof > Math.min(230, y + 48) || roof - y < 6
|| sampler.sample(x, roof + 1, z) <= 0) continue;
BlockPos floor = new BlockPos(x, y, z), ceiling = new BlockPos(x, roof, z);
var biome = biomes.getNoiseBiome(x >> 2, (y + 1) >> 2, z >> 2);
if (biome.is(CavernIslandBiomeSource.LUSH_CAVES)) found.add(new Candidate(Kind.LUSH, floor, ceiling));
else if (biome.is(CavernIslandBiomeSource.DRIPSTONE_CAVES)) found.add(new Candidate(Kind.DRIPSTONE, floor, ceiling));
else if (biome.is(CavernIslandBiomeSource.SULFUR_DEPTHS) && y < 176)
found.add(new Candidate(Kind.SULFUR, floor, ceiling));
if (offset[0] == 8 && offset[1] == 8 && Math.floorMod(chunkSeed(state.seed(), chunkX, chunkZ, 0x9501L), 8) == 0)
found.add(new Candidate(Kind.GEODE, floor, ceiling));
if (++floors >= 3) break;
}
}
// Geodes keep their entry free before this feature's moss, grass or pointed
// dripstone can occupy it. Stable sorting preserves every other candidate order.
found.sort(java.util.Comparator.comparingInt(candidate -> candidate.kind() == Kind.GEODE ? 0 : 1));
return List.copyOf(found);
}
@Override public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource ignored, BlockPos origin) {
if (!(generator instanceof NoiseBasedChunkGenerator noise) || !CavernHydrologyRuntime.enabled(noise)) return false;
var state = level.getLevel().getChunkSource().randomState();
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
WoodlandGroveGeometry.Sampler sampler = (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z);
int cx = origin.getX() >> 4, cz = origin.getZ() >> 4;
boolean changed = false, geyser = false, geode = false;
for (Candidate candidate : candidates(noise, state, cx, cz)) {
BlockPos floor = candidate.floor();
if (!stable(level, sampler, floor) || !level.getBlockState(floor.above()).isAir()) continue;
long seed = WoodlandGroveGeometry.seed(state.seed(), floor.getX(), floor.getY(), floor.getZ(), 0x9502L);
switch (candidate.kind()) {
case LUSH -> changed |= lush(level, noise, state, sampler, candidate, seed);
case DRIPSTONE -> changed |= dripstone(level, noise, state, sampler, candidate, seed);
case SULFUR -> {
if (!geyser && Math.floorMod(chunkSeed(state.seed(), cx, cz, 0x9503L), 2) == 0)
changed |= geyser = geyser(level, noise, state, sampler, candidate);
}
case GEODE -> { if (!geode) changed |= geode = geode(level, noise, state, sampler, candidate, seed); }
}
}
return changed;
}
private boolean lush(WorldGenLevel level, NoiseBasedChunkGenerator noise, RandomState state,
WoodlandGroveGeometry.Sampler sampler, Candidate site, long seed) {
BlockPos floor = site.floor(), roof = site.ceiling();
int gap = roof.getY() - floor.getY();
if (CavernHydrologyRuntime.protects(noise, state, floor, 3, gap + 2, 3)) return false;
boolean changed = false;
for (int dx = -2; dx <= 2; dx++) for (int dz = -2; dz <= 2; dz++) {
if (dx * dx + dz * dz > 5) continue;
BlockPos p = floor.offset(dx, 0, dz);
if (!stable(level, sampler, p) || !stable(level, sampler, p.below()) || !stable(level, sampler, p.below(2))
|| !level.getBlockState(p.above()).isAir()) continue;
setBlock(level, p, Blocks.MOSS_BLOCK.defaultBlockState());
long local = WoodlandGroveGeometry.seed(seed, p.getX(), p.getY(), p.getZ(), 0x9504L);
BlockState plant = (Math.floorMod(local, 5) == 0 ? Blocks.AZALEA : Math.floorMod(local, 3) == 0
? Blocks.MOSS_CARPET : Blocks.SHORT_GRASS).defaultBlockState();
if (plant.canSurvive(level, p.above())) setBlock(level, p.above(), plant);
changed = true;
}
// Real cave-vine heads and bodies remain harvestable and luminous via BERRIES.
int length = Math.min(gap - 3, 3 + Math.floorMod(seed, 6));
if (stable(level, sampler, roof) && stable(level, sampler, roof.above())) {
boolean free = true;
for (int d = 1; d <= length; d++) free &= level.getBlockState(roof.below(d)).isAir();
if (free) for (int d = 1; d <= length; d++) {
BlockPos p = roof.below(d);
BlockState vine = (d == length ? Blocks.CAVE_VINES : Blocks.CAVE_VINES_PLANT).defaultBlockState()
.setValue(CaveVines.BERRIES, d == length || Math.floorMod(seed + d, 3) == 0);
if (d == length) vine = vine.setValue(GrowingPlantHeadBlock.AGE, 25);
if (!vine.canSurvive(level, p)) break;
setBlock(level, p, vine); changed = true;
}
}
return changed;
}
private boolean dripstone(WorldGenLevel level, NoiseBasedChunkGenerator noise, RandomState state,
WoodlandGroveGeometry.Sampler sampler, Candidate site, long seed) {
BlockPos floor = site.floor(), roof = site.ceiling();
int gap = roof.getY() - floor.getY();
if (CavernHydrologyRuntime.protects(noise, state, floor, 1, gap + 2, 3)) return false;
int length = Math.min((gap - 3) / 2, 1 + Math.floorMod(seed, 4));
boolean changed = spike(level, sampler, floor, Direction.UP, length);
changed |= spike(level, sampler, roof, Direction.DOWN, length);
return changed;
}
private boolean spike(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler, BlockPos support, Direction direction, int length) {
if (length < 1 || !stable(level, sampler, support) || !stable(level, sampler, support.relative(direction.getOpposite()))) return false;
for (int d = 1; d <= length; d++) {
BlockPos p = support.relative(direction, d);
if (!level.getBlockState(p).isAir() || sampler.sample(p.getX(), p.getY(), p.getZ()) > 0) return false;
}
setBlock(level, support, Blocks.DRIPSTONE_BLOCK.defaultBlockState());
for (int d = 1; d <= length; d++) {
SpeleothemThickness thickness = d == length ? SpeleothemThickness.TIP
: d == length - 1 ? SpeleothemThickness.FRUSTUM : d == 1 ? SpeleothemThickness.BASE : SpeleothemThickness.MIDDLE;
BlockState pointed = Blocks.POINTED_DRIPSTONE.defaultBlockState().setValue(SpeleothemBlock.TIP_DIRECTION, direction)
.setValue(SpeleothemBlock.THICKNESS, thickness);
setBlock(level, support.relative(direction, d), pointed);
}
return true;
}
private boolean geyser(WorldGenLevel level, NoiseBasedChunkGenerator noise, RandomState state,
WoodlandGroveGeometry.Sampler sampler, Candidate site) {
BlockPos water = site.floor(), potent = water.below(), heat = water.below(2);
if (site.ceiling().getY() - water.getY() < 8
|| CavernHydrologyRuntime.protects(noise, state, water, 2, 8, 5)) return false;
for (int d = 0; d <= 4; d++) if (!stable(level, sampler, water.below(d))) return false;
for (Direction side : SIDES) for (int d = 0; d <= 2; d++)
if (!stable(level, sampler, water.below(d).relative(side))) return false;
for (int d = 1; d <= 7; d++) if (!level.getBlockState(water.above(d)).isAir()) return false;
// One source, four original rock walls, original deep support. Vanilla's
// periodic geyser ticker requires magma below potent sulfur and source water above.
setBlock(level, heat, Blocks.MAGMA_BLOCK.defaultBlockState());
setBlock(level, water, Blocks.WATER.defaultBlockState());
setBlock(level, potent, Blocks.POTENT_SULFUR.defaultBlockState().setValue(PotentSulfurBlock.STATE, PotentSulfurState.DORMANT));
level.getChunk(water).markPosForPostProcessing(water);
level.getChunk(potent).markPosForPostProcessing(potent);
return true;
}
private boolean geode(WorldGenLevel level, NoiseBasedChunkGenerator noise, RandomState state,
WoodlandGroveGeometry.Sampler sampler, Candidate site, long seed) {
BlockPos origin = site.floor().offset(-1, -9, -1);
// Include the vanilla write range, its crystal neighbours and the entrance.
// This asymmetric envelope is centred explicitly so its positive edge is protected too.
if (origin.getY() - 6 < 24 || !level.getBlockState(site.floor().above(2)).isAir()
|| CavernHydrologyRuntime.protects(noise, state, origin.offset(1, -6, 1), 7, 17, 0)) return false;
Map<BlockPos, BlockState> before = new LinkedHashMap<>();
for (int dx = -6; dx <= 8; dx++) for (int dz = -6; dz <= 8; dz++) for (int dy = -6; dy <= 11; dy++) {
BlockPos p = origin.offset(dx, dy, dz);
BlockState original = level.getBlockState(p);
// No block-entity data or fluid scheduler state needs to be restored.
if (original.hasBlockEntity() || !original.getFluidState().isEmpty()) return false;
before.put(p, original);
}
Feature feature = level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(GEODE).value();
if (!(feature instanceof GeodeFeature geode) || geode.minGenOffset() != -5 || geode.maxGenOffset() != 7) return false;
boolean accepted = false;
try {
if (!geode.place(level, noise, RandomSource.create(seed), origin) || !naturalGeodeChanges(level, sampler, before)) return false;
var entrance = geodeEntrance(level, sampler, site.floor().above(), origin, before);
if (entrance.isEmpty()) return false;
for (BlockPos p : entrance) if (!level.getBlockState(p).isAir()) setBlock(level, p, Blocks.AIR.defaultBlockState());
if (!naturalGeodeChanges(level, sampler, before)) return false;
accepted = true;
return true;
} finally {
if (!accepted) before.forEach((p, original) -> {
if (!level.getBlockState(p).equals(original)) setBlock(level, p, original);
});
}
}
/** Test the actual vanilla mask, not unused bounding-box corners. A geode may
* excavate original rock but cannot fill original air or remove earlier decoration. */
private static boolean naturalGeodeChanges(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler,
Map<BlockPos, BlockState> before) {
for (var entry : before.entrySet()) {
BlockPos p = entry.getKey();
BlockState original = entry.getValue(), current = level.getBlockState(p);
if (original.equals(current)) continue;
if (!carvableOriginal(level, sampler, p, original)) return false;
}
return true;
}
private static boolean carvableOriginal(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler,
BlockPos p, BlockState original) {
return original != null && original.isCollisionShapeFullBlock(level, p)
&& !original.hasBlockEntity() && original.getFluidState().isEmpty()
&& !original.is(BlockTags.LOGS) && !original.is(BlockTags.LEAVES)
&& !original.is(Blocks.BUDDING_AMETHYST) && !original.is(Blocks.MAGMA_BLOCK)
&& sampler.sample(p.getX(), p.getY(), p.getZ()) > 0;
}
private static List<BlockPos> geodeEntrance(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler,
BlockPos start, BlockPos origin, Map<BlockPos, BlockState> before) {
var previous = new java.util.HashMap<BlockPos, BlockPos>();
var distances = new java.util.HashMap<BlockPos, Integer>();
var queue = new java.util.ArrayDeque<BlockPos>();
queue.add(start); distances.put(start, 0);
BlockPos destination = null;
while (!queue.isEmpty()) {
BlockPos p = queue.removeFirst();
int distance = distances.get(p);
boolean adjacentBudding = false;
for (Direction direction : Direction.values())
adjacentBudding |= level.getBlockState(p.relative(direction)).is(Blocks.BUDDING_AMETHYST)
|| level.getBlockState(p.above().relative(direction)).is(Blocks.BUDDING_AMETHYST);
if (p.getY() <= start.getY() - 3 && level.getBlockState(p).isAir()
&& level.getBlockState(p.above()).isAir() && adjacentBudding) { destination = p; break; }
if (distance >= 16) continue;
for (Direction direction : Direction.values()) {
BlockPos next = p.relative(direction);
if (Math.abs(next.getX() - start.getX()) > 6 || Math.abs(next.getZ() - start.getZ()) > 6
|| next.getY() < origin.getY() - 5 || next.getY() > start.getY()
|| distances.containsKey(next) || !before.containsKey(next) || !before.containsKey(next.above())) continue;
if (!entranceBlock(level, sampler, next, before) || !entranceBlock(level, sampler, next.above(), before)) continue;
distances.put(next, distance + 1); previous.put(next, p); queue.addLast(next);
}
}
if (destination == null) return List.of();
var passage = new java.util.LinkedHashSet<BlockPos>();
for (BlockPos p = destination; p != null; p = previous.get(p)) { passage.add(p); passage.add(p.above()); }
return List.copyOf(passage);
}
private static boolean entranceBlock(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler,
BlockPos p, Map<BlockPos, BlockState> before) {
BlockState current = level.getBlockState(p);
if (current.isAir()) return true;
// Keep every budding block and attached crystal. Search around them instead.
if (current.is(Blocks.BUDDING_AMETHYST) || current.is(Blocks.AMETHYST_CLUSTER)
|| current.is(Blocks.SMALL_AMETHYST_BUD) || current.is(Blocks.MEDIUM_AMETHYST_BUD)
|| current.is(Blocks.LARGE_AMETHYST_BUD)) return false;
for (Direction direction : Direction.values()) {
BlockState neighbour = level.getBlockState(p.relative(direction));
if (neighbour.getBlock() instanceof AmethystClusterBlock
&& neighbour.getValue(AmethystClusterBlock.FACING) == direction) return false;
}
return carvableOriginal(level, sampler, p, before.get(p));
}
private static boolean stable(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler, BlockPos p) {
BlockState state = level.getBlockState(p);
return sampler.sample(p.getX(), p.getY(), p.getZ()) > 0 && state.isCollisionShapeFullBlock(level, p)
&& state.getFluidState().isEmpty() && !state.hasBlockEntity() && !state.is(BlockTags.LOGS)
&& !state.is(BlockTags.LEAVES) && !state.is(Blocks.BUDDING_AMETHYST) && !state.is(Blocks.MAGMA_BLOCK);
}
private static long chunkSeed(long seed, int x, int z, long salt) {
return WoodlandGroveGeometry.seed(seed, x * 16, 0, z * 16, salt);
}
}
@@ -0,0 +1,296 @@
package fr.koka.sanctuary.worldgen;
import com.mojang.serialization.MapCodec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Map;
import java.util.Optional;
import java.util.WeakHashMap;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.data.worldgen.features.TreeFeatures;
import net.minecraft.data.worldgen.features.VegetationFeatures;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.tags.BlockTags;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.VegetationBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.feature.Feature;
import net.minecraft.world.level.levelgen.feature.AbstractHugeMushroomFeature;
/** Alpha.9: sheltered vanilla mushroom trees and dark oak on natural lower floors,
* plus one globally selected remarkable tree.
* Placement runs only in new generation. No world journal or mutable placement counter is retained. */
public final class CavernGrovesFeature implements Feature {
public static final MapCodec<CavernGrovesFeature> CODEC = MapCodec.unit(CavernGrovesFeature::new);
private static final Map<RandomState, Optional<RemarkableTree>> REMARKABLE =
Collections.synchronizedMap(new WeakHashMap<>());
public record RemarkableTree(BlockPos base, String featureId, int radius, int height) {
public RemarkableTree { base = base.immutable(); }
}
private record Species(ResourceKey<Feature> key, int width, int radius, int height) {}
private record Candidate(int x, int z, long rank) {}
@FunctionalInterface
private interface Protection {
boolean intersects(BlockPos base, int radius, int height, int rootDepth);
}
private static final Species CHERRY = new Species(TreeFeatures.CHERRY, 1, 9, 15);
private static final Species[] SPECIES = {CHERRY, CHERRY, CHERRY, CHERRY,
new Species(TreeFeatures.SPRUCE, 1, 5, 15), new Species(TreeFeatures.JUNGLE_TREE_NO_VINE, 1, 5, 14),
new Species(TreeFeatures.ACACIA, 1, 7, 14), new Species(TreeFeatures.MANGROVE, 1, 10, 20),
new Species(TreeFeatures.PALE_OAK_BONEMEAL, 2, 6, 12)};
@Override public MapCodec<CavernGrovesFeature> codec() { return CODEC; }
public static Optional<RemarkableTree> remarkablePlan(ServerLevel level) {
if (!(level.getChunkSource().getGenerator() instanceof NoiseBasedChunkGenerator noise)) return Optional.empty();
return remarkablePlan(noise, level.getChunkSource().randomState());
}
public static Optional<RemarkableTree> remarkablePlan(NoiseBasedChunkGenerator noise, RandomState state) {
if (!CavernHydrologyRuntime.enabled(noise)) return Optional.empty();
return REMARKABLE.computeIfAbsent(state, ignored -> {
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
WoodlandGroveGeometry.Sampler sampler = (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z);
Protection protection = (base, radius, height, rootDepth) ->
CavernHydrologyRuntime.protects(noise, state, base, radius, height, rootDepth);
Species species = SPECIES[Math.floorMod(WoodlandGroveGeometry.seed(state.seed(), 0, 0, 0, 0x70EEL), SPECIES.length)];
var candidates = new ArrayList<Candidate>();
for (int x = -152; x <= 152; x += 16) for (int z = -152; z <= 152; z += 16) {
if (Math.hypot(x, z) <= 168) candidates.add(new Candidate(x, z,
WoodlandGroveGeometry.seed(state.seed(), x, 0, z, 0x71AFL)));
}
candidates.sort(Comparator.comparingLong(Candidate::rank).thenComparingInt(Candidate::x).thenComparingInt(Candidate::z));
for (Candidate candidate : candidates) {
int bed = WoodlandGroveGeometry.highestSurface(sampler, candidate.x(), candidate.z());
if (!WoodlandGroveGeometry.naturalFootprint(sampler, candidate.x(), bed, candidate.z(), species.width())) continue;
BlockPos base = new BlockPos(candidate.x(), bed + 1, candidate.z());
if (blocked(protection, base, species.radius() + 1, species.height(), rootDepth(species))) continue;
if (!WoodlandGroveGeometry.treeRoom(sampler, base.getX(), base.getY(), base.getZ(),
species.width(), species.radius(), species.height())) continue;
return Optional.of(new RemarkableTree(base, species.key().identifier().toString(), species.radius(), species.height()));
}
return Optional.empty();
});
}
@Override
public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource ignored, BlockPos origin) {
if (!(generator instanceof NoiseBasedChunkGenerator noise) || !CavernHydrologyRuntime.enabled(noise)) return false;
int minX = (origin.getX() >> 4) * 16, minZ = (origin.getZ() >> 4) * 16;
if (Math.abs((long) minX) > 272 || Math.abs((long) minZ) > 272) return false;
var state = level.getLevel().getChunkSource().randomState();
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
WoodlandGroveGeometry.Sampler sampler = (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z);
Protection protection = (base, radius, height, rootDepth) ->
CavernHydrologyRuntime.protects(noise, state, base, radius, height, rootDepth);
boolean changed = false;
int[] heights = new int[256];
Arrays.fill(heights, Integer.MIN_VALUE);
var remarkable = remarkablePlan(noise, state);
if (remarkable.isPresent()) {
var tree = remarkable.get();
BlockPos base = tree.base();
if ((base.getX() >> 4) == (minX >> 4) && (base.getZ() >> 4) == (minZ >> 4)) {
for (Species species : SPECIES) if (species.key().identifier().toString().equals(tree.featureId())) {
changed |= tree(level, generator, sampler, state.seed(), base, species, protection, 0x723BL);
break;
}
}
}
// The scarce tall rooms usually go to dark oak first; one quarter of chunks
// give mushroom trees first choice. This changes priority, never attempt budgets
// or natural fit requirements, and uses a separate deterministic chunk seed.
boolean mushroomsFirst = Math.floorMod(WoodlandGroveGeometry.seed(state.seed(), minX, 0, minZ, 0x8503L), 4) == 0;
if (mushroomsFirst) changed |= mushroomTrees(level, generator, sampler, state.seed(), protection, minX, minZ, heights);
// Four spaced origins per chunk; the 2x2 trunk and full canopy still have to fit the cave.
for (int x = minX + 4; x < minX + 16; x += 8) for (int z = minZ + 4; z < minZ + 16; z += 8) {
int highest = localTop(level, sampler, protection, minX, minZ, x, z, heights), placed = 0;
for (int bed = highest - 8; bed >= 40; bed--) {
BlockPos base = new BlockPos(x, bed + 1, z);
if (!ground(level, base.below()) || !level.getBlockState(base).isAir()) continue;
var biome = level.getBiome(base);
boolean dark = biome.is(CavernIslandBiomeSource.DARK_GROVE);
boolean bamboo = biome.is(CavernIslandBiomeSource.BAMBOO_GROVE);
if (!dark && !bamboo) continue;
long placementSeed = WoodlandGroveGeometry.seed(state.seed(), x, bed, z, 0x730DL);
if (dark) {
if (tree(level, generator, sampler, state.seed(), base,
new Species(TreeFeatures.DARK_OAK, 2, 6, 12), protection, 0x730DL)) { changed = true; placed++; }
} else if (Math.floorMod(placementSeed, 8) == 0) {
if (tree(level, generator, sampler, state.seed(), base,
new Species(TreeFeatures.JUNGLE_TREE_NO_VINE, 1, 5, 14), protection, 0x735BL)) { changed = true; placed++; }
} else if (Math.floorMod(placementSeed, 3) == 0
&& WoodlandGroveGeometry.naturalFootprint(sampler, x, bed, z, 1)
&& WoodlandGroveGeometry.treeRoom(sampler, x, bed + 1, z, 1, 3, 16)
&& !blocked(protection, base, 4, 17) && actualRoom(level, base, 1, 3, 16)) {
setBlock(level, base.below(), Blocks.PODZOL.defaultBlockState());
changed |= level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(VegetationFeatures.BAMBOO_NO_PODZOL)
.value().place(level, generator, RandomSource.create(placementSeed), base);
placed++;
}
if (placed >= 2) break;
}
}
if (!mushroomsFirst) changed |= mushroomTrees(level, generator, sampler, state.seed(), protection, minX, minZ, heights);
// Coherent ground patches: about 40% moss, 40% podzol, 20% mycelium.
// Small mushrooms are accents (one opportunity per hundred columns), not the canopy.
for (int x = minX; x < minX + 16; x++) for (int z = minZ; z < minZ + 16; z++) {
int highest = localTop(level, sampler, protection, minX, minZ, x, z, heights), floors = 0;
long detail = WoodlandGroveGeometry.seed(state.seed(), x, 0, z, 0x843EL);
boolean smallMushroomPlaced = false;
for (int bed = highest - 8; bed >= 32; bed--) {
BlockPos floor = new BlockPos(x, bed, z), plant = floor.above();
if (!ground(level, floor) || !level.getBlockState(plant).isAir()) continue;
var biome = level.getBiome(plant);
if (!biome.is(CavernIslandBiomeSource.DARK_GROVE) && !biome.is(CavernIslandBiomeSource.BAMBOO_GROVE)) continue;
if (!WoodlandGroveGeometry.naturalFootprint(sampler, x, bed, z, 1) || blocked(protection, plant, 1, 2)) continue;
long patch = WoodlandGroveGeometry.seed(state.seed(), Math.floorDiv(x, 7), 0, Math.floorDiv(z, 7), 0x742DL);
int soilKind = Math.floorMod(patch, 5);
BlockState soil = (soilKind < 2 ? Blocks.MOSS_BLOCK : soilKind < 4 ? Blocks.PODZOL : Blocks.MYCELIUM).defaultBlockState();
setBlock(level, floor, soil);
if (!smallMushroomPlaced && Math.floorMod(detail, 100) == 0) {
BlockState mushroom = (Math.floorMod(detail >>> 8, 2) == 0 ? Blocks.BROWN_MUSHROOM : Blocks.RED_MUSHROOM).defaultBlockState();
if (mushroom.canSurvive(level, plant)) {
setBlock(level, plant, mushroom);
smallMushroomPlaced = true;
}
}
changed = true;
if (++floors >= 3) break;
}
}
return changed;
}
private boolean mushroomTrees(WorldGenLevel level, ChunkGenerator generator, WoodlandGroveGeometry.Sampler sampler,
long seed, Protection protection, int minX, int minZ, int[] heights) {
boolean changed = false;
RandomSource mushroomSites = RandomSource.create(WoodlandGroveGeometry.seed(seed, minX, 0, minZ, 0x8501L));
int mushroomAttempts = 1 + mushroomSites.nextInt(3);
for (int attempt = 0; attempt < mushroomAttempts; attempt++) {
int x = minX + 3 + mushroomSites.nextInt(10), z = minZ + 3 + mushroomSites.nextInt(10);
int highest = localTop(level, sampler, protection, minX, minZ, x, z, heights);
for (int bed = highest - 8; bed >= 32; bed--) {
BlockPos base = new BlockPos(x, bed + 1, z);
if (!ground(level, base.below()) || !level.getBlockState(base).isAir()
|| !level.getBiome(base).is(CavernIslandBiomeSource.DARK_GROVE)) continue;
long mushroomSeed = WoodlandGroveGeometry.seed(seed, x, bed, z, 0x8502L + attempt);
var key = Math.floorMod(mushroomSeed, 2) == 0 ? TreeFeatures.HUGE_BROWN_MUSHROOM : TreeFeatures.HUGE_RED_MUSHROOM;
Feature feature = level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(key).value();
if (!(feature instanceof AbstractHugeMushroomFeature mushroom)) break;
int height = mushroom.getTreeHeight(RandomSource.create(mushroomSeed));
int radius = mushroom.foliageRadius();
if (!mushroomRoom(level, sampler, protection, base, radius, height)) continue;
BlockState originalFloor = level.getBlockState(base.below());
setBlock(level, base.below(), (Math.floorMod(mushroomSeed, 5) == 0 ? Blocks.MYCELIUM : Blocks.PODZOL).defaultBlockState());
// Vanilla consumes its height draw first. Replaying the same random seed
// gives precisely the height whose natural envelope was checked above.
boolean placed = mushroom.place(level, generator, RandomSource.create(mushroomSeed), base);
if (!placed) setBlock(level, base.below(), originalFloor);
changed |= placed;
break;
}
}
return changed;
}
private boolean tree(WorldGenLevel level, ChunkGenerator generator, WoodlandGroveGeometry.Sampler sampler, long seed,
BlockPos base, Species species, Protection protection, long salt) {
if (blocked(protection, base, species.radius() + 1, species.height(), rootDepth(species))
|| !WoodlandGroveGeometry.naturalFootprint(sampler, base.getX(), base.getY() - 1, base.getZ(), species.width())
|| !actualRoom(level, base, species.width(), species.radius(), species.height())
|| !WoodlandGroveGeometry.treeRoom(sampler, base.getX(), base.getY(), base.getZ(), species.width(), species.radius(), species.height())) return false;
for (int dx = 0; dx < species.width(); dx++) for (int dz = 0; dz < species.width(); dz++) {
BlockPos floor = base.offset(dx, -1, dz);
if (!ground(level, floor)) return false;
}
for (int dx = 0; dx < species.width(); dx++) for (int dz = 0; dz < species.width(); dz++) {
setBlock(level, base.offset(dx, -1, dz), Blocks.PODZOL.defaultBlockState());
}
return level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(species.key()).value()
.place(level, generator, RandomSource.create(WoodlandGroveGeometry.seed(seed, base.getX(), base.getY(), base.getZ(), salt)), base);
}
private static boolean actualRoom(WorldGenLevel level, BlockPos base, int width, int radius, int height) {
for (int y = 0; y <= height; y++) {
int from = y < 3 ? 0 : -radius, to = y < 3 ? width - 1 : radius;
for (int dx = from; dx <= to; dx++) for (int dz = from; dz <= to; dz++) {
BlockState current = level.getBlockState(base.offset(dx, y, dz));
if (!current.getFluidState().isEmpty() || !(current.isAir() || current.is(BlockTags.LEAVES)
|| current.getBlock() instanceof VegetationBlock)) return false;
}
}
return true;
}
private static boolean ground(WorldGenLevel level, BlockPos floor) {
BlockState state = level.getBlockState(floor);
if (state.is(Blocks.DEEPSLATE)) {
// Deep humid ledges can support organic soil. Check every tree foot rather
// than admitting this rock globally, including for the remarkable tree.
var biome = level.getBiome(floor.above());
return biome.is(CavernIslandBiomeSource.DARK_GROVE) || biome.is(CavernIslandBiomeSource.BAMBOO_GROVE);
}
return state.is(Blocks.STONE) || state.is(Blocks.DIRT) || state.is(Blocks.GRASS_BLOCK) || state.is(Blocks.PODZOL)
|| state.is(Blocks.MYCELIUM) || state.is(Blocks.ROOTED_DIRT) || state.is(Blocks.COARSE_DIRT)
|| state.is(Blocks.GRANITE) || state.is(Blocks.DIORITE) || state.is(Blocks.ANDESITE) || state.is(Blocks.TUFF)
|| state.is(Blocks.COBBLESTONE) || state.is(Blocks.MOSSY_COBBLESTONE) || state.is(Blocks.PACKED_MUD);
}
private static int localTop(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler, Protection protection,
int minX, int minZ, int x, int z, int[] heights) {
int index = (x - minX) * 16 + z - minZ;
if (heights[index] != Integer.MIN_VALUE) return heights[index];
// In an untouched column the heightmap bounds the original surface cheaply.
// A protected water/lava column may have been carved: recover its raw top
// without depending on the current hydrology plan's representation.
int ceiling = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z) - 1;
if (protection.intersects(new BlockPos(x, level.getMinY(), z), 0, level.getHeight() - 1, 0))
ceiling = level.getMaxY();
return heights[index] = WoodlandGroveGeometry.highestSurfaceBelow(sampler, x, z, ceiling);
}
private static boolean mushroomRoom(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler, Protection protection,
BlockPos base, int radius, int height) {
if (blocked(protection, base, radius + 1, height + 1, 4)
|| !WoodlandGroveGeometry.naturalFootprint(sampler, base.getX(), base.getY() - 1, base.getZ(), 1)) return false;
for (int depth = 1; depth <= 3; depth++) {
BlockPos floor = base.below(depth);
if (!level.getBlockState(floor).isCollisionShapeFullBlock(level, floor)) return false;
}
// A complete conservative column around the cap handles both brown umbrellas
// and the lower rounded red cap. Neither natural rock nor existing logs move.
for (int y = 0; y <= height + 1; y++) for (int dx = -radius; dx <= radius; dx++) for (int dz = -radius; dz <= radius; dz++) {
BlockPos p = base.offset(dx, y, dz);
BlockState current = level.getBlockState(p);
if (sampler.sample(p.getX(), p.getY(), p.getZ()) > 0 || !current.getFluidState().isEmpty()
|| !(current.isAir() || current.is(BlockTags.LEAVES))) return false;
}
// Being below another surface is not enough: the same column needs an actual
// original rock ceiling above the whole mushroom, not an open hillside.
for (int y = base.getY() + height + 2; y <= level.getMaxY(); y++)
if (sampler.sample(base.getX(), y, base.getZ()) > 0) return true;
return false;
}
private static int rootDepth(Species species) { return species.key().equals(TreeFeatures.MANGROVE) ? 16 : 4; }
private static boolean blocked(Protection protection, BlockPos base, int radius, int height) {
return blocked(protection, base, radius, height, 4);
}
private static boolean blocked(Protection protection, BlockPos base, int radius, int height, int rootDepth) {
return protection.intersects(base, radius, height, rootDepth);
}
}
@@ -0,0 +1,966 @@
package fr.koka.sanctuary.worldgen;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Set;
/** Alpha.9 river catchments, wider basins and rocky wall springs.
* This version is separate from NaturalHydrology so previously created worlds keep their original terrain.
* Results retain no chunks, sampler, random state or Minecraft objects. */
public final class CavernHydrology {
public static final int RADIUS = 192;
public static final int MAX_CARVE = 16;
public static final int MAX_DEPTH = 8;
public static final int MAX_LAKE_CARVE = 12;
public static final int MAX_RIVER_BANK_CARVE = 12;
private static final int GRID = 8;
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
private static final int[][] NEIGHBORS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}};
private CavernHydrology() {}
@FunctionalInterface
public interface Sampler { float sample(int x, int y, int z); }
public enum Kind { POND, LAKE, RIVER, TERRACE }
public enum ShoreMaterial { SAND, GRAVEL, CLAY, STONE, GRASS }
public record Point(int x, int z) {}
public record Bounds(int minX, int minZ, int maxX, int maxZ) {}
public record Position(int x, int y, int z) {}
public record FlowBounds(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {}
public record Spring(long id, Position source, Position outlet, List<Position> flowPath, FlowBounds flowBounds) {
public Spring { flowPath = List.copyOf(flowPath); }
}
public record Terrace(long id, List<Long> basinIds) {
public Terrace { basinIds = List.copyOf(basinIds); }
}
/** A retained upper basin supplies this outlet. toFeatureId=-1 denotes an open terminal fall. */
public record Spill(long id, long fromFeatureId, long toFeatureId, Position source, Position outlet,
List<Position> flowPath, FlowBounds flowBounds) {
public Spill { flowPath = List.copyOf(flowPath); }
}
/** Replaces existing solids bedY-sedimentDepth+1..bedY, with two intact natural supports below.
* Water occupies bedY+1..waterY; everything above it through carveTop is removed. For a dry
* terrace waterY=-1, and bedY+1..carveTop is removed (at most two original blocks). */
public record Cell(int x, int z, int waterY, int bedY, int carveTop,
ShoreMaterial material, long featureId, int sedimentDepth) {
public boolean hasWater() { return waterY >= 0; }
}
public record Feature(long id, Kind kind, int waterY, List<Point> path, int waterCells, Bounds bounds) {
public Feature { path = List.copyOf(path); }
}
public static final class Plan {
private final Map<Long, Cell> cells;
private final List<Cell> allCells;
private final Map<Long, List<Cell>> layers;
private final Map<Long, List<Cell>> chunks;
private final List<Feature> features;
private final List<Spring> springs;
private final Map<Long, List<Spring>> springChunks;
private final Set<Position> springFlow;
private final int sampledColumns;
private final long densitySamples;
private final List<Terrace> terraces;
private final List<Spill> spills;
private final Set<Position> spillOpenings;
private Plan(Map<Long, Cell> source, List<Feature> features, List<Spring> springs, Set<Position> springFlow, int columns, long samples) {
this(source, features, springs, springFlow, columns, samples, CavernTerraces.Result.EMPTY);
}
private Plan(Map<Long, Cell> source, List<Feature> features, List<Spring> springs, Set<Position> springFlow,
int columns, long samples, CavernTerraces.Result lower) {
List<Cell> combined = new ArrayList<>(source.values());
combined.addAll(lower.cells());
List<Cell> sorted = combined.stream()
.sorted(Comparator.comparingInt(Cell::x).thenComparingInt(Cell::z).thenComparingInt(Cell::bedY)).toList();
allCells = List.copyOf(sorted);
Map<Long, Cell> ordered = new LinkedHashMap<>();
Map<Long, List<Cell>> stacked = new HashMap<>();
Map<Long, List<Cell>> byChunk = new HashMap<>();
for (Cell cell : sorted) {
ordered.put(key(cell.x(), cell.z()), cell);
stacked.computeIfAbsent(key(cell.x(), cell.z()), ignored -> new ArrayList<>()).add(cell);
byChunk.computeIfAbsent(key(cell.x() >> 4, cell.z() >> 4), ignored -> new ArrayList<>()).add(cell);
}
byChunk.replaceAll((ignored, values) -> List.copyOf(values));
cells = Collections.unmodifiableMap(ordered);
stacked.replaceAll((ignored, values) -> List.copyOf(values));
layers = Map.copyOf(stacked);
chunks = Map.copyOf(byChunk);
List<Feature> allFeatures = new ArrayList<>(features);
allFeatures.addAll(lower.features());
this.features = List.copyOf(allFeatures);
this.springs = List.copyOf(springs);
Set<Position> allFlow = new HashSet<>(springFlow);
allFlow.addAll(lower.predictedFlow());
this.springFlow = Set.copyOf(allFlow);
terraces = List.copyOf(lower.terraces());
spills = List.copyOf(lower.spills());
spillOpenings = spills.stream().map(Spill::outlet).collect(java.util.stream.Collectors.toUnmodifiableSet());
Map<Long, List<Spring>> groupedSprings = new HashMap<>();
for (Spring spring : springs) groupedSprings.computeIfAbsent(
key(spring.source().x() >> 4, spring.source().z() >> 4), ignored -> new ArrayList<>()).add(spring);
groupedSprings.replaceAll((ignored, values) -> List.copyOf(values));
springChunks = Map.copyOf(groupedSprings);
sampledColumns = columns;
densitySamples = samples;
}
public Cell cellAt(int x, int z) { return cells.get(key(x, z)); }
public List<Cell> cellsAt(int x, int z) { return layers.getOrDefault(key(x, z), List.of()); }
/** Return the modified vertical interval, including its two unchanged support layers. */
public Cell cellAt(int x, int y, int z) {
for (Cell cell : cellsAt(x, z)) if (y >= cell.bedY() - cell.sedimentDepth() - 1
&& y <= Math.max(cell.waterY(), cell.carveTop())) return cell;
return null;
}
public Collection<Cell> cells() { return allCells; }
public List<Cell> cellsInChunk(int x, int z) { return chunks.getOrDefault(key(x, z), List.of()); }
public List<Feature> features() { return features; }
public boolean isRiverCell(Cell cell) { return features.stream().anyMatch(f -> f.kind() == Kind.RIVER && f.id() == cell.featureId()); }
public boolean isTerraceCell(Cell cell) { return features.stream().anyMatch(f -> f.kind() == Kind.TERRACE && f.id() == cell.featureId()); }
public List<Terrace> terraces() { return terraces; }
public List<Spill> spills() { return spills; }
public boolean isSpillOpening(int x, int y, int z) { return spillOpenings.contains(new Position(x, y, z)); }
public List<Spring> springs() { return springs; }
public List<Spring> springsInChunk(int x, int z) { return springChunks.getOrDefault(key(x, z), List.of()); }
/** Prediction in undecorated density, not a restriction on Minecraft fluid simulation. */
public boolean allowsSpringFlow(int x, int y, int z) { return springFlow.contains(new Position(x, y, z)); }
public int sampledColumns() { return sampledColumns; }
public long densitySamples() { return densitySamples; }
}
public static Plan create(long seed, Sampler sampler) {
return new Planner(seed, sampler).build();
}
private record Column(int top, int solidBottom) {}
private record Candidate(int x, int z, double score) {}
private record Node(int x, int z, double cost) {}
private static final Comparator<Node> NODE_ORDER = Comparator.comparingDouble(Node::cost)
.thenComparingInt(Node::x).thenComparingInt(Node::z);
private static final class Planner {
private final long seed;
private final Sampler sampler;
private final Map<Long, Column> columns = new HashMap<>();
private final Map<Long, Integer> coarse = new HashMap<>();
private final Map<Long, Cell> cells = new HashMap<>();
private final List<Feature> features = new ArrayList<>();
private final List<Spring> springs = new ArrayList<>();
private final Set<Position> springFlow = new HashSet<>();
private long samples;
Planner(long seed, Sampler sampler) { this.seed = seed; this.sampler = sampler; }
Plan build() {
List<Candidate> candidates = candidates();
addRiver();
int attempts = 0;
for (Candidate candidate : candidates) {
if (features.size() >= 4 || attempts++ >= 96) break;
Point center = new Point(candidate.x(), candidate.z());
if (nearWater(center.x(), center.z(), 38)) continue;
Column column = column(center.x(), center.z());
if (column.top() < 64 || column.top() - column.solidBottom() < 7) continue;
Kind kind = features.size() < 2 ? Kind.LAKE : Kind.POND;
int target = kind == Kind.LAKE ? 700 + (int) (random(seed, center.x(), center.z()) * 400)
: 100 + (int) (random(seed + 1, center.x(), center.z()) * 140);
// Prefer an existing low spot; a shallow cut is the fallback on level terrain.
for (int level : new int[]{column.top() + 1, column.top(), column.top() - 1, column.top() - 2}) {
Set<Long> footprint = growBasin(center, level, target);
if (footprint == null) continue;
long id = featureId(features.size());
Map<Long, Cell> wet = kind == Kind.LAKE ? lakeCells(footprint, level, id) : waterCells(footprint, level, id);
if (wet == null || !contained(wet)) continue;
addFeature(id, kind, level, List.of(center), wet);
break;
}
}
addRiverSpring();
addSedimentSheets();
addLowerWallSprings();
addSprings();
Plan surface = new Plan(cells, features, springs, springFlow, columns.size(), samples);
var lower = CavernTerraces.create(seed, sampler, surface);
return new Plan(cells, features, springs, springFlow, columns.size(), samples, lower);
}
/** A connected catchment is excavated only in a thick natural plateau. The route is
* selected on the actual fissured density, not painted across missing rock. Its two
* terminal pools share the same retained waterline; a separate rock spring feeds it. */
private void addRiver() {
List<Integer> heights = coarse.values().stream().filter(y -> y >= 160).sorted().toList();
if (heights.isEmpty()) return;
Set<Integer> levels = new java.util.LinkedHashSet<>();
for (double quantile : new double[]{.55, .35, .75, .15, .90}) {
int top = heights.get(Math.min(heights.size() - 1, (int) (heights.size() * quantile)));
for (int cut : new int[]{4, 8, 0}) levels.add(top - cut);
}
for (int level : levels) {
Set<Long> allowed = new HashSet<>();
for (int x = -168; x <= 168; x += 4) for (int z = -168; z <= 168; z += 4) {
if (!inside(x, z, 170)) continue;
boolean safe = riverRock(x, z, level);
for (int dx : new int[]{-3, 3}) for (int dz : new int[]{-3, 3}) {
if (safe && !riverRock(x + dx, z + dz, level)) safe = false;
}
if (safe) allowed.add(key(x, z));
}
for (int attempt = 0; attempt < 4 && allowed.size() > 100; attempt++) {
List<Point> route = longestRoute(allowed, level);
if (route.size() < 36) break;
Point first = route.getFirst(), last = route.getLast();
if (Math.hypot(last.x() - first.x(), last.z() - first.z()) < 100) break;
List<Point> path = smoothRoute(route);
Set<Long> wet = new HashSet<>();
Set<Long> bad = new HashSet<>();
for (Point at : path) {
int radius = field(at.x(), at.z(), 23, 8119) > .52 ? 3 : 2;
for (int dx = -radius - 1; dx <= radius + 1; dx++) for (int dz = -radius - 1; dz <= radius + 1; dz++) {
if (!riverRock(at.x() + dx, at.z() + dz, level)) {
for (int gx = -1; gx <= 1; gx++) for (int gz = -1; gz <= 1; gz++)
bad.add(key(Math.floorDiv(at.x(), 4) * 4 + gx * 4, Math.floorDiv(at.z(), 4) * 4 + gz * 4));
}
if (dx * dx + dz * dz <= radius * radius + 1) wet.add(key(at.x() + dx, at.z() + dz));
}
}
if (!bad.isEmpty()) { allowed.removeAll(bad); continue; }
// Both end pools are required and remain connected to the channel.
boolean pools = true;
for (Point endpoint : List.of(first, last)) {
Set<Long> pool = growRiverPool(endpoint, level);
if (pool.size() < 90) { pools = false; break; }
wet.addAll(pool);
}
if (!pools) { allowed.remove(key(first.x(), first.z())); allowed.remove(key(last.x(), last.z())); continue; }
long id = featureId(features.size());
Map<Long, Cell> river = new HashMap<>();
boolean safe = true;
for (long k : wet) {
Point at = point(k);
Column rock = column(at.x(), at.z());
int depth = 2;
boolean edge = false;
for (int[] d : CARDINALS) edge |= !wet.contains(key(at.x() + d[0], at.z() + d[1]));
if (edge) depth = 1;
int bed = level - depth;
if (rock.top() < bed || rock.top() - bed > MAX_CARVE || rock.solidBottom() > bed - 6) { safe = false; break; }
river.put(k, new Cell(at.x(), at.z(), level, bed, Math.max(level, rock.top()),
wetMaterial(at.x(), at.z(), edge), id, 3));
}
if (!safe || !contained(river)) break;
addFeature(id, Kind.RIVER, level, path, river);
return;
}
}
}
private Set<Long> growRiverPool(Point center, int level) {
Set<Long> result = new HashSet<>();
PriorityQueue<Node> queue = new PriorityQueue<>(NODE_ORDER);
Set<Long> seen = new HashSet<>();
queue.add(new Node(center.x(), center.z(), 0));
while (!queue.isEmpty() && result.size() < 240) {
Node at = queue.remove();
if (!seen.add(key(at.x(), at.z())) || Math.hypot(at.x() - center.x(), at.z() - center.z()) > 12
|| !riverRock(at.x(), at.z(), level)) continue;
boolean retained = true;
for (int[] d : CARDINALS) retained &= solid(at.x() + d[0], level, at.z() + d[1]);
if (!retained) continue;
result.add(key(at.x(), at.z()));
for (int[] d : CARDINALS) {
int x = at.x() + d[0], z = at.z() + d[1];
queue.add(new Node(x, z, Math.hypot(x - center.x(), z - center.z())
+ field(x, z, 8, 411) * 6 + Math.max(0, column(x,z).top() - level) * .4));
}
}
return result;
}
private boolean riverRock(int x, int z, int level) {
Column rock = column(x, z);
return rock.top() >= level && rock.top() <= level + 12 && rock.solidBottom() <= level - 8;
}
private List<Point> longestRoute(Set<Long> allowed, int level) {
Set<Long> remaining = new HashSet<>(allowed);
List<Point> longest = List.of();
while (!remaining.isEmpty()) {
long start = remaining.stream().min(Long::compare).orElseThrow();
Map<Long, Long> first = routeTree(start, allowed);
remaining.removeAll(first.keySet());
if (first.size() < 36) continue;
long far = lastKey(first);
Map<Long, Long> second = routeTree(far, first.keySet());
long end = lastKey(second);
Map<Long, Long> shaped = riverRouteTree(far, end, first.keySet(), level);
List<Point> path = new ArrayList<>();
for (long at = end; ; at = shaped.get(at)) {
path.add(point(at));
if (at == far) break;
}
if (path.size() > longest.size()) longest = path;
}
return longest;
}
private Map<Long, Long> riverRouteTree(long start, long end, Set<Long> allowed, int level) {
Map<Long, Long> parent = new HashMap<>();
Map<Long, Double> costs = new HashMap<>();
PriorityQueue<Node> queue = new PriorityQueue<>(NODE_ORDER);
Point origin = point(start);
parent.put(start, start); costs.put(start, 0.0); queue.add(new Node(origin.x(), origin.z(), 0));
while (!queue.isEmpty()) {
Node at = queue.remove();
long current = key(at.x(), at.z());
if (at.cost() > costs.getOrDefault(current, Double.POSITIVE_INFINITY)) continue;
if (current == end) break;
for (int[] d : NEIGHBORS) {
int x = at.x() + d[0] * 4, z = at.z() + d[1] * 4;
long next = key(x, z);
if (!allowed.contains(next)) continue;
double cost = at.cost() + Math.hypot(d[0], d[1]) * (1 + Math.max(0, column(x,z).top() - level) * .14
+ field(x, z, 27, 8113) * 1.8);
if (cost >= costs.getOrDefault(next, Double.POSITIVE_INFINITY)) continue;
parent.put(next, current); costs.put(next, cost); queue.add(new Node(x,z,cost));
}
}
return parent;
}
private List<Point> smoothRoute(List<Point> coarsePath) {
record Smooth(double x, double z) {}
List<Smooth> values = coarsePath.stream().map(p -> new Smooth(p.x(), p.z())).toList();
// Bend over several grid steps rather than rounding each voxel-sized corner.
for (int pass = 0; pass < 2; pass++) {
List<Smooth> next = new ArrayList<>();
for (int i = 0; i < values.size(); i++) {
if (i == 0 || i == values.size() - 1) { next.add(values.get(i)); continue; }
double x = 0, z = 0, sum = 0;
for (int offset = -4; offset <= 4; offset++) {
int weight = 5 - Math.abs(offset);
Smooth p = values.get(Math.clamp(i + offset, 0, values.size() - 1));
x += p.x() * weight; z += p.z() * weight; sum += weight;
}
next.add(new Smooth(x / sum, z / sum));
}
values = next;
}
for (int pass = 0; pass < 2; pass++) {
List<Smooth> next = new ArrayList<>(); next.add(values.getFirst());
for (int i = 1; i < values.size(); i++) {
Smooth a = values.get(i - 1), b = values.get(i);
next.add(new Smooth(a.x() * .75 + b.x() * .25, a.z() * .75 + b.z() * .25));
next.add(new Smooth(a.x() * .25 + b.x() * .75, a.z() * .25 + b.z() * .75));
}
next.add(values.getLast()); values = next;
}
List<Point> path = new ArrayList<>();
for (int i = 1; i < values.size(); i++) {
Smooth a = values.get(i - 1), b = values.get(i);
int steps = Math.max(1, (int) Math.ceil(Math.max(Math.abs(b.x() - a.x()), Math.abs(b.z() - a.z()))));
for (int step = 0; step <= steps; step++) {
double t = step / (double) steps;
Point p = new Point((int) Math.round(a.x() + (b.x() - a.x()) * t), (int) Math.round(a.z() + (b.z() - a.z()) * t));
if (path.isEmpty() || !path.getLast().equals(p)) path.add(p);
}
}
return path;
}
private Map<Long, Long> routeTree(long start, Set<Long> allowed) {
Map<Long, Long> parent = new LinkedHashMap<>();
ArrayDeque<Long> queue = new ArrayDeque<>();
parent.put(start, start); queue.add(start);
while (!queue.isEmpty()) {
long current = queue.removeFirst();
Point at = point(current);
for (int[] d : NEIGHBORS) {
long next = key(at.x() + d[0] * 4, at.z() + d[1] * 4);
if (allowed.contains(next) && !parent.containsKey(next)) { parent.put(next, current); queue.addLast(next); }
}
}
return parent;
}
private long lastKey(Map<Long, Long> values) {
long result = 0;
for (long key : values.keySet()) result = key;
return result;
}
/** A real spring emerges from one of the river's taller rock banks and drops into the
* connected retained channel. The source replaces rock; its mouth is the carved valley. */
private void addRiverSpring() {
Feature river = features.stream().filter(f -> f.kind() == Kind.RIVER).findFirst().orElse(null);
if (river == null) return;
List<Cell> bank = cells.values().stream().filter(c -> c.featureId() == river.id())
.sorted(Comparator.comparingInt(Cell::carveTop).reversed().thenComparingInt(Cell::x).thenComparingInt(Cell::z)).toList();
for (Cell wet : bank) for (int[] d : CARDINALS) {
int x = wet.x() + d[0], z = wet.z() + d[1];
if (cells.containsKey(key(x, z))) continue;
for (int height : new int[]{8, 7, 6, 5}) {
int y = wet.waterY() + height;
if (wet.carveTop() < y) continue;
Position source = new Position(x, y, z), outlet = new Position(wet.x(), y, wet.z());
boolean safe = true;
for (int sy = y - 3; sy <= y + 1; sy++) safe &= flowSolid(new Position(x, sy, z), outlet);
for (int[] side : CARDINALS) {
if (side[0] == -d[0] && side[1] == -d[1]) continue;
safe &= flowSolid(new Position(x + side[0], y, z + side[1]), outlet);
}
if (!safe) continue;
Flow flow = traceSpring(source, outlet);
if (flow == null || flow.volume().stream().noneMatch(p -> {
Cell c = cells.get(key(p.x(), p.z()));
return c != null && c.featureId() == river.id() && p.y() <= c.waterY() && p.y() > c.bedY();
})) continue;
long id = mix(seed ^ 0xA81BE2L);
springs.add(new Spring(id, source, outlet, flow.path(), flow.bounds()));
springFlow.addAll(flow.volume());
return;
}
}
}
private List<Candidate> candidates() {
for (int x = -RADIUS; x <= RADIUS; x += GRID) {
for (int z = -RADIUS; z <= RADIUS; z += GRID) {
if (inside(x, z, RADIUS)) coarse.put(key(x, z), coarseTop(x, z));
}
}
List<Candidate> result = new ArrayList<>();
for (int x = -RADIUS + GRID; x < RADIUS; x += GRID) {
for (int z = -RADIUS + GRID; z < RADIUS; z += GRID) {
int height = coarse.getOrDefault(key(x, z), -1);
if (height < 64 || !inside(x, z, RADIUS - 24)) continue;
double average = 0;
boolean safe = true;
for (int[] d : CARDINALS) {
int neighbor = coarse.getOrDefault(key(x + d[0] * GRID, z + d[1] * GRID), -1);
if (neighbor < 64 || Math.abs(neighbor - height) > 16) safe = false;
average += neighbor * 0.25;
}
if (safe) result.add(new Candidate(x, z,
(average - height) * 2.0 + random(seed + 2, x, z) * 3.0));
}
}
result.sort(Comparator.comparingDouble(Candidate::score).reversed()
.thenComparingInt(Candidate::x).thenComparingInt(Candidate::z));
return result;
}
private int coarseTop(int x, int z) {
for (int y = 383; y >= 32; y -= 4) if (solid(x, y, z)) return y;
return -1;
}
private Column column(int x, int z) {
long key = key(x, z);
Column old = columns.get(key);
if (old != null) return old;
int top = -1;
// Exact scan, including everything above the planned water: a cave roof cannot be
// mistaken for a surface. Only candidates and their nearby shore need this refinement.
for (int y = 383; y >= 24; y--) {
if (solid(x, y, z)) { top = y; break; }
}
int bottom = top;
if (top >= 0) {
while (bottom > Math.max(16, top - 40) && solid(x, bottom - 1, z)) bottom--;
}
Column created = new Column(top, bottom);
columns.put(key, created);
return created;
}
private boolean eligible(int x, int z, int level) {
if (level < 32 || level >= 383 || !inside(x, z, RADIUS - 2)) return false;
Cell old = cells.get(key(x, z));
if (old != null && old.hasWater() && old.waterY() != level) return false;
Column column = column(x, z);
return column.top() >= level - 3 && column.top() < level + 6
&& column.solidBottom() <= Math.min(column.top(), level - 1) - 4;
}
private Set<Long> growBasin(Point center, int level, int target) {
if (!eligible(center.x(), center.z(), level)) return null;
Set<Long> wet = new HashSet<>();
Map<Long, Double> costs = new HashMap<>();
PriorityQueue<Node> queue = new PriorityQueue<>(NODE_ORDER);
queue.add(new Node(center.x(), center.z(), 0));
costs.put(key(center.x(), center.z()), 0.0);
while (!queue.isEmpty() && wet.size() < target) {
Node current = queue.remove();
if (current.cost() > costs.getOrDefault(key(current.x(), current.z()), Double.POSITIVE_INFINITY)) continue;
if (!eligible(current.x(), current.z(), level)) continue;
wet.add(key(current.x(), current.z()));
for (int[] d : NEIGHBORS) {
int x = current.x() + d[0], z = current.z() + d[1];
if (Math.hypot(x - center.x(), z - center.z()) > 40 || wet.contains(key(x, z))) continue;
if (!eligible(x, z, level)) continue;
double step = Math.hypot(d[0], d[1]);
double cost = current.cost() + step * (1 + Math.max(0, column(x, z).top() - level) * 0.8
+ terrainNoise(x, z) * 4.5 + field(x + 71, z - 93, 21, 107) * 3.5);
if (cost >= costs.getOrDefault(key(x, z), Double.POSITIVE_INFINITY)) continue;
costs.put(key(x, z), cost);
queue.add(new Node(x, z, cost));
}
}
wet = connectedTo(wet, key(center.x(), center.z()));
if (wet.size() < Math.min(target, 30)) return null;
// Filling a hollow requires every lower connected outlet to be included. Abort rather
// than place a dam if this natural basin runs to a cliff, cave or an oversized catchment.
List<Long> work = new ArrayList<>(wet);
for (int i = 0; i < work.size(); i++) {
Point point = point(work.get(i));
for (int[] d : CARDINALS) {
int x = point.x() + d[0], z = point.z() + d[1];
long neighbor = key(x, z);
if (wet.contains(neighbor) || column(x, z).top() >= level) continue;
if (wet.size() >= 1200 || Math.hypot(x - center.x(), z - center.z()) > 48
|| !eligible(x, z, level)) return null;
wet.add(neighbor);
work.add(neighbor);
}
}
return wet;
}
private Map<Long, Cell> waterCells(Set<Long> footprint, int level, long id) {
Map<Long, Cell> result = new HashMap<>();
for (long key : footprint) {
Point p = point(key);
if (!eligible(p.x(), p.z(), level)) return null;
boolean edge = false;
for (int[] d : CARDINALS) edge |= !footprint.contains(key(p.x() + d[0], p.z() + d[1]));
Column column = column(p.x(), p.z());
int depth = Math.max(level - column.top() + 1, edge ? 1 : 2 + (terrainNoise(p.x(), p.z()) > 0.55 ? 1 : 0));
depth = Math.min(depth, 8 - Math.max(0, column.top() - level));
if (depth < 1 || depth > MAX_DEPTH) return null;
int bed = level - depth;
if (column.top() < bed || column.solidBottom() > bed - 2) return null;
ShoreMaterial material = wetMaterial(p.x(), p.z(), edge);
int sedimentDepth = sedimentDepth(p.x(), p.z());
if (column.solidBottom() > bed - sedimentDepth - 1) return null;
result.put(key, new Cell(p.x(), p.z(), level, bed, Math.max(level, column.top()),
material, id, sedimentDepth));
}
return result;
}
/** Deepen only sufficiently thick interiors, with one-block shore steps. Natural weak
* columns keep a shallower bed instead of receiving a manufactured lake foundation. */
private Map<Long, Cell> lakeCells(Set<Long> footprint, int level, long id) {
Map<Long, Integer> distance = new HashMap<>();
ArrayDeque<Long> queue = new ArrayDeque<>();
for (long k : footprint) {
Point at = point(k);
for (int[] d : CARDINALS) if (!footprint.contains(key(at.x() + d[0], at.z() + d[1]))) {
distance.put(k, 0); queue.addLast(k); break;
}
}
while (!queue.isEmpty()) {
long k = queue.removeFirst(); Point at = point(k);
for (int[] d : CARDINALS) {
long next = key(at.x() + d[0], at.z() + d[1]);
if (footprint.contains(next) && !distance.containsKey(next)) {
distance.put(next, distance.get(k) + 1); queue.addLast(next);
}
}
}
Map<Long, Cell> result = new HashMap<>();
for (long k : footprint) {
Point at = point(k);
if (!eligible(at.x(), at.z(), level)) return null;
Column rock = column(at.x(), at.z());
int sediment = sedimentDepth(at.x(), at.z());
int wanted = Math.max(level - rock.top() + 1, Math.min(MAX_DEPTH, 1 + distance.get(k)));
int depth = Math.min(wanted, Math.min(level - rock.solidBottom() - sediment - 1,
MAX_LAKE_CARVE - Math.max(0, rock.top() - level)));
if (depth < 1 || rock.top() < level - depth) return null;
int bed = level - depth;
result.put(k, new Cell(at.x(), at.z(), level, bed, Math.max(level, rock.top()),
wetMaterial(at.x(), at.z(), distance.get(k) == 0), id, sediment));
}
return result;
}
private boolean contained(Map<Long, Cell> wet) {
for (Cell cell : wet.values()) {
for (int y = cell.bedY() - 2; y <= cell.bedY(); y++) {
if (!solid(cell.x(), y, cell.z())) return false;
}
for (int[] d : CARDINALS) {
int x = cell.x() + d[0], z = cell.z() + d[1];
Cell neighbor = wet.get(key(x, z));
if (neighbor == null) neighbor = cells.get(key(x, z));
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) {
if (neighbor != null && neighbor.hasWater()) {
if (neighbor.waterY() != cell.waterY()) return false;
if (y > neighbor.bedY()) continue;
}
if (!solid(x, y, z)) return false;
}
}
}
return true;
}
private void addFeature(long id, Kind kind, int level, List<Point> path, Map<Long, Cell> wet) {
int minX = Integer.MAX_VALUE, minZ = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE, maxZ = Integer.MIN_VALUE;
int count = 0;
for (Cell cell : wet.values()) {
Cell old = cells.get(key(cell.x(), cell.z()));
if (old == null || !old.hasWater()) { cells.put(key(cell.x(), cell.z()), cell); count++; }
minX = Math.min(minX, cell.x()); minZ = Math.min(minZ, cell.z());
maxX = Math.max(maxX, cell.x()); maxZ = Math.max(maxZ, cell.z());
}
features.add(new Feature(id, kind, level, path, count, new Bounds(minX, minZ, maxX, maxZ)));
}
private static Set<Long> connectedTo(Set<Long> footprint, long start) {
Set<Long> connected = new HashSet<>();
List<Long> work = new ArrayList<>();
connected.add(start); work.add(start);
for (int i = 0; i < work.size(); i++) {
Point point = point(work.get(i));
for (int[] d : CARDINALS) {
long next = key(point.x() + d[0], point.z() + d[1]);
if (footprint.contains(next) && connected.add(next)) work.add(next);
}
}
return connected;
}
private record ShoreNode(int x, int z, int level, long feature, double cost) {}
/** Sediment follows shallow topography in broad, discontinuous sheets. A second nearby
* pocket can share the same sheet; the result is not a one-block material ring. */
private void addSedimentSheets() {
Comparator<ShoreNode> order = Comparator.comparingDouble(ShoreNode::cost)
.thenComparingInt(ShoreNode::x).thenComparingInt(ShoreNode::z).thenComparingLong(ShoreNode::feature);
Set<Long> rivers = features.stream().filter(f -> f.kind() == Kind.RIVER).map(Feature::id)
.collect(java.util.stream.Collectors.toSet());
PriorityQueue<ShoreNode> queue = new PriorityQueue<>(order);
Map<Long, Double> best = new HashMap<>();
for (Cell cell : cells.values()) if (cell.hasWater()) {
queue.add(new ShoreNode(cell.x(), cell.z(), cell.waterY(), cell.featureId(), 0));
best.put(key(cell.x(), cell.z()), 0.0);
}
while (!queue.isEmpty()) {
ShoreNode node = queue.remove();
if (node.cost() > best.getOrDefault(key(node.x(), node.z()), Double.POSITIVE_INFINITY)) continue;
boolean riverBank = rivers.contains(node.feature());
Column column = column(node.x(), node.z());
Cell existing = cells.get(key(node.x(), node.z()));
if (existing == null) {
boolean springRock = springs.stream().anyMatch(spring ->
Math.abs(spring.source().x() - node.x()) <= 4 && Math.abs(spring.source().z() - node.z()) <= 4);
if (springRock) continue;
double extent = riverBank ? 18 + field(node.x(), node.z(), 25, 131) * 6
: 9 + field(node.x(), node.z(), 25, 131) * 10;
if (node.cost() > extent) continue;
int delta = column.top() - node.level();
if (delta < 0 || delta > (riverBank ? 14 : 5)) continue;
// Low shore: waterline, then one-block steps. Cut at most two existing blocks;
// far deposits keep the original landform and feather back into grass/stone.
int terrace = node.level() + (int) Math.floor(node.cost() / (riverBank ? 2.5 : 4.5));
int cut = riverBank ? MAX_RIVER_BANK_CARVE : 2;
int bed = Math.max(node.level(), Math.max(column.top() - cut, Math.min(column.top(), terrace)));
int depth = sedimentDepth(node.x(), node.z());
if (column.solidBottom() > bed - depth - 1) continue;
boolean safe = true;
for (int[] d : CARDINALS) {
Cell next = cells.get(key(node.x() + d[0], node.z() + d[1]));
if (next != null && next.hasWater() && bed < next.waterY()) safe = false;
}
if (!safe) continue;
ShoreMaterial material = dryMaterial(node.x(), node.z(), node.cost() / extent);
cells.put(key(node.x(), node.z()), new Cell(node.x(), node.z(), -1, bed,
column.top(), material, node.feature(), depth));
}
for (int[] d : NEIGHBORS) {
int x = node.x() + d[0], z = node.z() + d[1];
if (!inside(x, z, RADIUS - 2)) continue;
Cell water = cells.get(key(x, z));
if (water != null && water.hasWater()) continue;
Column next = column(x, z);
if (next.top() < node.level() || next.top() > node.level() + (riverBank ? 14 : 5)) continue;
double cost = node.cost() + Math.hypot(d[0], d[1])
* (0.7 + Math.abs(next.top() - column.top()) * 0.32 + field(x, z, 12, 211) * 0.6);
if (cost > (riverBank ? 24 : 19) || cost >= best.getOrDefault(key(x, z), Double.POSITIVE_INFINITY)) continue;
best.put(key(x, z), cost);
queue.add(new ShoreNode(x, z, node.level(), node.feature(), cost));
}
}
}
private int sedimentDepth(int x, int z) { return 3 + (int) (field(x, z, 15, 319) * 2.999); }
private ShoreMaterial wetMaterial(int x, int z, boolean edge) {
double geology = field(x, z, 24, 503) * 0.75 + field(x, z, 7, 509) * 0.25;
if (geology < 0.46) return ShoreMaterial.STONE;
if (geology < 0.65) return ShoreMaterial.GRAVEL;
if (!edge && geology > 0.78) return ShoreMaterial.CLAY;
return ShoreMaterial.SAND;
}
private ShoreMaterial dryMaterial(int x, int z, double distance) {
double geology = field(x, z, 24, 503) * 0.75 + field(x, z, 7, 509) * 0.25;
double grass = field(x + 103, z - 137, 19, 521);
if (grass > 0.62 || distance > 0.60 + grass * 0.65) return ShoreMaterial.GRASS;
if (geology < 0.46) return ShoreMaterial.STONE;
if (geology < 0.65) return ShoreMaterial.GRAVEL;
return ShoreMaterial.SAND;
}
private record Cliff(int x, int z, int dx, int dz, double score) {}
private record FlowNode(Position position, int reach) {}
private record Flow(Set<Position> volume, List<Position> path, FlowBounds bounds) {}
/** Pick rare exposed rock niches at the edges of real upper surfaces. The source replaces
* one rock block under an existing roof. Its outlet is already air over an eight-block drop;
* no source, shelf, dam, rock column or ocean is ever placed in empty terrain. */
private void addSprings() {
List<Cliff> cliffs = new ArrayList<>();
for (int x = -264; x <= 264; x += GRID) for (int z = -264; z <= 264; z += GRID) {
if (!inside(x, z, 264)) continue;
int top = coarse.computeIfAbsent(key(x, z), ignored -> coarseTop(point(ignored).x(), point(ignored).z()));
if (top < 88) continue;
for (int[] d : CARDINALS) {
int nx = x + d[0] * GRID, nz = z + d[1] * GRID;
int lower = coarse.computeIfAbsent(key(nx, nz), ignored -> coarseTop(point(ignored).x(), point(ignored).z()));
if (top - lower < 16) continue;
cliffs.add(new Cliff(x, z, d[0], d[1],
Math.min(64, top - lower) * 0.06 + random(seed + 811, x + d[0], z + d[1]) * 3));
}
}
cliffs.sort(Comparator.comparingDouble(Cliff::score).reversed()
.thenComparingInt(Cliff::x).thenComparingInt(Cliff::z)
.thenComparingInt(Cliff::dx).thenComparingInt(Cliff::dz));
int attempts = 0;
for (Cliff cliff : cliffs) {
if (springs.size() >= 3 || attempts++ >= 160) break;
boolean placed = false;
for (int tangent : new int[]{0, -2, 2, -4, 4}) {
for (int along = 0; along < GRID; along++) {
int x = cliff.x() + cliff.dx() * along - cliff.dz() * tangent;
int z = cliff.z() + cliff.dz() * along + cliff.dx() * tangent;
if (!inside(x, z, 266)) continue;
int ox = x + cliff.dx(), oz = z + cliff.dz();
Column rock = column(x, z), outletColumn = column(ox, oz);
if (rock.top() < 80 || rock.top() - outletColumn.top() < 14) continue;
// Five blocks below the highest natural surface is below the soil cap.
int floor = Math.max(64, Math.max(rock.top() - 20, outletColumn.top() + 8));
for (int y = rock.top() - 5; y >= floor; y -= 3) {
Position source = new Position(x, y, z), outlet = new Position(ox, y, oz);
if (!separatedSpring(source) || !springNiche(source, cliff.dx(), cliff.dz()) || nearModifiedGround(source, 32)) continue;
Flow flow = traceSpring(source, outlet);
if (flow == null || flow.bounds().minY() > y - 12) continue;
long id = mix(seed ^ (0x5F81A6L + springs.size() * 0x9E3779B97F4A7C15L));
springs.add(new Spring(id, source, outlet, flow.path(), flow.bounds()));
springFlow.addAll(flow.volume());
placed = true;
break;
}
if (placed) break;
}
if (placed) break;
}
}
}
private boolean nearModifiedGround(Position source, int distance) {
for (Cell cell : cells.values()) {
int dy = Math.max(Math.max(cell.bedY() - cell.sedimentDepth() - source.y(), 0),
source.y() - cell.carveTop());
long dx = source.x() - cell.x(), dz = source.z() - cell.z();
if (dx * dx + dz * dz + (long) dy * dy < (long) distance * distance) return true;
}
return false;
}
private boolean separatedSpring(Position source) {
for (Spring spring : springs) {
double horizontal = Math.hypot(source.x() - spring.source().x(), source.z() - spring.source().z());
int dy = Math.abs(source.y() - spring.source().y());
if (Math.hypot(horizontal, dy) < 64 || dy < 12) return false;
}
return true;
}
private record Wall(int x, int y, int z, int dx, int dz, double score) {}
/** Probe a few exposed faces below the highest surface as well. This finds overhangs and
* lower strata that a highest-column heightmap cannot represent, without inventing a ledge. */
private void addLowerWallSprings() {
List<Wall> candidates = new ArrayList<>();
for (int x = -240; x <= 240; x += 16) for (int z = -240; z <= 240; z += 16) {
if (!inside(x, z, 240)) continue;
int top = coarse.computeIfAbsent(key(x, z), ignored -> coarseTop(point(ignored).x(), point(ignored).z()));
for (int y = top - 32; y >= 64; y -= 16) {
if (!solid(x, y, z)) continue;
for (int[] direction : CARDINALS) {
if (solid(x + direction[0] * 4, y, z + direction[1] * 4)) continue;
candidates.add(new Wall(x, y, z, direction[0], direction[1],
random(seed + 1217 + y, x + direction[0], z + direction[1]) * 3
+ Math.min(100, top - y) * 0.015));
}
}
}
candidates.sort(Comparator.comparingDouble(Wall::score).reversed()
.thenComparingInt(Wall::x).thenComparingInt(Wall::z).thenComparingInt(Wall::y)
.thenComparingInt(Wall::dx).thenComparingInt(Wall::dz));
int attempts = 0;
for (Wall candidate : candidates) {
if (springs.size() >= 2 || attempts++ >= 96) break;
boolean placed = false;
for (int tangent : new int[]{0, -2, 2}) {
for (int along = 0; along < 4; along++) {
int x = candidate.x() + candidate.dx() * along - candidate.dz() * tangent;
int z = candidate.z() + candidate.dz() * along + candidate.dx() * tangent;
for (int dy : new int[]{0, 3, -3, 6, -6}) {
int y = candidate.y() + dy;
if (y < 56 || column(x, z).top() - y < 24) continue;
Position source = new Position(x, y, z);
if (!separatedSpring(source) || !springNiche(source, candidate.dx(), candidate.dz())
|| nearModifiedGround(source, 32)) continue;
Position outlet = new Position(x + candidate.dx(), y, z + candidate.dz());
Flow flow = traceSpring(source, outlet);
if (flow == null || flow.bounds().minY() > y - 12) continue;
long id = mix(seed ^ (0x5F81A6L + springs.size() * 0x9E3779B97F4A7C15L));
springs.add(new Spring(id, source, outlet, flow.path(), flow.bounds()));
springFlow.addAll(flow.volume());
placed = true;
break;
}
if (placed) break;
}
if (placed) break;
}
}
}
private boolean springNiche(Position source, int dx, int dz) {
for (int y = source.y() - 3; y <= source.y() + 1; y++) {
if (!solid(source.x(), y, source.z())) return false;
}
for (int[] d : CARDINALS) {
if (d[0] == dx && d[1] == dz) continue;
if (!solid(source.x() + d[0], source.y(), source.z() + d[1])) return false;
}
for (int y = source.y(); y >= source.y() - 8; y--) {
if (solid(source.x() + dx, y, source.z() + dz)) return false;
}
return true;
}
/** Predict drainage through undecorated rock to select springs and inspection bounds.
* Descend through air first, then explore seven horizontal steps on a shelf. Later trees
* can divert the actual fluid: this prediction is not a bound on decorated-world physics.
* Integration tests trace actual water back to its source within the inspected chunk halo. */
private Flow traceSpring(Position source, Position outlet) {
Map<Position, Integer> reach = new HashMap<>();
Map<Position, Position> parent = new HashMap<>();
ArrayDeque<FlowNode> queue = new ArrayDeque<>();
reach.put(source, 7);
queue.add(new FlowNode(source, 7));
Position lowest = source;
while (!queue.isEmpty()) {
FlowNode node = queue.removeFirst();
Position at = node.position();
if (node.reach() < reach.getOrDefault(at, -1)) continue;
if (at.y() < lowest.y()) lowest = at;
if (reach.size() > 24000) return null;
if (at.y() <= 0) continue;
Position below = new Position(at.x(), at.y() - 1, at.z());
if (!flowSolid(below, source)) {
enqueueFlow(below, 7, at, queue, reach, parent);
continue;
}
if (node.reach() <= 0) continue;
for (int[] d : CARDINALS) {
Position next = new Position(at.x() + d[0], at.y(), at.z() + d[1]);
if (flowSolid(next, source)) continue;
enqueueFlow(next, node.reach() - 1, at, queue, reach, parent);
}
}
if (!reach.containsKey(outlet)) return null;
int minX = source.x(), maxX = minX, minY = source.y(), maxY = minY;
int minZ = source.z(), maxZ = minZ;
for (Position position : reach.keySet()) {
minX = Math.min(minX, position.x()); maxX = Math.max(maxX, position.x());
minY = Math.min(minY, position.y()); maxY = Math.max(maxY, position.y());
minZ = Math.min(minZ, position.z()); maxZ = Math.max(maxZ, position.z());
}
List<Position> path = new ArrayList<>();
for (Position at = lowest; at != null; at = parent.get(at)) path.add(at);
Collections.reverse(path);
return new Flow(Set.copyOf(reach.keySet()), path, new FlowBounds(minX, minY, minZ, maxX, maxY, maxZ));
}
private boolean flowSolid(Position at, Position source) {
if (at.equals(source)) return false;
Cell cell = cells.get(key(at.x(), at.z()));
if (cell != null && at.y() > cell.bedY() && at.y() <= cell.carveTop()) return false;
return solid(at.x(), at.y(), at.z());
}
private static void enqueueFlow(Position next, int available, Position from, ArrayDeque<FlowNode> queue,
Map<Position, Integer> reach, Map<Position, Position> parent) {
if (available <= reach.getOrDefault(next, -1)) return;
if (!reach.containsKey(next)) parent.put(next, from);
reach.put(next, available);
queue.addLast(new FlowNode(next, available));
}
private boolean nearWater(int x, int z, int distance) {
for (Feature feature : features) {
Point point = feature.path().getFirst();
if (Math.hypot(x - point.x(), z - point.z()) < distance) return true;
}
return false;
}
private boolean solid(int x, int y, int z) { samples++; return sampler.sample(x, y, z) > 0; }
private long featureId(int index) { return mix(seed ^ (0x51A7E7L + index * 0x9E3779B97F4A7C15L)); }
private double terrainNoise(int x, int z) { return field(x, z, 8, 31); }
private double field(int x, int z, int scale, long salt) {
int gx = Math.floorDiv(x, scale), gz = Math.floorDiv(z, scale);
double tx = smooth(Math.floorMod(x, scale) / (double) scale);
double tz = smooth(Math.floorMod(z, scale) / (double) scale);
double a = random(seed + salt, gx, gz) * (1 - tx) + random(seed + salt, gx + 1, gz) * tx;
double b = random(seed + salt, gx, gz + 1) * (1 - tx) + random(seed + salt, gx + 1, gz + 1) * tx;
return a * (1 - tz) + b * tz;
}
}
private static boolean inside(int x, int z, int radius) { return Math.hypot((double) x, z) < radius; }
private static long key(int x, int z) { return ((long) x << 32) | (z & 0xffffffffL); }
private static Point point(long key) { return new Point((int) (key >> 32), (int) key); }
private static double smooth(double t) { return t * t * (3 - 2 * t); }
private static double random(long seed, int x, int z) { return (mix(seed ^ key(x, z)) >>> 11) * 0x1.0p-53; }
private static long mix(long value) {
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
return value ^ (value >>> 31);
}
}
@@ -0,0 +1,191 @@
package fr.koka.sanctuary.worldgen;
import fr.koka.sanctuary.SanctuaryMod;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Map;
import java.util.WeakHashMap;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.RandomState;
/** Alpha.9 only: immutable plans cached by world state, applied to fresh terrain before decoration. */
public final class CavernHydrologyRuntime {
private static final Map<RandomState, CavernHydrology.Plan> PLANS =
Collections.synchronizedMap(new WeakHashMap<>());
private static final Map<RandomState, CavernLavaDeposit.Plan> LAVA_PLANS =
Collections.synchronizedMap(new WeakHashMap<>());
private CavernHydrologyRuntime() {}
public static boolean enabled(NoiseBasedChunkGenerator generator) {
return generator.generatorSettings().is(SanctuarySpawn.CAVERN_SETTINGS);
}
public static CavernHydrology.Plan cavernPlan(ServerLevel level) {
return plan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(),
level.getChunkSource().randomState());
}
public static CavernHydrology.Plan plan(NoiseBasedChunkGenerator generator, RandomState randomState) {
if (!enabled(generator)) throw new IllegalArgumentException("Cavern hydrology is not enabled for this generator");
return PLANS.computeIfAbsent(randomState, state -> {
long start = System.nanoTime();
var density = generator.generatorSettings().value().noiseRouter().finalDensity();
var plan = CavernHydrology.create(state.seed(),
(x, y, z) -> state.sampleBlockValueUncached(density, x, y, z));
SanctuaryMod.LOGGER.info("Sanctuary cavern hydrology: seed {}, {} features, {} columns, {} springs, {} ms",
state.seed(), plan.features().size(), plan.cells().size(), plan.springs().size(),
(System.nanoTime() - start) / 1_000_000);
SanctuaryMod.LOGGER.info("Sanctuary cavern catchments: {}", plan.features().stream().map(f -> f.kind() + " Y" + f.waterY() + " water=" + f.waterCells() + " path=" + f.path().size() + " " + f.bounds()).toList());
return plan;
});
}
public static CavernLavaDeposit.Plan lavaPlan(ServerLevel level) {
return lavaPlan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(),
level.getChunkSource().randomState());
}
public static CavernLavaDeposit.Plan lavaPlan(NoiseBasedChunkGenerator generator, RandomState randomState) {
// Acquire the water plan before the lava cache lock; the water planner never acquires it.
var water = plan(generator, randomState);
return LAVA_PLANS.computeIfAbsent(randomState, state -> {
long start = System.nanoTime();
var density = generator.generatorSettings().value().noiseRouter().finalDensity();
var lava = CavernLavaDeposit.create(state.seed(),
(x, y, z) -> state.sampleBlockValueUncached(density, x, y, z), water);
SanctuaryMod.LOGGER.info("Sanctuary starter lava: seed {}, {} sources, access {}, {} ms",
state.seed(), lava.lavaCells().size(), lava.access(), (System.nanoTime() - start) / 1_000_000);
return lava;
});
}
public static void apply(NoiseBasedChunkGenerator generator, RandomState randomState, ChunkAccess chunk) {
if (!enabled(generator)) return;
var chunkPos = chunk.getPos();
if (Math.abs((long) chunkPos.x() * 16) > IslandShape.TERRAIN_LIMIT + 16L
|| Math.abs((long) chunkPos.z() * 16) > IslandShape.TERRAIN_LIMIT + 16L) return;
var plan = plan(generator, randomState);
var cells = plan.cellsInChunk(chunkPos.x(), chunkPos.z());
var springs = plan.springsInChunk(chunkPos.x(), chunkPos.z());
var outlets = new java.util.ArrayList<>(plan.springs().stream().map(CavernHydrology.Spring::outlet)
.filter(outlet -> (outlet.x() >> 4) == chunkPos.x() && (outlet.z() >> 4) == chunkPos.z()).toList());
for (var spill : plan.spills()) {
var outlet = spill.outlet();
if ((outlet.x() >> 4) == chunkPos.x() && (outlet.z() >> 4) == chunkPos.z()) outlets.add(outlet);
}
var lava = lavaPlan(generator, randomState);
for (var fall : lava.falls()) {
var outlet = fall.outlet();
if ((outlet.x() >> 4) == chunkPos.x() && (outlet.z() >> 4) == chunkPos.z())
outlets.add(new CavernHydrology.Position(outlet.x(), outlet.y(), outlet.z()));
}
var lavaCells = lava.cellsInChunk(chunkPos.x(), chunkPos.z());
var lavaSupports = lava.supportsInChunk(chunkPos.x(), chunkPos.z());
if (cells.isEmpty() && springs.isEmpty() && outlets.isEmpty() && lavaCells.isEmpty() && lavaSupports.isEmpty()) return;
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
// Validate all replacements and two intact support layers before modifying this chunk.
for (var cell : cells) {
for (int y = cell.bedY() - cell.sedimentDepth() - 1; y <= cell.bedY(); y++) {
requireSolid(chunk, pos.set(cell.x(), y, cell.z()), randomState.seed());
}
}
for (var spring : springs) {
var source = spring.source();
requireSolid(chunk, pos.set(source.x(), source.y(), source.z()), randomState.seed());
}
for (var support : lavaSupports) {
requireSolid(chunk, pos.set(support.x(), support.y(), support.z()), randomState.seed());
if (chunk.getBlockState(pos).ignitedByLava()) {
throw new IllegalStateException("Lava pocket requires nonflammable natural support at " + pos);
}
}
for (var cell : lavaCells) {
requireSolid(chunk, pos.set(cell.x(), cell.y(), cell.z()), randomState.seed());
}
for (var cell : cells) {
for (int depth = 0; depth < cell.sedimentDepth(); depth++) {
pos.set(cell.x(), cell.bedY() - depth, cell.z());
BlockState material = CavernMaterials.sedimentBlock(cell, randomState.seed(), depth);
chunk.setBlockState(pos, material, 0);
}
for (int y = cell.bedY() + 1; y <= Math.max(cell.waterY(), cell.carveTop()); y++) {
pos.set(cell.x(), y, cell.z());
chunk.setBlockState(pos, cell.hasWater() && y <= cell.waterY()
? Blocks.WATER.defaultBlockState() : Blocks.AIR.defaultBlockState(), 0);
if (cell.hasWater() && y <= cell.waterY()) chunk.markPosForPostProcessing(pos);
}
}
for (var spring : springs) {
var source = spring.source();
pos.set(source.x(), source.y(), source.z());
chunk.setBlockState(pos, Blocks.WATER.defaultBlockState(), 0);
// Only the source is generated. Vanilla fluid ticks create the descending waterfall.
chunk.markPosForPostProcessing(pos);
}
for (var cell : lavaCells) {
pos.set(cell.x(), cell.y(), cell.z());
chunk.setBlockState(pos, (cell.lava() ? Blocks.LAVA : Blocks.AIR).defaultBlockState(), 0);
if (cell.lava()) chunk.markPosForPostProcessing(pos);
}
// This existing vanilla queue is consumed after neighbouring chunks finish decoration.
// Mark the outlet in its own chunk, including when its source belongs to a neighbour.
for (var outlet : outlets) {
pos.set(outlet.x(), outlet.y(), outlet.z());
chunk.markPosForPostProcessing(pos);
}
Heightmap.primeHeightmaps(chunk, EnumSet.of(Heightmap.Types.WORLD_SURFACE_WG,
Heightmap.Types.OCEAN_FLOOR_WG));
}
/** Reserve water, sediment and hot-rock volumes before placing vegetation. */
public static boolean protects(NoiseBasedChunkGenerator generator, RandomState randomState, BlockPos base,
int radius, int height, int rootDepth) {
var water = plan(generator, randomState);
var lava = lavaPlan(generator, randomState);
int low = base.getY() - rootDepth, high = base.getY() + height;
for (int x = base.getX() - radius; x <= base.getX() + radius; x++) {
for (int z = base.getZ() - radius; z <= base.getZ() + radius; z++) {
for (var cell : water.cellsAt(x, z))
if (low <= cell.carveTop() && high >= cell.bedY() - cell.sedimentDepth() - 1) return true;
}
}
for (var spring : water.springs()) {
var bounds = spring.flowBounds();
if (base.getX() + radius >= bounds.minX() - 2 && base.getX() - radius <= bounds.maxX() + 2
&& base.getZ() + radius >= bounds.minZ() - 2 && base.getZ() - radius <= bounds.maxZ() + 2
&& low <= bounds.maxY() + 2 && high >= bounds.minY() - 2) return true;
var source = spring.source();
if (Math.abs(base.getX() - source.x()) <= radius + 3 && Math.abs(base.getZ() - source.z()) <= radius + 3
&& low <= source.y() + 3 && high >= source.y() - 3) return true;
}
for (var bounds : lava.flowBounds()) {
if (base.getX() + radius >= bounds.minX() - 8 && base.getX() - radius <= bounds.maxX() + 8
&& base.getZ() + radius >= bounds.minZ() - 8 && base.getZ() - radius <= bounds.maxZ() + 8
&& low <= bounds.maxY() + 8 && high >= bounds.minY() - 8) return true;
}
for (var spill : water.spills()) {
var bounds = spill.flowBounds();
if (base.getX() + radius >= bounds.minX() - 3 && base.getX() - radius <= bounds.maxX() + 3
&& base.getZ() + radius >= bounds.minZ() - 3 && base.getZ() - radius <= bounds.maxZ() + 3
&& low <= bounds.maxY() + 3 && high >= bounds.minY() - 3) return true;
}
for (var cell : lava.cells()) if (Math.abs(base.getX() - cell.x()) <= radius + 2 && Math.abs(base.getZ() - cell.z()) <= radius + 2
&& low <= cell.y() + 2 && high >= cell.y() - 2) return true;
for (var support : lava.supports()) if (Math.abs(base.getX() - support.x()) <= radius + 2 && Math.abs(base.getZ() - support.z()) <= radius + 2
&& low <= support.y() + 2 && high >= support.y() - 2) return true;
return false;
}
private static void requireSolid(ChunkAccess chunk, BlockPos pos, long seed) {
if (!chunk.getBlockState(pos).isCollisionShapeFullBlock(chunk, pos)) {
throw new IllegalStateException("Cavern hydrology differs from terrain at " + pos + " for seed " + seed);
}
}
}
@@ -0,0 +1,99 @@
package fr.koka.sanctuary.worldgen;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import fr.koka.sanctuary.SanctuaryMod;
import java.util.stream.Stream;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeResolver;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.biome.Climate;
/** Seeded alpha.9 rift climate, separate from all saved earlier biome-source codecs. */
public final class CavernIslandBiomeSource extends BiomeSource {
public static final ResourceKey<Biome> OAK_FOREST = key("cavern_oak_forest");
public static final ResourceKey<Biome> BIRCH_FOREST = key("cavern_birch_forest");
public static final ResourceKey<Biome> CLEARING = key("cavern_clearing");
public static final ResourceKey<Biome> DRY_WOODLAND = key("cavern_dry_woodland");
public static final ResourceKey<Biome> ROCKY_HEATH = key("cavern_rocky_heath");
public static final ResourceKey<Biome> DARK_GROVE = key("cavern_dark_grove");
public static final ResourceKey<Biome> BAMBOO_GROVE = key("cavern_bamboo_grove");
public static final ResourceKey<Biome> SULFUR_DEPTHS = key("cavern_sulfur_depths");
public static final ResourceKey<Biome> LUSH_CAVES = key("cavern_lush_caves");
public static final ResourceKey<Biome> DRIPSTONE_CAVES = key("cavern_dripstone_caves");
public static final MapCodec<CavernIslandBiomeSource> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
Biome.CODEC.fieldOf("oak_forest").forGetter(source -> source.oakForest),
Biome.CODEC.fieldOf("birch_forest").forGetter(source -> source.birchForest),
Biome.CODEC.fieldOf("clearing").forGetter(source -> source.clearing),
Biome.CODEC.fieldOf("dry_woodland").forGetter(source -> source.dryWoodland),
Biome.CODEC.fieldOf("rocky_heath").forGetter(source -> source.rockyHeath),
Biome.CODEC.fieldOf("dark_grove").forGetter(source -> source.darkGrove),
Biome.CODEC.fieldOf("bamboo_grove").forGetter(source -> source.bambooGrove),
Biome.CODEC.fieldOf("sulfur_depths").forGetter(source -> source.sulfurDepths),
Biome.CODEC.fieldOf("lush_caves").forGetter(source -> source.lushCaves),
Biome.CODEC.fieldOf("dripstone_caves").forGetter(source -> source.dripstoneCaves)
).apply(instance, CavernIslandBiomeSource::new));
private final Holder<Biome> oakForest;
private final Holder<Biome> birchForest;
private final Holder<Biome> clearing;
private final Holder<Biome> dryWoodland;
private final Holder<Biome> rockyHeath;
private final Holder<Biome> darkGrove;
private final Holder<Biome> bambooGrove;
private final Holder<Biome> sulfurDepths;
private final Holder<Biome> lushCaves;
private final Holder<Biome> dripstoneCaves;
public CavernIslandBiomeSource(Holder<Biome> oakForest, Holder<Biome> birchForest, Holder<Biome> clearing,
Holder<Biome> dryWoodland, Holder<Biome> rockyHeath, Holder<Biome> darkGrove,
Holder<Biome> bambooGrove, Holder<Biome> sulfurDepths, Holder<Biome> lushCaves, Holder<Biome> dripstoneCaves) {
this.oakForest = oakForest;
this.birchForest = birchForest;
this.clearing = clearing;
this.dryWoodland = dryWoodland;
this.rockyHeath = rockyHeath;
this.darkGrove = darkGrove;
this.bambooGrove = bambooGrove;
this.sulfurDepths = sulfurDepths;
this.lushCaves = lushCaves;
this.dripstoneCaves = dripstoneCaves;
}
@Override
protected MapCodec<CavernIslandBiomeSource> codec() { return CODEC; }
@Override
protected Stream<Holder<Biome>> collectPossibleBiomes() {
return Stream.of(oakForest, birchForest, clearing, dryWoodland, rockyHeath, darkGrove, bambooGrove, sulfurDepths, lushCaves, dripstoneCaves);
}
@Override
public BiomeResolver createResolver(Climate.Sampler sampler) {
return (quartX, quartY, quartZ) -> {
int x = quartX * 4, y = quartY * 4, z = quartZ * 4;
float variation = sampler.temperature().sampleValue(x, 0, z);
float moisture = sampler.humidity().sampleValue(x, 0, z);
return switch (CavernBiomePalette.select(y, variation, moisture)) {
case OAK_FOREST -> oakForest;
case BIRCH_FOREST -> birchForest;
case CLEARING -> clearing;
case DRY_WOODLAND -> dryWoodland;
case ROCKY_HEATH -> rockyHeath;
case DARK_GROVE -> darkGrove;
case BAMBOO_GROVE -> bambooGrove;
case SULFUR_DEPTHS -> sulfurDepths;
case LUSH_CAVES -> lushCaves;
case DRIPSTONE_CAVES -> dripstoneCaves;
};
};
}
private static ResourceKey<Biome> key(String path) {
return ResourceKey.create(Registries.BIOME, SanctuaryMod.id(path));
}
}
@@ -0,0 +1,331 @@
package fr.koka.sanctuary.worldgen;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
/** Alpha.9 lava: an optional covered pocket and up to two deep rock outlets.
* All sources replace natural rock; Minecraft alone produces the falling lava. */
public final class CavernLavaDeposit {
public static final int RADIUS = 192;
public static final int WATER_CLEARANCE = 32;
public static final int LAVA_VOLUME = 18;
private static final int ENTRY_DISTANCE = 8;
private static final int[][] DIRECTIONS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
private static final Comparator<Position> POSITION_ORDER = Comparator.comparingInt(Position::x)
.thenComparingInt(Position::z).thenComparingInt(Position::y);
private static final Comparator<Cell> CELL_ORDER = Comparator.comparingInt(Cell::x)
.thenComparingInt(Cell::z).thenComparingInt(Cell::y);
private CavernLavaDeposit() {}
public record Position(int x, int y, int z) {}
/** lava=false removes existing rock to open the two-block-high chamber or approach. */
public record Cell(int x, int y, int z, boolean lava) {}
public static final class Plan {
private final List<Cell> cells;
private final List<Cell> lavaCells;
private final List<Position> supports;
private final Optional<Position> access;
private final Map<Long, List<Cell>> chunks;
private final Map<Long, List<Position>> supportChunks;
private final List<CavernLavaFlows.Fall> falls;
private final Map<Long, List<CavernLavaFlows.Fall>> fallChunks;
private Plan(List<Cell> cells, Set<Position> supports, Position access) {
this(cells, supports, access, List.of());
}
private Plan(List<Cell> cells, Set<Position> supports, Position access, List<CavernLavaFlows.Fall> falls) {
this.falls = List.copyOf(falls);
Map<Long, List<CavernLavaFlows.Fall>> groupedFalls = new HashMap<>();
for (var fall : falls) groupedFalls.computeIfAbsent(
key(fall.source().x() >> 4, fall.source().z() >> 4), ignored -> new ArrayList<>()).add(fall);
groupedFalls.replaceAll((ignored, values) -> List.copyOf(values));
fallChunks = Map.copyOf(groupedFalls);
this.cells = cells.stream().sorted(CELL_ORDER).toList();
lavaCells = this.cells.stream().filter(Cell::lava).toList();
this.supports = supports.stream().sorted(POSITION_ORDER).toList();
this.access = Optional.ofNullable(access);
Map<Long, List<Cell>> byChunk = new HashMap<>();
for (Cell cell : this.cells) byChunk.computeIfAbsent(key(cell.x() >> 4, cell.z() >> 4),
ignored -> new ArrayList<>()).add(cell);
byChunk.replaceAll((ignored, values) -> List.copyOf(values));
chunks = Map.copyOf(byChunk);
Map<Long, List<Position>> bySupportChunk = new HashMap<>();
for (Position support : this.supports) bySupportChunk.computeIfAbsent(key(support.x() >> 4, support.z() >> 4),
ignored -> new ArrayList<>()).add(support);
bySupportChunk.replaceAll((ignored, values) -> List.copyOf(values));
supportChunks = Map.copyOf(bySupportChunk);
}
public List<Cell> cells() { return cells; }
public List<Cell> lavaCells() { return lavaCells; }
public List<Position> supports() { return supports; }
public List<CavernLavaFlows.Fall> falls() { return falls; }
public List<CavernLavaFlows.Fall> fallsInChunk(int x, int z) { return fallChunks.getOrDefault(key(x, z), List.of()); }
public List<Position> flowSources() { return falls.stream().map(CavernLavaFlows.Fall::source).toList(); }
public List<CavernHydrology.FlowBounds> flowBounds() { return falls.stream().map(CavernLavaFlows.Fall::flowBounds).toList(); }
/** Player feet on a natural ledge, with three blocks of headroom below a roof or sky. */
public Optional<Position> access() { return access; }
public List<Cell> cellsInChunk(int x, int z) { return chunks.getOrDefault(key(x, z), List.of()); }
public List<Position> supportsInChunk(int x, int z) { return supportChunks.getOrDefault(key(x, z), List.of()); }
}
public static Plan create(long seed, CavernHydrology.Sampler sampler, CavernHydrology.Plan hydrology) {
Plan pocket = new Search(seed, sampler, hydrology).find();
List<CavernLavaFlows.Fall> falls = CavernLavaFlows.create(seed, sampler, hydrology, pocket);
List<Cell> cells = new ArrayList<>(pocket.cells());
Set<Position> supports = new HashSet<>(pocket.supports());
for (var fall : falls) {
Position source = fall.source();
cells.add(new Cell(source.x(), source.y(), source.z(), true));
supports.addAll(fall.supports());
}
return new Plan(cells, supports, pocket.access().orElse(null), falls);
}
private record Candidate(int x, int z, int dx, int dz, double score) {}
private record LowerCandidate(int x, int y, int z, int dx, int dz, double score) {}
private static final class Search {
private final long seed;
private final CavernHydrology.Sampler sampler;
private final CavernHydrology.Plan hydrology;
private final Map<Long, Integer> heights = new HashMap<>();
Search(long seed, CavernHydrology.Sampler sampler, CavernHydrology.Plan hydrology) {
this.seed = seed;
this.sampler = sampler;
this.hydrology = hydrology;
}
Plan find() {
Plan lower = findLowerPocket();
if (lower != null) return lower;
List<Candidate> candidates = new ArrayList<>();
for (int x = -184; x <= 184; x += 8) for (int z = -184; z <= 184; z += 8) {
if (!inside(x, z, RADIUS - 8)) continue;
int ledge = top(x, z);
if (ledge < 64 || ledge > 300) continue;
for (int[] direction : DIRECTIONS) {
int cx = x - direction[0] * ENTRY_DISTANCE, cz = z - direction[1] * ENTRY_DISTANCE;
if (!inside(cx, cz, RADIUS - 12) || !clearOfWater(cx, cz)) continue;
int rise = top(cx, cz) - ledge;
if (rise < 12) continue;
candidates.add(new Candidate(x, z, direction[0], direction[1],
random(seed, cx, cz) + Math.min(rise, 40) * 0.015));
}
}
candidates.sort(Comparator.comparingDouble(Candidate::score).reversed()
.thenComparingInt(Candidate::x).thenComparingInt(Candidate::z)
.thenComparingInt(Candidate::dx).thenComparingInt(Candidate::dz));
int attempts = 0;
for (Candidate candidate : candidates) {
if (attempts++ >= 192) break;
// Refine the coarse edge, including a ledge lying between two eight-block samples.
for (int along = 0; along < 8; along++) for (int tangent : new int[]{0, -3, 3}) {
int x = candidate.x() - candidate.dx() * along - candidate.dz() * tangent;
int z = candidate.z() - candidate.dz() * along + candidate.dx() * tangent;
Plan plan = tryNiche(x, z, candidate.dx(), candidate.dz());
if (plan != null) return plan;
}
}
return new Plan(List.of(), Set.of(), null);
}
/** Prefer a real lower ledge below an overhang. The chamber uses exactly the same rock
* shell as the outdoor fallback; only the entrance may now have a natural ceiling. */
private Plan findLowerPocket() {
List<LowerCandidate> candidates = new ArrayList<>();
for (int x = -176; x <= 176; x += 16) for (int z = -176; z <= 176; z += 16) {
if (!inside(x, z, RADIUS - 8)) continue;
for (int coarseY = 64; coarseY <= 160; coarseY += 8) {
if (!solid(x, coarseY, z) || solid(x, coarseY + 8, z)) continue;
int level = coarseY;
for (int y = coarseY + 7; y > coarseY; y--) {
if (solid(x, y, z)) { level = y; break; }
}
if (level > 160 || top(x, z) < level + 12) continue;
for (int[] direction : DIRECTIONS) {
int inwardX = x - direction[0] * 16, inwardZ = z - direction[1] * 16;
if (!inside(inwardX, inwardZ, RADIUS - 12)
|| !solid(inwardX, level + 2, inwardZ) || !solid(inwardX, level + 10, inwardZ)) continue;
candidates.add(new LowerCandidate(x, level, z, direction[0], direction[1],
random(seed + level + 0x108E2L, x - direction[0], z - direction[1])
+ (160 - level) * .002));
}
}
}
candidates.sort(Comparator.comparingDouble(LowerCandidate::score).reversed()
.thenComparingInt(LowerCandidate::x).thenComparingInt(LowerCandidate::z)
.thenComparingInt(LowerCandidate::y).thenComparingInt(LowerCandidate::dx).thenComparingInt(LowerCandidate::dz));
int attempts = 0;
for (LowerCandidate candidate : candidates) {
if (attempts++ >= 128) break;
for (int along = 0; along < 16; along++) for (int tangent : new int[]{0, -3, 3}) {
int x = candidate.x() - candidate.dx() * along - candidate.dz() * tangent;
int z = candidate.z() - candidate.dz() * along + candidate.dx() * tangent;
for (int level = Math.min(160, candidate.y() + 5); level >= Math.max(64, candidate.y() - 5); level--) {
// Reject solid cave walls and missing floors before expensive shell probes.
if (!solid(x, level, z) || solid(x, level + 1, z)) continue;
Plan plan = tryNiche(x, z, candidate.dx(), candidate.dz(), level, true);
if (plan != null) return plan;
}
}
}
return null;
}
private Plan tryNiche(int entryX, int entryZ, int dx, int dz) {
return tryNiche(entryX, entryZ, dx, dz, top(entryX, entryZ), false);
}
private Plan tryNiche(int entryX, int entryZ, int dx, int dz, int level, boolean underground) {
int cx = entryX - dx * ENTRY_DISTANCE, cz = entryZ - dz * ENTRY_DISTANCE;
if (!inside(cx, cz, RADIUS - 12) || level < 64 || level > (underground ? 160 : 300)
|| top(cx, cz) < level + 12) return null;
// A lower entrance has genuine overhead island rock, but does not need a sky view.
if (underground && top(entryX, entryZ) < level + 12) return null;
int airLimit = underground ? level + 3 : 383;
for (int y = level + 1; y <= airLimit; y++) if (solid(entryX, y, entryZ)) return null;
for (int y = level - 2; y <= level; y++) if (!solid(entryX, y, entryZ)) return null;
if (underground ? !clearOfWater(cx, level, cz) : !clearOfWater(cx, cz)) return null;
List<Cell> cells = new ArrayList<>();
Set<Position> changed = new HashSet<>();
Set<Position> supports = new HashSet<>();
for (int x = cx - 1; x <= cx + 1; x++) for (int z = cz - 1; z <= cz + 1; z++) {
for (int y = level - 1; y <= level + 2; y++) {
if (!solid(x, y, z)) return null;
add(cells, changed, x, y, z, y <= level);
}
}
// A dry lip separates the ledge from the pool. Only the air above it is opened.
for (int distance = 2; distance < ENTRY_DISTANCE; distance++) {
int x = cx + dx * distance, z = cz + dz * distance;
for (int y = level + 1; y <= level + 2; y++) {
if (solid(x, y, z)) add(cells, changed, x, y, z, false);
}
}
// Every carved column must lie under eight continuous natural rock blocks. This
// excludes soil-cap excavation at the entrance as well as exposed surface lava.
Map<Long, Integer> highest = new HashMap<>();
for (Cell cell : cells) highest.merge(key(cell.x(), cell.z()), cell.y(), Math::max);
for (var column : highest.entrySet()) {
int x = (int) (column.getKey() >> 32), z = (int) (long) column.getKey();
for (int y = column.getValue() + 1; y <= column.getValue() + 8; y++) {
if (!requireRock(supports, x, y, z)) return null;
}
}
// Three intact blocks under the pool, two-block-thick side walls, and a solid chamber
// surround prevent escape through an adjacent cave. No part of this shell is created.
for (int x = cx - 3; x <= cx + 3; x++) for (int z = cz - 3; z <= cz + 3; z++) {
for (int y = level - 4; y <= level + 2; y++) {
if (changed.contains(new Position(x, y, z))) continue;
if (!requireRock(supports, x, y, z)) return null;
}
}
// The natural roof also covers a conservative envelope around local lava fire spread.
// Its thickness keeps later surface vegetation well above the lava chamber.
for (int x = cx - 4; x <= cx + 4; x++) for (int z = cz - 4; z <= cz + 4; z++) {
for (int y = level + 3; y <= level + 10; y++) {
if (!requireRock(supports, x, y, z)) return null;
}
}
for (int distance = 2; distance <= ENTRY_DISTANCE; distance++) {
int x = cx + dx * distance, z = cz + dz * distance;
for (int y = level - 2; y <= level; y++) {
if (!requireRock(supports, x, y, z)) return null;
}
}
supports.removeAll(changed);
return new Plan(cells, supports, new Position(entryX, level + 1, entryZ));
}
private boolean requireRock(Set<Position> supports, int x, int y, int z) {
if (!solid(x, y, z)) return false;
supports.add(new Position(x, y, z));
return true;
}
private boolean clearOfWater(int x, int z) {
// The extra ten blocks include both the entrance and the complete protected shell.
int clearance = WATER_CLEARANCE + 10;
for (var cell : hydrology.cells()) {
long dx = x - cell.x(), dz = z - cell.z();
if (dx * dx + dz * dz < (long) clearance * clearance) return false;
}
for (var spring : hydrology.springs()) {
var bounds = spring.flowBounds();
long dx = Math.max(Math.max((long) bounds.minX() - x, 0), (long) x - bounds.maxX());
long dz = Math.max(Math.max((long) bounds.minZ() - z, 0), (long) z - bounds.maxZ());
if (dx * dx + dz * dz < (long) clearance * clearance) return false;
}
for (var spill : hydrology.spills()) {
var bounds = spill.flowBounds();
long dx = Math.max(Math.max((long) bounds.minX() - x, 0), (long) x - bounds.maxX());
long dz = Math.max(Math.max((long) bounds.minZ() - z, 0), (long) z - bounds.maxZ());
if (dx * dx + dz * dz < (long) clearance * clearance) return false;
}
return true;
}
private boolean clearOfWater(int x, int level, int z) {
// This box covers chamber, dry approach, intact shell and roof. A lake far above is
// allowed; an actual waterfall crossing this level still excludes the entire pocket.
var pocket = new CavernHydrology.FlowBounds(x - 10, level - 4, z - 10, x + 10, level + 10, z + 10);
for (var cell : hydrology.cells()) {
var ground = new CavernHydrology.FlowBounds(cell.x(), cell.bedY() - cell.sedimentDepth() - 1,
cell.z(), cell.x(), Math.max(cell.carveTop(), cell.waterY()), cell.z());
if (distanceSquared(pocket, ground) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
}
for (var spring : hydrology.springs()) {
if (distanceSquared(pocket, spring.flowBounds()) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
}
for (var spill : hydrology.spills()) {
if (distanceSquared(pocket, spill.flowBounds()) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
}
return true;
}
private static long distanceSquared(CavernHydrology.FlowBounds a, CavernHydrology.FlowBounds b) {
long dx = Math.max(0, Math.max((long) a.minX() - b.maxX(), (long) b.minX() - a.maxX()));
long dy = Math.max(0, Math.max((long) a.minY() - b.maxY(), (long) b.minY() - a.maxY()));
long dz = Math.max(0, Math.max((long) a.minZ() - b.maxZ(), (long) b.minZ() - a.maxZ()));
return dx * dx + dy * dy + dz * dz;
}
private int top(int x, int z) {
return heights.computeIfAbsent(key(x, z), ignored -> {
for (int y = 383; y >= 32; y -= 4) {
if (!solid(x, y, z)) continue;
for (int exact = Math.min(383, y + 3); exact > y; exact--) if (solid(x, exact, z)) return exact;
return y;
}
return -1;
});
}
private boolean solid(int x, int y, int z) { return sampler.sample(x, y, z) > 0; }
}
private static void add(List<Cell> cells, Set<Position> changed, int x, int y, int z, boolean lava) {
if (changed.add(new Position(x, y, z))) cells.add(new Cell(x, y, z, lava));
}
private static boolean inside(int x, int z, int radius) { return (long) x * x + (long) z * z < (long) radius * radius; }
private static long key(int x, int z) { return ((long) x << 32) | (z & 0xffffffffL); }
private static double random(long seed, int x, int z) {
long value = seed ^ key(x, z) ^ 0x1A7A5EEDL;
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
return ((value ^ (value >>> 31)) >>> 11) * 0x1.0p-53;
}
}
@@ -0,0 +1,241 @@
package fr.koka.sanctuary.worldgen;
import fr.koka.sanctuary.worldgen.CavernLavaDeposit.Position;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** Selects a few volcanic seeps in deep exposed rock. Sources replace rock; their outlets and
* entire falling columns are left empty for vanilla lava ticks. No retaining wall is generated. */
public final class CavernLavaFlows {
public static final int MIN_SOURCE_Y = 40;
public static final int MAX_SOURCE_Y = 160;
public static final int MAX_FALLS = 2;
public static final int HORIZONTAL_REACH = 3;
public static final int FIRE_CLEARANCE = 8;
public static final int WATER_CLEARANCE = 24;
private static final int RADIUS = 224;
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
private static final Comparator<Position> POSITION_ORDER = Comparator.comparingInt(Position::x)
.thenComparingInt(Position::z).thenComparingInt(Position::y);
private CavernLavaFlows() {}
/** flowBounds predicts undecorated rock drainage; it does not override Minecraft physics.
* supports contains only unchanged natural rock around the source, never the source itself. */
public record Fall(long id, Position source, Position outlet, List<Position> flowPath,
CavernHydrology.FlowBounds flowBounds, List<Position> supports) {
public Fall { flowPath = List.copyOf(flowPath); supports = List.copyOf(supports); }
}
public static List<Fall> create(long seed, CavernHydrology.Sampler sampler,
CavernHydrology.Plan water, CavernLavaDeposit.Plan pocket) {
return new Search(seed, sampler, water, pocket).find();
}
private record Candidate(int x, int y, int z, int dx, int dz, double score) {}
private record Node(Position at, int reach) {}
private record Trace(List<Position> path, CavernHydrology.FlowBounds bounds) {}
private static final class Search {
private final long seed;
private final CavernHydrology.Sampler sampler;
private final CavernHydrology.Plan water;
private final CavernLavaDeposit.Plan pocket;
private final List<Fall> falls = new ArrayList<>();
Search(long seed, CavernHydrology.Sampler sampler, CavernHydrology.Plan water, CavernLavaDeposit.Plan pocket) {
this.seed = seed; this.sampler = sampler; this.water = water; this.pocket = pocket;
}
List<Fall> find() {
List<Candidate> candidates = new ArrayList<>();
// Coarse probes below the surface can see a lower overhang that a heightmap hides.
for (int x = -216; x <= 216; x += 8) for (int z = -216; z <= 216; z += 8) {
if ((long) x * x + (long) z * z >= (long) RADIUS * RADIUS) continue;
for (int y = MIN_SOURCE_Y; y <= MAX_SOURCE_Y; y += 8) {
if (!solid(x, y, z)) continue;
for (int[] d : CARDINALS) {
if (solid(x + d[0] * 4, y, z + d[1] * 4)) continue;
double score = random(seed + y, x + d[0], z + d[1])
+ (MAX_SOURCE_Y - y) * .002;
candidates.add(new Candidate(x, y, z, d[0], d[1], score));
}
}
}
candidates.sort(Comparator.comparingDouble(Candidate::score).reversed()
.thenComparingInt(Candidate::x).thenComparingInt(Candidate::z)
.thenComparingInt(Candidate::y).thenComparingInt(Candidate::dx).thenComparingInt(Candidate::dz));
int attempts = 0;
for (Candidate candidate : candidates) {
if (falls.size() >= MAX_FALLS || attempts++ >= 160) break;
boolean placed = false;
for (int tangent : new int[]{0, -2, 2}) {
for (int along = 0; along < 4; along++) {
int x = candidate.x() + candidate.dx() * along - candidate.dz() * tangent;
int z = candidate.z() + candidate.dz() * along + candidate.dx() * tangent;
for (int dy : new int[]{0, 2, -2, 4, -4}) {
Position source = new Position(x, candidate.y() + dy, z);
if (source.y() < MIN_SOURCE_Y || source.y() > MAX_SOURCE_Y || !separated(source)) continue;
List<Position> supports = niche(source, candidate.dx(), candidate.dz());
if (supports == null) continue;
Position outlet = new Position(x + candidate.dx(), source.y(), z + candidate.dz());
Trace flow = trace(source, outlet);
if (flow == null || flow.bounds().minY() > source.y() - 12 || !clearOfWater(flow.bounds())) continue;
long id = mix(seed ^ (0x1A7AFA11L + falls.size() * 0x9E3779B97F4A7C15L));
falls.add(new Fall(id, source, outlet, flow.path(), flow.bounds(), supports));
placed = true;
break;
}
if (placed) break;
}
if (placed) break;
}
}
return List.copyOf(falls);
}
private boolean separated(Position source) {
CavernHydrology.FlowBounds point = bounds(source);
if (!clearOfWater(point)) return false;
for (Fall fall : falls) {
if (distanceSquared(source, fall.source()) < 80L * 80
|| distanceSquared(point, fall.flowBounds()) < 40L * 40) return false;
}
for (var cell : pocket.cells()) {
if (distanceSquared(source, new Position(cell.x(), cell.y(), cell.z())) < 32L * 32) return false;
}
return true;
}
private List<Position> niche(Position source, int dx, int dz) {
if (!solid(source)) return null;
Set<Position> supports = new HashSet<>();
// The source sits in rock, with a short intact roof, floor and three back/side faces.
// The spill column needs no floor: its absence is precisely what makes a cascade.
for (int dy = -3; dy <= 8; dy++) {
if (dy == 0) continue;
Position at = new Position(source.x(), source.y() + dy, source.z());
if (!solid(at)) return null;
supports.add(at);
}
for (int[] d : CARDINALS) {
if (d[0] == dx && d[1] == dz) continue;
for (int depth = 1; depth <= 2; depth++) {
Position at = new Position(source.x() + d[0] * depth, source.y(), source.z() + d[1] * depth);
if (!solid(at)) return null;
supports.add(at);
}
}
for (int dy = 0; dy >= -8; dy--) {
if (solid(source.x() + dx, source.y() + dy, source.z() + dz)) return null;
}
return supports.stream().sorted(POSITION_ORDER).toList();
}
private boolean clearOfWater(CavernHydrology.FlowBounds bounds) {
for (var cell : water.cells()) {
var column = new CavernHydrology.FlowBounds(cell.x(), cell.bedY() - cell.sedimentDepth() - 1,
cell.z(), cell.x(), Math.max(cell.carveTop(), cell.waterY()), cell.z());
if (distanceSquared(bounds, column) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
}
for (var spring : water.springs()) {
if (distanceSquared(bounds, spring.flowBounds()) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
}
for (var spill : water.spills()) {
if (distanceSquared(bounds, spill.flowBounds()) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
}
return true;
}
/** Descending lava resets its horizontal reach to three in this non-ultrawarm dimension.
* Explore all downhill branches through rock, so selection does not depend on chunk order. */
private Trace trace(Position source, Position outlet) {
Map<Position, Integer> reach = new HashMap<>();
Map<Position, Position> parent = new HashMap<>();
ArrayDeque<Node> queue = new ArrayDeque<>();
reach.put(source, HORIZONTAL_REACH);
queue.addLast(new Node(source, HORIZONTAL_REACH));
Position lowest = source;
while (!queue.isEmpty()) {
Node node = queue.removeFirst();
Position at = node.at();
if (node.reach() < reach.getOrDefault(at, -1)) continue;
if (at.y() < lowest.y()) lowest = at;
if (reach.size() > 12000 || Math.abs(at.x() - source.x()) > 64 || Math.abs(at.z() - source.z()) > 64) return null;
if (at.y() <= 0) continue;
Position below = new Position(at.x(), at.y() - 1, at.z());
if (!flowSolid(below, source)) {
enqueue(below, HORIZONTAL_REACH, at, reach, parent, queue);
continue;
}
if (node.reach() <= 0) continue;
for (int[] d : CARDINALS) {
Position next = new Position(at.x() + d[0], at.y(), at.z() + d[1]);
if (!flowSolid(next, source)) enqueue(next, node.reach() - 1, at, reach, parent, queue);
}
}
if (!reach.containsKey(outlet)) return null;
int minX = source.x(), maxX = minX, minY = source.y(), maxY = minY, minZ = source.z(), maxZ = minZ;
for (Position at : reach.keySet()) {
minX = Math.min(minX, at.x()); maxX = Math.max(maxX, at.x());
minY = Math.min(minY, at.y()); maxY = Math.max(maxY, at.y());
minZ = Math.min(minZ, at.z()); maxZ = Math.max(maxZ, at.z());
}
List<Position> path = new ArrayList<>();
for (Position at = lowest; at != null; at = parent.get(at)) path.add(at);
Collections.reverse(path);
return new Trace(List.copyOf(path), new CavernHydrology.FlowBounds(minX, minY, minZ, maxX, maxY, maxZ));
}
private boolean flowSolid(Position at, Position source) {
if (at.equals(source)) return false;
var cell = water.cellAt(at.x(), at.y(), at.z());
if (cell != null && at.y() > cell.bedY() && at.y() <= cell.carveTop()) return false;
return solid(at);
}
private boolean solid(Position at) { return solid(at.x(), at.y(), at.z()); }
private boolean solid(int x, int y, int z) { return sampler.sample(x, y, z) > 0; }
}
private static void enqueue(Position next, int available, Position from, Map<Position, Integer> reach,
Map<Position, Position> parent, ArrayDeque<Node> queue) {
if (available <= reach.getOrDefault(next, -1)) return;
if (!reach.containsKey(next)) parent.put(next, from);
reach.put(next, available);
queue.addLast(new Node(next, available));
}
private static CavernHydrology.FlowBounds bounds(Position at) {
return new CavernHydrology.FlowBounds(at.x(), at.y(), at.z(), at.x(), at.y(), at.z());
}
private static long distanceSquared(Position a, Position b) {
long dx = a.x() - b.x(), dy = a.y() - b.y(), dz = a.z() - b.z();
return dx * dx + dy * dy + dz * dz;
}
private static long distanceSquared(CavernHydrology.FlowBounds a, CavernHydrology.FlowBounds b) {
long dx = Math.max(0, Math.max((long) a.minX() - b.maxX(), (long) b.minX() - a.maxX()));
long dy = Math.max(0, Math.max((long) a.minY() - b.maxY(), (long) b.minY() - a.maxY()));
long dz = Math.max(0, Math.max((long) a.minZ() - b.maxZ(), (long) b.minZ() - a.maxZ()));
return dx * dx + dy * dy + dz * dz;
}
private static double random(long seed, int x, int z) {
return (mix(seed ^ ((long) x << 32) ^ (z & 0xffffffffL)) >>> 11) * 0x1.0p-53;
}
private static long mix(long value) {
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
return value ^ (value >>> 31);
}
}
@@ -0,0 +1,44 @@
package fr.koka.sanctuary.worldgen;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
/** Alpha.9 shore deposits: broad continuous patches through the existing sediment thickness. */
public final class CavernMaterials {
private CavernMaterials() {}
public static BlockState sedimentBlock(CavernHydrology.Cell cell, long seed, int depth) {
if (cell.material() == CavernHydrology.ShoreMaterial.GRASS) {
return (depth == 0 ? Blocks.GRASS_BLOCK : Blocks.DIRT).defaultBlockState();
}
if (cell.material() == CavernHydrology.ShoreMaterial.CLAY) return Blocks.CLAY.defaultBlockState();
double patch = patch(seed, cell.x(), cell.z(), 32);
Block material;
if (cell.hasWater() && patch < 0.24) material = Blocks.CLAY;
else if (patch < 0.53) material = Blocks.SAND;
else if (patch < 0.68) material = Blocks.GRAVEL;
else {
double rock = patch(seed ^ 0x4c6179657273L, cell.x(), cell.z(), 40);
material = rock < 0.28 ? Blocks.GRANITE : rock < 0.45 ? Blocks.DIORITE
: rock < 0.65 ? Blocks.ANDESITE : Blocks.STONE;
}
return material.defaultBlockState();
}
private static double patch(long seed, int x, int z, int scale) {
int gx = Math.floorDiv(x, scale), gz = Math.floorDiv(z, scale);
double tx = smooth(Math.floorMod(x, scale) / (double) scale);
double tz = smooth(Math.floorMod(z, scale) / (double) scale);
double near = lerp(value(seed, gx, gz), value(seed, gx + 1, gz), tx);
double far = lerp(value(seed, gx, gz + 1), value(seed, gx + 1, gz + 1), tx);
return lerp(near, far, tz);
}
private static double value(long seed, int x, int z) {
return (WoodlandGroveGeometry.seed(seed, x, 0, z, 0x53484f5245L) >>> 11) * 0x1.0p-53;
}
private static double smooth(double t) { return t * t * (3 - 2 * t); }
private static double lerp(double a, double b, double t) { return a + (b - a) * t; }
}
@@ -0,0 +1,41 @@
package fr.koka.sanctuary.worldgen;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.BlockPos;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.feature.Feature;
/** A few plants on existing suitable shores, after the forest has been decorated. */
public final class CavernShoreSugarCaneFeature implements Feature {
public static final MapCodec<CavernShoreSugarCaneFeature> CODEC = MapCodec.unit(CavernShoreSugarCaneFeature::new);
@Override
public MapCodec<CavernShoreSugarCaneFeature> codec() {
return CODEC;
}
@Override
public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource random, BlockPos origin) {
if (!(generator instanceof NoiseBasedChunkGenerator noise) || !CavernHydrologyRuntime.enabled(noise)) return false;
var plan = CavernHydrologyRuntime.plan(noise, level.getLevel().getChunkSource().randomState());
var cane = Blocks.SUGAR_CANE.defaultBlockState();
boolean placed = false;
for (var cell : plan.cellsInChunk(origin.getX() >> 4, origin.getZ() >> 4)) {
if (cell.hasWater() || random.nextInt(7) != 0) continue;
BlockPos base = new BlockPos(cell.x(), cell.bedY() + 1, cell.z());
if (!level.getBlockState(base).isAir() || !cane.canSurvive(level, base)) continue;
int height = 1 + random.nextInt(3);
for (int y = 0; y < height; y++) {
BlockPos pos = base.above(y);
if (!level.getBlockState(pos).isAir() || !cane.canSurvive(level, pos)) break;
setBlock(level, pos, cane);
placed = true;
}
}
return placed;
}
}
@@ -0,0 +1,108 @@
package fr.koka.sanctuary.worldgen;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.VegetationBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.material.Fluids;
/** One-time alpha.9 outlet finishing, using vanilla's persisted and consumed generation queue. */
public final class CavernSpringOutlets {
public record Pending(List<CavernHydrology.Position> sources, List<CavernHydrology.Position> outlets) {
public static final Pending EMPTY = new Pending(List.of(), List.of());
public Pending { sources = List.copyOf(sources); outlets = List.copyOf(outlets); }
public boolean isEmpty() { return sources.isEmpty() && outlets.isEmpty(); }
}
private CavernSpringOutlets() {}
/** Called before vanilla consumes the queue; a fully processed/reloaded chunk has no work. */
public static Pending capturePending(ServerLevel level, LevelChunk chunk) {
if (!(level.getChunkSource().getGenerator() instanceof NoiseBasedChunkGenerator noise)
|| !CavernHydrologyRuntime.enabled(noise)) return Pending.EMPTY;
boolean queued = false;
for (var section : chunk.getPostProcessing()) if (section != null && !section.isEmpty()) {
queued = true;
break;
}
if (!queued) return Pending.EMPTY;
var plan = CavernHydrologyRuntime.plan(noise, level.getChunkSource().randomState());
List<CavernHydrology.Position> sources = new ArrayList<>(), outlets = new ArrayList<>();
for (var spring : plan.springs()) {
if (markedHere(chunk, spring.source())) sources.add(spring.source());
if (markedHere(chunk, spring.outlet())) outlets.add(spring.outlet());
}
for (var spill : plan.spills()) {
if (markedHere(chunk, spill.source())) sources.add(spill.source());
if (markedHere(chunk, spill.outlet())) outlets.add(spill.outlet());
}
for (var fall : CavernHydrologyRuntime.lavaPlan(noise, level.getChunkSource().randomState()).falls()) {
var source = new CavernHydrology.Position(fall.source().x(), fall.source().y(), fall.source().z());
var outlet = new CavernHydrology.Position(fall.outlet().x(), fall.outlet().y(), fall.outlet().z());
if (markedHere(chunk, source)) sources.add(source);
if (markedHere(chunk, outlet)) outlets.add(outlet);
}
return new Pending(sources, outlets);
}
/** All neighbouring FEATURES have finished before a chunk reaches this stage. */
public static void finish(ServerLevel level, LevelChunk chunk, Pending pending) {
if (pending.isEmpty() || !(level.getChunkSource().getGenerator() instanceof NoiseBasedChunkGenerator noise)
|| !CavernHydrologyRuntime.enabled(noise)) return;
var random = level.getChunkSource().randomState();
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
for (var outlet : pending.outlets()) {
if (!owns(chunk, outlet)) throw new IllegalArgumentException("An outlet belongs to another chunk: " + outlet);
var modified = CavernHydrologyRuntime.plan(noise, random).cellAt(outlet.x(), outlet.y(), outlet.z());
boolean carved = modified != null && outlet.y() > modified.waterY() && outlet.y() > modified.bedY()
&& outlet.y() <= modified.carveTop();
if (!carved && random.sampleBlockValueUncached(density, outlet.x(), outlet.y(), outlet.z()) > 0) {
throw new IllegalStateException("A spring outlet must already be natural air: " + outlet);
}
BlockPos pos = blockPos(outlet);
BlockState current = chunk.getBlockState(pos);
if (removableDecoration(current)) {
// Only this declared air cell is reopened. Neighbour notification wakes the
// adjacent source even if its own chunk was post-processed first.
level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
}
}
for (var source : pending.sources()) {
if (!owns(chunk, source)) throw new IllegalArgumentException("A source belongs to another chunk: " + source);
BlockPos pos = blockPos(source);
BlockState current = chunk.getBlockState(pos);
if (current.getFluidState().isSource()) {
if (current.is(Blocks.WATER)) level.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(level));
else if (current.is(Blocks.LAVA)) level.scheduleTick(pos, Fluids.LAVA, Fluids.LAVA.getTickDelay(level));
}
}
}
private static boolean markedHere(LevelChunk chunk, CavernHydrology.Position position) {
if (!owns(chunk, position)) return false;
var queue = chunk.getPostProcessing()[chunk.getSectionIndex(position.y())];
return queue != null && queue.contains(ProtoChunk.packOffsetCoordinates(blockPos(position)));
}
private static boolean owns(LevelChunk chunk, CavernHydrology.Position position) {
return (position.x() >> 4) == chunk.getPos().x() && (position.z() >> 4) == chunk.getPos().z();
}
private static BlockPos blockPos(CavernHydrology.Position position) {
return new BlockPos(position.x(), position.y(), position.z());
}
private static boolean removableDecoration(BlockState state) {
if (state.hasBlockEntity()) return false;
return state.is(Blocks.GLOW_LICHEN) || state.is(Blocks.VINE) || state.is(Blocks.MOSS_CARPET)
|| state.getBlock() instanceof VegetationBlock || state.is(BlockTags.LEAVES) || state.is(BlockTags.LOGS);
}
}
@@ -0,0 +1,435 @@
package fr.koka.sanctuary.worldgen;
import fr.koka.sanctuary.worldgen.CavernHydrology.*;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Set;
import java.util.function.Predicate;
/** Small connected water terraces inside existing cave ledges. Each retained pool is excavated
* in rock; one declared edge may overflow. No dam, shelf, cave fill or fluid column is generated. */
public final class CavernTerraces {
public static final int MIN_Y = 56, MAX_Y = 208;
public static final int MAX_GROUPS = 2, MAX_DEPTH = 3, MAX_CARVE = 5;
private static final int RADIUS = 216, SEDIMENT = 3;
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
private static final int[][] NEIGHBORS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}};
private CavernTerraces() {}
public record Result(List<Cell> cells, List<Feature> features, List<Terrace> terraces,
List<Spill> spills, Set<Position> predictedFlow) {
public static final Result EMPTY = new Result(List.of(), List.of(), List.of(), List.of(), Set.of());
public Result {
cells = List.copyOf(cells); features = List.copyOf(features); terraces = List.copyOf(terraces);
spills = List.copyOf(spills); predictedFlow = Set.copyOf(predictedFlow);
}
}
public static Result create(long seed, Sampler sampler, CavernHydrology.Plan surface) {
return new Search(seed, sampler, surface).build();
}
private record Candidate(int x, int y, int z, int dx, int dz, double score) {}
private record Floor(int top, int bottom, int ceiling) {}
private record Node(int x, int z, double cost) {}
private record FlowNode(Position at, int reach) {}
private record Flow(Set<Position> volume, List<Position> path, FlowBounds bounds) {}
private record Pool(Map<Long, Cell> cells, Position source, Position outlet) {}
private static final Floor NO_FLOOR = new Floor(-1, -1, -1);
private static final class Search {
private final long seed;
private final Sampler sampler;
private final CavernHydrology.Plan surface;
private final Map<Position, Floor> floors = new HashMap<>();
private final List<Cell> acceptedCells = new ArrayList<>();
private final List<Feature> features = new ArrayList<>();
private final List<Terrace> terraces = new ArrayList<>();
private final List<Spill> spills = new ArrayList<>();
private final Set<Position> predicted = new HashSet<>();
Search(long seed, Sampler sampler, CavernHydrology.Plan surface) {
this.seed = seed; this.sampler = sampler; this.surface = surface;
}
Result build() {
List<Candidate> candidates = new ArrayList<>();
for (int x = -208; x <= 208; x += 8) for (int z = -208; z <= 208; z += 8) {
if (!inside(x, z)) continue;
for (int y = 64; y <= MAX_Y; y += 8) {
if (!solid(x, y, z) || solid(x, y + 8, z)) continue;
int level = y;
for (int exact = y + 7; exact > y; exact--) if (solid(x, exact, z)) { level = exact; break; }
if (level > MAX_Y || floor(x, z, level) == NO_FLOOR) continue;
for (int[] d : CARDINALS) if (!solid(x + d[0] * 4, level, z + d[1] * 4)) {
candidates.add(new Candidate(x, level, z, d[0], d[1], random(seed + level, x + d[0], z + d[1])));
}
}
}
candidates.sort(Comparator.comparingDouble(Candidate::score).reversed()
.thenComparingInt(Candidate::x).thenComparingInt(Candidate::z).thenComparingInt(Candidate::y)
.thenComparingInt(Candidate::dx).thenComparingInt(Candidate::dz));
int attempts = 0;
for (Candidate candidate : candidates) {
if (terraces.size() >= MAX_GROUPS || attempts++ >= 160) break;
boolean added = false;
for (int tangent : new int[]{0, -2, 2}) {
for (int along = 0; along < 4; along++) {
int x = candidate.x() + candidate.dx() * along - candidate.dz() * tangent;
int z = candidate.z() + candidate.dz() * along + candidate.dx() * tangent;
for (int dy : new int[]{0, -1, 1, -2, 2}) {
Position source = new Position(x, candidate.y() + dy, z);
Position outlet = new Position(x + candidate.dx(), source.y(), z + candidate.dz());
if (!mouth(source, outlet) || !separated(source)) continue;
if (addGroup(source, outlet)) { added = true; break; }
}
if (added) break;
}
if (added) break;
}
}
return new Result(acceptedCells, features, terraces, spills, predicted);
}
private boolean addGroup(Position source, Position outlet) {
long groupId = mix(seed ^ (0xA9CA7E2L + terraces.size() * 0x9E3779B97F4A7C15L));
long upperId = mix(groupId + 1), lowerId = mix(groupId + 2);
Pool upper = grow(source, source.y(), source, outlet, upperId);
if (upper == null) return false;
// The first impact on a slope is often a thin lip. Follow natural drainage across
// its ledges, then find a supported receiving pool at a genuinely lower level.
// The same bounded fluid trace is used before and after carving the receiving pool.
Flow drainage = trace(source, outlet, new ArrayList<>(upper.cells().values()));
if (drainage == null) return false;
List<Position> contacts = new ArrayList<>();
for (Position at : drainage.volume()) {
int level = at.y() - 1;
if (level < MIN_Y || level > source.y() - 4 || !solid(at.x(), level, at.z())) continue;
Floor ground = floor(at.x(), at.z(), level);
if (ground != NO_FLOOR && ground.bottom() <= level - 7)
contacts.add(new Position(at.x(), level, at.z()));
}
contacts.sort(Comparator.comparingInt(Position::y).reversed()
.thenComparingInt(Position::x).thenComparingInt(Position::z));
Pool lower = null;
Position landing = null;
int attempts = 0;
for (Position contact : contacts) {
if (attempts++ >= 48) break;
lower = grow(contact, contact.y(), null, null, lowerId);
if (lower != null) { landing = contact; break; }
}
if (lower == null) return false;
List<Cell> groupCells = new ArrayList<>(upper.cells().values());
groupCells.addAll(lower.cells().values());
if (overlapping(groupCells)) return false;
Flow inlet = trace(source, outlet, groupCells);
if (inlet == null || !reaches(inlet, lower)) return false;
// If the same lower ledge has another natural edge, connect it to an open terminal
// fall. Otherwise its second pool stays closed; the first inter-pool cascade remains.
Pool drained = terminalPool(landing, lowerId);
Flow terminal = null;
if (drained != null) {
List<Cell> replacement = new ArrayList<>(upper.cells().values());
replacement.addAll(drained.cells().values());
if (!overlapping(replacement)) {
Flow in = trace(source, outlet, replacement);
Flow out = trace(drained.source(), drained.outlet(), replacement);
if (in != null && reaches(in, drained) && out != null
&& out.bounds().minY() <= drained.source().y() - 12) {
lower = drained; inlet = in; terminal = out; groupCells = replacement;
}
}
}
acceptedCells.addAll(groupCells);
features.add(feature(upperId, upper, source));
features.add(feature(lowerId, lower, landing));
terraces.add(new Terrace(groupId, List.of(upperId, lowerId)));
spills.add(new Spill(mix(groupId + 3), upperId, lowerId, source, outlet, inlet.path(), inlet.bounds()));
predicted.addAll(inlet.volume());
if (terminal != null) {
spills.add(new Spill(mix(groupId + 4), lowerId, -1, lower.source(), lower.outlet(), terminal.path(), terminal.bounds()));
predicted.addAll(terminal.volume());
}
return true;
}
private Pool terminalPool(Position center, long id) {
List<Position> candidates = new ArrayList<>();
for (int dx = -12; dx <= 12; dx++) for (int dz = -12; dz <= 12; dz++) {
if (dx * dx + dz * dz > 144) continue;
candidates.add(new Position(center.x() + dx, center.y(), center.z() + dz));
}
candidates.sort(Comparator.comparingDouble((Position p) -> Math.hypot(p.x() - center.x(), p.z() - center.z()))
.thenComparingInt(Position::x).thenComparingInt(Position::z));
int tried = 0;
for (Position source : candidates) for (int[] d : CARDINALS) {
Position outlet = new Position(source.x() + d[0], source.y(), source.z() + d[1]);
if (!mouth(source, outlet)) continue;
if (tried++ >= 12) return null;
Pool pool = grow(center, center.y(), source, outlet, id);
if (pool != null) return pool;
}
return null;
}
private Pool grow(Position center, int level, Position source, Position outlet, long id) {
PriorityQueue<Node> queue = new PriorityQueue<>(Comparator.comparingDouble(Node::cost)
.thenComparingInt(Node::x).thenComparingInt(Node::z));
Map<Long, Double> costs = new HashMap<>();
Set<Long> wet = new HashSet<>();
long origin = key(center.x(), center.z());
queue.add(new Node(center.x(), center.z(), 0)); costs.put(origin, 0.0);
int target = 100 + (int) (random(seed + level, center.x(), center.z()) * 80);
while (!queue.isEmpty() && wet.size() < target) {
Node node = queue.remove(); long k = key(node.x(), node.z());
if (wet.contains(k) || node.cost() > costs.getOrDefault(k, Double.POSITIVE_INFINITY)
|| !poolColumn(node.x(), node.z(), level, source, outlet)) continue;
wet.add(k);
for (int[] d : NEIGHBORS) {
int x = node.x() + d[0], z = node.z() + d[1]; long next = key(x, z);
if (Math.hypot(x - center.x(), z - center.z()) > 16 || wet.contains(next)) continue;
double cost = node.cost() + Math.hypot(d[0], d[1]) * (1 + noise(x, z, 11) * 1.8);
if (cost >= costs.getOrDefault(next, Double.POSITIVE_INFINITY)) continue;
costs.put(next, cost); queue.add(new Node(x, z, cost));
}
}
wet = connected(wet, origin);
if (wet.size() < 40 || source != null && !wet.contains(key(source.x(), source.z()))) return null;
Map<Long, Cell> cells = new HashMap<>();
for (long k : wet) {
int x = (int) (k >> 32), z = (int) k;
boolean edge = false;
for (int[] d : CARDINALS) edge |= !wet.contains(key(x + d[0], z + d[1]));
int depth = edge ? 1 : 2 + (noise(x, z, 9) > .6 ? 1 : 0);
Floor floor = floor(x, z, level);
int bed = level - depth;
if (floor == NO_FLOOR || floor.top() - bed > MAX_CARVE || floor.bottom() > bed - SEDIMENT - 1) return null;
ShoreMaterial material = noise(x + 101, z - 87, 19) > .42 ? ShoreMaterial.CLAY : ShoreMaterial.STONE;
cells.put(k, new Cell(x, z, level, bed, floor.top(), material, id, SEDIMENT));
}
for (Cell cell : cells.values()) for (int[] d : CARDINALS) {
int x = cell.x() + d[0], z = cell.z() + d[1];
Cell next = cells.get(key(x, z));
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) {
if (next != null && y > next.bedY()) continue;
if (source != null && cell.x() == source.x() && cell.z() == source.z()
&& x == outlet.x() && z == outlet.z() && y == outlet.y()) continue;
if (!solid(x, y, z)) return null;
}
}
return new Pool(Map.copyOf(cells), source, outlet);
}
private boolean poolColumn(int x, int z, int level, Position source, Position outlet) {
if (!inside(x, z)) return false;
Floor floor = floor(x, z, level);
if (floor == NO_FLOOR || floor.bottom() > level - 7 || !available(x, z, level - 8, floor.top())) return false;
for (int[] d : CARDINALS) {
if (source != null && x == source.x() && z == source.z()
&& x + d[0] == outlet.x() && z + d[1] == outlet.z()) continue;
if (!solid(x + d[0], level, z + d[1])) return false;
}
return true;
}
private Floor floor(int x, int z, int level) {
return floors.computeIfAbsent(new Position(x, level, z), ignored -> {
if (level < MIN_Y || level > MAX_Y) return NO_FLOOR;
int top = -1;
for (int y = level + 2; y >= level; y--) {
if (solid(x, y, z)) { top = y; break; }
}
if (top < 0) return NO_FLOOR;
for (int y = top + 1; y <= top + 4; y++) if (solid(x, y, z)) return NO_FLOOR;
int ceiling = top + 5;
while (ceiling < 384 && !solid(x, ceiling, z)) ceiling++;
if (ceiling >= 384) return NO_FLOOR;
int bottom = top;
while (bottom > top - 16 && solid(x, bottom - 1, z)) bottom--;
return new Floor(top, bottom, ceiling);
});
}
private boolean mouth(Position source, Position outlet) {
if (source.y() < MIN_Y || source.y() > MAX_Y || !solid(source.x(), source.y(), source.z())) return false;
// An adjacent natural step may receive the water before it descends again. Requiring
// a vertical shaft here would discard the gentle ledges that form cave terraces.
if (solid(outlet.x(), outlet.y(), outlet.z())) return false;
return floor(source.x(), source.z(), source.y()) != NO_FLOOR;
}
private boolean available(int x, int z, int low, int high) {
for (Cell cell : surface.cellsAt(x, z)) if (low <= cell.carveTop() + 4
&& high >= cell.bedY() - cell.sedimentDepth() - 5) return false;
for (Cell cell : acceptedCells) if (cell.x() == x && cell.z() == z && low <= cell.carveTop() + 4
&& high >= cell.bedY() - cell.sedimentDepth() - 5) return false;
for (Spring spring : surface.springs()) if (inBounds(x, low, high, z, spring.flowBounds(), 6)) return false;
for (Spill spill : spills) if (inBounds(x, low, high, z, spill.flowBounds(), 6)) return false;
return true;
}
private boolean separated(Position source) {
for (Spill spill : spills) if (Math.hypot(source.x() - spill.source().x(), source.z() - spill.source().z()) < 64) return false;
return available(source.x(), source.z(), source.y() - 8, source.y() + 4);
}
private static boolean overlapping(List<Cell> cells) {
Map<Long, List<Cell>> columns = new HashMap<>();
for (Cell cell : cells) {
var existing = columns.computeIfAbsent(key(cell.x(), cell.z()), ignored -> new ArrayList<>());
for (Cell old : existing) if (cell.bedY() - cell.sedimentDepth() - 1 <= old.carveTop()
&& old.bedY() - old.sedimentDepth() - 1 <= cell.carveTop()) return true;
existing.add(cell);
}
return false;
}
private Flow trace(Position source, Position outlet, List<Cell> group) {
Map<Long, List<Cell>> changes = new HashMap<>();
Set<Position> retained = new HashSet<>();
for (Cell cell : group) {
changes.computeIfAbsent(key(cell.x(), cell.z()), ignored -> new ArrayList<>()).add(cell);
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) retained.add(new Position(cell.x(), y, cell.z()));
}
Map<Position, Boolean> solidCache = new HashMap<>();
Predicate<Position> open = at -> !solidCache.computeIfAbsent(at, p -> flowSolid(p, changes));
Predicate<Position> canEnter = at -> !retained.contains(at) && open.test(at);
Predicate<Position> hole = at -> open.test(new Position(at.x(), at.y() - 1, at.z()));
Map<Position, Integer> reach = new HashMap<>(); Map<Position, Position> parent = new HashMap<>();
ArrayDeque<FlowNode> queue = new ArrayDeque<>();
reach.put(source, 7); queue.addLast(new FlowNode(source, 7)); Position lowest = source;
while (!queue.isEmpty()) {
FlowNode node = queue.removeFirst(); Position at = node.at();
if (node.reach() < reach.getOrDefault(at, -1)) continue;
if (at.y() < lowest.y()) lowest = at;
if (reach.size() > 16000 || Math.abs(at.x() - source.x()) > 64 || Math.abs(at.z() - source.z()) > 64) return null;
// Vanilla never replaces an existing source. Record the arriving contact,
// but do not manufacture a route through the separately retained basin.
if (retained.contains(at) && !at.equals(source)) continue;
if (at.y() <= 0) continue;
Position below = new Position(at.x(), at.y() - 1, at.z());
if (open.test(below)) { enqueue(below, 7, at, reach, parent, queue); continue; }
if (node.reach() <= 0) continue;
for (Position next : downhillChoices(at, canEnter, hole))
enqueue(next, node.reach() - 1, at, reach, parent, queue);
}
if (!reach.containsKey(outlet)) return null;
int minX = source.x(), maxX = minX, minY = source.y(), maxY = minY, minZ = source.z(), maxZ = minZ;
for (Position at : reach.keySet()) {
minX = Math.min(minX, at.x()); maxX = Math.max(maxX, at.x()); minY = Math.min(minY, at.y()); maxY = Math.max(maxY, at.y());
minZ = Math.min(minZ, at.z()); maxZ = Math.max(maxZ, at.z());
}
List<Position> path = new ArrayList<>();
for (Position at = lowest; at != null; at = parent.get(at)) path.add(at);
Collections.reverse(path);
return new Flow(Set.copyOf(reach.keySet()), List.copyOf(path), new FlowBounds(minX, minY, minZ, maxX, maxY, maxZ));
}
private boolean flowSolid(Position at, Map<Long, List<Cell>> changes) {
for (Cell cell : changes.getOrDefault(key(at.x(), at.z()), List.of()))
if (at.y() > cell.bedY() && at.y() <= cell.carveTop()) return false;
Cell surfaceCell = surface.cellAt(at.x(), at.y(), at.z());
if (surfaceCell != null && at.y() > surfaceCell.bedY() && at.y() <= surfaceCell.carveTop()) return false;
return solid(at.x(), at.y(), at.z());
}
private static boolean reaches(Flow flow, Pool pool) {
return flow.volume().stream().anyMatch(at -> {
Cell cell = pool.cells().get(key(at.x(), at.z()));
return cell != null && at.y() > cell.bedY() && at.y() <= cell.waterY();
});
}
private static Feature feature(long id, Pool pool, Position center) {
int minX = Integer.MAX_VALUE, minZ = minX, maxX = Integer.MIN_VALUE, maxZ = maxX;
for (Cell cell : pool.cells().values()) {
minX = Math.min(minX, cell.x()); maxX = Math.max(maxX, cell.x()); minZ = Math.min(minZ, cell.z()); maxZ = Math.max(maxZ, cell.z());
}
return new Feature(id, Kind.TERRACE, center.y(), List.of(new Point(center.x(), center.z())), pool.cells().size(),
new Bounds(minX, minZ, maxX, maxZ));
}
private boolean solid(int x, int y, int z) { return sampler.sample(x, y, z) > 0; }
private double noise(int x, int z, int scale) {
int gx = Math.floorDiv(x, scale), gz = Math.floorDiv(z, scale);
double tx = smooth(Math.floorMod(x, scale) / (double) scale), tz = smooth(Math.floorMod(z, scale) / (double) scale);
double a = random(seed, gx, gz) * (1 - tx) + random(seed, gx + 1, gz) * tx;
double b = random(seed, gx, gz + 1) * (1 - tx) + random(seed, gx + 1, gz + 1) * tx;
return a * (1 - tz) + b * tz;
}
}
/** WaterFluid's bare-rock downhill choice: consider holes through four recursive
* steps, keep only the nearest directions (including ties), spread everywhere
* only when no direction finds a hole. Retained sources cannot be entered. */
static List<Position> downhillChoices(Position at, Predicate<Position> canEnter, Predicate<Position> hole) {
List<Position> selected = new ArrayList<>();
int best = 1000;
for (int[] d : CARDINALS) {
Position next = new Position(at.x() + d[0], at.y(), at.z() + d[1]);
if (!canEnter.test(next)) continue;
int cost = hole.test(next) ? 0 : slopeDistance(next, 1, -d[0], -d[1], canEnter, hole);
if (cost < best) selected.clear();
if (cost <= best) { selected.add(next); best = cost; }
}
return selected;
}
private static int slopeDistance(Position at, int distance, int backX, int backZ,
Predicate<Position> canEnter, Predicate<Position> hole) {
int best = 1000;
for (int[] d : CARDINALS) {
if (d[0] == backX && d[1] == backZ) continue;
Position next = new Position(at.x() + d[0], at.y(), at.z() + d[1]);
if (!canEnter.test(next)) continue;
if (hole.test(next)) return distance;
if (distance < 4) best = Math.min(best,
slopeDistance(next, distance + 1, -d[0], -d[1], canEnter, hole));
}
return best;
}
private static void enqueue(Position next, int amount, Position from, Map<Position, Integer> reach,
Map<Position, Position> parent, ArrayDeque<FlowNode> queue) {
if (amount <= reach.getOrDefault(next, -1)) return;
if (!reach.containsKey(next)) parent.put(next, from);
reach.put(next, amount); queue.addLast(new FlowNode(next, amount));
}
private static Set<Long> connected(Set<Long> footprint, long origin) {
if (!footprint.contains(origin)) return Set.of();
Set<Long> seen = new HashSet<>(); ArrayDeque<Long> queue = new ArrayDeque<>();
seen.add(origin); queue.add(origin);
while (!queue.isEmpty()) {
long at = queue.removeFirst(); int x = (int) (at >> 32), z = (int) at;
for (int[] d : CARDINALS) {
long next = key(x + d[0], z + d[1]);
if (footprint.contains(next) && seen.add(next)) queue.addLast(next);
}
}
return seen;
}
private static boolean inBounds(int x, int low, int high, int z, FlowBounds b, int margin) {
return x >= b.minX() - margin && x <= b.maxX() + margin && z >= b.minZ() - margin && z <= b.maxZ() + margin
&& low <= b.maxY() + margin && high >= b.minY() - margin;
}
private static boolean inside(int x, int z) { return (long) x * x + (long) z * z < (long) RADIUS * RADIUS; }
private static long key(int x, int z) { return ((long) x << 32) | (z & 0xffffffffL); }
private static double smooth(double t) { return t * t * (3 - 2 * t); }
private static double random(long seed, int x, int z) { return (mix(seed ^ key(x, z)) >>> 11) * 0x1.0p-53; }
private static long mix(long value) {
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
return value ^ (value >>> 31);
}
}
@@ -0,0 +1,20 @@
package fr.koka.sanctuary.worldgen;
/** Broad, seed-warped vertical bands; none changes terrain density or guarantees a resource. */
public final class LayeredBiomePalette {
public enum Kind { MEADOW, FLOWER_MEADOW, LOWER_GROVE, MOSSY_TERRACES, ROCKY_TERRACES }
private LayeredBiomePalette() {}
public static Kind select(int blockY, float variation, float moisture) {
double elevation = blockY - Math.clamp(variation, -1.0F, 1.0F) * 28
- Math.clamp(moisture, -1.0F, 1.0F) * 12;
if (elevation < 154) return moisture < -0.30F ? Kind.ROCKY_TERRACES : Kind.MOSSY_TERRACES;
if (elevation < 214) {
if (variation > 0.34F) return Kind.ROCKY_TERRACES;
return moisture > 0.02F ? Kind.LOWER_GROVE : Kind.MOSSY_TERRACES;
}
if (variation > 0.22F) return Kind.ROCKY_TERRACES;
return moisture > -0.12F ? Kind.FLOWER_MEADOW : Kind.MEADOW;
}
}
@@ -0,0 +1,633 @@
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.6 surface basins, rocky shores and layered 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 LayeredHydrology {
public static final int RADIUS = 192;
public static final int MAX_CARVE = 4;
public static final int MAX_DEPTH = 3;
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 LayeredHydrology() {}
@FunctionalInterface
public interface Sampler { float sample(int x, int y, int z); }
public enum Kind { POND, LAKE }
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 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();
int attempts = 0;
for (Candidate candidate : candidates) {
if (features.size() >= 5 || 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 ? 190 + (int) (random(seed, center.x(), center.z()) * 130)
: 55 + (int) (random(seed + 1, center.x(), center.z()) * 55);
// 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}) {
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;
}
}
addSedimentSheets();
addLowerWallSprings();
addSprings();
return new Plan(cells, features, springs, springFlow, columns.size(), samples);
}
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 - 16) && 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 + MAX_CARVE
&& 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()) > 28 || 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() >= 420 || Math.hypot(x - center.x(), z - center.z()) > 32
|| !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, MAX_CARVE - 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);
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;
Column column = column(node.x(), node.z());
Cell existing = cells.get(key(node.x(), node.z()));
if (existing == null) {
double extent = 9 + field(node.x(), node.z(), 25, 131) * 10;
if (node.cost() > extent) continue;
int delta = column.top() - node.level();
if (delta < 0 || delta > 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() / 4.5);
int bed = Math.max(node.level(), Math.max(column.top() - 2, 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() + 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 > 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 (nearModifiedGround(source, 32) || !separatedSpring(source) || !springNiche(source, cliff.dx(), cliff.dz())) 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());
double horizontal = Math.hypot(source.x() - cell.x(), source.z() - cell.z());
if (Math.hypot(horizontal, dy) < 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 (nearModifiedGround(source, 32) || !separatedSpring(source)
|| !springNiche(source, candidate.dx(), candidate.dz())) 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,148 @@
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.6 only: immutable plans cached by world state, applied to fresh terrain before decoration. */
public final class LayeredHydrologyRuntime {
private static final Map<RandomState, LayeredHydrology.Plan> PLANS =
Collections.synchronizedMap(new WeakHashMap<>());
private static final Map<RandomState, LayeredLavaDeposit.Plan> LAVA_PLANS =
Collections.synchronizedMap(new WeakHashMap<>());
private LayeredHydrologyRuntime() {}
public static boolean enabled(NoiseBasedChunkGenerator generator) {
return generator.generatorSettings().is(SanctuarySpawn.LAYERED_SETTINGS);
}
public static LayeredHydrology.Plan layeredPlan(ServerLevel level) {
return plan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(),
level.getChunkSource().randomState());
}
public static LayeredHydrology.Plan plan(NoiseBasedChunkGenerator generator, RandomState randomState) {
if (!enabled(generator)) throw new IllegalArgumentException("Layered 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 = LayeredHydrology.create(state.seed(),
(x, y, z) -> state.sampleBlockValueUncached(density, x, y, z));
SanctuaryMod.LOGGER.info("Sanctuary layered hydrology: seed {}, {} features, {} columns, {} springs, {} ms",
state.seed(), plan.features().size(), plan.cells().size(), plan.springs().size(),
(System.nanoTime() - start) / 1_000_000);
return plan;
});
}
public static LayeredLavaDeposit.Plan lavaPlan(ServerLevel level) {
return lavaPlan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(),
level.getChunkSource().randomState());
}
public static LayeredLavaDeposit.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 = LayeredLavaDeposit.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 = plan.springs().stream().map(LayeredHydrology.Spring::outlet)
.filter(outlet -> (outlet.x() >> 4) == chunkPos.x() && (outlet.z() >> 4) == chunkPos.z()).toList();
var lava = lavaPlan(generator, randomState);
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 = switch (cell.material()) {
case SAND -> Blocks.SAND.defaultBlockState();
case GRAVEL -> Blocks.GRAVEL.defaultBlockState();
case CLAY -> Blocks.CLAY.defaultBlockState();
case STONE -> Blocks.STONE.defaultBlockState();
case GRASS -> (depth == 0 ? Blocks.GRASS_BLOCK : Blocks.DIRT).defaultBlockState();
};
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));
}
private static void requireSolid(ChunkAccess chunk, BlockPos pos, long seed) {
if (!chunk.getBlockState(pos).isCollisionShapeFullBlock(chunk, pos)) {
throw new IllegalStateException("Layered hydrology differs from terrain at " + pos + " for seed " + seed);
}
}
}
@@ -0,0 +1,73 @@
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;
/** Alpha.6 palette: flowered upper meadows and distinct humid lower terraces. */
public final class LayeredIslandBiomeSource extends BiomeSource {
public static final ResourceKey<Biome> MEADOW = key("layered_meadow");
public static final ResourceKey<Biome> FLOWER_MEADOW = key("layered_flower_meadow");
public static final ResourceKey<Biome> LOWER_GROVE = key("layered_lower_grove");
public static final ResourceKey<Biome> MOSSY_TERRACES = key("layered_mossy_terraces");
public static final ResourceKey<Biome> ROCKY_TERRACES = key("layered_rocky_terraces");
public static final MapCodec<LayeredIslandBiomeSource> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
Biome.CODEC.fieldOf("meadow").forGetter(source -> source.meadow),
Biome.CODEC.fieldOf("flower_meadow").forGetter(source -> source.flowerMeadow),
Biome.CODEC.fieldOf("lower_grove").forGetter(source -> source.lowerGrove),
Biome.CODEC.fieldOf("mossy_terraces").forGetter(source -> source.mossyTerraces),
Biome.CODEC.fieldOf("rocky_terraces").forGetter(source -> source.rockyTerraces)
).apply(instance, LayeredIslandBiomeSource::new));
private final Holder<Biome> meadow;
private final Holder<Biome> flowerMeadow;
private final Holder<Biome> lowerGrove;
private final Holder<Biome> mossyTerraces;
private final Holder<Biome> rockyTerraces;
public LayeredIslandBiomeSource(Holder<Biome> meadow, Holder<Biome> flowerMeadow,
Holder<Biome> lowerGrove, Holder<Biome> mossyTerraces, Holder<Biome> rockyTerraces) {
this.meadow = meadow;
this.flowerMeadow = flowerMeadow;
this.lowerGrove = lowerGrove;
this.mossyTerraces = mossyTerraces;
this.rockyTerraces = rockyTerraces;
}
@Override
protected MapCodec<LayeredIslandBiomeSource> codec() { return CODEC; }
@Override
protected Stream<Holder<Biome>> collectPossibleBiomes() {
return Stream.of(meadow, flowerMeadow, lowerGrove, mossyTerraces, rockyTerraces);
}
@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 (LayeredBiomePalette.select(y, variation, moisture)) {
case MEADOW -> meadow;
case FLOWER_MEADOW -> flowerMeadow;
case LOWER_GROVE -> lowerGrove;
case MOSSY_TERRACES -> mossyTerraces;
case ROCKY_TERRACES -> rockyTerraces;
};
};
}
private static ResourceKey<Biome> key(String path) {
return ResourceKey.create(Registries.BIOME, SanctuaryMod.id(path));
}
}
@@ -0,0 +1,223 @@
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;
/** One small, covered lava pocket in existing rock, reached from an existing outdoor ledge.
* No solid blocks are added, and unsuitable seeds are allowed to produce no deposit. */
public final class LayeredLavaDeposit {
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 LayeredLavaDeposit() {}
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 Plan(List<Cell> cells, Set<Position> supports, Position access) {
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; }
/** Player feet on the existing ledge; this column was naturally open to the 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, LayeredHydrology.Sampler sampler, LayeredHydrology.Plan hydrology) {
return new Search(seed, sampler, hydrology).find();
}
private record Candidate(int x, int z, int dx, int dz, double score) {}
private static final class Search {
private final long seed;
private final LayeredHydrology.Sampler sampler;
private final LayeredHydrology.Plan hydrology;
private final Map<Long, Integer> heights = new HashMap<>();
Search(long seed, LayeredHydrology.Sampler sampler, LayeredHydrology.Plan hydrology) {
this.seed = seed;
this.sampler = sampler;
this.hydrology = hydrology;
}
Plan find() {
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);
}
private Plan tryNiche(int entryX, int entryZ, int dx, int dz) {
int cx = entryX - dx * ENTRY_DISTANCE, cz = entryZ - dz * ENTRY_DISTANCE;
if (!inside(cx, cz, RADIUS - 12) || !clearOfWater(cx, cz)) return null;
int level = top(entryX, entryZ);
if (level < 64 || level > 300 || top(cx, cz) < level + 12) return null;
// The player can discover and enter from an actual outdoor ledge, not a sealed cave.
for (int y = level + 1; y < 384; y++) if (solid(entryX, y, entryZ)) return null;
for (int y = level - 2; y <= level; y++) if (!solid(entryX, y, entryZ)) 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 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,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 LayeredShoreSugarCaneFeature implements Feature {
public static final MapCodec<LayeredShoreSugarCaneFeature> CODEC = MapCodec.unit(LayeredShoreSugarCaneFeature::new);
@Override
public MapCodec<LayeredShoreSugarCaneFeature> codec() {
return CODEC;
}
@Override
public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource random, BlockPos origin) {
if (!(generator instanceof NoiseBasedChunkGenerator noise) || !LayeredHydrologyRuntime.enabled(noise)) return false;
var plan = LayeredHydrologyRuntime.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,94 @@
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.6 outlet finishing, using vanilla's persisted and consumed generation queue. */
public final class LayeredSpringOutlets {
public record Pending(List<LayeredHydrology.Position> sources, List<LayeredHydrology.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 LayeredSpringOutlets() {}
/** 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)
|| !LayeredHydrologyRuntime.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 = LayeredHydrologyRuntime.plan(noise, level.getChunkSource().randomState());
List<LayeredHydrology.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());
}
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)
|| !LayeredHydrologyRuntime.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);
if (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.is(Blocks.WATER) && current.getFluidState().isSource()) {
level.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(level));
}
}
}
private static boolean markedHere(LevelChunk chunk, LayeredHydrology.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, LayeredHydrology.Position position) {
return (position.x() >> 4) == chunk.getPos().x() && (position.z() >> 4) == chunk.getPos().z();
}
private static BlockPos blockPos(LayeredHydrology.Position position) {
return new BlockPos(position.x(), position.y(), position.z());
}
private static boolean removableDecoration(BlockState state) {
if (state.hasBlockEntity()) return false;
return state.is(Blocks.GLOW_LICHEN) || state.is(Blocks.VINE) || state.is(Blocks.MOSS_CARPET)
|| state.getBlock() instanceof VegetationBlock || state.is(BlockTags.LEAVES) || state.is(BlockTags.LOGS);
}
}
@@ -0,0 +1,21 @@
package fr.koka.sanctuary.worldgen;
/** Geometry checks shared by lower-ledge decoration and density diagnostics. */
public final class LowerTerraceEcology {
@FunctionalInterface
public interface Sampler { float sample(int x, int y, int z); }
private LowerTerraceEcology() {}
public static int highestNaturalSurface(Sampler sampler, int x, int z) {
for (int y = 383; y >= 24; y--) if (sampler.sample(x, y, z) > 0) return y;
return -1;
}
/** A lower natural floor: three solid blocks, headroom, and real terrain at least eight blocks above. */
public static boolean isLowerTerrace(Sampler sampler, int x, int bedY, int z, int highestNaturalSurface) {
if (bedY < 32 || bedY > 312 || highestNaturalSurface < bedY + 8) return false;
for (int y = bedY - 2; y <= bedY; y++) if (sampler.sample(x, y, z) <= 0) return false;
return sampler.sample(x, bedY + 1, z) <= 0 && sampler.sample(x, bedY + 2, z) <= 0;
}
}
@@ -0,0 +1,126 @@
package fr.koka.sanctuary.worldgen;
import com.mojang.serialization.MapCodec;
import java.util.HashSet;
import java.util.Set;
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.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
import net.minecraft.world.level.levelgen.feature.Feature;
/** Decorates actual lower natural floors after the ordinary highest-surface vegetation.
* Each chunk owns its columns; planned water, sediments, lava and their supports are excluded. */
public final class LowerTerraceVegetationFeature implements Feature {
public static final MapCodec<LowerTerraceVegetationFeature> CODEC = MapCodec.unit(LowerTerraceVegetationFeature::new);
@Override
public MapCodec<LowerTerraceVegetationFeature> codec() { return CODEC; }
@Override
public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource random, BlockPos origin) {
if (!(generator instanceof NoiseBasedChunkGenerator noise) || !LayeredHydrologyRuntime.enabled(noise)) return false;
int minX = (origin.getX() >> 4) * 16, minZ = (origin.getZ() >> 4) * 16;
if (Math.abs((long) minX) > 288 || Math.abs((long) minZ) > 288) return false;
var state = level.getLevel().getChunkSource().randomState();
var water = LayeredHydrologyRuntime.plan(noise, state);
var lava = LayeredHydrologyRuntime.lavaPlan(noise, state);
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
LowerTerraceEcology.Sampler sampler = (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z);
var climate = state.createClimateSampler(SamplerContext.EMPTY_UNCACHED);
Set<Long> lavaColumns = new HashSet<>();
for (var cell : lava.cells()) protectColumn(lavaColumns, cell.x(), cell.z());
for (var support : lava.supports()) protectColumn(lavaColumns, support.x(), support.z());
boolean changed = false;
BlockPos.MutableBlockPos floor = new BlockPos.MutableBlockPos();
for (int x = minX; x < minX + 16; x++) for (int z = minZ; z < minZ + 16; z++) {
if (blockedColumn(water, lavaColumns, x, z)) continue;
// A coherent moisture patch controls coverage, rather than an alternating block grid.
float growth = climate.humidity().sampleValue(x + 317, 0, z - 191);
if (growth < -0.30F) continue;
int highest = -1, terraces = 0;
for (int y = 319; y >= 32; y--) {
floor.set(x, y, z);
BlockState ground = level.getBlockState(floor);
if (!replaceableGround(ground)) continue;
if (highest < 0) {
if (sampler.sample(x, y, z) > 0) highest = y;
continue;
}
if (y > highest - 8) continue;
BlockPos plantPos = floor.above();
if (!level.getBlockState(plantPos).isAir() || !level.getBlockState(plantPos.above()).isAir()) continue;
if (!LowerTerraceEcology.isLowerTerrace(sampler, x, y, z, highest)) continue;
if (!ground.isCollisionShapeFullBlock(level, floor)
|| !level.getBlockState(floor.below()).isCollisionShapeFullBlock(level, floor.below())
|| !level.getBlockState(floor.below(2)).isCollisionShapeFullBlock(level, floor.below(2))) continue;
var biome = level.getBiome(plantPos);
if (biome.is(LayeredIslandBiomeSource.ROCKY_TERRACES)) continue;
boolean mossy = biome.is(LayeredIslandBiomeSource.MOSSY_TERRACES);
// Moss survives shaded ledges without needing artificial lighting. Existing grass
// is retained outside the moss biome; no supporting terrain is added to air.
if (mossy || !ground.is(Blocks.GRASS_BLOCK)) {
setBlock(level, floor, Blocks.MOSS_BLOCK.defaultBlockState());
changed = true;
}
if (random.nextInt(5) < 2) {
BlockState plant = plant(mossy, random.nextInt(10));
if (plant.canSurvive(level, plantPos)) {
setBlock(level, plantPos, plant);
changed = true;
}
}
if (++terraces >= 4) break;
}
}
return changed;
}
private static BlockState plant(boolean mossy, int choice) {
if (mossy) return switch (choice) {
case 0 -> Blocks.FLOWERING_AZALEA.defaultBlockState();
case 1 -> Blocks.AZALEA.defaultBlockState();
case 2, 3 -> Blocks.FERN.defaultBlockState();
default -> Blocks.MOSS_CARPET.defaultBlockState();
};
return switch (choice) {
case 0 -> Blocks.OXEYE_DAISY.defaultBlockState();
case 1 -> Blocks.CORNFLOWER.defaultBlockState();
case 2 -> Blocks.AZURE_BLUET.defaultBlockState();
case 3 -> Blocks.DANDELION.defaultBlockState();
case 4 -> Blocks.BUSH.defaultBlockState();
default -> Blocks.SHORT_GRASS.defaultBlockState();
};
}
private static boolean replaceableGround(BlockState state) {
return state.is(Blocks.STONE) || state.is(Blocks.GRANITE) || state.is(Blocks.DIORITE)
|| state.is(Blocks.ANDESITE) || state.is(Blocks.TUFF) || state.is(Blocks.DIRT)
|| state.is(Blocks.GRASS_BLOCK) || state.is(Blocks.ROOTED_DIRT) || state.is(Blocks.COARSE_DIRT)
|| state.is(Blocks.MOSS_BLOCK);
}
private static boolean blockedColumn(LayeredHydrology.Plan water, Set<Long> lavaColumns, int x, int z) {
if (lavaColumns.contains(key(x, z))) return true;
for (int dx = -1; dx <= 1; dx++) for (int dz = -1; dz <= 1; dz++) {
if (water.cellAt(x + dx, z + dz) != null) return true;
}
for (var spring : water.springs()) if (inside(spring.flowBounds(), x, z)) return true;
return false;
}
private static boolean inside(LayeredHydrology.FlowBounds bounds, int x, int z) {
return x >= bounds.minX() - 2 && x <= bounds.maxX() + 2
&& z >= bounds.minZ() - 2 && z <= bounds.maxZ() + 2;
}
private static void protectColumn(Set<Long> columns, int x, int z) {
for (int dx = -2; dx <= 2; dx++) for (int dz = -2; dz <= 2; dz++) columns.add(key(x + dx, z + dz));
}
private static long key(int x, int z) { return ((long) x << 32) | (z & 0xffffffffL); }
}
@@ -0,0 +1,698 @@
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.5 surface water, broad natural sediment sheets and occasional exposed rock springs.
* This version is separate from SurfaceHydrology so previously created worlds keep alpha.4 terrain.
* Results retain no chunks, sampler, random state or Minecraft objects. */
public final class NaturalHydrology {
public static final int RADIUS = 192;
public static final int MAX_CARVE = 4;
public static final int MAX_DEPTH = 3;
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 NaturalHydrology() {}
@FunctionalInterface
public interface Sampler { float sample(int x, int y, int z); }
public enum Kind { POND, LAKE, STREAM }
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 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();
int attempts = 0;
for (Candidate candidate : candidates) {
if (features.size() >= 5 || 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 ? 190 + (int) (random(seed, center.x(), center.z()) * 130)
: 55 + (int) (random(seed + 1, center.x(), center.z()) * 55);
// 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}) {
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;
}
}
// One quiet stream connects a basin to a nearby surface refuge at the same water level.
// Its route follows low excavation cost through the relief; it has no falling outlets.
boolean stream = false;
for (Feature source : List.copyOf(features)) if (addStream(source)) { stream = true; break; }
if (!stream) addPlateauStream(candidates);
addSedimentSheets();
addSprings();
return new Plan(cells, features, springs, springFlow, columns.size(), samples);
}
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 - 16) && 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 + MAX_CARVE
&& 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()) > 28 || 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() >= 420 || Math.hypot(x - center.x(), z - center.z()) > 32
|| !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, MAX_CARVE - 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 boolean addStream(Feature source) {
Point start = source.path().getFirst();
int level = source.waterY();
Map<Long, Double> costs = new HashMap<>();
Map<Long, Long> parent = new HashMap<>();
PriorityQueue<Node> queue = new PriorityQueue<>(NODE_ORDER);
List<Node> refuges = new ArrayList<>();
long startKey = key(start.x(), start.z());
costs.put(startKey, 0.0);
queue.add(new Node(start.x(), start.z(), 0));
int visited = 0;
while (!queue.isEmpty() && visited++ < 1200) {
Node node = queue.remove();
long at = key(node.x(), node.z());
if (node.cost() > costs.getOrDefault(at, Double.POSITIVE_INFINITY)) continue;
double distance = Math.hypot(node.x() - start.x(), node.z() - start.z());
if (distance >= 44 && distance <= 76 && column(node.x(), node.z()).top() <= level + 3) refuges.add(node);
for (int[] d : NEIGHBORS) {
int x = node.x() + d[0] * 4, z = node.z() + d[1] * 4;
if (Math.hypot(x - start.x(), z - start.z()) > 88 || !eligible(x, z, level)) continue;
long next = key(x, z);
double cost = node.cost() + Math.hypot(d[0], d[1])
* (4 + Math.abs(column(x, z).top() - level) * 1.4 + terrainNoise(x, z) * 4);
if (cost >= costs.getOrDefault(next, Double.POSITIVE_INFINITY)) continue;
costs.put(next, cost); parent.put(next, at); queue.add(new Node(x, z, cost));
}
}
refuges.sort(Comparator.comparingDouble((Node n) -> n.cost() / Math.hypot(n.x() - start.x(), n.z() - start.z()))
.thenComparingInt(Node::x).thenComparingInt(Node::z));
int attempts = 0;
for (Node refuge : refuges) {
if (attempts++ >= 24) break;
List<Point> path = new ArrayList<>();
long cursor = key(refuge.x(), refuge.z());
while (cursor != startKey) { path.add(point(cursor)); cursor = parent.get(cursor); }
path.add(start); Collections.reverse(path);
Set<Long> corridor = new HashSet<>();
for (int i = 1; i < path.size(); i++) {
Point a = path.get(i - 1), b = path.get(i);
int length = Math.max(Math.abs(b.x() - a.x()), Math.abs(b.z() - a.z()));
for (int step = 0; step <= length; step++) {
int x = a.x() + (b.x() - a.x()) * step / length;
int z = a.z() + (b.z() - a.z()) * step / length;
int width = terrainNoise(x + 17, z + 29) > 0.65 ? 2 : 1;
for (int dx = -width; dx <= width; dx++) for (int dz = -width; dz <= width; dz++) {
if (dx * dx + dz * dz <= width * width + 1) corridor.add(key(x + dx, z + dz));
}
}
}
Set<Long> refugePool = growBasin(new Point(refuge.x(), refuge.z()), level, 36);
if (refugePool == null) continue;
corridor.addAll(refugePool);
if (!closeStreamOutlets(corridor, level)) continue;
long id = featureId(features.size());
Map<Long, Cell> wet = waterCells(corridor, level, id);
if (wet == null || !contained(wet)) continue;
addFeature(id, Kind.STREAM, level, path, wet);
return true;
}
return false;
}
/** Low lake floors may be trapped below every nearby plateau. Try a small natural surface
* refuge as a source too, retaining it only if a fully contained stream can actually leave it. */
private boolean addPlateauStream(List<Candidate> candidates) {
List<Candidate> plateaus = candidates.stream().filter(candidate -> flatness(candidate.x(), candidate.z()) <= 6)
.sorted(Comparator.comparingDouble((Candidate c) -> flatness(c.x(), c.z())
- random(seed + 91, c.x(), c.z()) * 2).thenComparingInt(Candidate::x).thenComparingInt(Candidate::z)).toList();
int attempts = 0;
for (Candidate candidate : plateaus) {
if (attempts++ >= 20) break;
Point center = new Point(candidate.x(), candidate.z());
if (nearWater(center.x(), center.z(), 24)) continue;
Column column = column(center.x(), center.z());
if (column.top() < 64 || column.top() - column.solidBottom() < 7) continue;
for (int level : new int[]{column.top(), column.top() - 1}) {
Set<Long> footprint = growBasin(center, level, 40);
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.POND, level, List.of(center), wet);
if (addStream(features.getLast())) return true;
features.removeLast();
cells.values().removeIf(cell -> cell.featureId() == id);
}
}
return false;
}
private int flatness(int x, int z) {
int low = coarse.getOrDefault(key(x, z), -1000), high = low;
for (int[] d : CARDINALS) {
int value = coarse.getOrDefault(key(x + d[0] * GRID, z + d[1] * GRID), -1000);
low = Math.min(low, value); high = Math.max(high, value);
}
return high - low;
}
/** Include small connected low spots beside a channel; reject large/deep/open outlets.
* This adds water to a verified natural hollow, never rock to close a leak. */
private boolean closeStreamOutlets(Set<Long> corridor, int level) {
int maximum = Math.min(900, corridor.size() + 180);
List<Long> work = new ArrayList<>(corridor);
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 next = key(x, z);
Cell old = cells.get(next);
if (corridor.contains(next) || old != null && old.hasWater() && old.waterY() == level
|| column(x, z).top() >= level) continue;
if (corridor.size() >= maximum || !eligible(x, z, level)) return false;
corridor.add(next); work.add(next);
}
}
return true;
}
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);
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;
Column column = column(node.x(), node.z());
Cell existing = cells.get(key(node.x(), node.z()));
if (existing == null) {
double extent = 9 + field(node.x(), node.z(), 25, 131) * 10;
if (node.cost() > extent) continue;
int delta = column.top() - node.level();
if (delta < 0 || delta > 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() / 4.5);
int bed = Math.max(node.level(), Math.max(column.top() - 2, 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() + 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 > 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.29) return ShoreMaterial.STONE;
if (geology < 0.50) 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.68 || distance > 0.60 + grass * 0.65) return ShoreMaterial.GRASS;
if (geology < 0.29) return ShoreMaterial.STONE;
if (geology < 0.50) 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) || nearModifiedGround(x, z, 32)) 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())) 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(int x, int z, int distance) {
for (Cell cell : cells.values()) if (Math.hypot(x - cell.x(), z - cell.z()) < distance) return true;
return false;
}
private boolean separatedSpring(Position source) {
for (Spring spring : springs) {
if (Math.hypot(source.x() - spring.source().x(), source.z() - spring.source().z()) < 64
|| Math.abs(source.y() - spring.source().y()) < 12) return false;
}
return true;
}
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,140 @@
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.5 only: immutable plans cached by world state, applied to fresh terrain before decoration. */
public final class NaturalHydrologyRuntime {
private static final Map<RandomState, NaturalHydrology.Plan> PLANS =
Collections.synchronizedMap(new WeakHashMap<>());
private static final Map<RandomState, StarterLavaDeposit.Plan> LAVA_PLANS =
Collections.synchronizedMap(new WeakHashMap<>());
private NaturalHydrologyRuntime() {}
public static boolean enabled(NoiseBasedChunkGenerator generator) {
return generator.generatorSettings().is(SanctuarySpawn.NATURAL_SETTINGS);
}
public static NaturalHydrology.Plan naturalPlan(ServerLevel level) {
return plan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(),
level.getChunkSource().randomState());
}
public static NaturalHydrology.Plan plan(NoiseBasedChunkGenerator generator, RandomState randomState) {
if (!enabled(generator)) throw new IllegalArgumentException("Natural 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 = NaturalHydrology.create(state.seed(),
(x, y, z) -> state.sampleBlockValueUncached(density, x, y, z));
SanctuaryMod.LOGGER.info("Sanctuary natural hydrology: seed {}, {} features, {} columns, {} springs, {} ms",
state.seed(), plan.features().size(), plan.cells().size(), plan.springs().size(),
(System.nanoTime() - start) / 1_000_000);
return plan;
});
}
public static StarterLavaDeposit.Plan lavaPlan(ServerLevel level) {
return lavaPlan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(),
level.getChunkSource().randomState());
}
public static StarterLavaDeposit.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 = StarterLavaDeposit.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 lava = lavaPlan(generator, randomState);
var lavaCells = lava.cellsInChunk(chunkPos.x(), chunkPos.z());
var lavaSupports = lava.supportsInChunk(chunkPos.x(), chunkPos.z());
if (cells.isEmpty() && springs.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 = switch (cell.material()) {
case SAND -> Blocks.SAND.defaultBlockState();
case GRAVEL -> Blocks.GRAVEL.defaultBlockState();
case CLAY -> Blocks.CLAY.defaultBlockState();
case STONE -> Blocks.STONE.defaultBlockState();
case GRASS -> (depth == 0 ? Blocks.GRASS_BLOCK : Blocks.DIRT).defaultBlockState();
};
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);
}
Heightmap.primeHeightmaps(chunk, EnumSet.of(Heightmap.Types.WORLD_SURFACE_WG,
Heightmap.Types.OCEAN_FLOOR_WG));
}
private static void requireSolid(ChunkAccess chunk, BlockPos pos, long seed) {
if (!chunk.getBlockState(pos).isCollisionShapeFullBlock(chunk, pos)) {
throw new IllegalStateException("Natural hydrology differs from terrain at " + pos + " for seed " + seed);
}
}
}
@@ -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 NaturalShoreSugarCaneFeature implements Feature {
public static final MapCodec<NaturalShoreSugarCaneFeature> CODEC = MapCodec.unit(NaturalShoreSugarCaneFeature::new);
@Override
public MapCodec<NaturalShoreSugarCaneFeature> codec() {
return CODEC;
}
@Override
public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource random, BlockPos origin) {
if (!(generator instanceof NoiseBasedChunkGenerator noise) || !NaturalHydrologyRuntime.enabled(noise)) return false;
var plan = NaturalHydrologyRuntime.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,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);
}
}
@@ -22,13 +22,32 @@ public final class SanctuarySpawn {
public static final ResourceKey<NoiseGeneratorSettings> HYDROLOGY_SETTINGS =
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("sanctuary_hydrology"));
public static final ResourceKey<NoiseGeneratorSettings> NATURAL_SETTINGS =
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("sanctuary_natural"));
public static final ResourceKey<NoiseGeneratorSettings> LAYERED_SETTINGS =
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("sanctuary_layered"));
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"));
public static final ResourceKey<NoiseGeneratorSettings> CAVERN_SETTINGS =
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("sanctuary_cavern"));
private SanctuarySpawn() {}
public static boolean usesSanctuaryGenerator(ServerLevel level) {
return level.dimension() == Level.OVERWORLD
&& level.getChunkSource().getGenerator() instanceof NoiseBasedChunkGenerator generator
&& (generator.generatorSettings().is(SETTINGS) || generator.generatorSettings().is(RAISED_SETTINGS)
|| generator.generatorSettings().is(HYDROLOGY_SETTINGS));
|| generator.generatorSettings().is(HYDROLOGY_SETTINGS)
|| generator.generatorSettings().is(NATURAL_SETTINGS)
|| generator.generatorSettings().is(LAYERED_SETTINGS)
|| generator.generatorSettings().is(WOODLAND_SETTINGS)
|| generator.generatorSettings().is(RIFT_SETTINGS)
|| generator.generatorSettings().is(CAVERN_SETTINGS));
}
/** Called only during first creation; saved spawns and /setworldspawn survive subsequent loads. */
@@ -0,0 +1,223 @@
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;
/** One small, covered lava pocket in existing rock, reached from an existing outdoor ledge.
* No solid blocks are added, and unsuitable seeds are allowed to produce no deposit. */
public final class StarterLavaDeposit {
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 StarterLavaDeposit() {}
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 Plan(List<Cell> cells, Set<Position> supports, Position access) {
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; }
/** Player feet on the existing ledge; this column was naturally open to the 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, NaturalHydrology.Sampler sampler, NaturalHydrology.Plan hydrology) {
return new Search(seed, sampler, hydrology).find();
}
private record Candidate(int x, int z, int dx, int dz, double score) {}
private static final class Search {
private final long seed;
private final NaturalHydrology.Sampler sampler;
private final NaturalHydrology.Plan hydrology;
private final Map<Long, Integer> heights = new HashMap<>();
Search(long seed, NaturalHydrology.Sampler sampler, NaturalHydrology.Plan hydrology) {
this.seed = seed;
this.sampler = sampler;
this.hydrology = hydrology;
}
Plan find() {
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);
}
private Plan tryNiche(int entryX, int entryZ, int dx, int dz) {
int cx = entryX - dx * ENTRY_DISTANCE, cz = entryZ - dz * ENTRY_DISTANCE;
if (!inside(cx, cz, RADIUS - 12) || !clearOfWater(cx, cz)) return null;
int level = top(entryX, entryZ);
if (level < 64 || level > 300 || top(cx, cz) < level + 12) return null;
// The player can discover and enter from an actual outdoor ledge, not a sealed cave.
for (int y = level + 1; y < 384; y++) if (solid(entryX, y, entryZ)) return null;
for (int y = level - 2; y <= level; y++) if (!solid(entryX, y, entryZ)) 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 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,64 @@
package fr.koka.sanctuary.worldgen;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import java.util.stream.Stream;
import net.minecraft.core.Holder;
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;
/** Horizontal temperate patches sampled from this world's seeded climate router. */
public final class TemperateIslandBiomeSource extends BiomeSource {
public static final MapCodec<TemperateIslandBiomeSource> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
Biome.CODEC.fieldOf("forest").forGetter(source -> source.forest),
Biome.CODEC.fieldOf("plains").forGetter(source -> source.plains),
Biome.CODEC.fieldOf("birch_forest").forGetter(source -> source.birchForest),
Biome.CODEC.fieldOf("flower_forest").forGetter(source -> source.flowerForest),
Biome.CODEC.fieldOf("dappled_forest").forGetter(source -> source.dappledForest)
).apply(instance, TemperateIslandBiomeSource::new));
private final Holder<Biome> forest;
private final Holder<Biome> plains;
private final Holder<Biome> birchForest;
private final Holder<Biome> flowerForest;
private final Holder<Biome> dappledForest;
public TemperateIslandBiomeSource(Holder<Biome> forest, Holder<Biome> plains,
Holder<Biome> birchForest, Holder<Biome> flowerForest, Holder<Biome> dappledForest) {
this.forest = forest;
this.plains = plains;
this.birchForest = birchForest;
this.flowerForest = flowerForest;
this.dappledForest = dappledForest;
}
@Override
protected MapCodec<TemperateIslandBiomeSource> codec() {
return CODEC;
}
@Override
protected Stream<Holder<Biome>> collectPossibleBiomes() {
return Stream.of(forest, plains, birchForest, flowerForest, dappledForest);
}
@Override
public BiomeResolver createResolver(Climate.Sampler sampler) {
// The resolver receives quart coordinates. The two bound density samplers use block
// coordinates, and already carry the world's seed through RandomState. Sampling Y=0
// keeps a shore and the rock beneath it in the same biome, including within overhangs.
return (quartX, quartY, quartZ) -> {
int x = quartX * 4;
int z = quartZ * 4;
float variation = sampler.temperature().sampleValue(x, 0, z);
float moisture = sampler.humidity().sampleValue(x, 0, z);
if (variation > 0.18F) return dappledForest;
if (variation < -0.28F) return plains;
if (moisture > 0.25F) return birchForest;
if (moisture < -0.30F) return flowerForest;
return forest;
};
}
}
@@ -0,0 +1,29 @@
package fr.koka.sanctuary.worldgen;
/** Alpha.7 keeps forest at every surface altitude; depth changes the sheltered interior palette. */
public final class WoodlandBiomePalette {
public enum Kind {
OAK_FOREST, BIRCH_FOREST, CLEARING, DRY_WOODLAND, ROCKY_HEATH,
DARK_GROVE, BAMBOO_GROVE, SULFUR_DEPTHS
}
private WoodlandBiomePalette() {}
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;
if (elevation < 112 && variation > 0.62F && moisture < -0.25F) return Kind.SULFUR_DEPTHS;
// Preserve the strongest horizontal rock patches of alpha.6, with a smaller footprint.
// Height alone never converts an otherwise forested mountain into exposed stone.
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,46 @@
package fr.koka.sanctuary.worldgen;
/** Natural supports and canopy space; independent of Minecraft blocks and decoration order. */
public final class WoodlandGroveGeometry {
@FunctionalInterface public interface Sampler { float sample(int x, int y, int z); }
private WoodlandGroveGeometry() {}
public static int highestSurface(Sampler sampler, int x, int z) {
return highestSurfaceBelow(sampler, x, z, 383);
}
/** The caller supplies a proven upper bound, including any removed natural surface. */
public static int highestSurfaceBelow(Sampler sampler, int x, int z, int ceiling) {
for (int y = Math.min(383, ceiling); y >= 24; y--) if (sampler.sample(x, y, z) > 0) return y;
return -1;
}
public static boolean naturalFootprint(Sampler sampler, int x, int bedY, int z, int width) {
if (bedY < 32 || bedY > 350) return false;
for (int dx = 0; dx < width; dx++) for (int dz = 0; dz < width; dz++) {
for (int dy = -2; dy <= 0; dy++) if (sampler.sample(x + dx, bedY + dy, z + dz) <= 0) return false;
if (sampler.sample(x + dx, bedY + 1, z + dz) > 0) return false;
}
return true;
}
public static boolean treeRoom(Sampler sampler, int x, int baseY, int z, int width, int radius, int height) {
if (baseY + height >= 384) return false;
for (int y = baseY; y <= baseY + height; y++) {
int from = y < baseY + 3 ? 0 : -radius;
int to = y < baseY + 3 ? width - 1 : radius;
for (int dx = from; dx <= to; dx++) for (int dz = from; dz <= to; dz++) {
if (sampler.sample(x + dx, y, z + dz) > 0) return false;
}
}
return true;
}
public static long seed(long worldSeed, int x, int y, int z, long salt) {
long value = worldSeed ^ ((long) x << 32) ^ (z & 0xffffffffL) ^ y * 0x9e3779b97f4a7c15L ^ salt;
value = (value ^ value >>> 30) * 0xbf58476d1ce4e5b9L;
value = (value ^ value >>> 27) * 0x94d049bb133111ebL;
return value ^ value >>> 31;
}
}
@@ -0,0 +1,236 @@
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;
/** Alpha.7: real vanilla trees 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 WoodlandGrovesFeature implements Feature {
public static final MapCodec<WoodlandGrovesFeature> CODEC = MapCodec.unit(WoodlandGrovesFeature::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) {}
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<WoodlandGrovesFeature> 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 (!WoodlandHydrologyRuntime.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);
var water = WoodlandHydrologyRuntime.plan(noise, state);
var lava = WoodlandHydrologyRuntime.lavaPlan(noise, state);
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(water, lava, 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) || !WoodlandHydrologyRuntime.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);
var water = WoodlandHydrologyRuntime.plan(noise, state);
var lava = WoodlandHydrologyRuntime.lavaPlan(noise, state);
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, water, lava, 0x723BL);
break;
}
}
}
// 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, water, 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.getBlockState(base.below())) || !level.getBlockState(base).isAir()) continue;
var biome = level.getBiome(base);
boolean dark = biome.is(WoodlandIslandBiomeSource.DARK_GROVE);
boolean bamboo = biome.is(WoodlandIslandBiomeSource.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), water, lava, 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), water, lava, 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(water, lava, 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;
}
}
// Ground cover uses coherent patches; mushrooms can survive on podzol/mycelium in shade.
for (int x = minX; x < minX + 16; x++) for (int z = minZ; z < minZ + 16; z++) {
int highest = localTop(level, sampler, water, minX, minZ, x, z, heights), floors = 0;
for (int bed = highest - 8; bed >= 32; bed--) {
BlockPos floor = new BlockPos(x, bed, z), plant = floor.above();
if (!ground(level.getBlockState(floor)) || !level.getBlockState(plant).isAir()) continue;
var biome = level.getBiome(plant);
if (!biome.is(WoodlandIslandBiomeSource.DARK_GROVE) && !biome.is(WoodlandIslandBiomeSource.BAMBOO_GROVE)) continue;
if (!WoodlandGroveGeometry.naturalFootprint(sampler, x, bed, z, 1) || blocked(water, lava, plant, 1, 2)) continue;
long patch = WoodlandGroveGeometry.seed(state.seed(), Math.floorDiv(x, 7), 0, Math.floorDiv(z, 7), 0x742DL);
BlockState soil = Math.floorMod(patch, 20) == 0 ? Blocks.MOSS_BLOCK.defaultBlockState()
: Math.floorMod(patch, 3) == 0 ? Blocks.MYCELIUM.defaultBlockState() : Blocks.PODZOL.defaultBlockState();
setBlock(level, floor, soil);
long detail = WoodlandGroveGeometry.seed(state.seed(), x, bed, z, 0x743EL);
if (Math.floorMod(detail, 5) == 0) {
BlockState mushroom = (Math.floorMod(detail, 2) == 0 ? Blocks.BROWN_MUSHROOM : Blocks.RED_MUSHROOM).defaultBlockState();
if (mushroom.canSurvive(level, plant)) setBlock(level, plant, mushroom);
}
changed = true;
if (++floors >= 3) break;
}
}
return changed;
}
private boolean tree(WorldGenLevel level, ChunkGenerator generator, WoodlandGroveGeometry.Sampler sampler, long seed,
BlockPos base, Species species, LayeredHydrology.Plan water, LayeredLavaDeposit.Plan lava, long salt) {
if (blocked(water, lava, 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.getBlockState(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(BlockState state) {
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);
}
private static int localTop(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler, LayeredHydrology.Plan water,
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];
// This feature runs first in the vegetation step. Ores preserve the surface; hydrology
// is the only preceding operation that removes it and records the original carveTop.
int ceiling = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z) - 1;
var modified = water.cellAt(x, z);
if (modified != null) ceiling = Math.max(ceiling, modified.carveTop());
return heights[index] = WoodlandGroveGeometry.highestSurfaceBelow(sampler, x, z, ceiling);
}
private static int rootDepth(Species species) { return species.key().equals(TreeFeatures.MANGROVE) ? 16 : 4; }
private static boolean blocked(LayeredHydrology.Plan water, LayeredLavaDeposit.Plan lava, BlockPos base, int radius, int height) {
return blocked(water, lava, base, radius, height, 4);
}
private static boolean blocked(LayeredHydrology.Plan water, LayeredLavaDeposit.Plan lava, BlockPos base,
int radius, int height, int rootDepth) {
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 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;
}
}
@@ -0,0 +1,142 @@
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.7 only: immutable plans cached by world state, applied to fresh terrain before decoration. */
public final class WoodlandHydrologyRuntime {
private static final Map<RandomState, LayeredHydrology.Plan> PLANS =
Collections.synchronizedMap(new WeakHashMap<>());
private static final Map<RandomState, LayeredLavaDeposit.Plan> LAVA_PLANS =
Collections.synchronizedMap(new WeakHashMap<>());
private WoodlandHydrologyRuntime() {}
public static boolean enabled(NoiseBasedChunkGenerator generator) {
return generator.generatorSettings().is(SanctuarySpawn.WOODLAND_SETTINGS);
}
public static LayeredHydrology.Plan woodlandPlan(ServerLevel level) {
return plan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(),
level.getChunkSource().randomState());
}
public static LayeredHydrology.Plan plan(NoiseBasedChunkGenerator generator, RandomState randomState) {
if (!enabled(generator)) throw new IllegalArgumentException("Woodland 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 = LayeredHydrology.create(state.seed(),
(x, y, z) -> state.sampleBlockValueUncached(density, x, y, z));
SanctuaryMod.LOGGER.info("Sanctuary woodland hydrology: seed {}, {} features, {} columns, {} springs, {} ms",
state.seed(), plan.features().size(), plan.cells().size(), plan.springs().size(),
(System.nanoTime() - start) / 1_000_000);
return plan;
});
}
public static LayeredLavaDeposit.Plan lavaPlan(ServerLevel level) {
return lavaPlan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(),
level.getChunkSource().randomState());
}
public static LayeredLavaDeposit.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 = LayeredLavaDeposit.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 = plan.springs().stream().map(LayeredHydrology.Spring::outlet)
.filter(outlet -> (outlet.x() >> 4) == chunkPos.x() && (outlet.z() >> 4) == chunkPos.z()).toList();
var lava = lavaPlan(generator, randomState);
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 = WoodlandMaterials.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));
}
private static void requireSolid(ChunkAccess chunk, BlockPos pos, long seed) {
if (!chunk.getBlockState(pos).isCollisionShapeFullBlock(chunk, pos)) {
throw new IllegalStateException("Woodland 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.7 woodland climate, separate from all saved earlier biome-source codecs. */
public final class WoodlandIslandBiomeSource extends BiomeSource {
public static final ResourceKey<Biome> OAK_FOREST = key("woodland_oak_forest");
public static final ResourceKey<Biome> BIRCH_FOREST = key("woodland_birch_forest");
public static final ResourceKey<Biome> CLEARING = key("woodland_clearing");
public static final ResourceKey<Biome> DRY_WOODLAND = key("woodland_dry_woodland");
public static final ResourceKey<Biome> ROCKY_HEATH = key("woodland_rocky_heath");
public static final ResourceKey<Biome> DARK_GROVE = key("woodland_dark_grove");
public static final ResourceKey<Biome> BAMBOO_GROVE = key("woodland_bamboo_grove");
public static final ResourceKey<Biome> SULFUR_DEPTHS = key("woodland_sulfur_depths");
public static final MapCodec<WoodlandIslandBiomeSource> 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, WoodlandIslandBiomeSource::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 WoodlandIslandBiomeSource(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<WoodlandIslandBiomeSource> 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 (WoodlandBiomePalette.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,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.7 shore deposits: broad continuous patches through the existing sediment thickness. */
public final class WoodlandMaterials {
private WoodlandMaterials() {}
public static BlockState sedimentBlock(LayeredHydrology.Cell cell, long seed, int depth) {
if (cell.material() == LayeredHydrology.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,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 WoodlandShoreSugarCaneFeature implements Feature {
public static final MapCodec<WoodlandShoreSugarCaneFeature> CODEC = MapCodec.unit(WoodlandShoreSugarCaneFeature::new);
@Override
public MapCodec<WoodlandShoreSugarCaneFeature> codec() {
return CODEC;
}
@Override
public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource random, BlockPos origin) {
if (!(generator instanceof NoiseBasedChunkGenerator noise) || !WoodlandHydrologyRuntime.enabled(noise)) return false;
var plan = WoodlandHydrologyRuntime.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,94 @@
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.7 outlet finishing, using vanilla's persisted and consumed generation queue. */
public final class WoodlandSpringOutlets {
public record Pending(List<LayeredHydrology.Position> sources, List<LayeredHydrology.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 WoodlandSpringOutlets() {}
/** 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)
|| !WoodlandHydrologyRuntime.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 = WoodlandHydrologyRuntime.plan(noise, level.getChunkSource().randomState());
List<LayeredHydrology.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());
}
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)
|| !WoodlandHydrologyRuntime.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);
if (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.is(Blocks.WATER) && current.getFluidState().isSource()) {
level.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(level));
}
}
}
private static boolean markedHere(LevelChunk chunk, LayeredHydrology.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, LayeredHydrology.Position position) {
return (position.x() >> 4) == chunk.getPos().x() && (position.z() >> 4) == chunk.getPos().z();
}
private static BlockPos blockPos(LayeredHydrology.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);
}
}
@@ -1,5 +1,41 @@
{
"generator.sanctuary.sanctuary": "Sanctuary",
"biome.sanctuary.starter_forest": "Sanctuary Forest",
"biome.sanctuary.starter_forest_hydrology": "Sanctuary Forest and Shores"
"biome.sanctuary.starter_forest_hydrology": "Sanctuary Forest and Shores",
"biome.sanctuary.temperate_forest": "Sanctuary Temperate Forest",
"biome.sanctuary.temperate_plains": "Sanctuary Plains",
"biome.sanctuary.temperate_birch_forest": "Sanctuary Birch Forest",
"biome.sanctuary.temperate_flower_forest": "Sanctuary Flower Forest",
"biome.sanctuary.temperate_dappled_forest": "Sanctuary Dappled Forest",
"biome.sanctuary.layered_meadow": "Highland Meadow",
"biome.sanctuary.layered_flower_meadow": "Flowered Highland Meadow",
"biome.sanctuary.layered_lower_grove": "Ledge Woodland",
"biome.sanctuary.layered_mossy_terraces": "Mossy Ledges",
"biome.sanctuary.layered_rocky_terraces": "Rocky Terraces",
"biome.sanctuary.woodland_oak_forest": "Sanctuary Oak Woodland",
"biome.sanctuary.woodland_birch_forest": "Sanctuary Birch Woodland",
"biome.sanctuary.woodland_clearing": "Flowered Clearing",
"biome.sanctuary.woodland_dry_woodland": "Dry Woodland",
"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.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",
"biome.sanctuary.cavern_oak_forest": "Sanctuary Oak Woodland",
"biome.sanctuary.cavern_birch_forest": "Sanctuary Birch Woodland",
"biome.sanctuary.cavern_clearing": "Flowered Clearing",
"biome.sanctuary.cavern_dry_woodland": "Dry Woodland",
"biome.sanctuary.cavern_rocky_heath": "Rocky Heath",
"biome.sanctuary.cavern_dark_grove": "Dark Undergrowth",
"biome.sanctuary.cavern_bamboo_grove": "Bamboo Grove",
"biome.sanctuary.cavern_sulfur_depths": "Sulfur Depths",
"biome.sanctuary.cavern_lush_caves": "Sanctuary Lush Caves",
"biome.sanctuary.cavern_dripstone_caves": "Sanctuary Dripstone Caves"
}
@@ -1,5 +1,41 @@
{
"generator.sanctuary.sanctuary": "Sanctuary",
"biome.sanctuary.starter_forest": "Forêt de Sanctuary",
"biome.sanctuary.starter_forest_hydrology": "Forêt et rives de Sanctuary"
"biome.sanctuary.starter_forest_hydrology": "Forêt et rives de Sanctuary",
"biome.sanctuary.temperate_forest": "Forêt tempérée de Sanctuary",
"biome.sanctuary.temperate_plains": "Prairies de Sanctuary",
"biome.sanctuary.temperate_birch_forest": "Boulaie de Sanctuary",
"biome.sanctuary.temperate_flower_forest": "Forêt fleurie de Sanctuary",
"biome.sanctuary.temperate_dappled_forest": "Forêt diaprée de Sanctuary",
"biome.sanctuary.layered_meadow": "Prairie des hauteurs",
"biome.sanctuary.layered_flower_meadow": "Prairie fleurie des hauteurs",
"biome.sanctuary.layered_lower_grove": "Sous-bois des corniches",
"biome.sanctuary.layered_mossy_terraces": "Corniches moussues",
"biome.sanctuary.layered_rocky_terraces": "Terrasses rocheuses",
"biome.sanctuary.woodland_oak_forest": "Chênaie de Sanctuary",
"biome.sanctuary.woodland_birch_forest": "Bétulaie de Sanctuary",
"biome.sanctuary.woodland_clearing": "Clairière fleurie",
"biome.sanctuary.woodland_dry_woodland": "Bois sec",
"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.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",
"biome.sanctuary.cavern_oak_forest": "Chênaie de Sanctuary",
"biome.sanctuary.cavern_birch_forest": "Bétulaie de Sanctuary",
"biome.sanctuary.cavern_clearing": "Clairière fleurie",
"biome.sanctuary.cavern_dry_woodland": "Bois sec",
"biome.sanctuary.cavern_rocky_heath": "Lande rocheuse",
"biome.sanctuary.cavern_dark_grove": "Sous-bois sombre",
"biome.sanctuary.cavern_bamboo_grove": "Bosquet de bambous",
"biome.sanctuary.cavern_sulfur_depths": "Profondeurs soufrées",
"biome.sanctuary.cavern_lush_caves": "Grottes luxuriantes de Sanctuary",
"biome.sanctuary.cavern_dripstone_caves": "Grottes de spéléothèmes de Sanctuary"
}
@@ -0,0 +1,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,167 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.sparse_jungle"
}
},
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [
{
"type": "minecraft:bat",
"count": 8,
"weight": 10
}
],
"creature": [
{
"type": "minecraft:sheep",
"count": 4,
"weight": 12
},
{
"type": "minecraft:pig",
"count": 4,
"weight": 10
},
{
"type": "minecraft:chicken",
"count": 4,
"weight": 10
},
{
"type": "minecraft:cow",
"count": 4,
"weight": 8
},
{
"type": "minecraft:chicken",
"count": 4,
"weight": 10
},
{
"type": "minecraft:wolf",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 2
},
"weight": 8
}
],
"monster": [
{
"type": "minecraft:spider",
"count": 4,
"weight": 100
},
{
"type": "minecraft:zombie",
"count": 4,
"weight": 95
},
{
"type": "minecraft:zombie_villager",
"count": 1,
"weight": 5
},
{
"type": "minecraft:skeleton",
"count": 4,
"weight": 100
},
{
"type": "minecraft:creeper",
"count": 4,
"weight": 100
},
{
"type": "minecraft:slime",
"count": 4,
"weight": 100
},
{
"type": "minecraft:enderman",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 1
},
"weight": 10
},
{
"type": "minecraft:witch",
"count": 1,
"weight": 5
}
],
"underground_water_creature": [
{
"type": "minecraft:glow_squid",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 4
},
"weight": 10
}
]
}
},
"modifier": "overlay"
},
"minecraft:visual/sky_color": "#79a6ff"
},
"carvers": [],
"downfall": 0.95,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"features": [
[],
[],
[],
[],
[],
[],
[
"sanctuary:rift_ore_coal",
"sanctuary:rift_ore_iron",
"sanctuary:rift_ore_copper",
"sanctuary:rift_ore_gold",
"sanctuary:rift_ore_redstone",
"sanctuary:rift_ore_lapis",
"sanctuary:rift_ore_diamond",
"sanctuary:rift_ore_emerald",
"sanctuary:rift_ore_coal_surface",
"sanctuary:rift_ore_iron_surface",
"sanctuary:rift_ore_copper_surface",
"sanctuary:rift_ore_coal_ledges",
"sanctuary:rift_ore_iron_ledges",
"sanctuary:rift_ore_copper_ledges"
],
[],
[],
[
"minecraft:glow_lichen",
"minecraft:brown_mushroom_normal",
"minecraft:red_mushroom_normal",
"sanctuary:cavern_groves",
"sanctuary:cavern_decorations",
"sanctuary:cavern_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.8
}
@@ -0,0 +1,162 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.forest"
}
},
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [
{
"type": "minecraft:bat",
"count": 8,
"weight": 10
}
],
"creature": [
{
"type": "minecraft:sheep",
"count": 4,
"weight": 12
},
{
"type": "minecraft:pig",
"count": 4,
"weight": 10
},
{
"type": "minecraft:chicken",
"count": 4,
"weight": 10
},
{
"type": "minecraft:cow",
"count": 4,
"weight": 8
}
],
"monster": [
{
"type": "minecraft:spider",
"count": 4,
"weight": 100
},
{
"type": "minecraft:zombie",
"count": 4,
"weight": 95
},
{
"type": "minecraft:zombie_villager",
"count": 1,
"weight": 5
},
{
"type": "minecraft:skeleton",
"count": 4,
"weight": 100
},
{
"type": "minecraft:creeper",
"count": 4,
"weight": 100
},
{
"type": "minecraft:slime",
"count": 4,
"weight": 100
},
{
"type": "minecraft:enderman",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 1
},
"weight": 10
},
{
"type": "minecraft:witch",
"count": 1,
"weight": 5
}
],
"underground_water_creature": [
{
"type": "minecraft:glow_squid",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 4
},
"weight": 10
}
]
}
},
"modifier": "overlay"
},
"minecraft:visual/sky_color": "#79a6ff"
},
"carvers": [],
"downfall": 0.7,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"features": [
[],
[],
[],
[],
[],
[],
[
"sanctuary:rift_ore_coal",
"sanctuary:rift_ore_iron",
"sanctuary:rift_ore_copper",
"sanctuary:rift_ore_gold",
"sanctuary:rift_ore_redstone",
"sanctuary:rift_ore_lapis",
"sanctuary:rift_ore_diamond",
"sanctuary:rift_ore_emerald",
"sanctuary:rift_ore_coal_surface",
"sanctuary:rift_ore_iron_surface",
"sanctuary:rift_ore_copper_surface",
"sanctuary:rift_ore_coal_ledges",
"sanctuary:rift_ore_iron_ledges",
"sanctuary:rift_ore_copper_ledges"
],
[],
[],
[
"minecraft:glow_lichen",
"minecraft:forest_flowers",
"minecraft:wildflowers_birch_forest",
"minecraft:trees_birch",
"minecraft:patch_bush",
"minecraft:flower_default",
"minecraft:patch_grass_forest",
"minecraft:brown_mushroom_normal",
"minecraft:red_mushroom_normal",
"minecraft:patch_pumpkin",
"minecraft:patch_sugar_cane",
"minecraft:patch_firefly_bush_near_water",
"sanctuary:cavern_groves",
"sanctuary:cavern_decorations",
"sanctuary:cavern_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,164 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.meadow"
}
},
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [
{
"type": "minecraft:bat",
"count": 8,
"weight": 10
}
],
"creature": [
{
"type": "minecraft:donkey",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 2,
"min_inclusive": 1
},
"weight": 1
},
{
"type": "minecraft:rabbit",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 2
},
"weight": 2
},
{
"type": "minecraft:sheep",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 2
},
"weight": 2
}
],
"monster": [
{
"type": "minecraft:spider",
"count": 4,
"weight": 100
},
{
"type": "minecraft:zombie",
"count": 4,
"weight": 95
},
{
"type": "minecraft:zombie_villager",
"count": 1,
"weight": 5
},
{
"type": "minecraft:skeleton",
"count": 4,
"weight": 100
},
{
"type": "minecraft:creeper",
"count": 4,
"weight": 100
},
{
"type": "minecraft:slime",
"count": 4,
"weight": 100
},
{
"type": "minecraft:enderman",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 1
},
"weight": 10
},
{
"type": "minecraft:witch",
"count": 1,
"weight": 5
}
],
"underground_water_creature": [
{
"type": "minecraft:glow_squid",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 4
},
"weight": 10
}
]
}
},
"modifier": "overlay"
},
"minecraft:visual/sky_color": "#79a6ff"
},
"carvers": [],
"downfall": 0.75,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"features": [
[],
[],
[],
[],
[],
[],
[
"sanctuary:rift_ore_coal",
"sanctuary:rift_ore_iron",
"sanctuary:rift_ore_copper",
"sanctuary:rift_ore_gold",
"sanctuary:rift_ore_redstone",
"sanctuary:rift_ore_lapis",
"sanctuary:rift_ore_diamond",
"sanctuary:rift_ore_emerald",
"sanctuary:rift_ore_coal_surface",
"sanctuary:rift_ore_iron_surface",
"sanctuary:rift_ore_copper_surface",
"sanctuary:rift_ore_coal_ledges",
"sanctuary:rift_ore_iron_ledges",
"sanctuary:rift_ore_copper_ledges"
],
[],
[],
[
"minecraft:glow_lichen",
"minecraft:patch_tall_grass_2",
"minecraft:patch_grass_meadow",
"minecraft:flower_meadow",
"minecraft:trees_meadow",
"minecraft:wildflowers_meadow",
"minecraft:flower_flower_forest",
"sanctuary:cavern_groves",
"sanctuary:cavern_decorations",
"sanctuary:cavern_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,153 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.forest"
}
},
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [
{
"type": "minecraft:bat",
"count": 8,
"weight": 10
}
],
"creature": [
{
"type": "minecraft:sheep",
"count": 4,
"weight": 12
},
{
"type": "minecraft:pig",
"count": 4,
"weight": 10
},
{
"type": "minecraft:chicken",
"count": 4,
"weight": 10
},
{
"type": "minecraft:cow",
"count": 4,
"weight": 8
}
],
"monster": [
{
"type": "minecraft:spider",
"count": 4,
"weight": 100
},
{
"type": "minecraft:zombie",
"count": 4,
"weight": 95
},
{
"type": "minecraft:zombie_villager",
"count": 1,
"weight": 5
},
{
"type": "minecraft:skeleton",
"count": 4,
"weight": 100
},
{
"type": "minecraft:creeper",
"count": 4,
"weight": 100
},
{
"type": "minecraft:slime",
"count": 4,
"weight": 100
},
{
"type": "minecraft:enderman",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 1
},
"weight": 10
},
{
"type": "minecraft:witch",
"count": 1,
"weight": 5
}
],
"underground_water_creature": [
{
"type": "minecraft:glow_squid",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 4
},
"weight": 10
}
]
}
},
"modifier": "overlay"
},
"minecraft:visual/sky_color": "#79a6ff"
},
"carvers": [],
"downfall": 0.9,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"features": [
[],
[],
[],
[],
[],
[],
[
"sanctuary:rift_ore_coal",
"sanctuary:rift_ore_iron",
"sanctuary:rift_ore_copper",
"sanctuary:rift_ore_gold",
"sanctuary:rift_ore_redstone",
"sanctuary:rift_ore_lapis",
"sanctuary:rift_ore_diamond",
"sanctuary:rift_ore_emerald",
"sanctuary:rift_ore_coal_surface",
"sanctuary:rift_ore_iron_surface",
"sanctuary:rift_ore_copper_surface",
"sanctuary:rift_ore_coal_ledges",
"sanctuary:rift_ore_iron_ledges",
"sanctuary:rift_ore_copper_ledges"
],
[],
[],
[
"minecraft:glow_lichen",
"minecraft:brown_mushroom_normal",
"minecraft:red_mushroom_normal",
"sanctuary:cavern_groves",
"sanctuary:cavern_decorations",
"sanctuary:cavern_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,153 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.forest"
}
},
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [
{
"type": "minecraft:bat",
"count": 8,
"weight": 10
}
],
"creature": [
{
"type": "minecraft:sheep",
"count": 4,
"weight": 12
},
{
"type": "minecraft:pig",
"count": 4,
"weight": 10
},
{
"type": "minecraft:chicken",
"count": 4,
"weight": 10
},
{
"type": "minecraft:cow",
"count": 4,
"weight": 8
}
],
"monster": [
{
"type": "minecraft:spider",
"count": 4,
"weight": 100
},
{
"type": "minecraft:zombie",
"count": 4,
"weight": 95
},
{
"type": "minecraft:zombie_villager",
"count": 1,
"weight": 5
},
{
"type": "minecraft:skeleton",
"count": 4,
"weight": 100
},
{
"type": "minecraft:creeper",
"count": 4,
"weight": 100
},
{
"type": "minecraft:slime",
"count": 4,
"weight": 100
},
{
"type": "minecraft:enderman",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 1
},
"weight": 10
},
{
"type": "minecraft:witch",
"count": 1,
"weight": 5
}
],
"underground_water_creature": [
{
"type": "minecraft:glow_squid",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 4
},
"weight": 10
}
]
}
},
"modifier": "overlay"
},
"minecraft:visual/sky_color": "#79a6ff"
},
"carvers": [],
"downfall": 0.55,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"features": [
[],
[],
[],
[],
[],
[],
[
"sanctuary:rift_ore_coal",
"sanctuary:rift_ore_iron",
"sanctuary:rift_ore_copper",
"sanctuary:rift_ore_gold",
"sanctuary:rift_ore_redstone",
"sanctuary:rift_ore_lapis",
"sanctuary:rift_ore_diamond",
"sanctuary:rift_ore_emerald",
"sanctuary:rift_ore_coal_surface",
"sanctuary:rift_ore_iron_surface",
"sanctuary:rift_ore_copper_surface",
"sanctuary:rift_ore_coal_ledges",
"sanctuary:rift_ore_iron_ledges",
"sanctuary:rift_ore_copper_ledges"
],
[],
[],
[
"minecraft:glow_lichen",
"minecraft:brown_mushroom_normal",
"minecraft:red_mushroom_normal",
"sanctuary:cavern_groves",
"sanctuary:cavern_decorations",
"sanctuary:cavern_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.5
}
@@ -0,0 +1,163 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.forest"
}
},
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [
{
"type": "minecraft:bat",
"count": 8,
"weight": 10
}
],
"creature": [
{
"type": "minecraft:sheep",
"count": 4,
"weight": 12
},
{
"type": "minecraft:pig",
"count": 4,
"weight": 10
},
{
"type": "minecraft:chicken",
"count": 4,
"weight": 10
},
{
"type": "minecraft:cow",
"count": 4,
"weight": 8
},
{
"type": "minecraft:wolf",
"count": 4,
"weight": 5
}
],
"monster": [
{
"type": "minecraft:spider",
"count": 4,
"weight": 100
},
{
"type": "minecraft:zombie",
"count": 4,
"weight": 95
},
{
"type": "minecraft:zombie_villager",
"count": 1,
"weight": 5
},
{
"type": "minecraft:skeleton",
"count": 4,
"weight": 100
},
{
"type": "minecraft:creeper",
"count": 4,
"weight": 100
},
{
"type": "minecraft:slime",
"count": 4,
"weight": 100
},
{
"type": "minecraft:enderman",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 1
},
"weight": 10
},
{
"type": "minecraft:witch",
"count": 1,
"weight": 5
}
],
"underground_water_creature": [
{
"type": "minecraft:glow_squid",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 4
},
"weight": 10
}
]
}
},
"modifier": "overlay"
},
"minecraft:visual/sky_color": "#79a6ff"
},
"carvers": [],
"downfall": 0.4,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"features": [
[],
[],
[],
[],
[],
[],
[
"sanctuary:rift_ore_coal",
"sanctuary:rift_ore_iron",
"sanctuary:rift_ore_copper",
"sanctuary:rift_ore_gold",
"sanctuary:rift_ore_redstone",
"sanctuary:rift_ore_lapis",
"sanctuary:rift_ore_diamond",
"sanctuary:rift_ore_emerald",
"sanctuary:rift_ore_coal_surface",
"sanctuary:rift_ore_iron_surface",
"sanctuary:rift_ore_copper_surface",
"sanctuary:rift_ore_coal_ledges",
"sanctuary:rift_ore_iron_ledges",
"sanctuary:rift_ore_copper_ledges"
],
[],
[],
[
"minecraft:glow_lichen",
"minecraft:forest_flowers",
"sanctuary:rift_sparse_trees",
"minecraft:patch_bush",
"minecraft:flower_default",
"minecraft:patch_grass_forest",
"minecraft:brown_mushroom_normal",
"minecraft:red_mushroom_normal",
"sanctuary:cavern_groves",
"sanctuary:cavern_decorations",
"sanctuary:cavern_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.75
}
@@ -0,0 +1,153 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.forest"
}
},
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [
{
"type": "minecraft:bat",
"count": 8,
"weight": 10
}
],
"creature": [
{
"type": "minecraft:sheep",
"count": 4,
"weight": 12
},
{
"type": "minecraft:pig",
"count": 4,
"weight": 10
},
{
"type": "minecraft:chicken",
"count": 4,
"weight": 10
},
{
"type": "minecraft:cow",
"count": 4,
"weight": 8
}
],
"monster": [
{
"type": "minecraft:spider",
"count": 4,
"weight": 100
},
{
"type": "minecraft:zombie",
"count": 4,
"weight": 95
},
{
"type": "minecraft:zombie_villager",
"count": 1,
"weight": 5
},
{
"type": "minecraft:skeleton",
"count": 4,
"weight": 100
},
{
"type": "minecraft:creeper",
"count": 4,
"weight": 100
},
{
"type": "minecraft:slime",
"count": 4,
"weight": 100
},
{
"type": "minecraft:enderman",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 1
},
"weight": 10
},
{
"type": "minecraft:witch",
"count": 1,
"weight": 5
}
],
"underground_water_creature": [
{
"type": "minecraft:glow_squid",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 4
},
"weight": 10
}
]
}
},
"modifier": "overlay"
},
"minecraft:visual/sky_color": "#79a6ff"
},
"carvers": [],
"downfall": 0.9,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"features": [
[],
[],
[],
[],
[],
[],
[
"sanctuary:rift_ore_coal",
"sanctuary:rift_ore_iron",
"sanctuary:rift_ore_copper",
"sanctuary:rift_ore_gold",
"sanctuary:rift_ore_redstone",
"sanctuary:rift_ore_lapis",
"sanctuary:rift_ore_diamond",
"sanctuary:rift_ore_emerald",
"sanctuary:rift_ore_coal_surface",
"sanctuary:rift_ore_iron_surface",
"sanctuary:rift_ore_copper_surface",
"sanctuary:rift_ore_coal_ledges",
"sanctuary:rift_ore_iron_ledges",
"sanctuary:rift_ore_copper_ledges"
],
[],
[],
[
"minecraft:glow_lichen",
"minecraft:brown_mushroom_normal",
"minecraft:red_mushroom_normal",
"sanctuary:cavern_groves",
"sanctuary:cavern_decorations",
"sanctuary:cavern_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.5
}
@@ -0,0 +1,166 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.forest"
}
},
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [
{
"type": "minecraft:bat",
"count": 8,
"weight": 10
}
],
"creature": [
{
"type": "minecraft:sheep",
"count": 4,
"weight": 12
},
{
"type": "minecraft:pig",
"count": 4,
"weight": 10
},
{
"type": "minecraft:chicken",
"count": 4,
"weight": 10
},
{
"type": "minecraft:cow",
"count": 4,
"weight": 8
},
{
"type": "minecraft:wolf",
"count": 4,
"weight": 5
}
],
"monster": [
{
"type": "minecraft:spider",
"count": 4,
"weight": 100
},
{
"type": "minecraft:zombie",
"count": 4,
"weight": 95
},
{
"type": "minecraft:zombie_villager",
"count": 1,
"weight": 5
},
{
"type": "minecraft:skeleton",
"count": 4,
"weight": 100
},
{
"type": "minecraft:creeper",
"count": 4,
"weight": 100
},
{
"type": "minecraft:slime",
"count": 4,
"weight": 100
},
{
"type": "minecraft:enderman",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 1
},
"weight": 10
},
{
"type": "minecraft:witch",
"count": 1,
"weight": 5
}
],
"underground_water_creature": [
{
"type": "minecraft:glow_squid",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 4
},
"weight": 10
}
]
}
},
"modifier": "overlay"
},
"minecraft:visual/sky_color": "#79a6ff"
},
"carvers": [],
"downfall": 0.8,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"features": [
[],
[],
[],
[],
[],
[],
[
"sanctuary:rift_ore_coal",
"sanctuary:rift_ore_iron",
"sanctuary:rift_ore_copper",
"sanctuary:rift_ore_gold",
"sanctuary:rift_ore_redstone",
"sanctuary:rift_ore_lapis",
"sanctuary:rift_ore_diamond",
"sanctuary:rift_ore_emerald",
"sanctuary:rift_ore_coal_surface",
"sanctuary:rift_ore_iron_surface",
"sanctuary:rift_ore_copper_surface",
"sanctuary:rift_ore_coal_ledges",
"sanctuary:rift_ore_iron_ledges",
"sanctuary:rift_ore_copper_ledges"
],
[],
[],
[
"minecraft:glow_lichen",
"minecraft:forest_flowers",
"minecraft:trees_birch_and_oak_leaf_litter",
"minecraft:patch_bush",
"minecraft:flower_default",
"minecraft:patch_grass_forest",
"minecraft:brown_mushroom_normal",
"minecraft:red_mushroom_normal",
"minecraft:patch_pumpkin",
"minecraft:patch_sugar_cane",
"minecraft:patch_firefly_bush_near_water",
"sanctuary:cavern_groves",
"sanctuary:cavern_decorations",
"sanctuary:cavern_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,158 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.meadow"
}
},
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [
{
"type": "minecraft:bat",
"count": 8,
"weight": 10
}
],
"creature": [
{
"type": "minecraft:donkey",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 2,
"min_inclusive": 1
},
"weight": 1
},
{
"type": "minecraft:rabbit",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 2
},
"weight": 2
},
{
"type": "minecraft:sheep",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 2
},
"weight": 2
}
],
"monster": [
{
"type": "minecraft:spider",
"count": 4,
"weight": 100
},
{
"type": "minecraft:zombie",
"count": 4,
"weight": 95
},
{
"type": "minecraft:zombie_villager",
"count": 1,
"weight": 5
},
{
"type": "minecraft:skeleton",
"count": 4,
"weight": 100
},
{
"type": "minecraft:creeper",
"count": 4,
"weight": 100
},
{
"type": "minecraft:slime",
"count": 4,
"weight": 100
},
{
"type": "minecraft:enderman",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 1
},
"weight": 10
},
{
"type": "minecraft:witch",
"count": 1,
"weight": 5
}
],
"underground_water_creature": [
{
"type": "minecraft:glow_squid",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 4
},
"weight": 10
}
]
}
},
"modifier": "overlay"
},
"minecraft:visual/sky_color": "#79a6ff"
},
"carvers": [],
"downfall": 0.65,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"features": [
[],
[],
[],
[],
[],
[],
[
"sanctuary:rift_ore_coal",
"sanctuary:rift_ore_iron",
"sanctuary:rift_ore_copper",
"sanctuary:rift_ore_gold",
"sanctuary:rift_ore_redstone",
"sanctuary:rift_ore_lapis",
"sanctuary:rift_ore_diamond",
"sanctuary:rift_ore_emerald",
"sanctuary:rift_ore_coal_surface",
"sanctuary:rift_ore_iron_surface",
"sanctuary:rift_ore_copper_surface",
"sanctuary:rift_ore_coal_ledges",
"sanctuary:rift_ore_iron_ledges",
"sanctuary:rift_ore_copper_ledges"
],
[],
[],
[
"minecraft:glow_lichen",
"sanctuary:cavern_groves",
"sanctuary:cavern_decorations",
"sanctuary:cavern_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,156 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.forest"
}
},
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [
{
"type": "minecraft:bat",
"count": 8,
"weight": 10
}
],
"creature": [
{
"type": "minecraft:sheep",
"count": 4,
"weight": 12
},
{
"type": "minecraft:pig",
"count": 4,
"weight": 10
},
{
"type": "minecraft:chicken",
"count": 4,
"weight": 10
},
{
"type": "minecraft:cow",
"count": 4,
"weight": 8
},
{
"type": "minecraft:wolf",
"count": 4,
"weight": 5
}
],
"monster": [
{
"type": "minecraft:spider",
"count": 4,
"weight": 100
},
{
"type": "minecraft:zombie",
"count": 4,
"weight": 95
},
{
"type": "minecraft:zombie_villager",
"count": 1,
"weight": 5
},
{
"type": "minecraft:skeleton",
"count": 4,
"weight": 100
},
{
"type": "minecraft:creeper",
"count": 4,
"weight": 100
},
{
"type": "minecraft:slime",
"count": 4,
"weight": 100
},
{
"type": "minecraft:enderman",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 1
},
"weight": 10
},
{
"type": "minecraft:witch",
"count": 1,
"weight": 5
}
],
"underground_water_creature": [
{
"type": "minecraft:glow_squid",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 4
},
"weight": 10
}
]
}
},
"modifier": "overlay"
},
"minecraft:visual/sky_color": "#79a6ff"
},
"carvers": [],
"downfall": 0.5,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"features": [
[],
[],
[],
[],
[],
[],
[
"sanctuary:rift_ore_coal",
"sanctuary:rift_ore_iron",
"sanctuary:rift_ore_copper",
"sanctuary:rift_ore_gold",
"sanctuary:rift_ore_redstone",
"sanctuary:rift_ore_lapis",
"sanctuary:rift_ore_diamond",
"sanctuary:rift_ore_emerald",
"sanctuary:rift_ore_coal_surface",
"sanctuary:rift_ore_iron_surface",
"sanctuary:rift_ore_copper_surface",
"sanctuary:rift_ore_coal_ledges",
"sanctuary:rift_ore_iron_ledges",
"sanctuary:rift_ore_copper_ledges"
],
[],
[],
[
"minecraft:glow_lichen",
"sanctuary:cavern_groves",
"sanctuary:cavern_decorations",
"sanctuary:cavern_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.8
}
@@ -0,0 +1,164 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.meadow"
}
},
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [
{
"type": "minecraft:bat",
"count": 8,
"weight": 10
}
],
"creature": [
{
"type": "minecraft:donkey",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 2,
"min_inclusive": 1
},
"weight": 1
},
{
"type": "minecraft:rabbit",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 2
},
"weight": 2
},
{
"type": "minecraft:sheep",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 2
},
"weight": 2
}
],
"monster": [
{
"type": "minecraft:spider",
"count": 4,
"weight": 100
},
{
"type": "minecraft:zombie",
"count": 4,
"weight": 95
},
{
"type": "minecraft:zombie_villager",
"count": 1,
"weight": 5
},
{
"type": "minecraft:skeleton",
"count": 4,
"weight": 100
},
{
"type": "minecraft:creeper",
"count": 4,
"weight": 100
},
{
"type": "minecraft:slime",
"count": 4,
"weight": 100
},
{
"type": "minecraft:enderman",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 1
},
"weight": 10
},
{
"type": "minecraft:witch",
"count": 1,
"weight": 5
}
],
"underground_water_creature": [
{
"type": "minecraft:glow_squid",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 4
},
"weight": 10
}
]
}
},
"modifier": "overlay"
},
"minecraft:visual/sky_color": "#79a6ff"
},
"carvers": [],
"downfall": 0.8,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"features": [
[],
[],
[],
[],
[],
[],
[
"minecraft:ore_dirt",
"minecraft:ore_granite_upper",
"minecraft:ore_granite_lower",
"minecraft:ore_diorite_upper",
"minecraft:ore_diorite_lower",
"minecraft:ore_andesite_upper",
"minecraft:ore_andesite_lower",
"minecraft:ore_tuff",
"sanctuary:island_ore_coal",
"sanctuary:island_ore_iron",
"sanctuary:island_ore_copper",
"sanctuary:island_ore_gold",
"sanctuary:island_ore_redstone",
"sanctuary:island_ore_lapis",
"sanctuary:island_ore_diamond"
],
[],
[],
[
"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:layered_shore_sugar_cane",
"sanctuary:lower_terrace_vegetation"
],
[
"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
}
],
"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": [
[],
[],
[],
[],
[],
[],
[
"minecraft:ore_dirt",
"minecraft:ore_granite_upper",
"minecraft:ore_granite_lower",
"minecraft:ore_diorite_upper",
"minecraft:ore_diorite_lower",
"minecraft:ore_andesite_upper",
"minecraft:ore_andesite_lower",
"minecraft:ore_tuff",
"sanctuary:island_ore_coal",
"sanctuary:island_ore_iron",
"sanctuary:island_ore_copper",
"sanctuary:island_ore_gold",
"sanctuary:island_ore_redstone",
"sanctuary:island_ore_lapis",
"sanctuary:island_ore_diamond"
],
[],
[],
[
"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:layered_shore_sugar_cane",
"sanctuary:lower_terrace_vegetation"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,175 @@
{
"attributes": {
"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:horse",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 2
},
"weight": 5
},
{
"type": "minecraft:donkey",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 3,
"min_inclusive": 1
},
"weight": 1
}
],
"monster": [
{
"type": "minecraft:spider",
"count": 4,
"weight": 100
},
{
"type": "minecraft:zombie",
"count": 4,
"weight": 90
},
{
"type": "minecraft:zombie_villager",
"count": 1,
"weight": 5
},
{
"type": "minecraft:zombie_horse",
"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": [
[],
[],
[],
[],
[],
[],
[
"minecraft:ore_dirt",
"minecraft:ore_granite_upper",
"minecraft:ore_granite_lower",
"minecraft:ore_diorite_upper",
"minecraft:ore_diorite_lower",
"minecraft:ore_andesite_upper",
"minecraft:ore_andesite_lower",
"minecraft:ore_tuff",
"sanctuary:island_ore_coal",
"sanctuary:island_ore_iron",
"sanctuary:island_ore_copper",
"sanctuary:island_ore_gold",
"sanctuary:island_ore_redstone",
"sanctuary:island_ore_lapis",
"sanctuary:island_ore_diamond"
],
[],
[],
[
"minecraft:glow_lichen",
"minecraft:patch_tall_grass_2",
"minecraft:patch_grass_meadow",
"minecraft:flower_meadow",
"minecraft:trees_meadow",
"minecraft:wildflowers_meadow",
"minecraft:patch_pumpkin",
"minecraft:patch_sugar_cane",
"minecraft:patch_firefly_bush_near_water",
"sanctuary:layered_shore_sugar_cane",
"sanctuary:lower_terrace_vegetation"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,147 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.lush_caves"
}
},
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [
{
"type": "minecraft:bat",
"count": 8,
"weight": 10
}
],
"axolotls": [
{
"type": "minecraft:axolotl",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 4
},
"weight": 10
}
],
"monster": [
{
"type": "minecraft:spider",
"count": 4,
"weight": 100
},
{
"type": "minecraft:zombie",
"count": 4,
"weight": 95
},
{
"type": "minecraft:zombie_villager",
"count": 1,
"weight": 5
},
{
"type": "minecraft:skeleton",
"count": 4,
"weight": 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
}
],
"water_ambient": [
{
"type": "minecraft:tropical_fish",
"count": 8,
"weight": 25
}
]
}
},
"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": [
[],
[],
[],
[],
[],
[],
[
"minecraft:ore_dirt",
"minecraft:ore_granite_upper",
"minecraft:ore_granite_lower",
"minecraft:ore_diorite_upper",
"minecraft:ore_diorite_lower",
"minecraft:ore_andesite_upper",
"minecraft:ore_andesite_lower",
"minecraft:ore_tuff",
"sanctuary:island_ore_coal",
"sanctuary:island_ore_iron",
"sanctuary:island_ore_copper",
"sanctuary:island_ore_gold",
"sanctuary:island_ore_redstone",
"sanctuary:island_ore_lapis",
"sanctuary:island_ore_diamond"
],
[],
[],
[
"minecraft:glow_lichen",
"sanctuary:layered_shore_sugar_cane",
"sanctuary:lower_terrace_vegetation"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,158 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.meadow"
}
},
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [
{
"type": "minecraft:bat",
"count": 8,
"weight": 10
}
],
"creature": [
{
"type": "minecraft:donkey",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 2,
"min_inclusive": 1
},
"weight": 1
},
{
"type": "minecraft:rabbit",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 2
},
"weight": 2
},
{
"type": "minecraft:sheep",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 2
},
"weight": 2
}
],
"monster": [
{
"type": "minecraft:spider",
"count": 4,
"weight": 100
},
{
"type": "minecraft:zombie",
"count": 4,
"weight": 95
},
{
"type": "minecraft:zombie_villager",
"count": 1,
"weight": 5
},
{
"type": "minecraft:skeleton",
"count": 4,
"weight": 100
},
{
"type": "minecraft:creeper",
"count": 4,
"weight": 100
},
{
"type": "minecraft:slime",
"count": 4,
"weight": 100
},
{
"type": "minecraft:enderman",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 1
},
"weight": 10
},
{
"type": "minecraft:witch",
"count": 1,
"weight": 5
}
],
"underground_water_creature": [
{
"type": "minecraft:glow_squid",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 4
},
"weight": 10
}
]
}
},
"modifier": "overlay"
},
"minecraft:visual/sky_color": "#79a6ff"
},
"carvers": [],
"downfall": 0.8,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"features": [
[],
[],
[],
[],
[],
[],
[
"minecraft:ore_dirt",
"minecraft:ore_granite_upper",
"minecraft:ore_granite_lower",
"minecraft:ore_diorite_upper",
"minecraft:ore_diorite_lower",
"minecraft:ore_andesite_upper",
"minecraft:ore_andesite_lower",
"minecraft:ore_tuff",
"sanctuary:island_ore_coal",
"sanctuary:island_ore_iron",
"sanctuary:island_ore_copper",
"sanctuary:island_ore_gold",
"sanctuary:island_ore_redstone",
"sanctuary:island_ore_lapis",
"sanctuary:island_ore_diamond"
],
[],
[],
[
"minecraft:glow_lichen",
"sanctuary:layered_shore_sugar_cane",
"sanctuary:lower_terrace_vegetation"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -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,158 @@
{
"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": "#7aa5ff"
},
"carvers": [],
"downfall": 0.65,
"effects": {
"water_color": "#3f76e4"
},
"features": [
[],
[],
[],
[],
[],
[],
[
"minecraft:ore_dirt",
"minecraft:ore_granite_upper",
"minecraft:ore_granite_lower",
"minecraft:ore_diorite_upper",
"minecraft:ore_diorite_lower",
"minecraft:ore_andesite_upper",
"minecraft:ore_andesite_lower",
"minecraft:ore_tuff",
"sanctuary:island_ore_coal",
"sanctuary:island_ore_iron",
"sanctuary:island_ore_copper",
"sanctuary:island_ore_gold",
"sanctuary:island_ore_redstone",
"sanctuary:island_ore_lapis",
"sanctuary:island_ore_diamond"
],
[],
[],
[
"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:natural_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.6
}
@@ -0,0 +1,174 @@
{
"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:rabbit",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 2
},
"weight": 4
},
{
"type": "minecraft:fox",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 2
},
"weight": 4
}
],
"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/fog_color": "#ccd8e2",
"minecraft:visual/sky_color": "#7ca3ff",
"minecraft:visual/water_fog_color": "#375154"
},
"carvers": [],
"downfall": 0.65,
"effects": {
"dry_foliage_color": "#8c3a04",
"foliage_color": "#e68e30",
"grass_color": "#df6827",
"water_color": "#375154"
},
"features": [
[],
[],
[],
[],
[],
[],
[
"minecraft:ore_dirt",
"minecraft:ore_granite_upper",
"minecraft:ore_granite_lower",
"minecraft:ore_diorite_upper",
"minecraft:ore_diorite_lower",
"minecraft:ore_andesite_upper",
"minecraft:ore_andesite_lower",
"minecraft:ore_tuff",
"sanctuary:island_ore_coal",
"sanctuary:island_ore_iron",
"sanctuary:island_ore_copper",
"sanctuary:island_ore_gold",
"sanctuary:island_ore_redstone",
"sanctuary:island_ore_lapis",
"sanctuary:island_ore_diamond"
],
[],
[],
[
"minecraft:glow_lichen",
"minecraft:trees_dappled_forest",
"minecraft:brown_mushroom_dappled_forest",
"minecraft:patch_red_shrub",
"minecraft:patch_grass_forest",
"sanctuary:natural_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.6
}

Some files were not shown because too many files have changed in this diff Show More