Add layered meadows, rocky shores and lower wall springs
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -1,5 +1,25 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.0-alpha.6 — 2026-09-08
|
||||
|
||||
- Palette tempérée plus douce, avec des couleurs communes pour l’herbe, le
|
||||
feuillage et l’eau. 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 l’altitude. 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 n’est 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
|
||||
|
||||
@@ -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 l’altitude. 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 d’eau 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 à l’obsidienne 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.
|
||||
|
||||
L’alpha.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).
|
||||
L’alpha.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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -134,6 +134,18 @@ vide. Le contrat et l’isolation 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 n’ont fourni aucun parcours retenu sur les graines de référence ;
|
||||
ce prototype n’est pas distribué. Ne pas imposer de rivière si le relief ne s’y 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 n’est encore fournie. La survie initiale doit
|
||||
permettre de produire et progresser au-delà du bois et de la pierre ; l’accès
|
||||
à toute la progression Minecraft n’est 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
@@ -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 l’alpha.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 l’humidité dépendent de la graine ; elles ne
|
||||
L’alpha.5 proposait cinq biomes tempérés dont Dappled Forest. L’alpha.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 l’altitude, y compris dans les strates inférieures.
|
||||
La variation locale et l’humidité 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 d’eau 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 l’eau dans le vide.
|
||||
Sans parcours suffisamment long, l’île conserve seulement ses petits bassins.
|
||||
L’alpha.6 livre cette variante à bassins seuls ; la grande rivière reste à développer.
|
||||
L’alpha.5 ajoute séparément quelques sources sortant d’une 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 l’alpha.5
|
||||
|
||||
L’île vise les premières étapes de survie, les outils, l’enchantement et la
|
||||
redstone. Les sept minerais (charbon, fer, cuivre, or, redstone, lapis et diamant)
|
||||
|
||||
+6
-2
@@ -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.
|
||||
|
||||
L’alpha.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
@@ -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 n’apparaî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 n’apparaî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 d’origine. Seuls les nouveaux
|
||||
paramètres activent les rives, biomes et sources de l’alpha.5.
|
||||
paramètres activent les biomes par altitude et les eaux de l’alpha.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,
|
||||
l’exclusion 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 l’emprise 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 d’entité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 d’eau réelles, sans eau orpheline ni voisin
|
||||
horizontal en dehors de l’emprise inspectée. Les seuls sites de surface sont
|
||||
des étangs et des lacs ; aucune rivière n’est 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 d’origine.
|
||||
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 l’examen
|
||||
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 s’agit pas d’un 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 l’emprise.
|
||||
|
||||
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 d’avant 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 n’est 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
@@ -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 d’argile. 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 d’eau 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.
|
||||
|
||||
L’ambiance de départ doit rester principalement forestière, avec des clairières,
|
||||
des bouleaux, des fleurs et Dappled Forest. Les plages s’inspirent des nappes de
|
||||
L’ambiance 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
|
||||
s’y prête. Quelques sources de paroi et ruissellements relient ces étages.
|
||||
Les plages s’inspirent des nappes de
|
||||
sable des générations Alpha/Beta : elles peuvent relier des poches d’eau, tandis
|
||||
que d’autres rives restent rocheuses ou enherbées. Les quelques cascades servent
|
||||
également d’ascenseurs d’eau pour circuler verticalement.
|
||||
|
||||
+64
-5
@@ -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. L’hydrologie v5 traite ensuite les lits et les rives près de la surface.
|
||||
blocs. L’hydrologie 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 n’est nécessaire.
|
||||
|
||||
L’alpha.5 associe ces biomes aux plages, aux eaux et aux petits filons décrits
|
||||
L’alpha.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. L’accè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
|
||||
|
||||
L’alpha.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 d’herbe et de feuillage. Les champs
|
||||
de climat liés à la seed déforment de larges bandes d’altitude, 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 n’appartient
|
||||
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 d’eau prévus et les supports de lave sont protégés.
|
||||
Il n’y a ni corniche construite dans l’air 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 l’alpha.5
|
||||
n’est 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 n’est creusé.
|
||||
|
||||
La profondeur reste limitée à trois blocs et l’incision à quatre. Le plan
|
||||
vérifie le fond et les rives avant d’appliquer ses écritures, dans les nouveaux
|
||||
chunks seulement. Le sable, le gravier, l’argile 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 d’un seul bloc source. Le post-traitement
|
||||
des nouveaux chunks libère uniquement leur ouverture, déjà vide dans la densité
|
||||
naturelle, pour qu’un 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 n’est préremplie et aucun nouveau passage n’est 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
@@ -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
|
||||
|
||||
@@ -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') }
|
||||
|
||||
+1308
File diff suppressed because it is too large
Load Diff
+9
-5
@@ -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;
|
||||
}
|
||||
}
|
||||
+41
@@ -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;
|
||||
}
|
||||
}
|
||||
+126
@@ -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"
|
||||
}
|
||||
|
||||
+164
@@ -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
|
||||
}
|
||||
+162
@@ -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
|
||||
}
|
||||
+147
@@ -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
|
||||
}
|
||||
+158
@@ -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
|
||||
}
|
||||
+17
@@ -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"
|
||||
]
|
||||
}
|
||||
+23
@@ -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": []
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"feature": { "type": "sanctuary:layered_shore_sugar_cane" },
|
||||
"placement": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"feature": {
|
||||
"type": "sanctuary:lower_terrace_vegetation"
|
||||
},
|
||||
"placement": []
|
||||
}
|
||||
+7
-7
@@ -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
@@ -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
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user