Add layered meadows, rocky shores and lower wall springs
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-08 19:16:45 +02:00
parent f621f921a1
commit f1e48fa969
43 changed files with 4087 additions and 55 deletions
+20
View File
@@ -1,5 +1,25 @@
# Changelog
## 0.1.0-alpha.6 — 2026-09-08
- Palette tempérée plus douce, avec des couleurs communes pour lherbe, le
feuillage et leau. Dappled Forest quitte l’île initiale et reste réservé
aux futurs continents ; les sauvegardes alpha.5 le conservent.
- Prairies fleuries et affleurements rocheux plus étendus, avec une répartition
des biomes selon laltitude. Les corniches inférieures peuvent porter de la
mousse, des fougères, des fleurs et des petits arbustes.
- Sources recherchées aussi dans les parois des strates inférieures. Les
cascades peuvent rejoindre un palier ou descendre dans le vide.
- Retrait des courts ruisseaux à niveau constant : l’île conserve ses étangs
et petits lacs. La grande rivière avec source, chute et bassin reste à
développer ; elle nest pas activée dans cette livraison.
- Nouvelle clé `sanctuary:sanctuary_layered`, avec les anciennes générations
conservées. Créer un **nouveau monde Sanctuary** pour cet incrément.
Validation : dix tests serveur réussis sur chacune des graines `0`, `42` et
`8675309`, avec 1 800 ticks de fluides, contrôles des corniches et respect des
modifications ultérieures aux sources. `check build assemblePack` réussi.
## 0.1.0-alpha.5 — 2026-09-08
- Plages plus larges et irrégulières, en nappes de sable, gravier et roche, avec
+15 -13
View File
@@ -22,9 +22,9 @@ 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.
- Î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.
- Île tempérée aux teintes douces : prairies fleuries, bosquets, mousse et
affleurements rocheux, avec des biomes qui varient aussi selon laltitude. 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
@@ -32,24 +32,26 @@ décrits dans la vision ne sont pas encore implémenté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.
- Étangs, petits lacs et cours deau calmes, plages plus larges et dépôts de
- Étangs, petits lacs, plages plus larges et dépôts de
trois à cinq blocs de sable, gravier, pierre ou terre enherbée. La canne à
sucre apparaît lorsque les rives respectent ses règles vanilla.
- Jusqu’à trois sources dans des niches rocheuses naturelles : Minecraft fait
couler leur eau en cascades, qui peuvent descendre jusque dans le vide.
- Les courts ruisseaux imposés sont retirés ; l’île garde ses étangs et petits
lacs. La grande rivière traversante reste à développer.
- Quelques sources dans des niches rocheuses naturelles, y compris aux étages
inférieurs : Minecraft fait couler leur eau en cascades, qui peuvent descendre jusque dans le vide.
- Une petite poche de lave couverte, accessible depuis une corniche naturelle,
peut fournir la matière nécessaire à lobsidienne lorsque le relief convient.
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, les océans, les ruines et
TerraMix, les continents déverrouillables, 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).
Lalpha.6 utilise de nouveaux paramètres de génération ; créer un **nouveau monde**
pour découvrir ses biomes par altitude et ses eaux. Les sauvegardes alpha.2
à alpha.5 conservent leur génération. Dappled Forest est désormais réservé aux
futurs continents et reste présent dans les anciennes sauvegardes alpha.5. La validation de cet incrément est suivie dans [Validation](docs/testing.md).
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
@@ -73,7 +75,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.5 |
| Sanctuary / pack | 0.1.0-alpha.6 |
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
@@ -100,7 +102,7 @@ décrits dans [Validation](docs/testing.md).
Résultats :
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.5.jar` : mod à installer avec
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.6.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).
+3 -1
View File
@@ -9,7 +9,9 @@ communautaire indépendant. Le dépôt ne redistribue pas le jeu.
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
une île finie. Les variantes alpha.6 réutilisent les végétations vanilla pour
les prairies fleuries et les bosquets ; Dappled Forest reste disponible pour
les sauvegardes alpha.5. 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.
+17
View File
@@ -134,6 +134,18 @@ vide. Le contrat et lisolation des anciennes générations sont décrits dans
[Génération](worldgen.md). Ce travail prépare les eaux des continents sans clore
WG-06 : océans, grandes rivières et ouverture des continents restent à développer.
**Incrément île initiale — alpha.6 :** retrait des courts ruisseaux, conservation
des bassins, présence de roche élargie et sources de paroi dans les strates
inférieures. Les écoulements sont contrôlés dans le moteur et les anciennes
générations sont préservées.
**Ticket suivant — grande rivière facultative :** chercher un long parcours qui
épouse l’île, avec source, chute et bassin, rives de sable, gravier et argile.
Le démontrer sur la vraie densité Minecraft puis après décoration et ticks de
fluide, avec une graine et des coordonnées reproductibles. Les essais du
prototype alpha.6 nont fourni aucun parcours retenu sur les graines de référence ;
ce prototype nest pas distribué. Ne pas imposer de rivière si le relief ne sy prête pas.
### WG-07 — Introduire un premier biome distinct et une structure
**Dépendance :** WG-06.
@@ -156,6 +168,11 @@ reprise du catalogue TerraMix nest encore fournie. La survie initiale doit
permettre de produire et progresser au-delà du bois et de la pierre ; laccès
à toute la progression Minecraft nest pas encore garanti pour chaque seed.
**Ticket palette — alpha.6 :** réserver les biomes très contrastés de la 26.3,
dont Dappled Forest, aux futurs continents. L’île combine prairies fleuries,
bosquets et zones rocheuses, avec des teintes cohérentes et une végétation
effective sur les corniches inférieures. Cela ne livre pas encore les continents.
## 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.
+11 -5
View File
@@ -30,9 +30,11 @@ 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.
À 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
Lalpha.5 proposait cinq biomes tempérés dont Dappled Forest. Lalpha.6 réserve
ce dernier aux futurs continents et donne à l’île une palette plus douce de
prairies, prairies fleuries, bosquets, corniches moussues et zones rocheuses.
La répartition tient compte de laltitude, y compris dans les strates inférieures.
La variation locale et lhumidité dépendent de la graine ; elles ne
constituent pas encore le champ de recherche directionnel des continents.
## Eau de surface et exploration
@@ -40,7 +42,11 @@ constituent pas encore le champ de recherche directionnel des continents.
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 bassins et cours deau calmes gardent un fond et des berges fermés.
Les bassins gardent un fond et des berges fermés. Une grande rivière reste
facultative : elle doit suivre le relief et former un parcours lisible avec
source, chute et bassin. Ses exutoires peuvent laisser couler leau dans le vide.
Sans parcours suffisamment long, l’île conserve seulement ses petits bassins.
Lalpha.6 livre cette variante à bassins seuls ; la grande rivière reste à développer.
Lalpha.5 ajoute séparément quelques sources sortant dune niche rocheuse
naturelle : leurs cascades peuvent descendre dans le vide et servir à remonter
verticalement. Cet écoulement est volontaire ; il ne doit pas entraîner des
@@ -79,7 +85,7 @@ 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
## Ressources de départ — depuis lalpha.5
L’île vise les premières étapes de survie, les outils, lenchantement et la
redstone. Les sept minerais (charbon, fer, cuivre, or, redstone, lapis et diamant)
+6 -2
View File
@@ -19,7 +19,7 @@ Après les vérifications, le commit et le push de la branche du ticket, le scri
de publication réalise les étapes de release et de canal ci-dessous :
```sh
python3 scripts/publish_pack.py --notes-file chemin/vers/notes.md --asset build/Sanctuary-0.1.0-alpha.5.mrpack
python3 scripts/publish_pack.py --notes-file chemin/vers/notes.md --asset build/Sanctuary-0.1.0-alpha.6.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.5/sanctuary-0.1.0-alpha.5.jar
python3 scripts/pack.py release https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.6/sanctuary-0.1.0-alpha.6.jar
```
4. Pousser le commit source vérifié. Créer une release Gitea correspondant à ce
@@ -123,6 +123,10 @@ séparé : créer un nouveau monde Sanctuary pour les découvrir. Les détails d
[Génération](worldgen.md). Les changements de version Minecraft ou de format
de sauvegarde demandent leur propre ticket et contrat de migration.
Lalpha.6 utilise à son tour `sanctuary:sanctuary_layered` pour ses biomes par
altitude et sa nouvelle hydrologie. Créer un nouveau monde Sanctuary ; les
sauvegardes alpha.5 conservent notamment leur palette avec Dappled Forest.
Références officielles : [installation packwiz](https://packwiz.infra.link/tutorials/installing/packwiz-installer/),
[commandes Prism](https://prismlauncher.org/wiki/help-pages/custom-commands/),
[bootstrap v0.0.3](https://github.com/packwiz/packwiz-installer-bootstrap/releases/tag/v0.0.3).
+85 -3
View File
@@ -80,8 +80,8 @@ 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. 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.
6. Aucun plancher solide napparaît sous l’île, à Y=0..15. Dans les générations
alpha.5 et alpha.6, 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.
@@ -91,7 +91,7 @@ Les neuf tests Sanctuary vérifient :
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.
paramètres activent les biomes par altitude et les eaux de lalpha.6.
La recherche de spawn parcourt l'île finie, par anneaux de quatre blocs jusqu'à
288 blocs du centre. Elle examine d'abord la hauteur brute pour éviter de
@@ -188,6 +188,88 @@ Pour cet essai visuel en monde de développement créatif, comparer la même vue
Y=63 ni assombrissement d'altitude au passage de Y=32. Les brouillards de l'eau,
de la météo et des effets d'aveuglement ou d'obscurité doivent rester présents.
## Contrôles alpha.6
`layeredHydrologySmoke` vérifie les bassins sans ruisseau de secours, les
supports des dépôts, leur déterminisme et les sources sur les faces des
strates inférieures. Les reliefs vides, trop minces ou percés ne sont pas comblés.
`layeredEcologySmoke` vérifie les sols et le dégagement des corniches,
lexclusion du sommet et du vide, ainsi que la variation verticale des biomes.
`LayeredHydrologyDiagnostics` inspecte les nouveaux paramètres et refuse
Dappled Forest dans leur palette. Il vérifie les couleurs communes, les biomes
des surfaces hautes et basses, la végétation réelle et les surfaces rocheuses.
Les plantes vanilla sur une corniche mince sont relevées séparément : elles
ne constituent pas une preuve des trois supports exigés par la nouvelle
décoration moussue. Leurs coordonnées et leurs supports sont exportés.
Les tickets de toute lemprise sont posés avant le chargement des chunks `FULL`.
Le test appelle ensuite `ServerLevel.waitForEntities(chunk, 0)` sur cette même
emprise : le serveur GameTest accélère ses ticks et un délai en ticks seul ne
prouve pas que les lectures asynchrones dentités sont terminées. Le prédicat
`isPositionTickingWithEntitiesLoaded` et les témoins d’écoulement restent requis.
Le diagnostic simule 1 800 ticks dans les chunks actifs. Les bassins doivent
retenir leur eau. Les écoulements extérieurs doivent être reliés à une source
rocheuse déclarée par des connexions deau réelles, sans eau orpheline ni voisin
horizontal en dehors de lemprise inspectée. Les seuls sites de surface sont
des étangs et des lacs ; aucune rivière nest activée dans cette livraison.
Les ouvertures des sources sont vérifiées après toute la décoration ; une
source bouchée par du lichen ne peut pas satisfaire le contrôle de descente.
Après ces mesures, un test de réentrée pose temporairement un lichen à une
bouche dont les marqueurs de génération ont été consommés. Un nouvel appel au
post-traitement doit le conserver ; le test restaure ensuite le bloc dorigine.
La preuve est dans `layered-hydrology-seed-<seed>-postprocessing-reentry.json`.
Les cartes `layered-hydrology-seed-<seed>-<phase>*.png` montrent les surfaces,
les corniches et les profils de sources. Les JSON associés contiennent les
résultats et leur emprise. Ces lectures de blocs ne remplacent pas lexamen
du paysage dans le client.
## Validation alpha.6 — 8 septembre 2026
Les dix tests serveur passent sur chacune des graines `0`, `42` et `8675309`,
dans des mondes neufs Minecraft 26.3-pre-2 avec Java 25, sur Apple M1 à 8 Go.
Chaque emprise est réellement active pendant 1 800 ticks mesurés. La commande
finale `check build assemblePack`, avec les exports de densité et le relevé
ressources général facultatifs désactivés, passe en **3 min 30 s** sur la graine 0.
Les six tests de forme, hydrologie, lave et écologie passent également.
| Graine | Spawn naturel | Eau des bassins | Eau des cascades | Canne | Lave | Chunks FULL | Plantes sur corniches épaisses |
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
| `0` | `(0, 252, 0)` | 1 533 | 414 | 20 | 18 | 151 | 11 021 |
| `42` | `(0, 249, 0)` | 1 376 | 913 | 4 | 18 | 149 | 6 334 |
| `8675309` | `(0, 247, 0)` | 1 590 | 2 350 | 10 | 18 | 160 | 9 532 |
Ces nombres décrivent uniquement les chunks inspectés, après décoration et
simulation. Les plantes incluent la végétation vanilla et la nouvelle décoration,
sur des corniches avec trois supports naturels ; les plantes de corniches
plus minces sont comptées séparément. Les cinq variantes de biome sont observées
sur les surfaces inférieures, avec respectivement cinq, trois et deux changements
verticaux contrôlés. Les surfaces inspectées comportent 5 805, 4 765 et 10 045
colonnes de pierre au sommet ; il ne sagit pas dun quota pour l’île entière.
Sur la graine 0, les sources `(-112, 103, 0)` et `(-108, 227, -96)` atteignent
Y=0 ; celle de `(-110, 148, -46)` rejoint un palier à Y=130. La première traverse
une frontière de chunks. Sur la graine 42, la source `(-62, 151, -82)` dégage
correctement son ouverture autrefois obstruée par du lichen et descend de
49 blocs ; les deux autres descendent de 103 et 236 blocs. Les trois cascades
de la graine 8675309 atteignent Y=0. Tous les écoulements observés sont reliés
à leur source, sans eau orpheline ni dépassement horizontal de lemprise.
Les bassins conservent leur eau ; les sédiments et leurs supports résistent aux
ticks de gravité. Les trois niches de lave gardent leurs 18 sources, leur coque
et leur accès sec. Le test de réentrée après consommation des marqueurs réussit
sur les trois graines : une modification ultérieure de la bouche reste intacte.
Les 52 fichiers historiques de génération suivis pour cette livraison gardent
leur SHA-256 davant modification.
Les cartes de surface, de corniches et les profils de cascade ont été examinés.
Ce sont des lectures de blocs ; le rendu dans le client reste à apprécier en
jeu. La grande rivière traversante nest pas activée : cette livraison adopte
les petits bassins et les sources de paroi. Les ressources restent observées
après génération, sans garantie de toute la progression Minecraft pour chaque graine.
## Validation alpha.5 — 8 septembre 2026
Les essais utilisent Minecraft 26.3-pre-2, Java 25 et le preset de production
+15 -5
View File
@@ -52,17 +52,27 @@ L'île de Sanctuary est le point de départ commun. Elle flotte dans un vide qui
L'île initiale doit être **tempérée et légèrement humide**, avec quelques zones
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,
pierre et terre enherbée. De petits étangs ou lacs de surface
peuvent accueillir de la canne à sucre. Lorsque le relief le permet, une grande
rivière suit la forme de l’île, depuis une source jusqu’à une cascade et un
bassin, avec des rives de sable, de gravier et dargile. Ce parcours doit former
un lieu reconnaissable ; il reste facultatif. À défaut, l’île conserve ses
petits bassins, sans imposer de courts ruisseaux. Le terrain est traité près de la surface,
avec des dépôts épais et des rives variées. De rares sources sortent de niches
rocheuses naturelles et leurs cascades peuvent descendre jusque dans le vide ;
les plans deau restent retenus et ne doivent pas provoquer une inondation globale. Les grands bassins souterrains
les bassins restent retenus et ne doivent pas provoquer une inondation globale.
Une rivière peut avoir des échappements et des chutes déclarés. Les grands bassins souterrains
remplis d'eau ne sont pas retenus pour cette île. Le terrain doit laisser voir un
dessous sculpté et dégagé, dans un monde de **384 blocs de hauteur**, avec des
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
Lambiance de départ doit rester douce et tempérée, avec des bosquets,
des prairies fleuries et de larges zones rocheuses. Les biomes très contrastés
de Minecraft 26.3, dont Dappled Forest, sont réservés aux futurs continents.
Les biomes et la végétation varient aussi en hauteur : les corniches inférieures
peuvent porter des bosquets, de la mousse et des fleurs lorsque leur surface
sy prête. Quelques sources de paroi et ruissellements relient ces étages.
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.
+64 -5
View File
@@ -35,7 +35,7 @@ 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 v5 traite ensuite les lits et les rives près de la surface.
blocs. Lhydrologie traite ensuite les lits et les rives près de la surface.
- Le pourtour est sculpté par retrait de matière à partir du rayon **128 blocs**.
Le contour de référence de 256 blocs est déformé par les bruits, entre 204 et
288 blocs ; il indique la fin de l'érosion, pas un mur de roche.
@@ -47,8 +47,9 @@ de test et les sauvegardes personnelles doivent rester distincts.
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.
- Les nouveaux mondes utilisent cinq biomes tempérés distincts, à dominante
forestière, dont Dappled Forest. Leur végétation et leurs animaux proviennent
- Les nouveaux mondes utilisent une palette tempérée de prairies, de bosquets,
de corniches moussues et de zones rocheuses, répartie aussi en hauteur.
Dappled Forest est réservé aux futurs continents. Leur végétation et leurs animaux proviennent
de Minecraft 26.3-pre-2. Les nouveaux identifiants ne reprennent pas les tags
de structures vanilla, afin de ne pas placer leurs structures dans le vide.
- Les carvers, géodes, donjons, grands lacs de lave et sources vanilla sont
@@ -56,7 +57,7 @@ de test et les sauvegardes personnelles doivent rester distincts.
rocheuses. Les arbres et les petits filons ont besoin du terrain existant.
Aucun mod de biomes externe nest nécessaire.
Lalpha.5 associe ces biomes aux plages, aux eaux et aux petits filons décrits
Lalpha.6 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
@@ -303,6 +304,63 @@ fluides dans les chunks des sites. Les cartes et profils PNG montrent des
lectures de blocs, pas un rendu Minecraft. La descente des
cascades est contrôlée après activation effective des chunks qui les simulent.
## Prairies et corniches — génération v6
Lalpha.6 utilise `sanctuary:sanctuary_layered` et le biome source
`sanctuary:layered_island`. Les formes et le ciel restent ceux de la génération
relevée. Les anciennes classes, biomes et règles de matériaux restent disponibles
pour les sauvegardes alpha.2 à alpha.5 ; seul le preset de création choisit v6.
### Une palette qui varie aussi en hauteur
Les cinq variantes partagent leurs teintes dherbe et de feuillage. Les champs
de climat liés à la seed déforment de larges bandes daltitude, autour de Y154
et Y214 : prairies et prairies fleuries sur le dessus, bosquets et corniches
moussues plus bas. Des zones rocheuses traversent ces bandes et remplacent la
couverture de terre par de la pierre sur le relief existant. Les prairies gardent
quelques arbres épars et leur végétation vanilla. Dappled Forest nappartient
plus à cette palette ; son utilisation sur de futurs continents reste à développer.
| Identifiant | Ambiance |
| --- | --- |
| `sanctuary:layered_meadow` | Prairie tempérée |
| `sanctuary:layered_flower_meadow` | Prairie fleurie |
| `sanctuary:layered_lower_grove` | Bosquet inférieur |
| `sanctuary:layered_mossy_terraces` | Corniches moussues |
| `sanctuary:layered_rocky_terraces` | Affleurements rocheux |
`LowerTerraceVegetationFeature` parcourt aussi les sols naturels sous les
surplombs, au moins huit blocs sous une surface supérieure. Elle demande trois
blocs pleins de support et deux blocs libres au-dessus. Elle peut remplacer la
surface existante par de la mousse et planter fougères, fleurs ou petits arbustes
lorsque leurs règles de survie le permettent. Les cellules hydrologiques, les
sources, les parcours deau prévus et les supports de lave sont protégés.
Il ny a ni corniche construite dans lair ni ajout d’éclairage artificiel.
### Bassins et sources de paroi
`LayeredHydrology` conserve les petits bassins et les dépôts épais, avec une
plus grande présence de pierre. Le court `STREAM` à niveau constant de lalpha.5
nest pas repris. Cette version retient uniquement les étangs et les petits
lacs ; elle ne cherche pas de grande rivière. Le parcours traversant avec
source, cascade et bassin reste un ticket distinct, à démontrer sur le relief
Minecraft réel. Aucun chenal de secours nest creusé.
La profondeur reste limitée à trois blocs et lincision à quatre. Le plan
vérifie le fond et les rives avant dappliquer ses écritures, dans les nouveaux
chunks seulement. Le sable, le gravier, largile et la roche suivent les rives.
Des niches de source sont également recherchées dans les strates inférieures,
et pas seulement près de la surface la plus haute. Leurs chutes sont produites
par les ticks de Minecraft à partir dun seul bloc source. Le post-traitement
des nouveaux chunks libère uniquement leur ouverture, déjà vide dans la densité
naturelle, pour quun lichen ou une plante ne bouche pas la source. Les positions
sont marquées dans la file vanilla pendant la génération, puis consommées une
seule fois. Un rechargement ne dégage pas une bouche modifiée par un joueur.
Aucune chute nest préremplie et aucun nouveau passage nest creusé dans la roche.
Les parcours, les écoulements et la végétation réellement observés, ainsi que
les limites du modèle, sont consignés dans [Validation](testing.md).
## Apparition commune
Une injection limitée à la création initiale cherche du sol naturel depuis
@@ -312,7 +370,8 @@ l'absence d'une telle surface, elle retient une colonne de sol praticable ; si
aucun emplacement naturel sûr n'existe, la création signale la seed concernée
plutôt que d'ajouter une plateforme. Elle ne s'applique qu'à l'Overworld
utilisant les paramètres `sanctuary:sanctuary`, `sanctuary:sanctuary_384` ou
`sanctuary:sanctuary_hydrology` ou `sanctuary:sanctuary_natural`.
`sanctuary:sanctuary_hydrology`, `sanctuary:sanctuary_natural` ou
`sanctuary:sanctuary_layered`.
Elle enregistre le spawn partagé dans la sauvegarde et ne le réinitialise pas
au redémarrage : les modifications administratives de `/setworldspawn` restent
conservées. Les lits et la dispersion habituelle du spawn suivent encore les
+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.5
pack_version=0.1.0-alpha.5
mod_version=0.1.0-alpha.6
pack_version=0.1.0-alpha.6
maven_group=fr.koka.sanctuary
+21
View File
@@ -109,3 +109,24 @@ tasks.register('starterLavaSmoke', JavaExec) {
}
tasks.named('check') { dependsOn('starterLavaSmoke') }
// Alpha.6 has separate contracts so saved alpha.5 worlds keep their generation.
tasks.register('layeredHydrologySmoke', JavaExec) {
group = 'verification'
description = 'Check retained basins, broad rock shores and lower wall springs.'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.LayeredHydrologySmoke'
}
tasks.named('check') { dependsOn('layeredHydrologySmoke') }
tasks.register('layeredEcologySmoke', JavaExec) {
group = 'verification'
description = 'Check altitude biome variation and naturally supported lower terraces.'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.LayeredEcologySmoke'
}
tasks.named('check') { dependsOn('layeredEcologySmoke') }
@@ -2,6 +2,7 @@ package fr.koka.sanctuary.gametest;
import fr.koka.sanctuary.worldgen.SanctuarySpawn;
import fr.koka.sanctuary.worldgen.NaturalHydrologyRuntime;
import fr.koka.sanctuary.worldgen.LayeredHydrologyRuntime;
import net.minecraft.tags.FluidTags;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import fr.koka.sanctuary.worldgen.IslandShape;
@@ -29,14 +30,16 @@ public final class SanctuaryWorldGameTests {
@GameTest(maxTicks = 3000)
public void generatedSurfaceWaterRemainsContained(GameTestHelper helper) throws java.io.IOException {
var generator = (NoiseBasedChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
if (NaturalHydrologyRuntime.enabled(generator)) NaturalHydrologyDiagnostics.start(helper);
if (LayeredHydrologyRuntime.enabled(generator)) LayeredHydrologyDiagnostics.start(helper);
else if (NaturalHydrologyRuntime.enabled(generator)) NaturalHydrologyDiagnostics.start(helper);
else HydrologyDiagnostics.start(helper);
}
@GameTest(maxTicks = 200)
public void hydrologyDoesNotActivateForSavedLegacySettings(GameTestHelper helper) {
var generator = (NoiseBasedChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
if (NaturalHydrologyRuntime.enabled(generator)) NaturalHydrologyDiagnostics.verifyLegacyIsolation(helper);
if (LayeredHydrologyRuntime.enabled(generator)) LayeredHydrologyDiagnostics.verifyLegacyIsolation(helper);
else if (NaturalHydrologyRuntime.enabled(generator)) NaturalHydrologyDiagnostics.verifyLegacyIsolation(helper);
else HydrologyDiagnostics.verifyLegacyIsolation(helper);
helper.succeed();
}
@@ -114,16 +117,17 @@ public final class SanctuaryWorldGameTests {
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),
helper.assertTrue(NaturalHydrologyRuntime.enabled(generator) || LayeredHydrologyRuntime.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),
&& (LayeredHydrologyRuntime.enabled(generator)
? LayeredHydrologyDiagnostics.isWithinSpringInspectionFootprint(LayeredHydrologyRuntime.layeredPlan(helper.getLevel()), x, z)
: NaturalHydrologyDiagnostics.isWithinSpringInspectionFootprint(NaturalHydrologyRuntime.naturalPlan(helper.getLevel()), x, z)),
"The lower margin has no artificial floor; water must lie in the finite spring inspection footprint, with provenance checked by the full fluid diagnostic: " + p);
}
}
@@ -3,6 +3,9 @@ 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.LayeredIslandBiomeSource;
import fr.koka.sanctuary.worldgen.LayeredShoreSugarCaneFeature;
import fr.koka.sanctuary.worldgen.LowerTerraceVegetationFeature;
import fr.koka.sanctuary.worldgen.ShoreSugarCaneFeature;
import net.fabricmc.api.ModInitializer;
import net.minecraft.core.Registry;
@@ -25,6 +28,9 @@ public final class SanctuaryMod implements ModInitializer {
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("shore_sugar_cane"), ShoreSugarCaneFeature.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("natural_shore_sugar_cane"), NaturalShoreSugarCaneFeature.CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("temperate_island"), TemperateIslandBiomeSource.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("layered_shore_sugar_cane"), LayeredShoreSugarCaneFeature.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("lower_terrace_vegetation"), LowerTerraceVegetationFeature.CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("layered_island"), LayeredIslandBiomeSource.CODEC);
LOGGER.info("Sanctuary initialized: the Sanctuary world preset is available.");
}
}
@@ -0,0 +1,29 @@
package fr.koka.sanctuary.mixin;
import fr.koka.sanctuary.worldgen.LayeredSpringOutlets;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.chunk.LevelChunk;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/** Finish only pending generation markers; completed saved chunks are never repaired on reload. */
@Mixin(LevelChunk.class)
public abstract class LayeredSpringOutletMixin {
@Unique
private LayeredSpringOutlets.Pending sanctuary$pendingSpringOutlets = LayeredSpringOutlets.Pending.EMPTY;
@Inject(method = "postProcessGeneration", at = @At("HEAD"))
private void sanctuary$captureSpringOutlets(ServerLevel level, CallbackInfo callback) {
sanctuary$pendingSpringOutlets = LayeredSpringOutlets.capturePending(level, (LevelChunk) (Object) this);
}
@Inject(method = "postProcessGeneration", at = @At("TAIL"))
private void sanctuary$finishSpringOutlets(ServerLevel level, CallbackInfo callback) {
var pending = sanctuary$pendingSpringOutlets;
sanctuary$pendingSpringOutlets = LayeredSpringOutlets.Pending.EMPTY;
LayeredSpringOutlets.finish(level, (LevelChunk) (Object) this, pending);
}
}
@@ -2,6 +2,7 @@ package fr.koka.sanctuary.mixin;
import fr.koka.sanctuary.worldgen.HydrologyRuntime;
import fr.koka.sanctuary.worldgen.NaturalHydrologyRuntime;
import fr.koka.sanctuary.worldgen.LayeredHydrologyRuntime;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import net.minecraft.core.Holder;
@@ -26,7 +27,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 (NaturalHydrologyRuntime.enabled(generator)) {
if (LayeredHydrologyRuntime.enabled(generator)) {
callback.setReturnValue(callback.getReturnValue().thenApply(generated -> {
LayeredHydrologyRuntime.apply(generator, randomState, generated);
return generated;
}));
} else if (NaturalHydrologyRuntime.enabled(generator)) {
callback.setReturnValue(callback.getReturnValue().thenApply(generated -> {
NaturalHydrologyRuntime.apply(generator, randomState, generated);
return generated;
@@ -0,0 +1,20 @@
package fr.koka.sanctuary.worldgen;
/** Broad, seed-warped vertical bands; none changes terrain density or guarantees a resource. */
public final class LayeredBiomePalette {
public enum Kind { MEADOW, FLOWER_MEADOW, LOWER_GROVE, MOSSY_TERRACES, ROCKY_TERRACES }
private LayeredBiomePalette() {}
public static Kind select(int blockY, float variation, float moisture) {
double elevation = blockY - Math.clamp(variation, -1.0F, 1.0F) * 28
- Math.clamp(moisture, -1.0F, 1.0F) * 12;
if (elevation < 154) return moisture < -0.30F ? Kind.ROCKY_TERRACES : Kind.MOSSY_TERRACES;
if (elevation < 214) {
if (variation > 0.34F) return Kind.ROCKY_TERRACES;
return moisture > 0.02F ? Kind.LOWER_GROVE : Kind.MOSSY_TERRACES;
}
if (variation > 0.22F) return Kind.ROCKY_TERRACES;
return moisture > -0.12F ? Kind.FLOWER_MEADOW : Kind.MEADOW;
}
}
@@ -0,0 +1,633 @@
package fr.koka.sanctuary.worldgen;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Set;
/** Alpha.6 surface basins, rocky shores and layered wall springs.
* This version is separate from NaturalHydrology so previously created worlds keep their original terrain.
* Results retain no chunks, sampler, random state or Minecraft objects. */
public final class LayeredHydrology {
public static final int RADIUS = 192;
public static final int MAX_CARVE = 4;
public static final int MAX_DEPTH = 3;
private static final int GRID = 8;
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
private static final int[][] NEIGHBORS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}};
private LayeredHydrology() {}
@FunctionalInterface
public interface Sampler { float sample(int x, int y, int z); }
public enum Kind { POND, LAKE }
public enum ShoreMaterial { SAND, GRAVEL, CLAY, STONE, GRASS }
public record Point(int x, int z) {}
public record Bounds(int minX, int minZ, int maxX, int maxZ) {}
public record Position(int x, int y, int z) {}
public record FlowBounds(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {}
public record Spring(long id, Position source, Position outlet, List<Position> flowPath, FlowBounds flowBounds) {
public Spring { flowPath = List.copyOf(flowPath); }
}
/** Replaces existing solids bedY-sedimentDepth+1..bedY, with two intact natural supports below.
* Water occupies bedY+1..waterY; everything above it through carveTop is removed. For a dry
* terrace waterY=-1, and bedY+1..carveTop is removed (at most two original blocks). */
public record Cell(int x, int z, int waterY, int bedY, int carveTop,
ShoreMaterial material, long featureId, int sedimentDepth) {
public boolean hasWater() { return waterY >= 0; }
}
public record Feature(long id, Kind kind, int waterY, List<Point> path, int waterCells, Bounds bounds) {
public Feature { path = List.copyOf(path); }
}
public static final class Plan {
private final Map<Long, Cell> cells;
private final Map<Long, List<Cell>> chunks;
private final List<Feature> features;
private final List<Spring> springs;
private final Map<Long, List<Spring>> springChunks;
private final Set<Position> springFlow;
private final int sampledColumns;
private final long densitySamples;
private Plan(Map<Long, Cell> source, List<Feature> features, List<Spring> springs, Set<Position> springFlow, int columns, long samples) {
List<Cell> sorted = source.values().stream()
.sorted(Comparator.comparingInt(Cell::x).thenComparingInt(Cell::z)).toList();
Map<Long, Cell> ordered = new LinkedHashMap<>();
Map<Long, List<Cell>> byChunk = new HashMap<>();
for (Cell cell : sorted) {
ordered.put(key(cell.x(), cell.z()), cell);
byChunk.computeIfAbsent(key(cell.x() >> 4, cell.z() >> 4), ignored -> new ArrayList<>()).add(cell);
}
byChunk.replaceAll((ignored, values) -> List.copyOf(values));
cells = Collections.unmodifiableMap(ordered);
chunks = Map.copyOf(byChunk);
this.features = List.copyOf(features);
this.springs = List.copyOf(springs);
this.springFlow = Set.copyOf(springFlow);
Map<Long, List<Spring>> groupedSprings = new HashMap<>();
for (Spring spring : springs) groupedSprings.computeIfAbsent(
key(spring.source().x() >> 4, spring.source().z() >> 4), ignored -> new ArrayList<>()).add(spring);
groupedSprings.replaceAll((ignored, values) -> List.copyOf(values));
springChunks = Map.copyOf(groupedSprings);
sampledColumns = columns;
densitySamples = samples;
}
public Cell cellAt(int x, int z) { return cells.get(key(x, z)); }
public Collection<Cell> cells() { return cells.values(); }
public List<Cell> cellsInChunk(int x, int z) { return chunks.getOrDefault(key(x, z), List.of()); }
public List<Feature> features() { return features; }
public List<Spring> springs() { return springs; }
public List<Spring> springsInChunk(int x, int z) { return springChunks.getOrDefault(key(x, z), List.of()); }
/** Prediction in undecorated density, not a restriction on Minecraft fluid simulation. */
public boolean allowsSpringFlow(int x, int y, int z) { return springFlow.contains(new Position(x, y, z)); }
public int sampledColumns() { return sampledColumns; }
public long densitySamples() { return densitySamples; }
}
public static Plan create(long seed, Sampler sampler) {
return new Planner(seed, sampler).build();
}
private record Column(int top, int solidBottom) {}
private record Candidate(int x, int z, double score) {}
private record Node(int x, int z, double cost) {}
private static final Comparator<Node> NODE_ORDER = Comparator.comparingDouble(Node::cost)
.thenComparingInt(Node::x).thenComparingInt(Node::z);
private static final class Planner {
private final long seed;
private final Sampler sampler;
private final Map<Long, Column> columns = new HashMap<>();
private final Map<Long, Integer> coarse = new HashMap<>();
private final Map<Long, Cell> cells = new HashMap<>();
private final List<Feature> features = new ArrayList<>();
private final List<Spring> springs = new ArrayList<>();
private final Set<Position> springFlow = new HashSet<>();
private long samples;
Planner(long seed, Sampler sampler) { this.seed = seed; this.sampler = sampler; }
Plan build() {
List<Candidate> candidates = candidates();
int attempts = 0;
for (Candidate candidate : candidates) {
if (features.size() >= 5 || attempts++ >= 96) break;
Point center = new Point(candidate.x(), candidate.z());
if (nearWater(center.x(), center.z(), 38)) continue;
Column column = column(center.x(), center.z());
if (column.top() < 64 || column.top() - column.solidBottom() < 7) continue;
Kind kind = features.size() < 2 ? Kind.LAKE : Kind.POND;
int target = kind == Kind.LAKE ? 190 + (int) (random(seed, center.x(), center.z()) * 130)
: 55 + (int) (random(seed + 1, center.x(), center.z()) * 55);
// Prefer an existing low spot; a shallow cut is the fallback on level terrain.
for (int level : new int[]{column.top() + 1, column.top(), column.top() - 1}) {
Set<Long> footprint = growBasin(center, level, target);
if (footprint == null) continue;
long id = featureId(features.size());
Map<Long, Cell> wet = waterCells(footprint, level, id);
if (wet == null || !contained(wet)) continue;
addFeature(id, kind, level, List.of(center), wet);
break;
}
}
addSedimentSheets();
addLowerWallSprings();
addSprings();
return new Plan(cells, features, springs, springFlow, columns.size(), samples);
}
private List<Candidate> candidates() {
for (int x = -RADIUS; x <= RADIUS; x += GRID) {
for (int z = -RADIUS; z <= RADIUS; z += GRID) {
if (inside(x, z, RADIUS)) coarse.put(key(x, z), coarseTop(x, z));
}
}
List<Candidate> result = new ArrayList<>();
for (int x = -RADIUS + GRID; x < RADIUS; x += GRID) {
for (int z = -RADIUS + GRID; z < RADIUS; z += GRID) {
int height = coarse.getOrDefault(key(x, z), -1);
if (height < 64 || !inside(x, z, RADIUS - 24)) continue;
double average = 0;
boolean safe = true;
for (int[] d : CARDINALS) {
int neighbor = coarse.getOrDefault(key(x + d[0] * GRID, z + d[1] * GRID), -1);
if (neighbor < 64 || Math.abs(neighbor - height) > 16) safe = false;
average += neighbor * 0.25;
}
if (safe) result.add(new Candidate(x, z,
(average - height) * 2.0 + random(seed + 2, x, z) * 3.0));
}
}
result.sort(Comparator.comparingDouble(Candidate::score).reversed()
.thenComparingInt(Candidate::x).thenComparingInt(Candidate::z));
return result;
}
private int coarseTop(int x, int z) {
for (int y = 383; y >= 32; y -= 4) if (solid(x, y, z)) return y;
return -1;
}
private Column column(int x, int z) {
long key = key(x, z);
Column old = columns.get(key);
if (old != null) return old;
int top = -1;
// Exact scan, including everything above the planned water: a cave roof cannot be
// mistaken for a surface. Only candidates and their nearby shore need this refinement.
for (int y = 383; y >= 24; y--) {
if (solid(x, y, z)) { top = y; break; }
}
int bottom = top;
if (top >= 0) {
while (bottom > Math.max(16, top - 16) && solid(x, bottom - 1, z)) bottom--;
}
Column created = new Column(top, bottom);
columns.put(key, created);
return created;
}
private boolean eligible(int x, int z, int level) {
if (level < 32 || level >= 383 || !inside(x, z, RADIUS - 2)) return false;
Cell old = cells.get(key(x, z));
if (old != null && old.hasWater() && old.waterY() != level) return false;
Column column = column(x, z);
return column.top() >= level - 3 && column.top() < level + MAX_CARVE
&& column.solidBottom() <= Math.min(column.top(), level - 1) - 4;
}
private Set<Long> growBasin(Point center, int level, int target) {
if (!eligible(center.x(), center.z(), level)) return null;
Set<Long> wet = new HashSet<>();
Map<Long, Double> costs = new HashMap<>();
PriorityQueue<Node> queue = new PriorityQueue<>(NODE_ORDER);
queue.add(new Node(center.x(), center.z(), 0));
costs.put(key(center.x(), center.z()), 0.0);
while (!queue.isEmpty() && wet.size() < target) {
Node current = queue.remove();
if (current.cost() > costs.getOrDefault(key(current.x(), current.z()), Double.POSITIVE_INFINITY)) continue;
if (!eligible(current.x(), current.z(), level)) continue;
wet.add(key(current.x(), current.z()));
for (int[] d : NEIGHBORS) {
int x = current.x() + d[0], z = current.z() + d[1];
if (Math.hypot(x - center.x(), z - center.z()) > 28 || wet.contains(key(x, z))) continue;
if (!eligible(x, z, level)) continue;
double step = Math.hypot(d[0], d[1]);
double cost = current.cost() + step * (1 + Math.max(0, column(x, z).top() - level) * 0.8
+ terrainNoise(x, z) * 4.5 + field(x + 71, z - 93, 21, 107) * 3.5);
if (cost >= costs.getOrDefault(key(x, z), Double.POSITIVE_INFINITY)) continue;
costs.put(key(x, z), cost);
queue.add(new Node(x, z, cost));
}
}
wet = connectedTo(wet, key(center.x(), center.z()));
if (wet.size() < Math.min(target, 30)) return null;
// Filling a hollow requires every lower connected outlet to be included. Abort rather
// than place a dam if this natural basin runs to a cliff, cave or an oversized catchment.
List<Long> work = new ArrayList<>(wet);
for (int i = 0; i < work.size(); i++) {
Point point = point(work.get(i));
for (int[] d : CARDINALS) {
int x = point.x() + d[0], z = point.z() + d[1];
long neighbor = key(x, z);
if (wet.contains(neighbor) || column(x, z).top() >= level) continue;
if (wet.size() >= 420 || Math.hypot(x - center.x(), z - center.z()) > 32
|| !eligible(x, z, level)) return null;
wet.add(neighbor);
work.add(neighbor);
}
}
return wet;
}
private Map<Long, Cell> waterCells(Set<Long> footprint, int level, long id) {
Map<Long, Cell> result = new HashMap<>();
for (long key : footprint) {
Point p = point(key);
if (!eligible(p.x(), p.z(), level)) return null;
boolean edge = false;
for (int[] d : CARDINALS) edge |= !footprint.contains(key(p.x() + d[0], p.z() + d[1]));
Column column = column(p.x(), p.z());
int depth = Math.max(level - column.top() + 1, edge ? 1 : 2 + (terrainNoise(p.x(), p.z()) > 0.55 ? 1 : 0));
depth = Math.min(depth, MAX_CARVE - Math.max(0, column.top() - level));
if (depth < 1 || depth > MAX_DEPTH) return null;
int bed = level - depth;
if (column.top() < bed || column.solidBottom() > bed - 2) return null;
ShoreMaterial material = wetMaterial(p.x(), p.z(), edge);
int sedimentDepth = sedimentDepth(p.x(), p.z());
if (column.solidBottom() > bed - sedimentDepth - 1) return null;
result.put(key, new Cell(p.x(), p.z(), level, bed, Math.max(level, column.top()),
material, id, sedimentDepth));
}
return result;
}
private boolean contained(Map<Long, Cell> wet) {
for (Cell cell : wet.values()) {
for (int y = cell.bedY() - 2; y <= cell.bedY(); y++) {
if (!solid(cell.x(), y, cell.z())) return false;
}
for (int[] d : CARDINALS) {
int x = cell.x() + d[0], z = cell.z() + d[1];
Cell neighbor = wet.get(key(x, z));
if (neighbor == null) neighbor = cells.get(key(x, z));
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) {
if (neighbor != null && neighbor.hasWater()) {
if (neighbor.waterY() != cell.waterY()) return false;
if (y > neighbor.bedY()) continue;
}
if (!solid(x, y, z)) return false;
}
}
}
return true;
}
private void addFeature(long id, Kind kind, int level, List<Point> path, Map<Long, Cell> wet) {
int minX = Integer.MAX_VALUE, minZ = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE, maxZ = Integer.MIN_VALUE;
int count = 0;
for (Cell cell : wet.values()) {
Cell old = cells.get(key(cell.x(), cell.z()));
if (old == null || !old.hasWater()) { cells.put(key(cell.x(), cell.z()), cell); count++; }
minX = Math.min(minX, cell.x()); minZ = Math.min(minZ, cell.z());
maxX = Math.max(maxX, cell.x()); maxZ = Math.max(maxZ, cell.z());
}
features.add(new Feature(id, kind, level, path, count, new Bounds(minX, minZ, maxX, maxZ)));
}
private static Set<Long> connectedTo(Set<Long> footprint, long start) {
Set<Long> connected = new HashSet<>();
List<Long> work = new ArrayList<>();
connected.add(start); work.add(start);
for (int i = 0; i < work.size(); i++) {
Point point = point(work.get(i));
for (int[] d : CARDINALS) {
long next = key(point.x() + d[0], point.z() + d[1]);
if (footprint.contains(next) && connected.add(next)) work.add(next);
}
}
return connected;
}
private record ShoreNode(int x, int z, int level, long feature, double cost) {}
/** Sediment follows shallow topography in broad, discontinuous sheets. A second nearby
* pocket can share the same sheet; the result is not a one-block material ring. */
private void addSedimentSheets() {
Comparator<ShoreNode> order = Comparator.comparingDouble(ShoreNode::cost)
.thenComparingInt(ShoreNode::x).thenComparingInt(ShoreNode::z).thenComparingLong(ShoreNode::feature);
PriorityQueue<ShoreNode> queue = new PriorityQueue<>(order);
Map<Long, Double> best = new HashMap<>();
for (Cell cell : cells.values()) if (cell.hasWater()) {
queue.add(new ShoreNode(cell.x(), cell.z(), cell.waterY(), cell.featureId(), 0));
best.put(key(cell.x(), cell.z()), 0.0);
}
while (!queue.isEmpty()) {
ShoreNode node = queue.remove();
if (node.cost() > best.getOrDefault(key(node.x(), node.z()), Double.POSITIVE_INFINITY)) continue;
Column column = column(node.x(), node.z());
Cell existing = cells.get(key(node.x(), node.z()));
if (existing == null) {
double extent = 9 + field(node.x(), node.z(), 25, 131) * 10;
if (node.cost() > extent) continue;
int delta = column.top() - node.level();
if (delta < 0 || delta > 5) continue;
// Low shore: waterline, then one-block steps. Cut at most two existing blocks;
// far deposits keep the original landform and feather back into grass/stone.
int terrace = node.level() + (int) Math.floor(node.cost() / 4.5);
int bed = Math.max(node.level(), Math.max(column.top() - 2, Math.min(column.top(), terrace)));
int depth = sedimentDepth(node.x(), node.z());
if (column.solidBottom() > bed - depth - 1) continue;
boolean safe = true;
for (int[] d : CARDINALS) {
Cell next = cells.get(key(node.x() + d[0], node.z() + d[1]));
if (next != null && next.hasWater() && bed < next.waterY()) safe = false;
}
if (!safe) continue;
ShoreMaterial material = dryMaterial(node.x(), node.z(), node.cost() / extent);
cells.put(key(node.x(), node.z()), new Cell(node.x(), node.z(), -1, bed,
column.top(), material, node.feature(), depth));
}
for (int[] d : NEIGHBORS) {
int x = node.x() + d[0], z = node.z() + d[1];
if (!inside(x, z, RADIUS - 2)) continue;
Cell water = cells.get(key(x, z));
if (water != null && water.hasWater()) continue;
Column next = column(x, z);
if (next.top() < node.level() || next.top() > node.level() + 5) continue;
double cost = node.cost() + Math.hypot(d[0], d[1])
* (0.7 + Math.abs(next.top() - column.top()) * 0.32 + field(x, z, 12, 211) * 0.6);
if (cost > 19 || cost >= best.getOrDefault(key(x, z), Double.POSITIVE_INFINITY)) continue;
best.put(key(x, z), cost);
queue.add(new ShoreNode(x, z, node.level(), node.feature(), cost));
}
}
}
private int sedimentDepth(int x, int z) { return 3 + (int) (field(x, z, 15, 319) * 2.999); }
private ShoreMaterial wetMaterial(int x, int z, boolean edge) {
double geology = field(x, z, 24, 503) * 0.75 + field(x, z, 7, 509) * 0.25;
if (geology < 0.46) return ShoreMaterial.STONE;
if (geology < 0.65) return ShoreMaterial.GRAVEL;
if (!edge && geology > 0.78) return ShoreMaterial.CLAY;
return ShoreMaterial.SAND;
}
private ShoreMaterial dryMaterial(int x, int z, double distance) {
double geology = field(x, z, 24, 503) * 0.75 + field(x, z, 7, 509) * 0.25;
double grass = field(x + 103, z - 137, 19, 521);
if (grass > 0.62 || distance > 0.60 + grass * 0.65) return ShoreMaterial.GRASS;
if (geology < 0.46) return ShoreMaterial.STONE;
if (geology < 0.65) return ShoreMaterial.GRAVEL;
return ShoreMaterial.SAND;
}
private record Cliff(int x, int z, int dx, int dz, double score) {}
private record FlowNode(Position position, int reach) {}
private record Flow(Set<Position> volume, List<Position> path, FlowBounds bounds) {}
/** Pick rare exposed rock niches at the edges of real upper surfaces. The source replaces
* one rock block under an existing roof. Its outlet is already air over an eight-block drop;
* no source, shelf, dam, rock column or ocean is ever placed in empty terrain. */
private void addSprings() {
List<Cliff> cliffs = new ArrayList<>();
for (int x = -264; x <= 264; x += GRID) for (int z = -264; z <= 264; z += GRID) {
if (!inside(x, z, 264)) continue;
int top = coarse.computeIfAbsent(key(x, z), ignored -> coarseTop(point(ignored).x(), point(ignored).z()));
if (top < 88) continue;
for (int[] d : CARDINALS) {
int nx = x + d[0] * GRID, nz = z + d[1] * GRID;
int lower = coarse.computeIfAbsent(key(nx, nz), ignored -> coarseTop(point(ignored).x(), point(ignored).z()));
if (top - lower < 16) continue;
cliffs.add(new Cliff(x, z, d[0], d[1],
Math.min(64, top - lower) * 0.06 + random(seed + 811, x + d[0], z + d[1]) * 3));
}
}
cliffs.sort(Comparator.comparingDouble(Cliff::score).reversed()
.thenComparingInt(Cliff::x).thenComparingInt(Cliff::z)
.thenComparingInt(Cliff::dx).thenComparingInt(Cliff::dz));
int attempts = 0;
for (Cliff cliff : cliffs) {
if (springs.size() >= 3 || attempts++ >= 160) break;
boolean placed = false;
for (int tangent : new int[]{0, -2, 2, -4, 4}) {
for (int along = 0; along < GRID; along++) {
int x = cliff.x() + cliff.dx() * along - cliff.dz() * tangent;
int z = cliff.z() + cliff.dz() * along + cliff.dx() * tangent;
if (!inside(x, z, 266)) continue;
int ox = x + cliff.dx(), oz = z + cliff.dz();
Column rock = column(x, z), outletColumn = column(ox, oz);
if (rock.top() < 80 || rock.top() - outletColumn.top() < 14) continue;
// Five blocks below the highest natural surface is below the soil cap.
int floor = Math.max(64, Math.max(rock.top() - 20, outletColumn.top() + 8));
for (int y = rock.top() - 5; y >= floor; y -= 3) {
Position source = new Position(x, y, z), outlet = new Position(ox, y, oz);
if (nearModifiedGround(source, 32) || !separatedSpring(source) || !springNiche(source, cliff.dx(), cliff.dz())) continue;
Flow flow = traceSpring(source, outlet);
if (flow == null || flow.bounds().minY() > y - 12) continue;
long id = mix(seed ^ (0x5F81A6L + springs.size() * 0x9E3779B97F4A7C15L));
springs.add(new Spring(id, source, outlet, flow.path(), flow.bounds()));
springFlow.addAll(flow.volume());
placed = true;
break;
}
if (placed) break;
}
if (placed) break;
}
}
}
private boolean nearModifiedGround(Position source, int distance) {
for (Cell cell : cells.values()) {
int dy = Math.max(Math.max(cell.bedY() - cell.sedimentDepth() - source.y(), 0),
source.y() - cell.carveTop());
double horizontal = Math.hypot(source.x() - cell.x(), source.z() - cell.z());
if (Math.hypot(horizontal, dy) < distance) return true;
}
return false;
}
private boolean separatedSpring(Position source) {
for (Spring spring : springs) {
double horizontal = Math.hypot(source.x() - spring.source().x(), source.z() - spring.source().z());
int dy = Math.abs(source.y() - spring.source().y());
if (Math.hypot(horizontal, dy) < 64 || dy < 12) return false;
}
return true;
}
private record Wall(int x, int y, int z, int dx, int dz, double score) {}
/** Probe a few exposed faces below the highest surface as well. This finds overhangs and
* lower strata that a highest-column heightmap cannot represent, without inventing a ledge. */
private void addLowerWallSprings() {
List<Wall> candidates = new ArrayList<>();
for (int x = -240; x <= 240; x += 16) for (int z = -240; z <= 240; z += 16) {
if (!inside(x, z, 240)) continue;
int top = coarse.computeIfAbsent(key(x, z), ignored -> coarseTop(point(ignored).x(), point(ignored).z()));
for (int y = top - 32; y >= 64; y -= 16) {
if (!solid(x, y, z)) continue;
for (int[] direction : CARDINALS) {
if (solid(x + direction[0] * 4, y, z + direction[1] * 4)) continue;
candidates.add(new Wall(x, y, z, direction[0], direction[1],
random(seed + 1217 + y, x + direction[0], z + direction[1]) * 3
+ Math.min(100, top - y) * 0.015));
}
}
}
candidates.sort(Comparator.comparingDouble(Wall::score).reversed()
.thenComparingInt(Wall::x).thenComparingInt(Wall::z).thenComparingInt(Wall::y)
.thenComparingInt(Wall::dx).thenComparingInt(Wall::dz));
int attempts = 0;
for (Wall candidate : candidates) {
if (springs.size() >= 2 || attempts++ >= 96) break;
boolean placed = false;
for (int tangent : new int[]{0, -2, 2}) {
for (int along = 0; along < 4; along++) {
int x = candidate.x() + candidate.dx() * along - candidate.dz() * tangent;
int z = candidate.z() + candidate.dz() * along + candidate.dx() * tangent;
for (int dy : new int[]{0, 3, -3, 6, -6}) {
int y = candidate.y() + dy;
if (y < 56 || column(x, z).top() - y < 24) continue;
Position source = new Position(x, y, z);
if (nearModifiedGround(source, 32) || !separatedSpring(source)
|| !springNiche(source, candidate.dx(), candidate.dz())) continue;
Position outlet = new Position(x + candidate.dx(), y, z + candidate.dz());
Flow flow = traceSpring(source, outlet);
if (flow == null || flow.bounds().minY() > y - 12) continue;
long id = mix(seed ^ (0x5F81A6L + springs.size() * 0x9E3779B97F4A7C15L));
springs.add(new Spring(id, source, outlet, flow.path(), flow.bounds()));
springFlow.addAll(flow.volume());
placed = true;
break;
}
if (placed) break;
}
if (placed) break;
}
}
}
private boolean springNiche(Position source, int dx, int dz) {
for (int y = source.y() - 3; y <= source.y() + 1; y++) {
if (!solid(source.x(), y, source.z())) return false;
}
for (int[] d : CARDINALS) {
if (d[0] == dx && d[1] == dz) continue;
if (!solid(source.x() + d[0], source.y(), source.z() + d[1])) return false;
}
for (int y = source.y(); y >= source.y() - 8; y--) {
if (solid(source.x() + dx, y, source.z() + dz)) return false;
}
return true;
}
/** Predict drainage through undecorated rock to select springs and inspection bounds.
* Descend through air first, then explore seven horizontal steps on a shelf. Later trees
* can divert the actual fluid: this prediction is not a bound on decorated-world physics.
* Integration tests trace actual water back to its source within the inspected chunk halo. */
private Flow traceSpring(Position source, Position outlet) {
Map<Position, Integer> reach = new HashMap<>();
Map<Position, Position> parent = new HashMap<>();
ArrayDeque<FlowNode> queue = new ArrayDeque<>();
reach.put(source, 7);
queue.add(new FlowNode(source, 7));
Position lowest = source;
while (!queue.isEmpty()) {
FlowNode node = queue.removeFirst();
Position at = node.position();
if (node.reach() < reach.getOrDefault(at, -1)) continue;
if (at.y() < lowest.y()) lowest = at;
if (reach.size() > 24000) return null;
if (at.y() <= 0) continue;
Position below = new Position(at.x(), at.y() - 1, at.z());
if (!flowSolid(below, source)) {
enqueueFlow(below, 7, at, queue, reach, parent);
continue;
}
if (node.reach() <= 0) continue;
for (int[] d : CARDINALS) {
Position next = new Position(at.x() + d[0], at.y(), at.z() + d[1]);
if (flowSolid(next, source)) continue;
enqueueFlow(next, node.reach() - 1, at, queue, reach, parent);
}
}
if (!reach.containsKey(outlet)) return null;
int minX = source.x(), maxX = minX, minY = source.y(), maxY = minY;
int minZ = source.z(), maxZ = minZ;
for (Position position : reach.keySet()) {
minX = Math.min(minX, position.x()); maxX = Math.max(maxX, position.x());
minY = Math.min(minY, position.y()); maxY = Math.max(maxY, position.y());
minZ = Math.min(minZ, position.z()); maxZ = Math.max(maxZ, position.z());
}
List<Position> path = new ArrayList<>();
for (Position at = lowest; at != null; at = parent.get(at)) path.add(at);
Collections.reverse(path);
return new Flow(Set.copyOf(reach.keySet()), path, new FlowBounds(minX, minY, minZ, maxX, maxY, maxZ));
}
private boolean flowSolid(Position at, Position source) {
if (at.equals(source)) return false;
Cell cell = cells.get(key(at.x(), at.z()));
if (cell != null && at.y() > cell.bedY() && at.y() <= cell.carveTop()) return false;
return solid(at.x(), at.y(), at.z());
}
private static void enqueueFlow(Position next, int available, Position from, ArrayDeque<FlowNode> queue,
Map<Position, Integer> reach, Map<Position, Position> parent) {
if (available <= reach.getOrDefault(next, -1)) return;
if (!reach.containsKey(next)) parent.put(next, from);
reach.put(next, available);
queue.addLast(new FlowNode(next, available));
}
private boolean nearWater(int x, int z, int distance) {
for (Feature feature : features) {
Point point = feature.path().getFirst();
if (Math.hypot(x - point.x(), z - point.z()) < distance) return true;
}
return false;
}
private boolean solid(int x, int y, int z) { samples++; return sampler.sample(x, y, z) > 0; }
private long featureId(int index) { return mix(seed ^ (0x51A7E7L + index * 0x9E3779B97F4A7C15L)); }
private double terrainNoise(int x, int z) { return field(x, z, 8, 31); }
private double field(int x, int z, int scale, long salt) {
int gx = Math.floorDiv(x, scale), gz = Math.floorDiv(z, scale);
double tx = smooth(Math.floorMod(x, scale) / (double) scale);
double tz = smooth(Math.floorMod(z, scale) / (double) scale);
double a = random(seed + salt, gx, gz) * (1 - tx) + random(seed + salt, gx + 1, gz) * tx;
double b = random(seed + salt, gx, gz + 1) * (1 - tx) + random(seed + salt, gx + 1, gz + 1) * tx;
return a * (1 - tz) + b * tz;
}
}
private static boolean inside(int x, int z, int radius) { return Math.hypot((double) x, z) < radius; }
private static long key(int x, int z) { return ((long) x << 32) | (z & 0xffffffffL); }
private static Point point(long key) { return new Point((int) (key >> 32), (int) key); }
private static double smooth(double t) { return t * t * (3 - 2 * t); }
private static double random(long seed, int x, int z) { return (mix(seed ^ key(x, z)) >>> 11) * 0x1.0p-53; }
private static long mix(long value) {
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
return value ^ (value >>> 31);
}
}
@@ -0,0 +1,148 @@
package fr.koka.sanctuary.worldgen;
import fr.koka.sanctuary.SanctuaryMod;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Map;
import java.util.WeakHashMap;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.RandomState;
/** Alpha.6 only: immutable plans cached by world state, applied to fresh terrain before decoration. */
public final class LayeredHydrologyRuntime {
private static final Map<RandomState, LayeredHydrology.Plan> PLANS =
Collections.synchronizedMap(new WeakHashMap<>());
private static final Map<RandomState, LayeredLavaDeposit.Plan> LAVA_PLANS =
Collections.synchronizedMap(new WeakHashMap<>());
private LayeredHydrologyRuntime() {}
public static boolean enabled(NoiseBasedChunkGenerator generator) {
return generator.generatorSettings().is(SanctuarySpawn.LAYERED_SETTINGS);
}
public static LayeredHydrology.Plan layeredPlan(ServerLevel level) {
return plan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(),
level.getChunkSource().randomState());
}
public static LayeredHydrology.Plan plan(NoiseBasedChunkGenerator generator, RandomState randomState) {
if (!enabled(generator)) throw new IllegalArgumentException("Layered hydrology is not enabled for this generator");
return PLANS.computeIfAbsent(randomState, state -> {
long start = System.nanoTime();
var density = generator.generatorSettings().value().noiseRouter().finalDensity();
var plan = LayeredHydrology.create(state.seed(),
(x, y, z) -> state.sampleBlockValueUncached(density, x, y, z));
SanctuaryMod.LOGGER.info("Sanctuary layered hydrology: seed {}, {} features, {} columns, {} springs, {} ms",
state.seed(), plan.features().size(), plan.cells().size(), plan.springs().size(),
(System.nanoTime() - start) / 1_000_000);
return plan;
});
}
public static LayeredLavaDeposit.Plan lavaPlan(ServerLevel level) {
return lavaPlan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(),
level.getChunkSource().randomState());
}
public static LayeredLavaDeposit.Plan lavaPlan(NoiseBasedChunkGenerator generator, RandomState randomState) {
// Acquire the water plan before the lava cache lock; the water planner never acquires it.
var water = plan(generator, randomState);
return LAVA_PLANS.computeIfAbsent(randomState, state -> {
long start = System.nanoTime();
var density = generator.generatorSettings().value().noiseRouter().finalDensity();
var lava = LayeredLavaDeposit.create(state.seed(),
(x, y, z) -> state.sampleBlockValueUncached(density, x, y, z), water);
SanctuaryMod.LOGGER.info("Sanctuary starter lava: seed {}, {} sources, access {}, {} ms",
state.seed(), lava.lavaCells().size(), lava.access(), (System.nanoTime() - start) / 1_000_000);
return lava;
});
}
public static void apply(NoiseBasedChunkGenerator generator, RandomState randomState, ChunkAccess chunk) {
if (!enabled(generator)) return;
var chunkPos = chunk.getPos();
if (Math.abs((long) chunkPos.x() * 16) > IslandShape.TERRAIN_LIMIT + 16L
|| Math.abs((long) chunkPos.z() * 16) > IslandShape.TERRAIN_LIMIT + 16L) return;
var plan = plan(generator, randomState);
var cells = plan.cellsInChunk(chunkPos.x(), chunkPos.z());
var springs = plan.springsInChunk(chunkPos.x(), chunkPos.z());
var outlets = plan.springs().stream().map(LayeredHydrology.Spring::outlet)
.filter(outlet -> (outlet.x() >> 4) == chunkPos.x() && (outlet.z() >> 4) == chunkPos.z()).toList();
var lava = lavaPlan(generator, randomState);
var lavaCells = lava.cellsInChunk(chunkPos.x(), chunkPos.z());
var lavaSupports = lava.supportsInChunk(chunkPos.x(), chunkPos.z());
if (cells.isEmpty() && springs.isEmpty() && outlets.isEmpty() && lavaCells.isEmpty() && lavaSupports.isEmpty()) return;
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
// Validate all replacements and two intact support layers before modifying this chunk.
for (var cell : cells) {
for (int y = cell.bedY() - cell.sedimentDepth() - 1; y <= cell.bedY(); y++) {
requireSolid(chunk, pos.set(cell.x(), y, cell.z()), randomState.seed());
}
}
for (var spring : springs) {
var source = spring.source();
requireSolid(chunk, pos.set(source.x(), source.y(), source.z()), randomState.seed());
}
for (var support : lavaSupports) {
requireSolid(chunk, pos.set(support.x(), support.y(), support.z()), randomState.seed());
if (chunk.getBlockState(pos).ignitedByLava()) {
throw new IllegalStateException("Lava pocket requires nonflammable natural support at " + pos);
}
}
for (var cell : lavaCells) {
requireSolid(chunk, pos.set(cell.x(), cell.y(), cell.z()), randomState.seed());
}
for (var cell : cells) {
for (int depth = 0; depth < cell.sedimentDepth(); depth++) {
pos.set(cell.x(), cell.bedY() - depth, cell.z());
BlockState material = switch (cell.material()) {
case SAND -> Blocks.SAND.defaultBlockState();
case GRAVEL -> Blocks.GRAVEL.defaultBlockState();
case CLAY -> Blocks.CLAY.defaultBlockState();
case STONE -> Blocks.STONE.defaultBlockState();
case GRASS -> (depth == 0 ? Blocks.GRASS_BLOCK : Blocks.DIRT).defaultBlockState();
};
chunk.setBlockState(pos, material, 0);
}
for (int y = cell.bedY() + 1; y <= Math.max(cell.waterY(), cell.carveTop()); y++) {
pos.set(cell.x(), y, cell.z());
chunk.setBlockState(pos, cell.hasWater() && y <= cell.waterY()
? Blocks.WATER.defaultBlockState() : Blocks.AIR.defaultBlockState(), 0);
if (cell.hasWater() && y <= cell.waterY()) chunk.markPosForPostProcessing(pos);
}
}
for (var spring : springs) {
var source = spring.source();
pos.set(source.x(), source.y(), source.z());
chunk.setBlockState(pos, Blocks.WATER.defaultBlockState(), 0);
// Only the source is generated. Vanilla fluid ticks create the descending waterfall.
chunk.markPosForPostProcessing(pos);
}
for (var cell : lavaCells) {
pos.set(cell.x(), cell.y(), cell.z());
chunk.setBlockState(pos, (cell.lava() ? Blocks.LAVA : Blocks.AIR).defaultBlockState(), 0);
if (cell.lava()) chunk.markPosForPostProcessing(pos);
}
// This existing vanilla queue is consumed after neighbouring chunks finish decoration.
// Mark the outlet in its own chunk, including when its source belongs to a neighbour.
for (var outlet : outlets) {
pos.set(outlet.x(), outlet.y(), outlet.z());
chunk.markPosForPostProcessing(pos);
}
Heightmap.primeHeightmaps(chunk, EnumSet.of(Heightmap.Types.WORLD_SURFACE_WG,
Heightmap.Types.OCEAN_FLOOR_WG));
}
private static void requireSolid(ChunkAccess chunk, BlockPos pos, long seed) {
if (!chunk.getBlockState(pos).isCollisionShapeFullBlock(chunk, pos)) {
throw new IllegalStateException("Layered hydrology differs from terrain at " + pos + " for seed " + seed);
}
}
}
@@ -0,0 +1,73 @@
package fr.koka.sanctuary.worldgen;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import fr.koka.sanctuary.SanctuaryMod;
import java.util.stream.Stream;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeResolver;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.biome.Climate;
/** Alpha.6 palette: flowered upper meadows and distinct humid lower terraces. */
public final class LayeredIslandBiomeSource extends BiomeSource {
public static final ResourceKey<Biome> MEADOW = key("layered_meadow");
public static final ResourceKey<Biome> FLOWER_MEADOW = key("layered_flower_meadow");
public static final ResourceKey<Biome> LOWER_GROVE = key("layered_lower_grove");
public static final ResourceKey<Biome> MOSSY_TERRACES = key("layered_mossy_terraces");
public static final ResourceKey<Biome> ROCKY_TERRACES = key("layered_rocky_terraces");
public static final MapCodec<LayeredIslandBiomeSource> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
Biome.CODEC.fieldOf("meadow").forGetter(source -> source.meadow),
Biome.CODEC.fieldOf("flower_meadow").forGetter(source -> source.flowerMeadow),
Biome.CODEC.fieldOf("lower_grove").forGetter(source -> source.lowerGrove),
Biome.CODEC.fieldOf("mossy_terraces").forGetter(source -> source.mossyTerraces),
Biome.CODEC.fieldOf("rocky_terraces").forGetter(source -> source.rockyTerraces)
).apply(instance, LayeredIslandBiomeSource::new));
private final Holder<Biome> meadow;
private final Holder<Biome> flowerMeadow;
private final Holder<Biome> lowerGrove;
private final Holder<Biome> mossyTerraces;
private final Holder<Biome> rockyTerraces;
public LayeredIslandBiomeSource(Holder<Biome> meadow, Holder<Biome> flowerMeadow,
Holder<Biome> lowerGrove, Holder<Biome> mossyTerraces, Holder<Biome> rockyTerraces) {
this.meadow = meadow;
this.flowerMeadow = flowerMeadow;
this.lowerGrove = lowerGrove;
this.mossyTerraces = mossyTerraces;
this.rockyTerraces = rockyTerraces;
}
@Override
protected MapCodec<LayeredIslandBiomeSource> codec() { return CODEC; }
@Override
protected Stream<Holder<Biome>> collectPossibleBiomes() {
return Stream.of(meadow, flowerMeadow, lowerGrove, mossyTerraces, rockyTerraces);
}
@Override
public BiomeResolver createResolver(Climate.Sampler sampler) {
return (quartX, quartY, quartZ) -> {
int x = quartX * 4, y = quartY * 4, z = quartZ * 4;
float variation = sampler.temperature().sampleValue(x, 0, z);
float moisture = sampler.humidity().sampleValue(x, 0, z);
return switch (LayeredBiomePalette.select(y, variation, moisture)) {
case MEADOW -> meadow;
case FLOWER_MEADOW -> flowerMeadow;
case LOWER_GROVE -> lowerGrove;
case MOSSY_TERRACES -> mossyTerraces;
case ROCKY_TERRACES -> rockyTerraces;
};
};
}
private static ResourceKey<Biome> key(String path) {
return ResourceKey.create(Registries.BIOME, SanctuaryMod.id(path));
}
}
@@ -0,0 +1,223 @@
package fr.koka.sanctuary.worldgen;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
/** One small, covered lava pocket in existing rock, reached from an existing outdoor ledge.
* No solid blocks are added, and unsuitable seeds are allowed to produce no deposit. */
public final class LayeredLavaDeposit {
public static final int RADIUS = 192;
public static final int WATER_CLEARANCE = 32;
public static final int LAVA_VOLUME = 18;
private static final int ENTRY_DISTANCE = 8;
private static final int[][] DIRECTIONS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
private static final Comparator<Position> POSITION_ORDER = Comparator.comparingInt(Position::x)
.thenComparingInt(Position::z).thenComparingInt(Position::y);
private static final Comparator<Cell> CELL_ORDER = Comparator.comparingInt(Cell::x)
.thenComparingInt(Cell::z).thenComparingInt(Cell::y);
private LayeredLavaDeposit() {}
public record Position(int x, int y, int z) {}
/** lava=false removes existing rock to open the two-block-high chamber or approach. */
public record Cell(int x, int y, int z, boolean lava) {}
public static final class Plan {
private final List<Cell> cells;
private final List<Cell> lavaCells;
private final List<Position> supports;
private final Optional<Position> access;
private final Map<Long, List<Cell>> chunks;
private final Map<Long, List<Position>> supportChunks;
private Plan(List<Cell> cells, Set<Position> supports, Position access) {
this.cells = cells.stream().sorted(CELL_ORDER).toList();
lavaCells = this.cells.stream().filter(Cell::lava).toList();
this.supports = supports.stream().sorted(POSITION_ORDER).toList();
this.access = Optional.ofNullable(access);
Map<Long, List<Cell>> byChunk = new HashMap<>();
for (Cell cell : this.cells) byChunk.computeIfAbsent(key(cell.x() >> 4, cell.z() >> 4),
ignored -> new ArrayList<>()).add(cell);
byChunk.replaceAll((ignored, values) -> List.copyOf(values));
chunks = Map.copyOf(byChunk);
Map<Long, List<Position>> bySupportChunk = new HashMap<>();
for (Position support : this.supports) bySupportChunk.computeIfAbsent(key(support.x() >> 4, support.z() >> 4),
ignored -> new ArrayList<>()).add(support);
bySupportChunk.replaceAll((ignored, values) -> List.copyOf(values));
supportChunks = Map.copyOf(bySupportChunk);
}
public List<Cell> cells() { return cells; }
public List<Cell> lavaCells() { return lavaCells; }
public List<Position> supports() { return supports; }
/** Player feet on the existing ledge; this column was naturally open to the sky. */
public Optional<Position> access() { return access; }
public List<Cell> cellsInChunk(int x, int z) { return chunks.getOrDefault(key(x, z), List.of()); }
public List<Position> supportsInChunk(int x, int z) { return supportChunks.getOrDefault(key(x, z), List.of()); }
}
public static Plan create(long seed, LayeredHydrology.Sampler sampler, LayeredHydrology.Plan hydrology) {
return new Search(seed, sampler, hydrology).find();
}
private record Candidate(int x, int z, int dx, int dz, double score) {}
private static final class Search {
private final long seed;
private final LayeredHydrology.Sampler sampler;
private final LayeredHydrology.Plan hydrology;
private final Map<Long, Integer> heights = new HashMap<>();
Search(long seed, LayeredHydrology.Sampler sampler, LayeredHydrology.Plan hydrology) {
this.seed = seed;
this.sampler = sampler;
this.hydrology = hydrology;
}
Plan find() {
List<Candidate> candidates = new ArrayList<>();
for (int x = -184; x <= 184; x += 8) for (int z = -184; z <= 184; z += 8) {
if (!inside(x, z, RADIUS - 8)) continue;
int ledge = top(x, z);
if (ledge < 64 || ledge > 300) continue;
for (int[] direction : DIRECTIONS) {
int cx = x - direction[0] * ENTRY_DISTANCE, cz = z - direction[1] * ENTRY_DISTANCE;
if (!inside(cx, cz, RADIUS - 12) || !clearOfWater(cx, cz)) continue;
int rise = top(cx, cz) - ledge;
if (rise < 12) continue;
candidates.add(new Candidate(x, z, direction[0], direction[1],
random(seed, cx, cz) + Math.min(rise, 40) * 0.015));
}
}
candidates.sort(Comparator.comparingDouble(Candidate::score).reversed()
.thenComparingInt(Candidate::x).thenComparingInt(Candidate::z)
.thenComparingInt(Candidate::dx).thenComparingInt(Candidate::dz));
int attempts = 0;
for (Candidate candidate : candidates) {
if (attempts++ >= 192) break;
// Refine the coarse edge, including a ledge lying between two eight-block samples.
for (int along = 0; along < 8; along++) for (int tangent : new int[]{0, -3, 3}) {
int x = candidate.x() - candidate.dx() * along - candidate.dz() * tangent;
int z = candidate.z() - candidate.dz() * along + candidate.dx() * tangent;
Plan plan = tryNiche(x, z, candidate.dx(), candidate.dz());
if (plan != null) return plan;
}
}
return new Plan(List.of(), Set.of(), null);
}
private Plan tryNiche(int entryX, int entryZ, int dx, int dz) {
int cx = entryX - dx * ENTRY_DISTANCE, cz = entryZ - dz * ENTRY_DISTANCE;
if (!inside(cx, cz, RADIUS - 12) || !clearOfWater(cx, cz)) return null;
int level = top(entryX, entryZ);
if (level < 64 || level > 300 || top(cx, cz) < level + 12) return null;
// The player can discover and enter from an actual outdoor ledge, not a sealed cave.
for (int y = level + 1; y < 384; y++) if (solid(entryX, y, entryZ)) return null;
for (int y = level - 2; y <= level; y++) if (!solid(entryX, y, entryZ)) return null;
List<Cell> cells = new ArrayList<>();
Set<Position> changed = new HashSet<>();
Set<Position> supports = new HashSet<>();
for (int x = cx - 1; x <= cx + 1; x++) for (int z = cz - 1; z <= cz + 1; z++) {
for (int y = level - 1; y <= level + 2; y++) {
if (!solid(x, y, z)) return null;
add(cells, changed, x, y, z, y <= level);
}
}
// A dry lip separates the ledge from the pool. Only the air above it is opened.
for (int distance = 2; distance < ENTRY_DISTANCE; distance++) {
int x = cx + dx * distance, z = cz + dz * distance;
for (int y = level + 1; y <= level + 2; y++) {
if (solid(x, y, z)) add(cells, changed, x, y, z, false);
}
}
// Every carved column must lie under eight continuous natural rock blocks. This
// excludes soil-cap excavation at the entrance as well as exposed surface lava.
Map<Long, Integer> highest = new HashMap<>();
for (Cell cell : cells) highest.merge(key(cell.x(), cell.z()), cell.y(), Math::max);
for (var column : highest.entrySet()) {
int x = (int) (column.getKey() >> 32), z = (int) (long) column.getKey();
for (int y = column.getValue() + 1; y <= column.getValue() + 8; y++) {
if (!requireRock(supports, x, y, z)) return null;
}
}
// Three intact blocks under the pool, two-block-thick side walls, and a solid chamber
// surround prevent escape through an adjacent cave. No part of this shell is created.
for (int x = cx - 3; x <= cx + 3; x++) for (int z = cz - 3; z <= cz + 3; z++) {
for (int y = level - 4; y <= level + 2; y++) {
if (changed.contains(new Position(x, y, z))) continue;
if (!requireRock(supports, x, y, z)) return null;
}
}
// The natural roof also covers a conservative envelope around local lava fire spread.
// Its thickness keeps later surface vegetation well above the lava chamber.
for (int x = cx - 4; x <= cx + 4; x++) for (int z = cz - 4; z <= cz + 4; z++) {
for (int y = level + 3; y <= level + 10; y++) {
if (!requireRock(supports, x, y, z)) return null;
}
}
for (int distance = 2; distance <= ENTRY_DISTANCE; distance++) {
int x = cx + dx * distance, z = cz + dz * distance;
for (int y = level - 2; y <= level; y++) {
if (!requireRock(supports, x, y, z)) return null;
}
}
supports.removeAll(changed);
return new Plan(cells, supports, new Position(entryX, level + 1, entryZ));
}
private boolean requireRock(Set<Position> supports, int x, int y, int z) {
if (!solid(x, y, z)) return false;
supports.add(new Position(x, y, z));
return true;
}
private boolean clearOfWater(int x, int z) {
// The extra ten blocks include both the entrance and the complete protected shell.
int clearance = WATER_CLEARANCE + 10;
for (var cell : hydrology.cells()) {
long dx = x - cell.x(), dz = z - cell.z();
if (dx * dx + dz * dz < (long) clearance * clearance) return false;
}
for (var spring : hydrology.springs()) {
var bounds = spring.flowBounds();
long dx = Math.max(Math.max((long) bounds.minX() - x, 0), (long) x - bounds.maxX());
long dz = Math.max(Math.max((long) bounds.minZ() - z, 0), (long) z - bounds.maxZ());
if (dx * dx + dz * dz < (long) clearance * clearance) return false;
}
return true;
}
private int top(int x, int z) {
return heights.computeIfAbsent(key(x, z), ignored -> {
for (int y = 383; y >= 32; y -= 4) {
if (!solid(x, y, z)) continue;
for (int exact = Math.min(383, y + 3); exact > y; exact--) if (solid(x, exact, z)) return exact;
return y;
}
return -1;
});
}
private boolean solid(int x, int y, int z) { return sampler.sample(x, y, z) > 0; }
}
private static void add(List<Cell> cells, Set<Position> changed, int x, int y, int z, boolean lava) {
if (changed.add(new Position(x, y, z))) cells.add(new Cell(x, y, z, lava));
}
private static boolean inside(int x, int z, int radius) { return (long) x * x + (long) z * z < (long) radius * radius; }
private static long key(int x, int z) { return ((long) x << 32) | (z & 0xffffffffL); }
private static double random(long seed, int x, int z) {
long value = seed ^ key(x, z) ^ 0x1A7A5EEDL;
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
return ((value ^ (value >>> 31)) >>> 11) * 0x1.0p-53;
}
}
@@ -0,0 +1,41 @@
package fr.koka.sanctuary.worldgen;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.BlockPos;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.feature.Feature;
/** A few plants on existing suitable shores, after the forest has been decorated. */
public final class LayeredShoreSugarCaneFeature implements Feature {
public static final MapCodec<LayeredShoreSugarCaneFeature> CODEC = MapCodec.unit(LayeredShoreSugarCaneFeature::new);
@Override
public MapCodec<LayeredShoreSugarCaneFeature> codec() {
return CODEC;
}
@Override
public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource random, BlockPos origin) {
if (!(generator instanceof NoiseBasedChunkGenerator noise) || !LayeredHydrologyRuntime.enabled(noise)) return false;
var plan = LayeredHydrologyRuntime.plan(noise, level.getLevel().getChunkSource().randomState());
var cane = Blocks.SUGAR_CANE.defaultBlockState();
boolean placed = false;
for (var cell : plan.cellsInChunk(origin.getX() >> 4, origin.getZ() >> 4)) {
if (cell.hasWater() || random.nextInt(7) != 0) continue;
BlockPos base = new BlockPos(cell.x(), cell.bedY() + 1, cell.z());
if (!level.getBlockState(base).isAir() || !cane.canSurvive(level, base)) continue;
int height = 1 + random.nextInt(3);
for (int y = 0; y < height; y++) {
BlockPos pos = base.above(y);
if (!level.getBlockState(pos).isAir() || !cane.canSurvive(level, pos)) break;
setBlock(level, pos, cane);
placed = true;
}
}
return placed;
}
}
@@ -0,0 +1,94 @@
package fr.koka.sanctuary.worldgen;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.VegetationBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.material.Fluids;
/** One-time alpha.6 outlet finishing, using vanilla's persisted and consumed generation queue. */
public final class LayeredSpringOutlets {
public record Pending(List<LayeredHydrology.Position> sources, List<LayeredHydrology.Position> outlets) {
public static final Pending EMPTY = new Pending(List.of(), List.of());
public Pending { sources = List.copyOf(sources); outlets = List.copyOf(outlets); }
public boolean isEmpty() { return sources.isEmpty() && outlets.isEmpty(); }
}
private LayeredSpringOutlets() {}
/** Called before vanilla consumes the queue; a fully processed/reloaded chunk has no work. */
public static Pending capturePending(ServerLevel level, LevelChunk chunk) {
if (!(level.getChunkSource().getGenerator() instanceof NoiseBasedChunkGenerator noise)
|| !LayeredHydrologyRuntime.enabled(noise)) return Pending.EMPTY;
boolean queued = false;
for (var section : chunk.getPostProcessing()) if (section != null && !section.isEmpty()) {
queued = true;
break;
}
if (!queued) return Pending.EMPTY;
var plan = LayeredHydrologyRuntime.plan(noise, level.getChunkSource().randomState());
List<LayeredHydrology.Position> sources = new ArrayList<>(), outlets = new ArrayList<>();
for (var spring : plan.springs()) {
if (markedHere(chunk, spring.source())) sources.add(spring.source());
if (markedHere(chunk, spring.outlet())) outlets.add(spring.outlet());
}
return new Pending(sources, outlets);
}
/** All neighbouring FEATURES have finished before a chunk reaches this stage. */
public static void finish(ServerLevel level, LevelChunk chunk, Pending pending) {
if (pending.isEmpty() || !(level.getChunkSource().getGenerator() instanceof NoiseBasedChunkGenerator noise)
|| !LayeredHydrologyRuntime.enabled(noise)) return;
var random = level.getChunkSource().randomState();
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
for (var outlet : pending.outlets()) {
if (!owns(chunk, outlet)) throw new IllegalArgumentException("An outlet belongs to another chunk: " + outlet);
if (random.sampleBlockValueUncached(density, outlet.x(), outlet.y(), outlet.z()) > 0) {
throw new IllegalStateException("A spring outlet must already be natural air: " + outlet);
}
BlockPos pos = blockPos(outlet);
BlockState current = chunk.getBlockState(pos);
if (removableDecoration(current)) {
// Only this declared air cell is reopened. Neighbour notification wakes the
// adjacent source even if its own chunk was post-processed first.
level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
}
}
for (var source : pending.sources()) {
if (!owns(chunk, source)) throw new IllegalArgumentException("A source belongs to another chunk: " + source);
BlockPos pos = blockPos(source);
BlockState current = chunk.getBlockState(pos);
if (current.is(Blocks.WATER) && current.getFluidState().isSource()) {
level.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(level));
}
}
}
private static boolean markedHere(LevelChunk chunk, LayeredHydrology.Position position) {
if (!owns(chunk, position)) return false;
var queue = chunk.getPostProcessing()[chunk.getSectionIndex(position.y())];
return queue != null && queue.contains(ProtoChunk.packOffsetCoordinates(blockPos(position)));
}
private static boolean owns(LevelChunk chunk, LayeredHydrology.Position position) {
return (position.x() >> 4) == chunk.getPos().x() && (position.z() >> 4) == chunk.getPos().z();
}
private static BlockPos blockPos(LayeredHydrology.Position position) {
return new BlockPos(position.x(), position.y(), position.z());
}
private static boolean removableDecoration(BlockState state) {
if (state.hasBlockEntity()) return false;
return state.is(Blocks.GLOW_LICHEN) || state.is(Blocks.VINE) || state.is(Blocks.MOSS_CARPET)
|| state.getBlock() instanceof VegetationBlock || state.is(BlockTags.LEAVES) || state.is(BlockTags.LOGS);
}
}
@@ -0,0 +1,21 @@
package fr.koka.sanctuary.worldgen;
/** Geometry checks shared by lower-ledge decoration and density diagnostics. */
public final class LowerTerraceEcology {
@FunctionalInterface
public interface Sampler { float sample(int x, int y, int z); }
private LowerTerraceEcology() {}
public static int highestNaturalSurface(Sampler sampler, int x, int z) {
for (int y = 383; y >= 24; y--) if (sampler.sample(x, y, z) > 0) return y;
return -1;
}
/** A lower natural floor: three solid blocks, headroom, and real terrain at least eight blocks above. */
public static boolean isLowerTerrace(Sampler sampler, int x, int bedY, int z, int highestNaturalSurface) {
if (bedY < 32 || bedY > 312 || highestNaturalSurface < bedY + 8) return false;
for (int y = bedY - 2; y <= bedY; y++) if (sampler.sample(x, y, z) <= 0) return false;
return sampler.sample(x, bedY + 1, z) <= 0 && sampler.sample(x, bedY + 2, z) <= 0;
}
}
@@ -0,0 +1,126 @@
package fr.koka.sanctuary.worldgen;
import com.mojang.serialization.MapCodec;
import java.util.HashSet;
import java.util.Set;
import net.minecraft.core.BlockPos;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
import net.minecraft.world.level.levelgen.feature.Feature;
/** Decorates actual lower natural floors after the ordinary highest-surface vegetation.
* Each chunk owns its columns; planned water, sediments, lava and their supports are excluded. */
public final class LowerTerraceVegetationFeature implements Feature {
public static final MapCodec<LowerTerraceVegetationFeature> CODEC = MapCodec.unit(LowerTerraceVegetationFeature::new);
@Override
public MapCodec<LowerTerraceVegetationFeature> codec() { return CODEC; }
@Override
public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource random, BlockPos origin) {
if (!(generator instanceof NoiseBasedChunkGenerator noise) || !LayeredHydrologyRuntime.enabled(noise)) return false;
int minX = (origin.getX() >> 4) * 16, minZ = (origin.getZ() >> 4) * 16;
if (Math.abs((long) minX) > 288 || Math.abs((long) minZ) > 288) return false;
var state = level.getLevel().getChunkSource().randomState();
var water = LayeredHydrologyRuntime.plan(noise, state);
var lava = LayeredHydrologyRuntime.lavaPlan(noise, state);
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
LowerTerraceEcology.Sampler sampler = (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z);
var climate = state.createClimateSampler(SamplerContext.EMPTY_UNCACHED);
Set<Long> lavaColumns = new HashSet<>();
for (var cell : lava.cells()) protectColumn(lavaColumns, cell.x(), cell.z());
for (var support : lava.supports()) protectColumn(lavaColumns, support.x(), support.z());
boolean changed = false;
BlockPos.MutableBlockPos floor = new BlockPos.MutableBlockPos();
for (int x = minX; x < minX + 16; x++) for (int z = minZ; z < minZ + 16; z++) {
if (blockedColumn(water, lavaColumns, x, z)) continue;
// A coherent moisture patch controls coverage, rather than an alternating block grid.
float growth = climate.humidity().sampleValue(x + 317, 0, z - 191);
if (growth < -0.30F) continue;
int highest = -1, terraces = 0;
for (int y = 319; y >= 32; y--) {
floor.set(x, y, z);
BlockState ground = level.getBlockState(floor);
if (!replaceableGround(ground)) continue;
if (highest < 0) {
if (sampler.sample(x, y, z) > 0) highest = y;
continue;
}
if (y > highest - 8) continue;
BlockPos plantPos = floor.above();
if (!level.getBlockState(plantPos).isAir() || !level.getBlockState(plantPos.above()).isAir()) continue;
if (!LowerTerraceEcology.isLowerTerrace(sampler, x, y, z, highest)) continue;
if (!ground.isCollisionShapeFullBlock(level, floor)
|| !level.getBlockState(floor.below()).isCollisionShapeFullBlock(level, floor.below())
|| !level.getBlockState(floor.below(2)).isCollisionShapeFullBlock(level, floor.below(2))) continue;
var biome = level.getBiome(plantPos);
if (biome.is(LayeredIslandBiomeSource.ROCKY_TERRACES)) continue;
boolean mossy = biome.is(LayeredIslandBiomeSource.MOSSY_TERRACES);
// Moss survives shaded ledges without needing artificial lighting. Existing grass
// is retained outside the moss biome; no supporting terrain is added to air.
if (mossy || !ground.is(Blocks.GRASS_BLOCK)) {
setBlock(level, floor, Blocks.MOSS_BLOCK.defaultBlockState());
changed = true;
}
if (random.nextInt(5) < 2) {
BlockState plant = plant(mossy, random.nextInt(10));
if (plant.canSurvive(level, plantPos)) {
setBlock(level, plantPos, plant);
changed = true;
}
}
if (++terraces >= 4) break;
}
}
return changed;
}
private static BlockState plant(boolean mossy, int choice) {
if (mossy) return switch (choice) {
case 0 -> Blocks.FLOWERING_AZALEA.defaultBlockState();
case 1 -> Blocks.AZALEA.defaultBlockState();
case 2, 3 -> Blocks.FERN.defaultBlockState();
default -> Blocks.MOSS_CARPET.defaultBlockState();
};
return switch (choice) {
case 0 -> Blocks.OXEYE_DAISY.defaultBlockState();
case 1 -> Blocks.CORNFLOWER.defaultBlockState();
case 2 -> Blocks.AZURE_BLUET.defaultBlockState();
case 3 -> Blocks.DANDELION.defaultBlockState();
case 4 -> Blocks.BUSH.defaultBlockState();
default -> Blocks.SHORT_GRASS.defaultBlockState();
};
}
private static boolean replaceableGround(BlockState state) {
return state.is(Blocks.STONE) || state.is(Blocks.GRANITE) || state.is(Blocks.DIORITE)
|| state.is(Blocks.ANDESITE) || state.is(Blocks.TUFF) || state.is(Blocks.DIRT)
|| state.is(Blocks.GRASS_BLOCK) || state.is(Blocks.ROOTED_DIRT) || state.is(Blocks.COARSE_DIRT)
|| state.is(Blocks.MOSS_BLOCK);
}
private static boolean blockedColumn(LayeredHydrology.Plan water, Set<Long> lavaColumns, int x, int z) {
if (lavaColumns.contains(key(x, z))) return true;
for (int dx = -1; dx <= 1; dx++) for (int dz = -1; dz <= 1; dz++) {
if (water.cellAt(x + dx, z + dz) != null) return true;
}
for (var spring : water.springs()) if (inside(spring.flowBounds(), x, z)) return true;
return false;
}
private static boolean inside(LayeredHydrology.FlowBounds bounds, int x, int z) {
return x >= bounds.minX() - 2 && x <= bounds.maxX() + 2
&& z >= bounds.minZ() - 2 && z <= bounds.maxZ() + 2;
}
private static void protectColumn(Set<Long> columns, int x, int z) {
for (int dx = -2; dx <= 2; dx++) for (int dz = -2; dz <= 2; dz++) columns.add(key(x + dx, z + dz));
}
private static long key(int x, int z) { return ((long) x << 32) | (z & 0xffffffffL); }
}
@@ -25,6 +25,9 @@ public final class SanctuarySpawn {
public static final ResourceKey<NoiseGeneratorSettings> NATURAL_SETTINGS =
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("sanctuary_natural"));
public static final ResourceKey<NoiseGeneratorSettings> LAYERED_SETTINGS =
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("sanctuary_layered"));
private SanctuarySpawn() {}
public static boolean usesSanctuaryGenerator(ServerLevel level) {
@@ -32,7 +35,8 @@ public final class SanctuarySpawn {
&& level.getChunkSource().getGenerator() instanceof NoiseBasedChunkGenerator generator
&& (generator.generatorSettings().is(SETTINGS) || generator.generatorSettings().is(RAISED_SETTINGS)
|| generator.generatorSettings().is(HYDROLOGY_SETTINGS)
|| generator.generatorSettings().is(NATURAL_SETTINGS));
|| generator.generatorSettings().is(NATURAL_SETTINGS)
|| generator.generatorSettings().is(LAYERED_SETTINGS));
}
/** Called only during first creation; saved spawns and /setworldspawn survive subsequent loads. */
@@ -6,5 +6,10 @@
"biome.sanctuary.temperate_plains": "Sanctuary Plains",
"biome.sanctuary.temperate_birch_forest": "Sanctuary Birch Forest",
"biome.sanctuary.temperate_flower_forest": "Sanctuary Flower Forest",
"biome.sanctuary.temperate_dappled_forest": "Sanctuary Dappled Forest"
"biome.sanctuary.temperate_dappled_forest": "Sanctuary Dappled Forest",
"biome.sanctuary.layered_meadow": "Highland Meadow",
"biome.sanctuary.layered_flower_meadow": "Flowered Highland Meadow",
"biome.sanctuary.layered_lower_grove": "Ledge Woodland",
"biome.sanctuary.layered_mossy_terraces": "Mossy Ledges",
"biome.sanctuary.layered_rocky_terraces": "Rocky Terraces"
}
@@ -6,5 +6,10 @@
"biome.sanctuary.temperate_plains": "Prairies de Sanctuary",
"biome.sanctuary.temperate_birch_forest": "Boulaie de Sanctuary",
"biome.sanctuary.temperate_flower_forest": "Forêt fleurie de Sanctuary",
"biome.sanctuary.temperate_dappled_forest": "Forêt diaprée de Sanctuary"
"biome.sanctuary.temperate_dappled_forest": "Forêt diaprée de Sanctuary",
"biome.sanctuary.layered_meadow": "Prairie des hauteurs",
"biome.sanctuary.layered_flower_meadow": "Prairie fleurie des hauteurs",
"biome.sanctuary.layered_lower_grove": "Sous-bois des corniches",
"biome.sanctuary.layered_mossy_terraces": "Corniches moussues",
"biome.sanctuary.layered_rocky_terraces": "Terrasses rocheuses"
}
@@ -0,0 +1,164 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.meadow"
}
},
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [
{
"type": "minecraft:bat",
"count": 8,
"weight": 10
}
],
"creature": [
{
"type": "minecraft:donkey",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 2,
"min_inclusive": 1
},
"weight": 1
},
{
"type": "minecraft:rabbit",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 2
},
"weight": 2
},
{
"type": "minecraft:sheep",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 2
},
"weight": 2
}
],
"monster": [
{
"type": "minecraft:spider",
"count": 4,
"weight": 100
},
{
"type": "minecraft:zombie",
"count": 4,
"weight": 95
},
{
"type": "minecraft:zombie_villager",
"count": 1,
"weight": 5
},
{
"type": "minecraft:skeleton",
"count": 4,
"weight": 100
},
{
"type": "minecraft:creeper",
"count": 4,
"weight": 100
},
{
"type": "minecraft:slime",
"count": 4,
"weight": 100
},
{
"type": "minecraft:enderman",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 1
},
"weight": 10
},
{
"type": "minecraft:witch",
"count": 1,
"weight": 5
}
],
"underground_water_creature": [
{
"type": "minecraft:glow_squid",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 4
},
"weight": 10
}
]
}
},
"modifier": "overlay"
},
"minecraft:visual/sky_color": "#79a6ff"
},
"carvers": [],
"downfall": 0.8,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"features": [
[],
[],
[],
[],
[],
[],
[
"minecraft:ore_dirt",
"minecraft:ore_granite_upper",
"minecraft:ore_granite_lower",
"minecraft:ore_diorite_upper",
"minecraft:ore_diorite_lower",
"minecraft:ore_andesite_upper",
"minecraft:ore_andesite_lower",
"minecraft:ore_tuff",
"sanctuary:island_ore_coal",
"sanctuary:island_ore_iron",
"sanctuary:island_ore_copper",
"sanctuary:island_ore_gold",
"sanctuary:island_ore_redstone",
"sanctuary:island_ore_lapis",
"sanctuary:island_ore_diamond"
],
[],
[],
[
"minecraft:glow_lichen",
"minecraft:patch_tall_grass_2",
"minecraft:patch_grass_meadow",
"minecraft:flower_meadow",
"minecraft:trees_meadow",
"minecraft:wildflowers_meadow",
"minecraft:flower_flower_forest",
"sanctuary:layered_shore_sugar_cane",
"sanctuary:lower_terrace_vegetation"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,162 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.forest"
}
},
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [
{
"type": "minecraft:bat",
"count": 8,
"weight": 10
}
],
"creature": [
{
"type": "minecraft:sheep",
"count": 4,
"weight": 12
},
{
"type": "minecraft:pig",
"count": 4,
"weight": 10
},
{
"type": "minecraft:chicken",
"count": 4,
"weight": 10
},
{
"type": "minecraft:cow",
"count": 4,
"weight": 8
}
],
"monster": [
{
"type": "minecraft:spider",
"count": 4,
"weight": 100
},
{
"type": "minecraft:zombie",
"count": 4,
"weight": 95
},
{
"type": "minecraft:zombie_villager",
"count": 1,
"weight": 5
},
{
"type": "minecraft:skeleton",
"count": 4,
"weight": 100
},
{
"type": "minecraft:creeper",
"count": 4,
"weight": 100
},
{
"type": "minecraft:slime",
"count": 4,
"weight": 100
},
{
"type": "minecraft:enderman",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 1
},
"weight": 10
},
{
"type": "minecraft:witch",
"count": 1,
"weight": 5
}
],
"underground_water_creature": [
{
"type": "minecraft:glow_squid",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 4
},
"weight": 10
}
]
}
},
"modifier": "overlay"
},
"minecraft:visual/sky_color": "#79a6ff"
},
"carvers": [],
"downfall": 0.8,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"features": [
[],
[],
[],
[],
[],
[],
[
"minecraft:ore_dirt",
"minecraft:ore_granite_upper",
"minecraft:ore_granite_lower",
"minecraft:ore_diorite_upper",
"minecraft:ore_diorite_lower",
"minecraft:ore_andesite_upper",
"minecraft:ore_andesite_lower",
"minecraft:ore_tuff",
"sanctuary:island_ore_coal",
"sanctuary:island_ore_iron",
"sanctuary:island_ore_copper",
"sanctuary:island_ore_gold",
"sanctuary:island_ore_redstone",
"sanctuary:island_ore_lapis",
"sanctuary:island_ore_diamond"
],
[],
[],
[
"minecraft:glow_lichen",
"minecraft:forest_flowers",
"minecraft:wildflowers_birch_forest",
"minecraft:trees_birch",
"minecraft:patch_bush",
"minecraft:flower_default",
"minecraft:patch_grass_forest",
"minecraft:brown_mushroom_normal",
"minecraft:red_mushroom_normal",
"minecraft:patch_pumpkin",
"minecraft:patch_sugar_cane",
"minecraft:patch_firefly_bush_near_water",
"sanctuary:layered_shore_sugar_cane",
"sanctuary:lower_terrace_vegetation"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,175 @@
{
"attributes": {
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [
{
"type": "minecraft:bat",
"count": 8,
"weight": 10
}
],
"creature": [
{
"type": "minecraft:sheep",
"count": 4,
"weight": 12
},
{
"type": "minecraft:pig",
"count": 4,
"weight": 10
},
{
"type": "minecraft:chicken",
"count": 4,
"weight": 10
},
{
"type": "minecraft:cow",
"count": 4,
"weight": 8
},
{
"type": "minecraft:horse",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 2
},
"weight": 5
},
{
"type": "minecraft:donkey",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 3,
"min_inclusive": 1
},
"weight": 1
}
],
"monster": [
{
"type": "minecraft:spider",
"count": 4,
"weight": 100
},
{
"type": "minecraft:zombie",
"count": 4,
"weight": 90
},
{
"type": "minecraft:zombie_villager",
"count": 1,
"weight": 5
},
{
"type": "minecraft:zombie_horse",
"count": 1,
"weight": 5
},
{
"type": "minecraft:skeleton",
"count": 4,
"weight": 100
},
{
"type": "minecraft:creeper",
"count": 4,
"weight": 100
},
{
"type": "minecraft:slime",
"count": 4,
"weight": 100
},
{
"type": "minecraft:enderman",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 1
},
"weight": 10
},
{
"type": "minecraft:witch",
"count": 1,
"weight": 5
}
],
"underground_water_creature": [
{
"type": "minecraft:glow_squid",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 4
},
"weight": 10
}
]
}
},
"modifier": "overlay"
},
"minecraft:visual/sky_color": "#79a6ff"
},
"carvers": [],
"downfall": 0.8,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"features": [
[],
[],
[],
[],
[],
[],
[
"minecraft:ore_dirt",
"minecraft:ore_granite_upper",
"minecraft:ore_granite_lower",
"minecraft:ore_diorite_upper",
"minecraft:ore_diorite_lower",
"minecraft:ore_andesite_upper",
"minecraft:ore_andesite_lower",
"minecraft:ore_tuff",
"sanctuary:island_ore_coal",
"sanctuary:island_ore_iron",
"sanctuary:island_ore_copper",
"sanctuary:island_ore_gold",
"sanctuary:island_ore_redstone",
"sanctuary:island_ore_lapis",
"sanctuary:island_ore_diamond"
],
[],
[],
[
"minecraft:glow_lichen",
"minecraft:patch_tall_grass_2",
"minecraft:patch_grass_meadow",
"minecraft:flower_meadow",
"minecraft:trees_meadow",
"minecraft:wildflowers_meadow",
"minecraft:patch_pumpkin",
"minecraft:patch_sugar_cane",
"minecraft:patch_firefly_bush_near_water",
"sanctuary:layered_shore_sugar_cane",
"sanctuary:lower_terrace_vegetation"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,147 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.lush_caves"
}
},
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [
{
"type": "minecraft:bat",
"count": 8,
"weight": 10
}
],
"axolotls": [
{
"type": "minecraft:axolotl",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 4
},
"weight": 10
}
],
"monster": [
{
"type": "minecraft:spider",
"count": 4,
"weight": 100
},
{
"type": "minecraft:zombie",
"count": 4,
"weight": 95
},
{
"type": "minecraft:zombie_villager",
"count": 1,
"weight": 5
},
{
"type": "minecraft:skeleton",
"count": 4,
"weight": 100
},
{
"type": "minecraft:creeper",
"count": 4,
"weight": 100
},
{
"type": "minecraft:slime",
"count": 4,
"weight": 100
},
{
"type": "minecraft:enderman",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 1
},
"weight": 10
},
{
"type": "minecraft:witch",
"count": 1,
"weight": 5
}
],
"underground_water_creature": [
{
"type": "minecraft:glow_squid",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 4
},
"weight": 10
}
],
"water_ambient": [
{
"type": "minecraft:tropical_fish",
"count": 8,
"weight": 25
}
]
}
},
"modifier": "overlay"
},
"minecraft:visual/sky_color": "#79a6ff"
},
"carvers": [],
"downfall": 0.8,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"features": [
[],
[],
[],
[],
[],
[],
[
"minecraft:ore_dirt",
"minecraft:ore_granite_upper",
"minecraft:ore_granite_lower",
"minecraft:ore_diorite_upper",
"minecraft:ore_diorite_lower",
"minecraft:ore_andesite_upper",
"minecraft:ore_andesite_lower",
"minecraft:ore_tuff",
"sanctuary:island_ore_coal",
"sanctuary:island_ore_iron",
"sanctuary:island_ore_copper",
"sanctuary:island_ore_gold",
"sanctuary:island_ore_redstone",
"sanctuary:island_ore_lapis",
"sanctuary:island_ore_diamond"
],
[],
[],
[
"minecraft:glow_lichen",
"sanctuary:layered_shore_sugar_cane",
"sanctuary:lower_terrace_vegetation"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,158 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.meadow"
}
},
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [
{
"type": "minecraft:bat",
"count": 8,
"weight": 10
}
],
"creature": [
{
"type": "minecraft:donkey",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 2,
"min_inclusive": 1
},
"weight": 1
},
{
"type": "minecraft:rabbit",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 2
},
"weight": 2
},
{
"type": "minecraft:sheep",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 2
},
"weight": 2
}
],
"monster": [
{
"type": "minecraft:spider",
"count": 4,
"weight": 100
},
{
"type": "minecraft:zombie",
"count": 4,
"weight": 95
},
{
"type": "minecraft:zombie_villager",
"count": 1,
"weight": 5
},
{
"type": "minecraft:skeleton",
"count": 4,
"weight": 100
},
{
"type": "minecraft:creeper",
"count": 4,
"weight": 100
},
{
"type": "minecraft:slime",
"count": 4,
"weight": 100
},
{
"type": "minecraft:enderman",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 1
},
"weight": 10
},
{
"type": "minecraft:witch",
"count": 1,
"weight": 5
}
],
"underground_water_creature": [
{
"type": "minecraft:glow_squid",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 4
},
"weight": 10
}
]
}
},
"modifier": "overlay"
},
"minecraft:visual/sky_color": "#79a6ff"
},
"carvers": [],
"downfall": 0.8,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"features": [
[],
[],
[],
[],
[],
[],
[
"minecraft:ore_dirt",
"minecraft:ore_granite_upper",
"minecraft:ore_granite_lower",
"minecraft:ore_diorite_upper",
"minecraft:ore_diorite_lower",
"minecraft:ore_andesite_upper",
"minecraft:ore_andesite_lower",
"minecraft:ore_tuff",
"sanctuary:island_ore_coal",
"sanctuary:island_ore_iron",
"sanctuary:island_ore_copper",
"sanctuary:island_ore_gold",
"sanctuary:island_ore_redstone",
"sanctuary:island_ore_lapis",
"sanctuary:island_ore_diamond"
],
[],
[],
[
"minecraft:glow_lichen",
"sanctuary:layered_shore_sugar_cane",
"sanctuary:lower_terrace_vegetation"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,17 @@
{
"type": "minecraft:sequence",
"sequence": [
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:biome",
"biome_is": "sanctuary:layered_rocky_terraces"
},
"then_run": {
"type": "minecraft:block",
"result_state": "minecraft:stone"
}
},
"sanctuary:starter_island"
]
}
@@ -0,0 +1,23 @@
{
"default_block": "minecraft:stone",
"default_fluid": "minecraft:air",
"disable_mob_generation": false,
"legacy_random_source": true,
"material_rule": "sanctuary:layered_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,4 @@
{
"feature": { "type": "sanctuary:layered_shore_sugar_cane" },
"placement": []
}
@@ -0,0 +1,6 @@
{
"feature": {
"type": "sanctuary:lower_terrace_vegetation"
},
"placement": []
}
@@ -5,14 +5,14 @@
"generator": {
"type": "minecraft:noise",
"biome_source": {
"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"
"type": "sanctuary:layered_island",
"meadow": "sanctuary:layered_meadow",
"flower_meadow": "sanctuary:layered_flower_meadow",
"lower_grove": "sanctuary:layered_lower_grove",
"mossy_terraces": "sanctuary:layered_mossy_terraces",
"rocky_terraces": "sanctuary:layered_rocky_terraces"
},
"settings": "sanctuary:sanctuary_natural"
"settings": "sanctuary:sanctuary_layered"
}
},
"minecraft:the_end": {
@@ -4,7 +4,8 @@
"compatibilityLevel": "JAVA_25",
"mixins": [
"InitialSpawnMixin",
"SurfaceHydrologyMixin"
"SurfaceHydrologyMixin",
"LayeredSpringOutletMixin"
],
"client": [
"client.SanctuarySkyMixin",
@@ -0,0 +1,56 @@
package fr.koka.sanctuary.worldgen;
import java.util.EnumSet;
/** Lower-layer decoration must find an existing substantial ledge, never manufacture its floor. */
public final class LayeredEcologySmoke {
public static void main(String[] args) {
LowerTerraceEcology.Sampler layered = (x, y, z) ->
(y >= 240 && y <= 248) || (y >= 170 && y <= 190) ? 1 : -1;
int highest = LowerTerraceEcology.highestNaturalSurface(layered, 0, 0);
require(highest == 248, "The highest natural layer must be identified independently of plants");
require(LowerTerraceEcology.isLowerTerrace(layered, 0, 190, 0, highest),
"A thick lower floor with headroom and an upper layer must qualify");
require(!LowerTerraceEcology.isLowerTerrace(layered, 0, highest, 0, highest),
"Ordinary highest-surface vegetation must not be decorated again as a lower terrace");
require(!LowerTerraceEcology.isLowerTerrace(layered, 0, 189, 0, highest),
"A buried block is not an exposed floor");
LowerTerraceEcology.Sampler thin = (x, y, z) -> y < 189 ? -1 : layered.sample(x, y, z);
require(!LowerTerraceEcology.isLowerTerrace(thin, 0, 190, 0, highest),
"A floor thinner than three natural blocks must remain untouched");
LowerTerraceEcology.Sampler perforated = (x, y, z) -> y == 189 ? 0 : layered.sample(x, y, z);
require(!LowerTerraceEcology.isLowerTerrace(perforated, 0, 190, 0, highest),
"Zero-density holes in a floor must not become supports");
LowerTerraceEcology.Sampler lowRoof = (x, y, z) -> y == 192 ? 1 : layered.sample(x, y, z);
require(!LowerTerraceEcology.isLowerTerrace(lowRoof, 0, 190, 0, highest),
"A terrace without two blocks of natural headroom must remain untouched");
LowerTerraceEcology.Sampler single = (x, y, z) -> y >= 170 && y <= 190 ? 1 : -1;
require(!LowerTerraceEcology.isLowerTerrace(single, 0, 190, 0,
LowerTerraceEcology.highestNaturalSurface(single, 0, 0)), "An isolated highest surface is not a lower layer");
require(LowerTerraceEcology.highestNaturalSurface((x, y, z) -> -1, 0, 0) == -1,
"The void must contain no invented surface");
require(!LowerTerraceEcology.isLowerTerrace((x, y, z) -> -1, 0, 100, 0, -1),
"The void must receive no lower-terrace decoration");
var lower = LayeredBiomePalette.select(96, 0, 0.2F);
var middle = LayeredBiomePalette.select(184, 0, 0.2F);
var upper = LayeredBiomePalette.select(260, 0, 0.2F);
require(lower != middle && lower != upper && middle != upper,
"The same horizontal climate must expose distinct biomes on separate layers");
require(upper == LayeredBiomePalette.Kind.FLOWER_MEADOW,
"A moist temperate upper layer must retain a flowered meadow");
require(LayeredBiomePalette.select(220, -0.2F, 0.2F)
!= LayeredBiomePalette.select(220, 0.2F, 0.2F), "Layer boundaries must follow horizontal climate variation");
EnumSet<LayeredBiomePalette.Kind> palette = EnumSet.noneOf(LayeredBiomePalette.Kind.class);
for (int y = 32; y <= 312; y += 4) for (int v = -10; v <= 10; v++) for (int m = -10; m <= 10; m++) {
palette.add(LayeredBiomePalette.select(y, v / 10.0F, m / 10.0F));
}
require(palette.equals(EnumSet.allOf(LayeredBiomePalette.Kind.class)),
"Every declared starter biome must be reachable within the island's vertical bounds");
System.out.println("LayeredEcologySmoke: supported lower floors, headroom, void exclusion, distinct vertical biomes and warped boundaries passed.");
}
private static void require(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
}
@@ -0,0 +1,150 @@
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 LayeredHydrologySmoke {
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
public static void main(String[] args) {
LayeredHydrology.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 = LayeredHydrology.create(0, terrain);
var replay = LayeredHydrology.create(0, terrain);
var changed = LayeredHydrology.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);
LayeredHydrology.Sampler pierced = (x, y, z) ->
Math.floorMod(x, 37) < 2 && Math.floorMod(z, 41) < 2 ? -0.45F : terrain.sample(x, y, z);
var aroundShafts = LayeredHydrology.create(0, pierced);
require(!aroundShafts.features().isEmpty(), "Safe areas around natural shafts should remain usable");
verify(aroundShafts, pierced);
require(LayeredHydrology.create(0, (x, y, z) -> -1).cells().isEmpty(), "An empty world must not acquire a guaranteed basin");
var roof = LayeredHydrology.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 = LayeredHydrology.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 = LayeredHydrology.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(LayeredHydrology.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() == LayeredHydrology.ShoreMaterial.STONE),
"Some natural shores should be rocky");
require(flat.cells().stream().anyMatch(cell -> cell.material() == LayeredHydrology.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().allMatch(feature -> feature.path().size() == 1),
"Surface features should be basins rather than forced short channels");
LayeredHydrology.Sampler longSlope = (x, y, z) -> {
int top = 230 - Math.floorDiv(x + 192, 16);
return Math.hypot(x, z) < 235 && y >= 80 && y <= top ? 0.45F : -0.45F;
};
var layered = LayeredHydrology.create(0, longSlope);
verify(layered, longSlope);
require(layered.springs().stream().anyMatch(spring -> spring.source().y()
<= 230 - Math.floorDiv(spring.source().x() + 192, 16) - 24),
"The layered terrain must exercise a spring well below the highest surface");
System.out.println("LayeredHydrologySmoke: closed surface basins, layered rock springs and safe deposits passed.");
}
private static void verify(LayeredHydrology.Plan plan, LayeredHydrology.Sampler terrain) {
Map<String, LayeredHydrology.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);
LayeredHydrology.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() <= LayeredHydrology.MAX_DEPTH, "Invalid shallow-water depth");
require(cell.carveTop() - cell.bedY() <= LayeredHydrology.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: " + cell + " toward " + x + "," + y + "," + z + " neighbor=" + neighbor);
}
}
}
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()) {
int dy = Math.max(Math.max(cell.bedY() - cell.sedimentDepth() - source.y(), 0), source.y() - cell.carveTop());
require(Math.hypot(Math.hypot(source.x() - cell.x(), source.z() - cell.z()), dy) >= 32,
"Rock springs must stay away from modified terrain in three dimensions");
}
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");
LayeredHydrology.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);
}
}
+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.5.mrpack
packwiz modrinth export --output ../Sanctuary-0.1.0-alpha.6.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.5"
version = "0.1.0-alpha.6"
pack-format = "packwiz:1.1.0"
[index]