Add natural shores, rock springs and temperate island biomes
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-08 16:10:11 +02:00
parent 6e94c1c539
commit f621f921a1
53 changed files with 4589 additions and 80 deletions
+26
View File
@@ -1,5 +1,31 @@
# Changelog
## 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
+20 -7
View File
@@ -22,22 +22,35 @@ décrits dans la vision ne sont pas encore implémentés.
- 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 à dominante forestière : forêt, plaines, bouleaux, forêt fleurie
et Dappled Forest, avec leur végétation et leurs animaux vanilla. Les formes
de terrain reprennent le générateur 26.2, adapté à l'API 26.3.
- Petits filons de charbon, fer, cuivre, or, redstone, lapis et diamant à des
altitudes adaptées au terrain relevé. 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 et cours deau calmes, 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.
- Jusqu’à trois sources dans des niches rocheuses naturelles : Minecraft fait
couler leur eau en cascades, qui peuvent descendre jusque dans le vide.
- Une petite poche de lave couverte, accessible depuis une corniche naturelle,
peut fournir la matière nécessaire à lobsidienne lorsque le relief convient.
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 grandes rivières, les océans, les ruines et
les autres fonctionnalités restent à construire par tickets.
Lalpha.5 utilise de nouveaux paramètres de génération ; créer un **nouveau monde**
pour découvrir ses biomes, ses plages et ses sources. Les sauvegardes alpha.2,
alpha.3 et alpha.4 conservent leur génération. La validation de cet incrément
est suivie dans [Validation](docs/testing.md).
Le premier champ de recherche climatique est défini : nord froid, sud chaud,
ouest sec et est humide, avec leurs combinaisons diagonales. Un outil de
développement relève les ressources après génération ; le système d'expansion
@@ -60,7 +73,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.5 |
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 +100,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.5.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).
+5 -3
View File
@@ -7,9 +7,11 @@ 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 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.
+20 -4
View File
@@ -118,13 +118,21 @@ 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.
### WG-07 — Introduire un premier biome distinct et une structure
@@ -140,6 +148,14 @@ 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.
## 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.
+25 -6
View File
@@ -30,18 +30,21 @@ 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.
À partir de lalpha.5, l’île initiale utilise une mosaïque de cinq biomes
tempérés : forêt, plaine, boulaie, forêt fleurie et forêt diaprée (Dappled
Forest). La variation locale et lhumidité dépendent de la graine ; elles ne
constituent pas encore le champ de recherche directionnel des continents.
## 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 et cours deau calmes gardent un fond et des berges fermés.
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 +79,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 — alpha.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
+5 -3
View File
@@ -19,7 +19,7 @@ Après les vérifications, le commit et le push de la branche du ticket, le scri
de publication réalise les étapes de release et de canal ci-dessous :
```sh
python3 scripts/publish_pack.py --notes-file chemin/vers/notes.md --asset build/Sanctuary-0.1.0-alpha.4.mrpack
python3 scripts/publish_pack.py --notes-file chemin/vers/notes.md --asset build/Sanctuary-0.1.0-alpha.5.mrpack
```
Les options sont facultatives. Ce script **publie** sur le Git configuré dans
@@ -38,7 +38,7 @@ La procédure complète, également utilisable manuellement :
3. Préparer les métadonnées de la livraison, avec l'URL exacte du futur JAR :
```sh
python3 scripts/pack.py release https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.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.5/sanctuary-0.1.0-alpha.5.jar
```
4. Pousser le commit source vérifié. Créer une release Gitea correspondant à ce
@@ -117,7 +117,9 @@ 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.
+114 -20
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
@@ -74,16 +80,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 la génération
alpha.5, 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. Les cinq
biomes et les minerais sont inspectés sur le terrain. Une réserve de lave
présente conserve ses 18 sources, ses supports et son accès après simulation.
9. Les anciennes clés gardent leur traitement dorigine. Seuls les nouveaux
paramètres activent les rives, biomes et sources de lalpha.5.
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 +137,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 +165,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 +188,85 @@ 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.
## 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
+19 -2
View File
@@ -51,12 +51,29 @@ 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
plus sèches, notamment de larges plages et dépôts irréguliers de sable, gravier,
pierre et terre enherbée. Une rivière et de petits étangs ou lacs de surface
peuvent accueillir de la canne à sucre. 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 plans deau restent retenus et ne doivent pas provoquer une inondation globale. 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
nuages relevés et sans disque noir artificiel apparaissant sous un seuil d'altitude.
Lambiance de départ doit rester principalement forestière, avec des clairières,
des bouleaux, des fleurs et Dappled Forest. Les plages sinspirent des nappes de
sable des générations Alpha/Beta : elles peuvent relier des poches deau, tandis
que dautres rives restent rocheuses ou enherbées. Les quelques cascades servent
également dascenseurs deau pour circuler verticalement.
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. L’état livré figure 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**.
+144 -19
View File
@@ -35,30 +35,33 @@ de test et les sauvegardes personnelles doivent rester distincts.
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.
blocs. Lhydrologie v5 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 cinq biomes tempérés distincts, à dominante
forestière, dont Dappled Forest. Leur végétation et leurs animaux proviennent
de Minecraft 26.3-pre-2. Les nouveaux identifiants ne reprennent pas les tags
de structures vanilla, afin de ne pas placer leurs structures dans le vide.
- Les carvers, géodes, donjons, grands lacs de lave et sources vanilla sont
retirés. Leau de Sanctuary suit son propre plan de surface et de niches
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.5 associe ces biomes aux plages, aux eaux et aux petits 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.
## Sculpture du pourtour — conservée depuis la génération v2
@@ -119,8 +122,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 +178,130 @@ 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.
## Apparition commune
@@ -187,7 +312,7 @@ 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` ou `sanctuary:sanctuary_natural`.
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
+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.5
pack_version=0.1.0-alpha.5
maven_group=fr.koka.sanctuary
+20
View File
@@ -89,3 +89,23 @@ 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') }
@@ -1,6 +1,9 @@
package fr.koka.sanctuary.gametest;
import fr.koka.sanctuary.worldgen.SanctuarySpawn;
import fr.koka.sanctuary.worldgen.NaturalHydrologyRuntime;
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 +26,18 @@ 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 (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 (NaturalHydrologyRuntime.enabled(generator)) NaturalHydrologyDiagnostics.verifyLegacyIsolation(helper);
else HydrologyDiagnostics.verifyLegacyIsolation(helper);
helper.succeed();
}
@@ -104,8 +111,23 @@ 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),
"The legacy island must leave its lower margin entirely empty");
var plan = NaturalHydrologyRuntime.naturalPlan(helper.getLevel());
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);
helper.assertTrue(state.isAir() || state.getFluidState().is(FluidTags.WATER)
&& NaturalHydrologyDiagnostics.isWithinSpringInspectionFootprint(plan, 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,8 @@
package fr.koka.sanctuary;
import fr.koka.sanctuary.worldgen.MainIslandDensity;
import fr.koka.sanctuary.worldgen.NaturalShoreSugarCaneFeature;
import fr.koka.sanctuary.worldgen.TemperateIslandBiomeSource;
import fr.koka.sanctuary.worldgen.ShoreSugarCaneFeature;
import net.fabricmc.api.ModInitializer;
import net.minecraft.core.Registry;
@@ -21,6 +23,8 @@ 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);
LOGGER.info("Sanctuary initialized: the Sanctuary world preset is available.");
}
}
@@ -1,6 +1,7 @@
package fr.koka.sanctuary.mixin;
import fr.koka.sanctuary.worldgen.HydrologyRuntime;
import fr.koka.sanctuary.worldgen.NaturalHydrologyRuntime;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import net.minecraft.core.Holder;
@@ -25,7 +26,12 @@ public abstract class SurfaceHydrologyMixin {
StructureManager structures, BiomeManager biomeManager, WorldGenRegion region,
Set<Holder<Biome>> biomes, CallbackInfoReturnable<CompletableFuture<ChunkAccess>> callback) {
var generator = (NoiseBasedChunkGenerator) (Object) this;
if (HydrologyRuntime.enabled(generator)) {
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,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;
}
}
@@ -22,13 +22,17 @@ 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"));
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));
}
/** 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;
};
}
}
@@ -1,5 +1,10 @@
{
"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"
}
@@ -1,5 +1,10 @@
{
"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"
}
@@ -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
}
@@ -0,0 +1,165 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.flower_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": 3,
"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/sky_color": "#79a6ff"
},
"carvers": [],
"downfall": 0.8,
"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:flower_forest_flowers",
"minecraft:trees_flower_forest",
"minecraft:flower_flower_forest",
"minecraft:patch_grass_badlands",
"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.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.8,
"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: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:natural_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,173 @@
{
"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": "#78a7ff"
},
"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:patch_tall_grass_2",
"minecraft:patch_bush",
"minecraft:trees_plains",
"minecraft:flower_plains",
"minecraft:patch_grass_plain",
"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.8
}
@@ -0,0 +1,6 @@
{
"type": "minecraft:noise",
"noise": "sanctuary:temperate_moisture",
"xz_scale": 1.0,
"y_scale": 0.0
}
@@ -0,0 +1,6 @@
{
"type": "minecraft:noise",
"noise": "sanctuary:temperate_variation",
"xz_scale": 1.0,
"y_scale": 0.0
}
@@ -0,0 +1,83 @@
{
"type": "minecraft:ore",
"discard_chance_on_air_exposure": 0.25,
"size": 6,
"targets": [
{
"state": "minecraft:coal_ore",
"target": {
"predicate_type": "minecraft:any_of",
"rules": [
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
},
{
"max_inclusive": 2031,
"min_inclusive": 0,
"predicate_type": "minecraft:height_match"
}
]
},
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:not",
"rule": {
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
}
},
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:stone_ore_replaceables"
}
]
}
]
}
},
{
"state": "minecraft:deepslate_coal_ore",
"target": {
"predicate_type": "minecraft:any_of",
"rules": [
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
},
{
"max_inclusive": 8,
"min_inclusive": -2032,
"predicate_type": "minecraft:height_match"
}
]
},
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:not",
"rule": {
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
}
},
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:deepslate_ore_replaceables"
}
]
}
]
}
}
]
}
@@ -0,0 +1,83 @@
{
"type": "minecraft:ore",
"discard_chance_on_air_exposure": 0.25,
"size": 4,
"targets": [
{
"state": "minecraft:copper_ore",
"target": {
"predicate_type": "minecraft:any_of",
"rules": [
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
},
{
"max_inclusive": 2031,
"min_inclusive": 0,
"predicate_type": "minecraft:height_match"
}
]
},
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:not",
"rule": {
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
}
},
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:stone_ore_replaceables"
}
]
}
]
}
},
{
"state": "minecraft:deepslate_copper_ore",
"target": {
"predicate_type": "minecraft:any_of",
"rules": [
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
},
{
"max_inclusive": 8,
"min_inclusive": -2032,
"predicate_type": "minecraft:height_match"
}
]
},
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:not",
"rule": {
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
}
},
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:deepslate_ore_replaceables"
}
]
}
]
}
}
]
}
@@ -0,0 +1,83 @@
{
"type": "minecraft:ore",
"discard_chance_on_air_exposure": 0.7,
"size": 3,
"targets": [
{
"state": "minecraft:diamond_ore",
"target": {
"predicate_type": "minecraft:any_of",
"rules": [
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
},
{
"max_inclusive": 2031,
"min_inclusive": 0,
"predicate_type": "minecraft:height_match"
}
]
},
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:not",
"rule": {
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
}
},
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:stone_ore_replaceables"
}
]
}
]
}
},
{
"state": "minecraft:deepslate_diamond_ore",
"target": {
"predicate_type": "minecraft:any_of",
"rules": [
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
},
{
"max_inclusive": 8,
"min_inclusive": -2032,
"predicate_type": "minecraft:height_match"
}
]
},
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:not",
"rule": {
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
}
},
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:deepslate_ore_replaceables"
}
]
}
]
}
}
]
}
@@ -0,0 +1,83 @@
{
"type": "minecraft:ore",
"discard_chance_on_air_exposure": 0.4,
"size": 4,
"targets": [
{
"state": "minecraft:gold_ore",
"target": {
"predicate_type": "minecraft:any_of",
"rules": [
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
},
{
"max_inclusive": 2031,
"min_inclusive": 0,
"predicate_type": "minecraft:height_match"
}
]
},
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:not",
"rule": {
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
}
},
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:stone_ore_replaceables"
}
]
}
]
}
},
{
"state": "minecraft:deepslate_gold_ore",
"target": {
"predicate_type": "minecraft:any_of",
"rules": [
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
},
{
"max_inclusive": 8,
"min_inclusive": -2032,
"predicate_type": "minecraft:height_match"
}
]
},
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:not",
"rule": {
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
}
},
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:deepslate_ore_replaceables"
}
]
}
]
}
}
]
}
@@ -0,0 +1,83 @@
{
"type": "minecraft:ore",
"discard_chance_on_air_exposure": 0.25,
"size": 4,
"targets": [
{
"state": "minecraft:iron_ore",
"target": {
"predicate_type": "minecraft:any_of",
"rules": [
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
},
{
"max_inclusive": 2031,
"min_inclusive": 0,
"predicate_type": "minecraft:height_match"
}
]
},
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:not",
"rule": {
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
}
},
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:stone_ore_replaceables"
}
]
}
]
}
},
{
"state": "minecraft:deepslate_iron_ore",
"target": {
"predicate_type": "minecraft:any_of",
"rules": [
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
},
{
"max_inclusive": 8,
"min_inclusive": -2032,
"predicate_type": "minecraft:height_match"
}
]
},
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:not",
"rule": {
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
}
},
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:deepslate_ore_replaceables"
}
]
}
]
}
}
]
}
@@ -0,0 +1,83 @@
{
"type": "minecraft:ore",
"discard_chance_on_air_exposure": 0.4,
"size": 3,
"targets": [
{
"state": "minecraft:lapis_ore",
"target": {
"predicate_type": "minecraft:any_of",
"rules": [
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
},
{
"max_inclusive": 2031,
"min_inclusive": 0,
"predicate_type": "minecraft:height_match"
}
]
},
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:not",
"rule": {
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
}
},
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:stone_ore_replaceables"
}
]
}
]
}
},
{
"state": "minecraft:deepslate_lapis_ore",
"target": {
"predicate_type": "minecraft:any_of",
"rules": [
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
},
{
"max_inclusive": 8,
"min_inclusive": -2032,
"predicate_type": "minecraft:height_match"
}
]
},
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:not",
"rule": {
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
}
},
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:deepslate_ore_replaceables"
}
]
}
]
}
}
]
}
@@ -0,0 +1,83 @@
{
"type": "minecraft:ore",
"discard_chance_on_air_exposure": 0.4,
"size": 4,
"targets": [
{
"state": "minecraft:redstone_ore",
"target": {
"predicate_type": "minecraft:any_of",
"rules": [
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
},
{
"max_inclusive": 2031,
"min_inclusive": 0,
"predicate_type": "minecraft:height_match"
}
]
},
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:not",
"rule": {
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
}
},
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:stone_ore_replaceables"
}
]
}
]
}
},
{
"state": "minecraft:deepslate_redstone_ore",
"target": {
"predicate_type": "minecraft:any_of",
"rules": [
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
},
{
"max_inclusive": 8,
"min_inclusive": -2032,
"predicate_type": "minecraft:height_match"
}
]
},
{
"predicate_type": "minecraft:all_of",
"rules": [
{
"predicate_type": "minecraft:not",
"rule": {
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:height_specific_ore_replaceables"
}
},
{
"predicate_type": "minecraft:tag_match",
"tag": "minecraft:deepslate_ore_replaceables"
}
]
}
]
}
}
]
}
@@ -0,0 +1,9 @@
{
"base_octave": -5,
"octave_count": 3,
"amplitude_modifiers": [
1.0,
0.5,
0.25
]
}
@@ -0,0 +1,9 @@
{
"base_octave": -6,
"octave_count": 3,
"amplitude_modifiers": [
1.0,
0.5,
0.25
]
}
@@ -0,0 +1,23 @@
{
"default_block": "minecraft:stone",
"default_fluid": "minecraft:air",
"disable_mob_generation": false,
"legacy_random_source": true,
"material_rule": "sanctuary:starter_island",
"noise": {
"height": 384,
"min_y": 0
},
"noise_router": {
"chunk_surface_level": 0.0,
"continents": 0.0,
"depth": 0.0,
"erosion": 0.0,
"final_density": "sanctuary:final_density_384",
"ridges": 0.0,
"temperature": "sanctuary:temperate_variation",
"vegetation": "sanctuary:temperate_moisture"
},
"sea_level": -64,
"spawn_target": []
}
@@ -0,0 +1,27 @@
{
"feature": "sanctuary:island_ore_coal",
"placement": [
{
"type": "minecraft:count",
"count": 2
},
{
"type": "minecraft:in_square"
},
{
"type": "minecraft:height_range",
"height": {
"type": "minecraft:trapezoid",
"min_inclusive": {
"absolute": 96
},
"max_inclusive": {
"absolute": 288
}
}
},
{
"type": "minecraft:biome"
}
]
}
@@ -0,0 +1,27 @@
{
"feature": "sanctuary:island_ore_copper",
"placement": [
{
"type": "minecraft:count",
"count": 1
},
{
"type": "minecraft:in_square"
},
{
"type": "minecraft:height_range",
"height": {
"type": "minecraft:trapezoid",
"min_inclusive": {
"absolute": 96
},
"max_inclusive": {
"absolute": 248
}
}
},
{
"type": "minecraft:biome"
}
]
}
@@ -0,0 +1,27 @@
{
"feature": "sanctuary:island_ore_diamond",
"placement": [
{
"type": "minecraft:rarity_filter",
"chance": 4
},
{
"type": "minecraft:in_square"
},
{
"type": "minecraft:height_range",
"height": {
"type": "minecraft:trapezoid",
"min_inclusive": {
"absolute": 104
},
"max_inclusive": {
"absolute": 208
}
}
},
{
"type": "minecraft:biome"
}
]
}
@@ -0,0 +1,27 @@
{
"feature": "sanctuary:island_ore_gold",
"placement": [
{
"type": "minecraft:count",
"count": 1
},
{
"type": "minecraft:in_square"
},
{
"type": "minecraft:height_range",
"height": {
"type": "minecraft:trapezoid",
"min_inclusive": {
"absolute": 104
},
"max_inclusive": {
"absolute": 224
}
}
},
{
"type": "minecraft:biome"
}
]
}
@@ -0,0 +1,27 @@
{
"feature": "sanctuary:island_ore_iron",
"placement": [
{
"type": "minecraft:count",
"count": 2
},
{
"type": "minecraft:in_square"
},
{
"type": "minecraft:height_range",
"height": {
"type": "minecraft:trapezoid",
"min_inclusive": {
"absolute": 88
},
"max_inclusive": {
"absolute": 248
}
}
},
{
"type": "minecraft:biome"
}
]
}
@@ -0,0 +1,27 @@
{
"feature": "sanctuary:island_ore_lapis",
"placement": [
{
"type": "minecraft:rarity_filter",
"chance": 2
},
{
"type": "minecraft:in_square"
},
{
"type": "minecraft:height_range",
"height": {
"type": "minecraft:trapezoid",
"min_inclusive": {
"absolute": 112
},
"max_inclusive": {
"absolute": 232
}
}
},
{
"type": "minecraft:biome"
}
]
}
@@ -0,0 +1,27 @@
{
"feature": "sanctuary:island_ore_redstone",
"placement": [
{
"type": "minecraft:count",
"count": 1
},
{
"type": "minecraft:in_square"
},
{
"type": "minecraft:height_range",
"height": {
"type": "minecraft:trapezoid",
"min_inclusive": {
"absolute": 96
},
"max_inclusive": {
"absolute": 224
}
}
},
{
"type": "minecraft:biome"
}
]
}
@@ -0,0 +1,4 @@
{
"feature": { "type": "sanctuary:natural_shore_sugar_cane" },
"placement": []
}
@@ -5,10 +5,14 @@
"generator": {
"type": "minecraft:noise",
"biome_source": {
"type": "minecraft:fixed",
"biome": "sanctuary:starter_forest_hydrology"
"type": "sanctuary:temperate_island",
"forest": "sanctuary:temperate_forest",
"plains": "sanctuary:temperate_plains",
"birch_forest": "sanctuary:temperate_birch_forest",
"flower_forest": "sanctuary:temperate_flower_forest",
"dappled_forest": "sanctuary:temperate_dappled_forest"
},
"settings": "sanctuary:sanctuary_hydrology"
"settings": "sanctuary:sanctuary_natural"
}
},
"minecraft:the_end": {
@@ -0,0 +1,140 @@
package fr.koka.sanctuary.worldgen;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** Determinism and containment checks independent of Minecraft, using controlled floating terrain. */
public final class NaturalHydrologySmoke {
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
public static void main(String[] args) {
NaturalHydrology.Sampler terrain = (x, y, z) -> {
int top = 200 + (int) Math.round(2 * Math.sin(x / 24.0) + 2 * Math.cos(z / 31.0));
return Math.hypot(x, z) < 220 && y <= top && y >= 145 ? 0.45F : -0.45F;
};
var first = NaturalHydrology.create(0, terrain);
var replay = NaturalHydrology.create(0, terrain);
var changed = NaturalHydrology.create(42, terrain);
require(!first.features().isEmpty(), "The safe rolling surface should contain surface water");
require(List.copyOf(first.cells()).equals(List.copyOf(replay.cells())) && first.features().equals(replay.features())
&& first.springs().equals(replay.springs()),
"The same seed and density must replay exactly");
require(!List.copyOf(first.cells()).equals(List.copyOf(changed.cells())), "The world seed must influence water placement");
verify(first, terrain);
verify(changed, terrain);
NaturalHydrology.Sampler pierced = (x, y, z) ->
Math.floorMod(x, 37) < 2 && Math.floorMod(z, 41) < 2 ? -0.45F : terrain.sample(x, y, z);
var aroundShafts = NaturalHydrology.create(0, pierced);
require(!aroundShafts.features().isEmpty(), "Safe areas around natural shafts should remain usable");
verify(aroundShafts, pierced);
require(NaturalHydrology.create(0, (x, y, z) -> -1).cells().isEmpty(), "An empty world must not acquire a guaranteed basin");
var roof = NaturalHydrology.create(0, (x, y, z) ->
Math.hypot(x, z) < 220 && (y == 240 || y >= 145 && y <= 200) ? 0.45F : -0.45F);
require(roof.cells().isEmpty(), "Do not create underground lakes below a thin higher roof");
var slab = NaturalHydrology.create(0, (x, y, z) ->
Math.hypot(x, z) < 220 && y >= 199 && y <= 200 ? 0.45F : -0.45F);
require(slab.cells().isEmpty(), "A thin floating slab cannot safely support water");
var flat = NaturalHydrology.create(0, (x, y, z) ->
Math.hypot(x, z) < 220 && y >= 145 && y <= 200 ? 0.45F : -0.45F);
verify(flat, (x, y, z) -> Math.hypot(x, z) < 220 && y >= 145 && y <= 200 ? 0.45F : -0.45F);
require(!flat.springs().isEmpty(), "A natural cliff over the void must exercise rock spring placement");
long wet = flat.cells().stream().filter(NaturalHydrology.Cell::hasWater).count();
require(flat.cells().size() > wet * 2, "Sediment sheets must extend beyond a narrow one-block ring");
require(flat.cells().stream().anyMatch(cell -> cell.material() == NaturalHydrology.ShoreMaterial.STONE),
"Some natural shores should be rocky");
require(flat.cells().stream().anyMatch(cell -> cell.material() == NaturalHydrology.ShoreMaterial.GRASS),
"Some natural shores should retain grassy terrain");
require(flat.cells().stream().anyMatch(cell -> cell.sedimentDepth() == 5), "Some deposits should be five blocks thick");
require(flat.features().stream().anyMatch(feature -> feature.kind() == NaturalHydrology.Kind.STREAM),
"The controlled safe plateau must exercise the stream planner");
System.out.println("NaturalHydrologySmoke: shallow closed water, broad layered shores, deterministic rock springs and bounded flows passed.");
}
private static void verify(NaturalHydrology.Plan plan, NaturalHydrology.Sampler terrain) {
Map<String, NaturalHydrology.Cell> byChunks = new HashMap<>();
// Reverse chunk traversal must reconstruct the same cells without generating neighbours.
for (int cx = 18; cx >= -18; cx--) for (int cz = 18; cz >= -18; cz--) {
var chunk = plan.cellsInChunk(cx, cz);
NaturalHydrology.Cell previous = null;
for (var cell : chunk) {
require(cell.x() >> 4 == cx && cell.z() >> 4 == cz, "A chunk slice writes outside its chunk");
require(previous == null || previous.x() < cell.x() || previous.x() == cell.x() && previous.z() < cell.z(),
"Cell order must be stable for seeded shore vegetation");
previous = cell;
require(byChunks.put(cell.x() + "," + cell.z(), cell) == null, "A cell belongs to multiple chunk slices");
}
}
require(byChunks.size() == plan.cells().size(), "Chunk slices omitted planned cells");
for (var cell : plan.cells()) {
require(cell.equals(byChunks.get(cell.x() + "," + cell.z())), "Chunk traversal changed the plan");
for (int y = cell.bedY() - cell.sedimentDepth() - 1; y <= cell.bedY(); y++) {
require(terrain.sample(cell.x(), y, cell.z()) > 0, "The entire sediment layer and two supporting blocks must already exist");
}
require(cell.sedimentDepth() >= 3 && cell.sedimentDepth() <= 5, "Sediment thickness must vary between 3 and 5");
if (!cell.hasWater()) {
require(cell.carveTop() - cell.bedY() >= 0 && cell.carveTop() - cell.bedY() <= 2,
"Dry terraces must remove at most two blocks");
continue;
}
require(cell.waterY() - cell.bedY() >= 1 && cell.waterY() - cell.bedY() <= NaturalHydrology.MAX_DEPTH, "Invalid shallow-water depth");
require(cell.carveTop() - cell.bedY() <= NaturalHydrology.MAX_CARVE, "Excavation exceeds four blocks");
for (int y = cell.carveTop() + 1; y < 384; y++) {
require(terrain.sample(cell.x(), y, cell.z()) <= 0, "Water placed below a natural roof");
}
for (int[] d : CARDINALS) {
int x = cell.x() + d[0], z = cell.z() + d[1];
var neighbor = plan.cellAt(x, z);
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) {
boolean plannedWater = neighbor != null && neighbor.hasWater()
&& y > neighbor.bedY() && y <= neighbor.waterY();
boolean carvedAir = neighbor != null && y > neighbor.bedY() && y <= neighbor.carveTop()
&& !plannedWater;
require(plannedWater || !carvedAir && terrain.sample(x, y, z) > 0,
"A water cell has an open horizontal outlet, including excavated banks");
}
}
}
for (var spring : plan.springs()) {
var source = spring.source();
var outlet = spring.outlet();
require(terrain.sample(source.x(), source.y(), source.z()) > 0, "Spring must replace existing rock");
require(terrain.sample(source.x(), source.y() + 1, source.z()) > 0, "Rock spring must have a natural roof");
require(Math.abs(source.x() - outlet.x()) + Math.abs(source.z() - outlet.z()) == 1
&& source.y() == outlet.y(), "Spring outlet must be adjacent to its source");
for (int[] direction : CARDINALS) {
int x = source.x() + direction[0], z = source.z() + direction[1];
if (x == outlet.x() && z == outlet.z()) continue;
require(terrain.sample(x, source.y(), z) > 0, "A spring niche must have three natural lateral walls");
}
for (int y = source.y() - 3; y < source.y(); y++) {
require(terrain.sample(source.x(), y, source.z()) > 0, "Source needs an intact natural rock floor");
}
for (int y = source.y(); y >= source.y() - 8; y--) {
require(terrain.sample(outlet.x(), y, outlet.z()) <= 0, "Waterfall needs an exposed natural drop");
}
for (var cell : plan.cells()) {
require(Math.hypot(source.x() - cell.x(), source.z() - cell.z()) >= 32,
"Rock springs must stay away from modified closed-water terrain");
}
require(spring.flowBounds().minY() <= source.y() - 12, "Spring requires a real descending route");
require(spring.flowPath().getFirst().equals(source), "Flow path starts at the single carved source");
NaturalHydrology.Position previous = null;
for (var position : spring.flowPath()) {
require(plan.allowsSpringFlow(position.x(), position.y(), position.z()), "Path must lie inside the flow envelope");
require(position.equals(source) || terrain.sample(position.x(), position.y(), position.z()) <= 0,
"Waterfall path must use existing air; no cliff excavation or rock filling");
if (previous != null) require(position.y() <= previous.y()
&& Math.abs(position.x() - previous.x()) + Math.abs(position.y() - previous.y())
+ Math.abs(position.z() - previous.z()) == 1, "Flow path must be contiguous and never ascend");
previous = position;
}
require(plan.springsInChunk(source.x() >> 4, source.z() >> 4).contains(spring), "Spring belongs to its source chunk");
require(!plan.allowsSpringFlow(10000, source.y(), 10000), "Bare-terrain prediction must exclude unrelated void water");
}
}
private static void require(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
}
@@ -0,0 +1,130 @@
package fr.koka.sanctuary.worldgen;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/** Small lava supply: existing rock, dry access, natural containment, and no mandatory fallback. */
public final class StarterLavaDepositSmoke {
private static final int[][] DIRECTIONS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
public static void main(String[] args) {
var noWater = NaturalHydrology.create(0, (x, y, z) -> -1);
NaturalHydrology.Sampler cliff = (x, y, z) -> Math.hypot(x, z) < 220
&& y >= 100 && y <= (x <= 0 ? 240 : 200) ? 1 : -1;
var first = StarterLavaDeposit.create(0, cliff, noWater);
var replay = StarterLavaDeposit.create(0, cliff, noWater);
var otherSeed = StarterLavaDeposit.create(42, cliff, noWater);
require(first.lavaCells().size() == 18, "A thick natural cliff should provide one small deposit");
require(first.cells().equals(replay.cells()) && first.supports().equals(replay.supports())
&& first.access().equals(replay.access()), "The same seed must replay the entire plan");
require(!first.cells().equals(otherSeed.cells()), "The seed must influence which suitable ledge is chosen");
verify(first, cliff, noWater);
verify(otherSeed, cliff, noWater);
try {
first.cells().clear();
throw new AssertionError("Plan cells must be immutable");
} catch (UnsupportedOperationException expected) {
// Passed.
}
require(StarterLavaDeposit.create(0, (x, y, z) -> -1, noWater).cells().isEmpty(), "No lava in the void");
require(StarterLavaDeposit.create(0, (x, y, z) -> 1, noWater).cells().isEmpty(), "No sealed underground pocket");
require(StarterLavaDeposit.create(0, (x, y, z) -> y < 199 ? -1 : cliff.sample(x, y, z), noWater)
.cells().isEmpty(), "Thin natural floors must not receive a manufactured foundation");
require(StarterLavaDeposit.create(0, (x, y, z) -> y == 198 && Math.floorMod(x, 2) == 0
? -1 : cliff.sample(x, y, z), noWater).cells().isEmpty(), "Perforated floors must reject the deposit");
NaturalHydrology.Sampler thinRoof = (x, y, z) -> Math.hypot(x, z) < 220
&& y >= 100 && y <= (x <= 0 ? 207 : 200) ? 1 : -1;
require(StarterLavaDeposit.create(0, thinRoof, noWater).cells().isEmpty(), "No exposed or thinly covered lava");
// Exercise real water exclusions as well; no lava is an acceptable result on a crowded cliff.
var water = NaturalHydrology.create(0, cliff);
verify(StarterLavaDeposit.create(0, cliff, water), cliff, water);
System.out.println("StarterLavaDepositSmoke: deterministic 18-source pocket, natural floor/walls/roof, walkable access, water separation and no fallback passed.");
}
private static void verify(StarterLavaDeposit.Plan plan, NaturalHydrology.Sampler terrain, NaturalHydrology.Plan water) {
Map<StarterLavaDeposit.Position, StarterLavaDeposit.Cell> changes = new HashMap<>();
Set<StarterLavaDeposit.Position> supports = new HashSet<>();
for (int cx = 12; cx >= -12; cx--) for (int cz = 12; cz >= -12; cz--) {
for (var cell : plan.cellsInChunk(cx, cz)) {
require(cell.x() >> 4 == cx && cell.z() >> 4 == cz, "A modification crossed its owning chunk");
var pos = new StarterLavaDeposit.Position(cell.x(), cell.y(), cell.z());
require(changes.put(pos, cell) == null, "Repeated modification in chunk slices");
}
for (var support : plan.supportsInChunk(cx, cz)) {
require(support.x() >> 4 == cx && support.z() >> 4 == cz, "A support crossed its owning chunk");
require(supports.add(support), "Repeated support in chunk slices");
}
}
require(changes.size() == plan.cells().size() && supports.size() == plan.supports().size(), "Chunk order lost data");
for (var support : supports) {
require(!changes.containsKey(support), "A natural support is also being excavated");
require(terrain.sample(support.x(), support.y(), support.z()) > 0, "Manufactured rock support");
}
require(plan.lavaCells().size() == 0 || plan.lavaCells().size() == StarterLavaDeposit.LAVA_VOLUME,
"The planner must place at most one 18-source pocket");
for (var cell : plan.cells()) {
require(cell.y() >= 0 && cell.y() < 384 && Math.hypot(cell.x(), cell.z()) < StarterLavaDeposit.RADIUS,
"Lava excavation exceeded island bounds");
require(terrain.sample(cell.x(), cell.y(), cell.z()) > 0, "A modification replaced natural air");
for (var shore : water.cells()) require(Math.hypot(cell.x() - shore.x(), cell.z() - shore.z()) >= 32,
"Lava or its access intersects the water exclusion distance");
for (var spring : water.springs()) {
var b = spring.flowBounds();
double dx = Math.max(Math.max(b.minX() - cell.x(), 0), cell.x() - b.maxX());
double dz = Math.max(Math.max(b.minZ() - cell.z(), 0), cell.z() - b.maxZ());
require(Math.hypot(dx, dz) >= 32, "A waterfall permission volume can reach the deposit");
}
if (!cell.lava()) continue;
var below = new StarterLavaDeposit.Position(cell.x(), cell.y() - 1, cell.z());
require(retainedSolidOrLava(below, changes, terrain), "Lava has an open floor");
for (int[] direction : DIRECTIONS) require(retainedSolidOrLava(new StarterLavaDeposit.Position(
cell.x() + direction[0], cell.y(), cell.z() + direction[1]), changes, terrain), "Lava has a side outlet");
}
if (plan.lavaCells().isEmpty()) {
require(plan.access().isEmpty() && plan.cells().isEmpty() && plan.supports().isEmpty(), "An empty result left partial excavation");
return;
}
var entry = plan.access().orElseThrow();
for (int y = entry.y(); y < 384; y++) require(terrain.sample(entry.x(), y, entry.z()) <= 0,
"Access does not start from naturally open air");
// A player can walk from the outdoor ledge to the dry lip next to the lava, without digging
// or crossing a fluid. Merely opening an isolated cavity would not satisfy this check.
Set<StarterLavaDeposit.Position> seen = new HashSet<>();
ArrayDeque<StarterLavaDeposit.Position> queue = new ArrayDeque<>();
seen.add(entry); queue.add(entry);
boolean reached = false;
while (!queue.isEmpty() && seen.size() <= 1600) {
var at = queue.removeFirst();
for (int[] direction : DIRECTIONS) {
var floor = new StarterLavaDeposit.Position(at.x() + direction[0], at.y() - 1, at.z() + direction[1]);
var fluid = changes.get(floor);
reached |= fluid != null && fluid.lava();
var next = new StarterLavaDeposit.Position(at.x() + direction[0], at.y(), at.z() + direction[1]);
if (Math.abs(next.x() - entry.x()) > 18 || Math.abs(next.z() - entry.z()) > 18 || seen.contains(next)) continue;
if (!air(next, changes, terrain) || !air(new StarterLavaDeposit.Position(next.x(), next.y() + 1, next.z()), changes, terrain)) continue;
if (changes.containsKey(floor) || terrain.sample(floor.x(), floor.y(), floor.z()) <= 0) continue;
seen.add(next); queue.addLast(next);
}
}
require(reached, "The pocket has no dry, two-block-high access from the natural ledge");
}
private static boolean air(StarterLavaDeposit.Position at, Map<StarterLavaDeposit.Position, StarterLavaDeposit.Cell> changes,
NaturalHydrology.Sampler terrain) {
var change = changes.get(at);
return change == null ? terrain.sample(at.x(), at.y(), at.z()) <= 0 : !change.lava();
}
private static boolean retainedSolidOrLava(StarterLavaDeposit.Position at,
Map<StarterLavaDeposit.Position, StarterLavaDeposit.Cell> changes, NaturalHydrology.Sampler terrain) {
var change = changes.get(at);
return change == null ? terrain.sample(at.x(), at.y(), at.z()) > 0 : change.lava();
}
private static void require(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
}
+1 -1
View File
@@ -51,7 +51,7 @@ Pour produire un fichier importable par un lanceur compatible Modrinth :
```sh
cd build/packwiz # depuis la racine du dépôt
packwiz modrinth export --output ../Sanctuary-0.1.0-alpha.4.mrpack
packwiz modrinth export --output ../Sanctuary-0.1.0-alpha.5.mrpack
```
L'export contient le JAR Sanctuary local et la référence vérifiée de Fabric API.
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "0.1.0-alpha.4"
version = "0.1.0-alpha.5"
pack-format = "packwiz:1.1.0"
[index]