Add living caverns and connected water terraces for alpha.9
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -1,5 +1,27 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.0-alpha.9 — 2026-09-09
|
||||
|
||||
- Cavités luxuriantes éclairées par leur végétation, secteurs à spéléothèmes
|
||||
et géodes d’améthyste recherchés sous l’île, sur des supports naturels.
|
||||
- Province de soufre limitée dans les profondeurs ; geysers utilisant
|
||||
les blocs et conditions de fonctionnement de Minecraft 26.3-pre-2.
|
||||
- Forêts et rivière de surface conservées ; recherche de lacs plus profonds
|
||||
et volumineux et de groupes facultatifs de bassins en terrasses reliés par
|
||||
des cascades. Les « rizières » désignent le relief aquatique, sans culture
|
||||
de riz ni mécanique d’ascenseur ajoutée au joueur.
|
||||
- Nouvelle clé `sanctuary:sanctuary_cavern`, réservée aux nouveaux mondes.
|
||||
Les générations jusqu’à l’alpha.8 restent séparées ; aucune sauvegarde
|
||||
existante n’est convertie ou régénérée.
|
||||
|
||||
Validation : au moins dix tests requis réussis sur chacune des graines `0`,
|
||||
`42` et `8675309`, avec 1 800 ticks de fluides ; `check build assemblePack`
|
||||
réussi. Les preuves cumulées comprennent deux bassins en terrasses reliés,
|
||||
un geyser dormant puis éruptif, des baies lumineuses, des spéléothèmes et de
|
||||
l’améthyste bourgeonnante. Les mesures et leurs périmètres figurent dans
|
||||
[Validation](docs/testing.md). Publication et synchronisations Prism restent
|
||||
suivies séparément. Aucun site ni stock n’est garanti sur chaque graine.
|
||||
|
||||
## 0.1.0-alpha.8 — 2026-09-09
|
||||
|
||||
- Forêts de surface conservées, avec de nouvelles failles courbes qui retirent
|
||||
|
||||
@@ -17,7 +17,7 @@ conserve les intentions ; le [backlog](docs/backlog.md) prépare les premiers
|
||||
tickets. Les systèmes d'expansion, d'économie, de progression et les dimensions
|
||||
décrits dans la vision ne sont pas encore implémentés.
|
||||
|
||||
## Premier incrément
|
||||
## Socle livré jusqu’à l’alpha.8
|
||||
|
||||
- Mod `sanctuary` indépendant et pack Sanctuary construit avec packwiz.
|
||||
- Preset **Sanctuary** sélectionnable à la création d'un monde, avec une île
|
||||
@@ -57,14 +57,34 @@ le terrain : le spawn cherche un emplacement naturel. Le catalogue
|
||||
TerraMix, les continents déverrouillables, les océans, les ruines et
|
||||
les autres fonctionnalités restent à construire par tickets.
|
||||
|
||||
L’alpha.8 utilise la nouvelle génération `sanctuary:sanctuary_rift` ; créer un
|
||||
**nouveau monde** pour l’essayer. Les anciennes générations, jusqu’à l’alpha.7,
|
||||
gardent leurs paramètres ; aucune sauvegarde n’est régénérée. Dappled Forest
|
||||
reste réservé aux futurs continents et présent dans les anciennes sauvegardes
|
||||
alpha.5. **La validation moteur de l’alpha.8 passe sur les trois graines de
|
||||
référence**, avec 1 800 ticks de fluides ; aucune rivière, coulée ou quantité
|
||||
de ressources n’est garantie pour toutes les graines. Les observations et
|
||||
leurs limites figurent dans [Validation](docs/testing.md).
|
||||
La génération alpha.8, `sanctuary:sanctuary_rift`, passe les contrôles moteur
|
||||
sur les trois graines de référence, avec 1 800 ticks de fluides. Ces résultats
|
||||
ne valident pas par avance l’incrément suivant. Les observations et leurs
|
||||
limites figurent dans [Validation](docs/testing.md).
|
||||
|
||||
## Alpha.9 : cavités vivantes et terrasses d’eau
|
||||
|
||||
L’incrément ajoute des cavités luxuriantes et lumineuses sous l’île, des
|
||||
secteurs à spéléothèmes, des géodes d’améthyste et des poches de soufre moins
|
||||
envahissantes, avec de vrais geysers de soufre lorsque leurs conditions sont
|
||||
réunies. Les forêts de surface sont conservées ; la recherche de lacs plus
|
||||
profonds et volumineux accompagne des groupes facultatifs de bassins naturels
|
||||
étagés. Leurs cascades relient les paliers et permettent de circuler par la nage.
|
||||
L’image des « rizières » décrit ce paysage d’eau en terrasses : aucune culture
|
||||
de riz n’est ajoutée.
|
||||
|
||||
**La validation moteur alpha.9 passe sur les trois graines de référence.**
|
||||
Les preuves de terrasses, geysers et décor souterrain sont cumulées sur ces
|
||||
trois mondes, sans imposer chaque élément à chacun. La distribution est
|
||||
vérifiée séparément, selon le guide packwiz. Le
|
||||
nouveau réglage `sanctuary:sanctuary_cavern` nécessite de créer un **nouveau
|
||||
monde Sanctuary**. Les anciennes générations jusqu’à l’alpha.8 gardent leurs
|
||||
paramètres ; aucune sauvegarde n’est régénérée. Les cavités restent dans
|
||||
l’Overworld, sans créer la dimension des cavernes prévue par la vision.
|
||||
Dappled Forest reste réservé aux futurs continents et présent dans les
|
||||
anciennes sauvegardes alpha.5. Aucun lac, groupe de terrasses, geyser ou stock
|
||||
de ressources n’est garanti sur chaque graine. Voir le ticket local
|
||||
[WG-10](docs/backlog.md).
|
||||
|
||||
Le premier champ de recherche climatique est défini : nord froid, sud chaud,
|
||||
ouest sec et est humide, avec leurs combinaisons diagonales. Un outil de
|
||||
@@ -88,7 +108,7 @@ sera une mise à jour explicite, avec vérification des API et des sauvegardes.
|
||||
| Fabric API | 0.160.0+26.3 |
|
||||
| Fabric Loom | 1.17.20 |
|
||||
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
|
||||
| Sanctuary / pack | 0.1.0-alpha.8 |
|
||||
| Sanctuary / pack | 0.1.0-alpha.9 |
|
||||
|
||||
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
|
||||
uniquement la bibliothèque standard et repère aussi une installation Python
|
||||
@@ -115,7 +135,7 @@ décrits dans [Validation](docs/testing.md).
|
||||
|
||||
Résultats :
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.8.jar` : mod à installer avec
|
||||
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.9.jar` : mod à installer avec
|
||||
Fabric API sur la version Minecraft indiquée.
|
||||
- `build/packwiz/` : pack de développement complet, avec le mod construit et
|
||||
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
|
||||
|
||||
@@ -13,6 +13,7 @@ visuels, multijoueurs et de redémarrage indiqués dans ce document restent à
|
||||
faire avant de clôturer tous leurs critères. WG-04 à WG-07 restent ouverts,
|
||||
avec les incréments de l’île initiale décrits ci-dessous. WG-08 est validé ;
|
||||
WG-09 est livré en alpha.8, avec mesures moteur et mise à jour Prism vérifiées.
|
||||
WG-10 dispose de mesures moteur alpha.9 ; la distribution reste suivie séparément.
|
||||
|
||||
Résultat visé : un client et un serveur Fabric compatibles peuvent ouvrir un monde Sanctuary, plusieurs joueurs y arrivent sur une même île sûre et les chunks extérieurs restent vides. La génération est reproductible et ses limites sont documentées.
|
||||
|
||||
@@ -254,6 +255,59 @@ est synchronisée deux fois, avec sauvegardes et réglages conservés. Le ticket
|
||||
est livré ; l’appréciation visuelle et l’équilibrage en partie restent à
|
||||
éprouver. Ce ticket local n’est pas une issue distante publiée.
|
||||
|
||||
### WG-10 — Cavités luxuriantes et terrasses d’eau — alpha.9
|
||||
|
||||
**Branche :** `codex/lush-caverns-and-water-terraces`.
|
||||
|
||||
**But :** rendre les cavités sous Sanctuary plus vivantes et lumineuses,
|
||||
diversifier leurs ressources et créer des eaux plus volumineuses, sans
|
||||
remplacer les forêts et la rivière de surface appréciées en alpha.8.
|
||||
|
||||
**Critères d’acceptation :**
|
||||
|
||||
- Des cavités luxuriantes portent une végétation réellement lumineuse et
|
||||
enracinée dans les surfaces et plafonds existants. Les observations doivent
|
||||
distinguer un biome déclaré, ses blocs décoratifs et leur lumière réelle.
|
||||
- Des secteurs à spéléothèmes et des géodes d’améthyste sont recherchés dans
|
||||
la roche compatible ; leurs supports, leurs volumes et leur accessibilité
|
||||
sont contrôlés dans les vrais chunks, sans géodes suspendues dans le vide.
|
||||
- Le soufre occupe moins de roche qu’en alpha.8, sur un périmètre de comparaison
|
||||
explicite. Les geysers éventuels utilisent le soufre actif et les conditions
|
||||
exactes de Minecraft 26.3-pre-2 ; leur fonctionnement doit être démontré,
|
||||
sans confondre un bloc jaune avec un geyser actif.
|
||||
- Les forêts, la rivière et les bassins de surface restent présents lorsque
|
||||
le relief le permet. Les lacs plus profonds et volumineux sont mesurés en
|
||||
blocs d’eau réels, avec fond et parois retenus après simulation.
|
||||
- Des groupes facultatifs de deux bassins naturels en terrasses
|
||||
peuvent relier plusieurs niveaux par des cascades déclarées. Chaque palier
|
||||
doit avoir ses supports et son propre volume retenu ; une cascade isolée
|
||||
ou des plans d’eau sans liaison ne valident pas un groupe de terrasses.
|
||||
Le second bassin peut avoir une sortie terminale vers les roches ou le vide,
|
||||
sans créer un troisième palier.
|
||||
- Ces cascades offrent un passage vertical selon les règles de nage de
|
||||
Minecraft. « Rizières » désigne seulement leur disposition paysagère :
|
||||
aucune culture de riz ni téléportation n’entre dans ce ticket.
|
||||
- Les sorties d’eau sont contrôlées après décoration et après 1 800 ticks,
|
||||
y compris aux frontières de chunks. Leur finition initiale doit préserver
|
||||
les aménagements du joueur aux rechargements suivants.
|
||||
- Le nouveau réglage `sanctuary:sanctuary_cavern` s’applique aux nouveaux
|
||||
mondes. Les identifiants et traitements alpha.1 à alpha.8 restent séparés ;
|
||||
aucune sauvegarde personnelle n’est modifiée par les essais.
|
||||
- Les graines `0`, `42` et `8675309` disposent de mesures identifiées par
|
||||
version. Les sites absents sont signalés ; aucun quota de ressources ni
|
||||
bassin de secours ne remplit un relief incompatible.
|
||||
- Après validation : `check build assemblePack`, artefacts immuables vérifiés,
|
||||
deux synchronisations isolées puis deux dans la même instance Prism,
|
||||
sauvegardes et réglages conservés.
|
||||
|
||||
**Validation moteur au 9 septembre 2026 :** les trois graines de référence
|
||||
passent les contrôles finaux. Les preuves cumulées démontrent un groupe de
|
||||
deux paliers reliés, un geyser actif et les décors souterrains demandés ; les
|
||||
absences locales et périmètres de mesure figurent dans [Validation](testing.md).
|
||||
Le build est réussi. La publication et la synchronisation de la même instance
|
||||
Prism restent vérifiées lors de la distribution. Ce ticket local n’est pas
|
||||
une issue distante publiée ; l’essai visuel et l’équilibrage restent ouverts.
|
||||
|
||||
## Réserve de thèmes futurs
|
||||
|
||||
Ces thèmes servent à retrouver la vision, pas à demander leur implémentation immédiate. On en extrait un ticket seulement lorsqu'il devient utile au prochain incrément jouable.
|
||||
|
||||
+14
-2
@@ -15,6 +15,11 @@ de l'ancien pack 26.2.
|
||||
|
||||
## Livrer une mise à jour
|
||||
|
||||
L’alpha.9 est en préparation dans les sources. Les exemples de commandes
|
||||
ci-dessous ciblent cette prochaine version ; ils ne signifient pas qu’elle
|
||||
est publiée. Le canal reste sur la dernière livraison vérifiée jusqu’à la
|
||||
validation du build, des essais moteur et des nouveaux artefacts.
|
||||
|
||||
Dernière livraison vérifiée le 9 septembre 2026 :
|
||||
[Sanctuary 0.1.0-alpha.8](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.8),
|
||||
depuis le commit source `80e78e0`. Le JAR, le `.mrpack` et le ZIP d’amorçage
|
||||
@@ -36,7 +41,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.8.mrpack
|
||||
python3 scripts/publish_pack.py --notes-file chemin/vers/notes.md --asset build/Sanctuary-0.1.0-alpha.9.mrpack
|
||||
```
|
||||
|
||||
Les options sont facultatives. Ce script **publie** sur le Git configuré dans
|
||||
@@ -55,7 +60,7 @@ La procédure complète, également utilisable manuellement :
|
||||
3. Préparer les métadonnées de la livraison, avec l'URL exacte du futur JAR :
|
||||
|
||||
```sh
|
||||
python3 scripts/pack.py release https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.8/sanctuary-0.1.0-alpha.8.jar
|
||||
python3 scripts/pack.py release https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.9/sanctuary-0.1.0-alpha.9.jar
|
||||
```
|
||||
|
||||
4. Pousser le commit source vérifié. Créer une release Gitea correspondant à ce
|
||||
@@ -154,6 +159,13 @@ nouveau monde Sanctuary pour les découvrir. La même instance Prism reçoit
|
||||
le nouveau JAR par le canal stable ; ses sauvegardes et réglages sont conservés.
|
||||
Les mondes alpha.7 et antérieurs gardent leurs paramètres de génération.
|
||||
|
||||
L’alpha.9 prépare `sanctuary:sanctuary_cavern` pour les cavités luxuriantes,
|
||||
les géodes, les geysers et les terrasses d’eau. Un **nouveau monde Sanctuary**
|
||||
sera nécessaire pour essayer cet incrément après sa validation. Les mondes
|
||||
alpha.8 conservent `sanctuary:sanctuary_rift` ; leur contenu n’est pas retouché
|
||||
par une mise à jour de la même instance Prism. Les anciennes sauvegardes et
|
||||
les réglages restent à conserver lors des deux synchronisations de livraison.
|
||||
|
||||
Références officielles : [installation packwiz](https://packwiz.infra.link/tutorials/installing/packwiz-installer/),
|
||||
[commandes Prism](https://prismlauncher.org/wiki/help-pages/custom-commands/),
|
||||
[bootstrap v0.0.3](https://github.com/packwiz/packwiz-installer-bootstrap/releases/tag/v0.0.3).
|
||||
|
||||
+167
-1
@@ -65,7 +65,8 @@ Le test capture donc le spawn juste avant ce déplacement, après l'initialisati
|
||||
normale du monde, et examine les coordonnées absolues de l'île.
|
||||
|
||||
Les scénarios suivants couvrent le socle et les générations historiques ;
|
||||
les contrôles et résultats propres à l’alpha.8 figurent plus bas :
|
||||
les contrôles validés pour l’alpha.9 et les résultats historiques
|
||||
propres à l’alpha.8 figurent plus bas :
|
||||
|
||||
1. Le vrai générateur Sanctuary est chargé et le spawn collectif repose sur une
|
||||
surface déjà présente dans la densité naturelle, avec un sol plein de 3×3
|
||||
@@ -189,6 +190,171 @@ Pour cet essai visuel en monde de développement créatif, comparer la même vue
|
||||
Y=63 ni assombrissement d'altitude au passage de Y=32. Les brouillards de l'eau,
|
||||
de la météo et des effets d'aveuglement ou d'obscurité doivent rester présents.
|
||||
|
||||
## Contrôles et validation alpha.9 — 9 septembre 2026
|
||||
|
||||
Les mondes neufs des graines `0`, `42` et `8675309` utilisent les paramètres
|
||||
de production `sanctuary:sanctuary_cavern`. Chaque journal termine au moins
|
||||
dix tests requis et sa tâche Gradle sans échec, avec 1 800 ticks de fluides.
|
||||
Les sources naturelles d’eau et de lave, les débordements et les geysers
|
||||
démarrent uniquement avec leurs ticks de production. Le test programme
|
||||
seulement un témoin artificiel indépendant pour vérifier que les ticks
|
||||
fonctionnent ; il ne réveille pas les sources naturelles.
|
||||
Le build complet `check build assemblePack` réussit en 6m 24s,
|
||||
avec 15 tâches de tests purs de comportement.
|
||||
Les rapports sont postérieurs au gel du code final ; les résultats de
|
||||
l’alpha.8 et les essais alpha.9 intermédiaires ne servent pas de preuve ici.
|
||||
|
||||
La présence d’un groupe de terrasses connecté, d’un geyser réellement actif,
|
||||
de baies lumineuses, de spéléothèmes et d’améthyste bourgeonnante est vérifiée
|
||||
sur l’ensemble des trois graines. Chaque élément n’est pas imposé à chaque
|
||||
graine ; leurs absences locales restent visibles dans les tableaux.
|
||||
|
||||
| Graine | Chunks FULL | Failles / témoins | Chênes / bouleaux supérieurs | Chênes noirs inférieurs | Gros champignons | Petits sous plafond | Minerais dehors : charbon / fer / cuivre |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| `0` | 251 | 3 / 3 | 321 / 251 | 18 | 14 | 211 | 506 / 178 / 147 |
|
||||
| `42` | 246 | 2 / 2 | 362 / 177 | 11 | 8 | 69 | 617 / 269 / 193 |
|
||||
| `8675309` | 242 | 3 / 3 | 248 / 125 | 14 | 11 | 131 | 761 / 317 / 328 |
|
||||
|
||||
Les arbres sont des groupes de pieds de troncs sur sol naturel, avec feuilles
|
||||
observées ; les gros champignons ont une tige et un chapeau connectés.
|
||||
|
||||
| Graine | Blocs à baies lumineuses | Blocs émissifs | Spéléothèmes pointus | Améthyste bourgeonnante | Blocs d’améthyste | Geysers dormants + éruptifs | Sources d’eau de geyser |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| `0` | 363 | 391 | 501 | 214 | 818 | 1 | 1 |
|
||||
| `42` | 280 | 308 | 592 | 210 | 945 | 4 | 4 |
|
||||
| `8675309` | 446 | 476 | 380 | 141 | 504 | 5 | 5 |
|
||||
|
||||
Le décor souterrain est compté sur les blocs initiaux réellement générés.
|
||||
Ses témoins sont revérifiés après les fluides : support, conservation de
|
||||
l’améthyste bourgeonnante et lumière du moteur pour les blocs émissifs.
|
||||
Ces nombres comptent des blocs, pas des cavités ni des géodes distinctes.
|
||||
L’améthyste bourgeonnante est reliée à une cavité naturelle par un parcours
|
||||
d’air vérifié avant et après les ticks ; les JSON donnent la position de
|
||||
chaque raccord et les exemples de blocs souterrains, limités à 64 témoins.
|
||||
Les geysers observés possèdent la vraie entité de bloc `potent_sulfur`,
|
||||
du magma dessous et une source d’eau retenue au-dessus. Un même geyser
|
||||
doit passer par les états dormant et éruptif pendant les ticks, sans
|
||||
activation artificielle par le test.
|
||||
|
||||
| Graine | Niveaux Y des bassins | Colonnes d’eau par bassin | Descente depuis la bouche | Nouveaux blocs écoulés accessibles | Deux paliers connectés |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `0` | 148 / 140 | 53 / 76 | 148 | 1 230 | Oui, après ticks |
|
||||
| `0` | 177 / 163 | 169 / 47 | 177 | 3 370 | Oui, après ticks |
|
||||
| `42` | 129 / 114 | 116 / 53 | 129 | 181 | Oui, après ticks |
|
||||
| `42` | 112 / 107 | 105 / 161 | 112 | 354 | Oui, après ticks |
|
||||
| `8675309` | 154 / 149 | 128 / 149 | 7 | 7 | Oui, après ticks |
|
||||
| `8675309` | 153 / 112 | 87 / 58 | 153 | 871 | Oui, après ticks |
|
||||
|
||||
Chaque groupe réunit deux bassins retenus à des niveaux différents. Le
|
||||
diagnostic suit l’eau réelle depuis chaque bouche, horizontalement ou en
|
||||
descendant, sans mouvement vers le haut, jusqu’au bassin cible ;
|
||||
un exutoire terminal sans bassin cible n’est pas compté comme cette liaison.
|
||||
La descente est mesurée sur ce parcours propre à la bouche. Les nouveaux
|
||||
blocs accessibles en sont les eaux écoulées qui n’étaient ni retenues dans
|
||||
les bassins ni présentes avant la simulation. Les exutoires terminaux
|
||||
facultatifs doivent descendre d’au moins douze blocs réels.
|
||||
|
||||
Les terrasses sont limitées à trois blocs de profondeur et cinq blocs
|
||||
d’incision ; les lacs, à huit blocs de profondeur et douze blocs d’incision.
|
||||
Pour les terrasses, le diagnostic exige aussi quatre blocs d’air naturel
|
||||
au-dessus de chaque colonne excavée, en plus des fonds et supports conservés.
|
||||
|
||||
| Graine | Longueur rivière | Déplacement | Niveau Y | Colonnes d’eau | Colonnes des bassins terminaux | Sources liées |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| `0` | 409,8 | 199,7 | 243 | 2 863 | 240 / 241 | 1 |
|
||||
| `42` | 247,8 | 171,0 | 247 | 1 712 | 254 / 247 | 1 |
|
||||
| `8675309` | 340,7 | 251,3 | 239 | 2 247 | 265 / 244 | 1 |
|
||||
|
||||
La longueur additionne les déplacements du tracé, diagonales comprises. Les
|
||||
bassins terminaux et l’arrivée de leur cascade sont contrôlés après les ticks.
|
||||
|
||||
| Graine | Colonnes du lac | Blocs d’eau réels | Profondeur retenue maximale |
|
||||
| --- | --- | --- | --- |
|
||||
| `0` | 728 | 3 746 | 8 |
|
||||
| `42` | 722 | 3 027 | 8 |
|
||||
| `8675309` | 992 | 4 978 | 8 |
|
||||
|
||||
Chaque ligne décrit un site classé `LAKE`, après simulation. Les étangs,
|
||||
bassins de rivière et terrasses sont consignés dans leurs propres catégories.
|
||||
|
||||
| Graine | Eau retenue, geysers inclus | Dont sources de geyser | Eau hors retenues | Eau totale | Lave totale | Sources de lave | Coulées | Accès poche de lave |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| `0` | 9 845 | 1 | 4 971 | 14 816 | 144 | 20 | 2 | `(96, 100, -45)` |
|
||||
| `42` | 7 267 | 4 | 903 | 8 170 | 144 | 20 | 2 | `(67, 133, -108)` |
|
||||
| `8675309` | 10 268 | 5 | 1 230 | 11 498 | 144 | 20 | 2 | `(-93, 156, -21)` |
|
||||
|
||||
Ces volumes sont mesurés après 1 800 ticks actifs ; ils ne mesurent pas un
|
||||
débit et ne signifient pas que les coulées ont atteint leur état final.
|
||||
L’eau retenue correspond au volume attendu après décoration et comprend
|
||||
les sources propres aux geysers. Les composantes extérieures sont rattachées
|
||||
aux sources rocheuses ou aux débordements déclarés, avec leurs raccords
|
||||
réels. La lave des coulées est distinguée de la poche couverte.
|
||||
|
||||
| Graine | Sols de mousse | Sols de podzol | Sols de mycélium |
|
||||
| --- | --- | --- | --- |
|
||||
| `0` | 13 175 | 10 936 | 5 074 |
|
||||
| `42` | 4 897 | 3 712 | 2 200 |
|
||||
| `8675309` | 10 058 | 6 714 | 3 293 |
|
||||
|
||||
Les stocks suivants portent sur les seuls chunks inspectés, avant les
|
||||
ticks de fluides. Les variantes ordinaires et d’ardoise de chaque minerai
|
||||
sont additionnées. Ce relevé ne couvre pas toute l’île et ne déclenche
|
||||
jamais de remplissage pour atteindre un quota.
|
||||
|
||||
| Minerai | Graine 0 | Graine 42 | Graine 8675309 |
|
||||
| --- | --- | --- | --- |
|
||||
| Charbon | 7 858 | 6 788 | 4 992 |
|
||||
| Fer | 3 224 | 2 700 | 2 172 |
|
||||
| Cuivre | 2 439 | 2 132 | 1 621 |
|
||||
| Or | 930 | 687 | 507 |
|
||||
| Redstone | 924 | 631 | 523 |
|
||||
| Lapis | 278 | 279 | 137 |
|
||||
| Diamant | 162 | 86 | 88 |
|
||||
| Émeraude | 64 | 41 | 42 |
|
||||
|
||||
| Matière | Graine 0 | Graine 42 | Graine 8675309 |
|
||||
| --- | --- | --- | --- |
|
||||
| Tuf | 657 813 | 491 736 | 239 434 |
|
||||
| Cobblestone | 285 441 | 172 282 | 162 791 |
|
||||
| Cobblestone moussue | 593 694 | 420 042 | 229 846 |
|
||||
| Boue compactée | 168 698 | 167 276 | 158 922 |
|
||||
| Ardoise des abîmes | 1 443 679 | 901 821 | 858 181 |
|
||||
| Roche noire | 539 269 | 275 994 | 324 101 |
|
||||
| Basalte | 605 828 | 390 889 | 307 652 |
|
||||
| Soufre | 87 811 | 57 838 | 82 209 |
|
||||
| Cinabre | 11 797 | 13 597 | 20 095 |
|
||||
|
||||
La sélection des chunks suit les sites d’eau et d’écologie ; elle n’est pas
|
||||
un échantillon représentatif de toute l’île. Ces stocks ne démontrent pas
|
||||
l’accessibilité de chaque bloc et ne prouvent pas, seuls, une réduction du
|
||||
soufre par rapport à une autre version sur un périmètre différent.
|
||||
Le tableau des minerais extérieurs sélectionne charbon, fer et cuivre ; les
|
||||
JSON gardent les autres familles effectivement observées avec une face d’air.
|
||||
|
||||
| Graine | Réentrée source d’eau | Réentrée source de lave | Réentrée débordement |
|
||||
| --- | --- | --- | --- |
|
||||
| `0` | 1 vérifiée(s) | 1 vérifiée(s) | 1 vérifiée(s) |
|
||||
| `42` | 1 vérifiée(s) | 1 vérifiée(s) | 1 vérifiée(s) |
|
||||
| `8675309` | 1 vérifiée(s) | 1 vérifiée(s) | 1 vérifiée(s) |
|
||||
|
||||
Après consommation des marqueurs, les rappels du post-traitement préservent
|
||||
les modifications témoins apportées aux bouches ; les blocs sont restaurés
|
||||
ensuite. Les 176 fichiers historiques suivis hors raccordement
|
||||
conservent leur SHA-256. Aucun monde personnel n’est ouvert par ces tests.
|
||||
|
||||
Preuves locales ignorées : `build/alpha9-check-build-final.log` (build et graine 0),
|
||||
`build/alpha9-seed42-final.log`, `build/alpha9-seed8675309-final.log` et les rapports
|
||||
`cavern-seed-<graine>-after-1800-ticks.json` et `-postprocessing-reentry.json`
|
||||
sous `mods/sanctuary/build/run/gameTest/diagnostics/`.
|
||||
`build/report-alpha9-validation.py` extrait les mesures et vérifie les preuves
|
||||
cumulées, le gel des sources et les journaux. Le hash final du JAR est lu
|
||||
dans `build/alpha9-jar-sha256.txt`, sans valeur codée dans l’extracteur.
|
||||
|
||||
Ces résultats ne garantissent aucune géode, terrasse, rivière, geyser ou
|
||||
ressource sur chaque graine. L’appréciation visuelle et l’équilibrage en
|
||||
partie restent ouverts. La publication et les deux synchronisations de
|
||||
la même instance Prism sont vérifiées séparément lors de la distribution.
|
||||
|
||||
## Contrôles et validation alpha.8 — 9 septembre 2026
|
||||
|
||||
Les trois essais utilisent des mondes neufs et le preset de production
|
||||
|
||||
+19
-5
@@ -61,9 +61,13 @@ petits bassins, sans imposer de courts ruisseaux. Le terrain est traité près d
|
||||
avec des dépôts épais et des rives variées. De rares sources sortent de niches
|
||||
rocheuses naturelles et leurs cascades peuvent descendre jusque dans le vide ;
|
||||
les bassins restent retenus et ne doivent pas provoquer une inondation globale.
|
||||
Une rivière peut avoir des échappements et des chutes déclarés. Les 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
|
||||
Une rivière peut avoir des échappements et des chutes déclarés. Les eaux
|
||||
intérieures doivent rester localisées et retenues, sans aquifère qui remplit
|
||||
globalement les cavités. Des bassins naturels étagés, évoquant des rizières,
|
||||
peuvent se relier par des cascades et offrir des passages verticaux par la nage.
|
||||
Cette image ne prévoit aucune culture de riz dans le mod Sanctuary.
|
||||
Le terrain doit laisser voir un dessous sculpté et dégagé, dans un monde de
|
||||
**384 blocs de hauteur**, avec des
|
||||
nuages relevés et sans disque noir artificiel apparaissant sous un seuil d'altitude.
|
||||
|
||||
L’ambiance de départ doit rester douce et tempérée, avec de véritables forêts
|
||||
@@ -77,8 +81,9 @@ masse artificielle au centre de l’île.
|
||||
|
||||
La géologie change en descendant : tuf, cobblestone, pierres moussues et boue
|
||||
compactée dans les intérieurs humides, puis ardoise des abîmes, roche noire et
|
||||
basalte. Des poches de soufre doivent être réellement explorables. Quelques
|
||||
sources profondes peuvent former des coulées de lave ; elles ne doivent pas
|
||||
basalte. Des poches de soufre doivent être réellement explorables et rester
|
||||
ponctuelles, avec de vrais geysers actifs lorsque les conditions s’y prêtent.
|
||||
Quelques sources profondes peuvent former des coulées de lave ; elles ne doivent pas
|
||||
transformer les profondeurs en grands bassins remplis. L’émeraude rejoint les
|
||||
minerais à rechercher, avec l’or, le diamant, la redstone et le lapis.
|
||||
|
||||
@@ -91,6 +96,15 @@ cerisier rose ou une autre de ces essences rares, lorsque le terrain convient.
|
||||
Les biomes très contrastés de Minecraft 26.3, dont Dappled Forest, restent
|
||||
réservés aux futurs continents.
|
||||
|
||||
Les cavités sous l’île doivent aussi accueillir des ambiances luxuriantes
|
||||
avec de la végétation lumineuse, des secteurs à stalactites et stalagmites et
|
||||
des géodes d’améthyste. Leur décor doit utiliser les sols et plafonds du relief,
|
||||
sans reconstruire l’île ni déposer de structures flottantes sans support.
|
||||
L’alpha.9 introduit ces ambiances et recherche des lacs plus profonds et
|
||||
volumineux ; les résultats et leurs limites sont documentés dans les contrôles moteur. Ces cavités appartiennent à
|
||||
l’Overworld de Sanctuary et ne remplacent pas le futur projet de dimension
|
||||
des cavernes.
|
||||
|
||||
L’île doit offrir les ressources nécessaires aux premières étapes de Minecraft,
|
||||
en quantités modestes pour pousser à explorer les expansions lors des grands
|
||||
projets. Les minerais doivent être trouvables aux altitudes du terrain. Les
|
||||
|
||||
+110
-7
@@ -5,10 +5,11 @@ entourée de vide sans répétition d'îles à grande distance. C'est une premi
|
||||
tranche de génération pour Minecraft **26.3-pre-2 / Fabric**, pas encore le
|
||||
système d'expansion collective.
|
||||
|
||||
L’alpha.8 utilise les paramètres `sanctuary:sanctuary_rift`. Ses contrôles
|
||||
dans les vrais chunks passent sur les trois graines de référence, avec
|
||||
1 800 ticks de fluides. Les observations et limites de cette version sont
|
||||
consignées séparément des générations précédentes dans [Validation](testing.md).
|
||||
L’alpha.9 utilise les paramètres `sanctuary:sanctuary_cavern`, pour de nouveaux
|
||||
mondes uniquement. Sa validation moteur passe sur les trois graines de référence. L’alpha.8 conserve
|
||||
`sanctuary:sanctuary_rift` et ses résultats sur trois graines avec 1 800 ticks
|
||||
de fluides ; ils ne valident pas la génération v9. Les observations de chaque
|
||||
version restent séparées dans [Validation](testing.md).
|
||||
|
||||
## Créer un monde
|
||||
|
||||
@@ -27,7 +28,10 @@ La seed est libre. Modifier `level-type` ne convertit pas un monde existant :
|
||||
utiliser un nouveau `level-name` pour tester une autre génération. Les fichiers
|
||||
de test et les sauvegardes personnelles doivent rester distincts.
|
||||
|
||||
## Ce qui est généré
|
||||
## Socle de génération conservé
|
||||
|
||||
Les points suivants décrivent le socle livré jusqu’à l’alpha.8. Les évolutions
|
||||
alpha.9 et leur état de validation sont précisés dans la section v9 ci-dessous.
|
||||
|
||||
- Le générateur reste celui de Minecraft, `minecraft:noise`.
|
||||
- La forme reprend le champ de bruit flottant actif en 26.2 : `old_blended_noise`
|
||||
@@ -59,8 +63,10 @@ de test et les sauvegardes personnelles doivent rester distincts.
|
||||
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
|
||||
retirés. L’eau de Sanctuary suit son propre plan de surface et de niches
|
||||
- Jusqu’à l’alpha.8, les carvers, géodes, donjons, grands lacs de lave et sources
|
||||
vanilla sont retirés. L’alpha.9 ajoute une intégration bornée de géodes et
|
||||
de geysers, sans réactiver l’ensemble de la décoration souterraine vanilla.
|
||||
L’eau de Sanctuary suit son propre plan de surface et de niches
|
||||
rocheuses. Les arbres et les petits filons ont besoin du terrain existant.
|
||||
Aucun mod de biomes externe n’est nécessaire.
|
||||
|
||||
@@ -71,6 +77,103 @@ chaque seed. L’accès à chaque progression vanilla et les ressources renouvel
|
||||
nécessitent des vérifications concrètes. Les stocks réels sont relevés après la
|
||||
génération, sans corriger une île pour atteindre un quota.
|
||||
|
||||
## Cavités et terrasses d’eau — génération v9
|
||||
|
||||
Le nouveau réglage est `sanctuary:sanctuary_cavern`, avec le biome source
|
||||
`sanctuary:cavern_island` et la règle de matière `sanctuary:cavern_island`.
|
||||
La densité réutilise `sanctuary:final_density_rift` sans changer cet identifiant
|
||||
historique. Ses nouvelles données et traitements
|
||||
doivent rester distincts de `sanctuary:sanctuary_rift` et de toutes les
|
||||
générations précédentes. Le preset sélectionne la nouvelle génération à la
|
||||
création ; une mise à jour du JAR ne convertit pas une sauvegarde ni ne
|
||||
régénère ses chunks. Les cavités restent dans l’Overworld sur 384 blocs de
|
||||
hauteur ; la dimension des cavernes décrite dans la vision n’est pas ajoutée.
|
||||
|
||||
### Ambiances et ressources
|
||||
|
||||
Le ticket conserve les forêts de surface et ajoute des cavités luxuriantes
|
||||
éclairées par leur végétation, des secteurs à spéléothèmes et des géodes
|
||||
d’améthyste sur des emplacements compatibles. Les sols, plafonds, coquilles
|
||||
et ouvertures doivent être observés après la décoration réelle : une valeur
|
||||
de biome ne prouve ni le décor ni son éclairage. L’objectif est d’utiliser
|
||||
les volumes naturels de l’île, sans remplir ses trous pour accueillir une
|
||||
structure.
|
||||
|
||||
Les dix biomes ont leurs propres identifiants : `cavern_oak_forest`,
|
||||
`cavern_birch_forest`, `cavern_clearing`, `cavern_dry_woodland`,
|
||||
`cavern_rocky_heath`, `cavern_dark_grove`, `cavern_bamboo_grove`,
|
||||
`cavern_sulfur_depths`, `cavern_lush_caves` et `cavern_dripstone_caves`, dans
|
||||
le namespace `sanctuary`. Les deux nouvelles ambiances luxuriantes et à
|
||||
spéléothèmes sont limitées à Y<184 par la palette ; leur décoration recherche
|
||||
des sols entre Y=40 et Y=182, avec trois blocs naturels de support et un
|
||||
plafond situé de 6 à 48 blocs au-dessus.
|
||||
|
||||
Dans les cavités luxuriantes, la mousse remplace le sol compatible et de
|
||||
petites plantes l’accompagnent. Les vraies lianes `cave_vines` et
|
||||
`cave_vines_plant` pendent sur 3 à 8 blocs ; leurs baies émettent de la lumière
|
||||
et restent récoltables. Ce décor n’ajoute pas d’eau. Les spéléothèmes utilisent
|
||||
les blocs `pointed_dripstone` orientés, sur un appui de `dripstone_block`
|
||||
remplaçant de la roche existante, avec une longueur de 1 à 4 blocs selon
|
||||
l’espace disponible.
|
||||
|
||||
La géode utilise une configuration vanilla compacte avec améthyste,
|
||||
améthyste bourgeonnante, calcite et basalte lisse. Son enveloppe doit tenir
|
||||
dans la roche préexistante et un passage de deux blocs de haut la relie à
|
||||
la cavité, en préservant l’améthyste bourgeonnante et les cristaux attachés.
|
||||
Un chunk sur huit est candidat ; au plus une pose réussie est retenue par
|
||||
chunk candidat. Cela ne garantit pas de géode sur une graine.
|
||||
|
||||
Le soufre devient admissible dans une bande climatique plus étroite sous
|
||||
Y=176. La réduction de cette sélection peut être testée indépendamment du
|
||||
terrain ; seule une comparaison des vrais blocs sur un périmètre commun
|
||||
mesure sa diminution dans l’île. Un geyser éventuel creuse un seul bloc de
|
||||
sol pour sa source d’eau, avec `potent_sulfur` dessous et `magma_block` encore
|
||||
en dessous. Le fond, les quatre parois et la hauteur libre sont contrôlés.
|
||||
Le cycle du geyser reste celui de Minecraft ; ce décor est distinct des
|
||||
bassins hydrologiques et son eau doit être comptée séparément.
|
||||
|
||||
Leur présence, leur activité, l’éclairage et la cohabitation avec les eaux
|
||||
et la végétation sont contrôlés dans les mondes de test. Les preuves de ces
|
||||
éléments sont cumulées sur les trois graines. Un site incompatible peut
|
||||
être rejeté ; aucun geyser ni stock n’est garanti pour chaque graine.
|
||||
|
||||
### Lacs et bassins étagés
|
||||
|
||||
Le plan hydrologique conserve la recherche de rivière de
|
||||
l’alpha.8 et vise de plus grands lacs de surface. Les cibles actuelles sont
|
||||
de l’ordre de **700 à 1 100 colonnes** par grand lac, jusqu’à **7 à 8 blocs**
|
||||
de profondeur intérieure et une incision limitée à **12 blocs**. Ce sont des
|
||||
bornes de recherche ; les dimensions réelles sont relevées séparément, sans quota d’eau à
|
||||
remplir. Les fonds, les parois et les dépôts exigent du terrain naturel.
|
||||
|
||||
Les terrasses sont recherchées dans des cavités entre **Y=56 et Y=208**,
|
||||
avec deux paliers par groupe et jusqu’à deux groupes facultatifs.
|
||||
Un palier vise environ **40 à 180 colonnes**, une profondeur de **1 à 3 blocs**
|
||||
et une incision d’au plus **5 blocs**. Le fond doit conserver trois couches
|
||||
de sédiments et deux couches de support naturel ; le plafond et l’espace
|
||||
disponible sont vérifiés. Les mesures moteur précisent les sites effectivement retenus.
|
||||
|
||||
La recherche suit le choix de pente de l’eau vanilla : parmi les passages
|
||||
libres, elle privilégie ceux qui rejoignent la chute la plus proche. Un
|
||||
bassin inférieur doit intercepter ce trajet réel, sans détour creusé pour
|
||||
le rejoindre. Les sorties entre paliers sont déclarées et les cascades sont formées par les
|
||||
ticks de fluides Minecraft. Le premier débordement doit atteindre le second
|
||||
bassin. Celui-ci peut avoir un exutoire terminal qui s’écoule sur les roches
|
||||
ou vers le vide, sans troisième bassin ajouté. Le générateur ne doit ni peindre une colonne
|
||||
d’eau jusqu’au vide ni colmater un trou pour retenir un bassin. Les volumes
|
||||
retenus et les liaisons réelles sont vérifiés séparément après décoration
|
||||
et simulation, y compris aux frontières de chunks. Les cascades peuvent
|
||||
servir à monter ou descendre selon la nage vanilla ; aucune mécanique
|
||||
d’ascenseur supplémentaire n’est prévue. « Rizières » décrit seulement
|
||||
l’aspect du paysage en terrasses, sans ajouter une culture de riz.
|
||||
|
||||
**Validation moteur au 9 septembre 2026 :** les trois graines de référence
|
||||
passent les contrôles après 1 800 ticks, avec preuves cumulées des terrasses
|
||||
connectées, geysers actifs, baies lumineuses, spéléothèmes et améthyste
|
||||
bourgeonnante. Le build complet réussit. Les résultats et leurs limites sont
|
||||
consignés dans [Validation](testing.md). Aucune présence universelle n’est
|
||||
garantie ; la distribution est vérifiée séparément selon le guide packwiz.
|
||||
|
||||
## Failles, géologie et ressources — génération v8
|
||||
|
||||
Les nouveaux mondes utilisent `sanctuary:sanctuary_rift`, la densité
|
||||
|
||||
+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.8
|
||||
pack_version=0.1.0-alpha.8
|
||||
mod_version=0.1.0-alpha.9
|
||||
pack_version=0.1.0-alpha.9
|
||||
maven_group=fr.koka.sanctuary
|
||||
|
||||
@@ -179,3 +179,27 @@ tasks.register('riftLavaSmoke', JavaExec) {
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.RiftLavaSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('riftLavaSmoke') }
|
||||
|
||||
tasks.register('cavernPaletteSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.CavernPaletteSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('cavernPaletteSmoke') }
|
||||
|
||||
tasks.register('cavernHydrologySmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.CavernHydrologySmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('cavernHydrologySmoke') }
|
||||
|
||||
tasks.register('cavernLavaSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.CavernLavaSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('cavernLavaSmoke') }
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+17
-3
@@ -5,6 +5,7 @@ import fr.koka.sanctuary.worldgen.NaturalHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.LayeredHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.WoodlandHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.RiftHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.CavernHydrologyRuntime;
|
||||
import net.minecraft.tags.FluidTags;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.IslandShape;
|
||||
@@ -32,7 +33,8 @@ public final class SanctuaryWorldGameTests {
|
||||
@GameTest(maxTicks = 3000)
|
||||
public void generatedSurfaceWaterRemainsContained(GameTestHelper helper) throws java.io.IOException {
|
||||
var generator = (NoiseBasedChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
if (RiftHydrologyRuntime.enabled(generator)) RiftDiagnostics.start(helper);
|
||||
if (CavernHydrologyRuntime.enabled(generator)) CavernDiagnostics.start(helper);
|
||||
else if (RiftHydrologyRuntime.enabled(generator)) RiftDiagnostics.start(helper);
|
||||
else if (WoodlandHydrologyRuntime.enabled(generator)) WoodlandDiagnostics.start(helper);
|
||||
else if (LayeredHydrologyRuntime.enabled(generator)) LayeredHydrologyDiagnostics.start(helper);
|
||||
else if (NaturalHydrologyRuntime.enabled(generator)) NaturalHydrologyDiagnostics.start(helper);
|
||||
@@ -42,7 +44,8 @@ public final class SanctuaryWorldGameTests {
|
||||
@GameTest(maxTicks = 200)
|
||||
public void hydrologyDoesNotActivateForSavedLegacySettings(GameTestHelper helper) {
|
||||
var generator = (NoiseBasedChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
if (RiftHydrologyRuntime.enabled(generator)) RiftDiagnostics.verifyLegacyIsolation(helper);
|
||||
if (CavernHydrologyRuntime.enabled(generator)) CavernDiagnostics.verifyLegacyIsolation(helper);
|
||||
else if (RiftHydrologyRuntime.enabled(generator)) RiftDiagnostics.verifyLegacyIsolation(helper);
|
||||
else if (WoodlandHydrologyRuntime.enabled(generator)) WoodlandDiagnostics.verifyLegacyIsolation(helper);
|
||||
else if (LayeredHydrologyRuntime.enabled(generator)) LayeredHydrologyDiagnostics.verifyLegacyIsolation(helper);
|
||||
else if (NaturalHydrologyRuntime.enabled(generator)) NaturalHydrologyDiagnostics.verifyLegacyIsolation(helper);
|
||||
@@ -124,13 +127,24 @@ public final class SanctuaryWorldGameTests {
|
||||
if (chunk.getSection(0).hasOnlyAir()) continue;
|
||||
var generator = (NoiseBasedChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
helper.assertTrue(NaturalHydrologyRuntime.enabled(generator) || LayeredHydrologyRuntime.enabled(generator)
|
||||
|| WoodlandHydrologyRuntime.enabled(generator) || RiftHydrologyRuntime.enabled(generator),
|
||||
|| WoodlandHydrologyRuntime.enabled(generator) || RiftHydrologyRuntime.enabled(generator) || CavernHydrologyRuntime.enabled(generator),
|
||||
"The legacy island must leave its lower margin entirely empty");
|
||||
for (int x = chunk.getPos().getMinBlockX(); x <= chunk.getPos().getMaxBlockX(); x++) {
|
||||
for (int z = chunk.getPos().getMinBlockZ(); z <= chunk.getPos().getMaxBlockZ(); z++) {
|
||||
for (int y = 0; y < 16; y++) {
|
||||
BlockPos p = new BlockPos(x, y, z);
|
||||
var state = chunk.getBlockState(p);
|
||||
if (CavernHydrologyRuntime.enabled(generator)) {
|
||||
boolean water = state.getFluidState().is(FluidTags.WATER)
|
||||
&& CavernDiagnostics.isWithinSpringInspectionFootprint(CavernHydrologyRuntime.cavernPlan(helper.getLevel()), x, z);
|
||||
final int bx = x, bz = z;
|
||||
boolean lava = state.getFluidState().is(FluidTags.LAVA)
|
||||
&& CavernHydrologyRuntime.lavaPlan(helper.getLevel()).flowBounds().stream().anyMatch(b ->
|
||||
bx >= (Math.floorDiv(b.minX(),16) - 1) * 16 && bx <= (Math.floorDiv(b.maxX(),16) + 2) * 16 - 1
|
||||
&& bz >= (Math.floorDiv(b.minZ(),16) - 1) * 16 && bz <= (Math.floorDiv(b.maxZ(),16) + 2) * 16 - 1);
|
||||
helper.assertTrue(state.isAir() || water || lava, "Lower margin must remain void except declared fluid falls: " + p);
|
||||
continue;
|
||||
}
|
||||
if (RiftHydrologyRuntime.enabled(generator)) {
|
||||
boolean water = state.getFluidState().is(FluidTags.WATER)
|
||||
&& RiftDiagnostics.isWithinSpringInspectionFootprint(RiftHydrologyRuntime.riftPlan(helper.getLevel()), x, z);
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package fr.koka.sanctuary;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.CavernIslandBiomeSource;
|
||||
import fr.koka.sanctuary.worldgen.CavernGrovesFeature;
|
||||
import fr.koka.sanctuary.worldgen.CavernDecorationsFeature;
|
||||
import fr.koka.sanctuary.worldgen.CavernShoreSugarCaneFeature;
|
||||
import fr.koka.sanctuary.worldgen.RiftIslandDensity;
|
||||
import fr.koka.sanctuary.worldgen.RiftIslandBiomeSource;
|
||||
import fr.koka.sanctuary.worldgen.RiftGrovesFeature;
|
||||
@@ -45,6 +49,10 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("rift_island"), RiftIslandBiomeSource.CODEC);
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("rift_groves"), RiftGrovesFeature.CODEC);
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("rift_shore_sugar_cane"), RiftShoreSugarCaneFeature.CODEC);
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("cavern_island"), CavernIslandBiomeSource.CODEC);
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("cavern_groves"), CavernGrovesFeature.CODEC);
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("cavern_decorations"), CavernDecorationsFeature.CODEC);
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("cavern_shore_sugar_cane"), CavernShoreSugarCaneFeature.CODEC);
|
||||
LOGGER.info("Sanctuary initialized: the Sanctuary world preset is available.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package fr.koka.sanctuary.mixin;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.CavernSpringOutlets;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/** Finish only pending generation markers; completed saved chunks are never repaired on reload. */
|
||||
@Mixin(LevelChunk.class)
|
||||
public abstract class CavernSpringOutletMixin {
|
||||
@Unique
|
||||
private CavernSpringOutlets.Pending sanctuary$pendingCavernOutlets = CavernSpringOutlets.Pending.EMPTY;
|
||||
|
||||
@Inject(method = "postProcessGeneration", at = @At("HEAD"))
|
||||
private void sanctuary$captureCavernOutlets(ServerLevel level, CallbackInfo callback) {
|
||||
sanctuary$pendingCavernOutlets = CavernSpringOutlets.capturePending(level, (LevelChunk) (Object) this);
|
||||
}
|
||||
|
||||
@Inject(method = "postProcessGeneration", at = @At("TAIL"))
|
||||
private void sanctuary$finishCavernOutlets(ServerLevel level, CallbackInfo callback) {
|
||||
var pending = sanctuary$pendingCavernOutlets;
|
||||
sanctuary$pendingCavernOutlets = CavernSpringOutlets.Pending.EMPTY;
|
||||
CavernSpringOutlets.finish(level, (LevelChunk) (Object) this, pending);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import fr.koka.sanctuary.worldgen.NaturalHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.LayeredHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.WoodlandHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.RiftHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.CavernHydrologyRuntime;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import net.minecraft.core.Holder;
|
||||
@@ -29,7 +30,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 (RiftHydrologyRuntime.enabled(generator)) {
|
||||
if (CavernHydrologyRuntime.enabled(generator)) {
|
||||
callback.setReturnValue(callback.getReturnValue().thenApply(generated -> {
|
||||
CavernHydrologyRuntime.apply(generator, randomState, generated);
|
||||
return generated;
|
||||
}));
|
||||
} else if (RiftHydrologyRuntime.enabled(generator)) {
|
||||
callback.setReturnValue(callback.getReturnValue().thenApply(generated -> {
|
||||
RiftHydrologyRuntime.apply(generator, randomState, generated);
|
||||
return generated;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
/** Alpha.9 keeps the upper woodland climate and specializes sheltered lower layers. */
|
||||
public final class CavernBiomePalette {
|
||||
public enum Kind {
|
||||
OAK_FOREST, BIRCH_FOREST, CLEARING, DRY_WOODLAND, ROCKY_HEATH,
|
||||
DARK_GROVE, BAMBOO_GROVE, SULFUR_DEPTHS, LUSH_CAVES, DRIPSTONE_CAVES
|
||||
}
|
||||
|
||||
private CavernBiomePalette() {}
|
||||
|
||||
public static Kind select(int blockY, float variation, float moisture) {
|
||||
double elevation = blockY - Math.clamp(variation, -1.0F, 1.0F) * 16
|
||||
- Math.clamp(moisture, -1.0F, 1.0F) * 10;
|
||||
// Small climate pockets replace alpha.8's broad sulfur province. Neither
|
||||
// mineral pockets nor cave vegetation can take over the upper forest.
|
||||
if (blockY < 176 && elevation < 168 && variation > 0.22F && variation < 0.42F
|
||||
&& moisture > -0.42F && moisture < -0.12F) return Kind.SULFUR_DEPTHS;
|
||||
if (variation > 0.48F) return Kind.ROCKY_HEATH;
|
||||
if (blockY < 184 && elevation < 180) {
|
||||
if (moisture > 0.05F && variation > -0.32F) return Kind.LUSH_CAVES;
|
||||
if (moisture < -0.18F && variation < 0.22F) return Kind.DRIPSTONE_CAVES;
|
||||
}
|
||||
if (elevation < 214) {
|
||||
if (variation < -0.45F && moisture > 0.40F) return Kind.BAMBOO_GROVE;
|
||||
double shelteredMoisture = moisture + Math.min(0.55, (214 - elevation) / 160);
|
||||
if (shelteredMoisture > -0.08) return Kind.DARK_GROVE;
|
||||
}
|
||||
if (moisture < -0.55F && variation > -0.25F) return Kind.DRY_WOODLAND;
|
||||
if (Math.abs(variation) < 0.09F && moisture < 0.35F) return Kind.CLEARING;
|
||||
if (variation < -0.33F) return Kind.BIRCH_FOREST;
|
||||
return Kind.OAK_FOREST;
|
||||
}
|
||||
}
|
||||
+296
@@ -0,0 +1,296 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.AmethystClusterBlock;
|
||||
import net.minecraft.world.level.block.CaveVines;
|
||||
import net.minecraft.world.level.block.GrowingPlantHeadBlock;
|
||||
import net.minecraft.world.level.block.PotentSulfurBlock;
|
||||
import net.minecraft.world.level.block.SpeleothemBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.PotentSulfurState;
|
||||
import net.minecraft.world.level.block.state.properties.SpeleothemThickness;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.feature.Feature;
|
||||
import net.minecraft.world.level.levelgen.feature.GeodeFeature;
|
||||
|
||||
/** Decorations in existing lower cavities only. All writes belong to the owner's chunk. */
|
||||
public final class CavernDecorationsFeature implements Feature {
|
||||
public static final MapCodec<CavernDecorationsFeature> CODEC = MapCodec.unit(CavernDecorationsFeature::new);
|
||||
private static final ResourceKey<Feature> GEODE = ResourceKey.create(Registries.FEATURE, SanctuaryMod.id("cavern_amethyst_geode"));
|
||||
private static final Direction[] SIDES = {Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST};
|
||||
public enum Kind { LUSH, DRIPSTONE, SULFUR, GEODE }
|
||||
public record Candidate(Kind kind, BlockPos floor, BlockPos ceiling) {
|
||||
public Candidate { floor = floor.immutable(); ceiling = ceiling.immutable(); }
|
||||
}
|
||||
|
||||
@Override public MapCodec<CavernDecorationsFeature> codec() { return CODEC; }
|
||||
|
||||
/** Pure, bounded probes for development diagnostics; no chunk loads or successful-placement promise. */
|
||||
public static List<Candidate> candidates(NoiseBasedChunkGenerator generator, RandomState state, int chunkX, int chunkZ) {
|
||||
if (!CavernHydrologyRuntime.enabled(generator) || Math.abs(chunkX) > 17 || Math.abs(chunkZ) > 17) return List.of();
|
||||
var density = generator.generatorSettings().value().noiseRouter().finalDensity();
|
||||
WoodlandGroveGeometry.Sampler sampler = (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z);
|
||||
var biomes = generator.getBiomeSource().createUncachedResolver(state);
|
||||
List<Candidate> found = new ArrayList<>();
|
||||
for (int[] offset : new int[][]{{4, 4}, {4, 12}, {12, 4}, {12, 12}, {8, 8}}) {
|
||||
int x = chunkX * 16 + offset[0], z = chunkZ * 16 + offset[1], floors = 0;
|
||||
for (int y = 182; y >= 40; y--) {
|
||||
if (sampler.sample(x, y, z) <= 0 || sampler.sample(x, y + 1, z) > 0
|
||||
|| sampler.sample(x, y + 2, z) > 0 || sampler.sample(x, y - 1, z) <= 0
|
||||
|| sampler.sample(x, y - 2, z) <= 0) continue;
|
||||
int roof = y + 3;
|
||||
while (roof <= Math.min(230, y + 48) && sampler.sample(x, roof, z) <= 0) roof++;
|
||||
if (roof > Math.min(230, y + 48) || roof - y < 6
|
||||
|| sampler.sample(x, roof + 1, z) <= 0) continue;
|
||||
BlockPos floor = new BlockPos(x, y, z), ceiling = new BlockPos(x, roof, z);
|
||||
var biome = biomes.getNoiseBiome(x >> 2, (y + 1) >> 2, z >> 2);
|
||||
if (biome.is(CavernIslandBiomeSource.LUSH_CAVES)) found.add(new Candidate(Kind.LUSH, floor, ceiling));
|
||||
else if (biome.is(CavernIslandBiomeSource.DRIPSTONE_CAVES)) found.add(new Candidate(Kind.DRIPSTONE, floor, ceiling));
|
||||
else if (biome.is(CavernIslandBiomeSource.SULFUR_DEPTHS) && y < 176)
|
||||
found.add(new Candidate(Kind.SULFUR, floor, ceiling));
|
||||
if (offset[0] == 8 && offset[1] == 8 && Math.floorMod(chunkSeed(state.seed(), chunkX, chunkZ, 0x9501L), 8) == 0)
|
||||
found.add(new Candidate(Kind.GEODE, floor, ceiling));
|
||||
if (++floors >= 3) break;
|
||||
}
|
||||
}
|
||||
// Geodes keep their entry free before this feature's moss, grass or pointed
|
||||
// dripstone can occupy it. Stable sorting preserves every other candidate order.
|
||||
found.sort(java.util.Comparator.comparingInt(candidate -> candidate.kind() == Kind.GEODE ? 0 : 1));
|
||||
return List.copyOf(found);
|
||||
}
|
||||
|
||||
@Override public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource ignored, BlockPos origin) {
|
||||
if (!(generator instanceof NoiseBasedChunkGenerator noise) || !CavernHydrologyRuntime.enabled(noise)) return false;
|
||||
var state = level.getLevel().getChunkSource().randomState();
|
||||
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
|
||||
WoodlandGroveGeometry.Sampler sampler = (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z);
|
||||
int cx = origin.getX() >> 4, cz = origin.getZ() >> 4;
|
||||
boolean changed = false, geyser = false, geode = false;
|
||||
for (Candidate candidate : candidates(noise, state, cx, cz)) {
|
||||
BlockPos floor = candidate.floor();
|
||||
if (!stable(level, sampler, floor) || !level.getBlockState(floor.above()).isAir()) continue;
|
||||
long seed = WoodlandGroveGeometry.seed(state.seed(), floor.getX(), floor.getY(), floor.getZ(), 0x9502L);
|
||||
switch (candidate.kind()) {
|
||||
case LUSH -> changed |= lush(level, noise, state, sampler, candidate, seed);
|
||||
case DRIPSTONE -> changed |= dripstone(level, noise, state, sampler, candidate, seed);
|
||||
case SULFUR -> {
|
||||
if (!geyser && Math.floorMod(chunkSeed(state.seed(), cx, cz, 0x9503L), 2) == 0)
|
||||
changed |= geyser = geyser(level, noise, state, sampler, candidate);
|
||||
}
|
||||
case GEODE -> { if (!geode) changed |= geode = geode(level, noise, state, sampler, candidate, seed); }
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private boolean lush(WorldGenLevel level, NoiseBasedChunkGenerator noise, RandomState state,
|
||||
WoodlandGroveGeometry.Sampler sampler, Candidate site, long seed) {
|
||||
BlockPos floor = site.floor(), roof = site.ceiling();
|
||||
int gap = roof.getY() - floor.getY();
|
||||
if (CavernHydrologyRuntime.protects(noise, state, floor, 3, gap + 2, 3)) return false;
|
||||
boolean changed = false;
|
||||
for (int dx = -2; dx <= 2; dx++) for (int dz = -2; dz <= 2; dz++) {
|
||||
if (dx * dx + dz * dz > 5) continue;
|
||||
BlockPos p = floor.offset(dx, 0, dz);
|
||||
if (!stable(level, sampler, p) || !stable(level, sampler, p.below()) || !stable(level, sampler, p.below(2))
|
||||
|| !level.getBlockState(p.above()).isAir()) continue;
|
||||
setBlock(level, p, Blocks.MOSS_BLOCK.defaultBlockState());
|
||||
long local = WoodlandGroveGeometry.seed(seed, p.getX(), p.getY(), p.getZ(), 0x9504L);
|
||||
BlockState plant = (Math.floorMod(local, 5) == 0 ? Blocks.AZALEA : Math.floorMod(local, 3) == 0
|
||||
? Blocks.MOSS_CARPET : Blocks.SHORT_GRASS).defaultBlockState();
|
||||
if (plant.canSurvive(level, p.above())) setBlock(level, p.above(), plant);
|
||||
changed = true;
|
||||
}
|
||||
// Real cave-vine heads and bodies remain harvestable and luminous via BERRIES.
|
||||
int length = Math.min(gap - 3, 3 + Math.floorMod(seed, 6));
|
||||
if (stable(level, sampler, roof) && stable(level, sampler, roof.above())) {
|
||||
boolean free = true;
|
||||
for (int d = 1; d <= length; d++) free &= level.getBlockState(roof.below(d)).isAir();
|
||||
if (free) for (int d = 1; d <= length; d++) {
|
||||
BlockPos p = roof.below(d);
|
||||
BlockState vine = (d == length ? Blocks.CAVE_VINES : Blocks.CAVE_VINES_PLANT).defaultBlockState()
|
||||
.setValue(CaveVines.BERRIES, d == length || Math.floorMod(seed + d, 3) == 0);
|
||||
if (d == length) vine = vine.setValue(GrowingPlantHeadBlock.AGE, 25);
|
||||
if (!vine.canSurvive(level, p)) break;
|
||||
setBlock(level, p, vine); changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private boolean dripstone(WorldGenLevel level, NoiseBasedChunkGenerator noise, RandomState state,
|
||||
WoodlandGroveGeometry.Sampler sampler, Candidate site, long seed) {
|
||||
BlockPos floor = site.floor(), roof = site.ceiling();
|
||||
int gap = roof.getY() - floor.getY();
|
||||
if (CavernHydrologyRuntime.protects(noise, state, floor, 1, gap + 2, 3)) return false;
|
||||
int length = Math.min((gap - 3) / 2, 1 + Math.floorMod(seed, 4));
|
||||
boolean changed = spike(level, sampler, floor, Direction.UP, length);
|
||||
changed |= spike(level, sampler, roof, Direction.DOWN, length);
|
||||
return changed;
|
||||
}
|
||||
|
||||
private boolean spike(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler, BlockPos support, Direction direction, int length) {
|
||||
if (length < 1 || !stable(level, sampler, support) || !stable(level, sampler, support.relative(direction.getOpposite()))) return false;
|
||||
for (int d = 1; d <= length; d++) {
|
||||
BlockPos p = support.relative(direction, d);
|
||||
if (!level.getBlockState(p).isAir() || sampler.sample(p.getX(), p.getY(), p.getZ()) > 0) return false;
|
||||
}
|
||||
setBlock(level, support, Blocks.DRIPSTONE_BLOCK.defaultBlockState());
|
||||
for (int d = 1; d <= length; d++) {
|
||||
SpeleothemThickness thickness = d == length ? SpeleothemThickness.TIP
|
||||
: d == length - 1 ? SpeleothemThickness.FRUSTUM : d == 1 ? SpeleothemThickness.BASE : SpeleothemThickness.MIDDLE;
|
||||
BlockState pointed = Blocks.POINTED_DRIPSTONE.defaultBlockState().setValue(SpeleothemBlock.TIP_DIRECTION, direction)
|
||||
.setValue(SpeleothemBlock.THICKNESS, thickness);
|
||||
setBlock(level, support.relative(direction, d), pointed);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean geyser(WorldGenLevel level, NoiseBasedChunkGenerator noise, RandomState state,
|
||||
WoodlandGroveGeometry.Sampler sampler, Candidate site) {
|
||||
BlockPos water = site.floor(), potent = water.below(), heat = water.below(2);
|
||||
if (site.ceiling().getY() - water.getY() < 8
|
||||
|| CavernHydrologyRuntime.protects(noise, state, water, 2, 8, 5)) return false;
|
||||
for (int d = 0; d <= 4; d++) if (!stable(level, sampler, water.below(d))) return false;
|
||||
for (Direction side : SIDES) for (int d = 0; d <= 2; d++)
|
||||
if (!stable(level, sampler, water.below(d).relative(side))) return false;
|
||||
for (int d = 1; d <= 7; d++) if (!level.getBlockState(water.above(d)).isAir()) return false;
|
||||
// One source, four original rock walls, original deep support. Vanilla's
|
||||
// periodic geyser ticker requires magma below potent sulfur and source water above.
|
||||
setBlock(level, heat, Blocks.MAGMA_BLOCK.defaultBlockState());
|
||||
setBlock(level, water, Blocks.WATER.defaultBlockState());
|
||||
setBlock(level, potent, Blocks.POTENT_SULFUR.defaultBlockState().setValue(PotentSulfurBlock.STATE, PotentSulfurState.DORMANT));
|
||||
level.getChunk(water).markPosForPostProcessing(water);
|
||||
level.getChunk(potent).markPosForPostProcessing(potent);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean geode(WorldGenLevel level, NoiseBasedChunkGenerator noise, RandomState state,
|
||||
WoodlandGroveGeometry.Sampler sampler, Candidate site, long seed) {
|
||||
BlockPos origin = site.floor().offset(-1, -9, -1);
|
||||
// Include the vanilla write range, its crystal neighbours and the entrance.
|
||||
// This asymmetric envelope is centred explicitly so its positive edge is protected too.
|
||||
if (origin.getY() - 6 < 24 || !level.getBlockState(site.floor().above(2)).isAir()
|
||||
|| CavernHydrologyRuntime.protects(noise, state, origin.offset(1, -6, 1), 7, 17, 0)) return false;
|
||||
Map<BlockPos, BlockState> before = new LinkedHashMap<>();
|
||||
for (int dx = -6; dx <= 8; dx++) for (int dz = -6; dz <= 8; dz++) for (int dy = -6; dy <= 11; dy++) {
|
||||
BlockPos p = origin.offset(dx, dy, dz);
|
||||
BlockState original = level.getBlockState(p);
|
||||
// No block-entity data or fluid scheduler state needs to be restored.
|
||||
if (original.hasBlockEntity() || !original.getFluidState().isEmpty()) return false;
|
||||
before.put(p, original);
|
||||
}
|
||||
Feature feature = level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(GEODE).value();
|
||||
if (!(feature instanceof GeodeFeature geode) || geode.minGenOffset() != -5 || geode.maxGenOffset() != 7) return false;
|
||||
boolean accepted = false;
|
||||
try {
|
||||
if (!geode.place(level, noise, RandomSource.create(seed), origin) || !naturalGeodeChanges(level, sampler, before)) return false;
|
||||
var entrance = geodeEntrance(level, sampler, site.floor().above(), origin, before);
|
||||
if (entrance.isEmpty()) return false;
|
||||
for (BlockPos p : entrance) if (!level.getBlockState(p).isAir()) setBlock(level, p, Blocks.AIR.defaultBlockState());
|
||||
if (!naturalGeodeChanges(level, sampler, before)) return false;
|
||||
accepted = true;
|
||||
return true;
|
||||
} finally {
|
||||
if (!accepted) before.forEach((p, original) -> {
|
||||
if (!level.getBlockState(p).equals(original)) setBlock(level, p, original);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Test the actual vanilla mask, not unused bounding-box corners. A geode may
|
||||
* excavate original rock but cannot fill original air or remove earlier decoration. */
|
||||
private static boolean naturalGeodeChanges(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler,
|
||||
Map<BlockPos, BlockState> before) {
|
||||
for (var entry : before.entrySet()) {
|
||||
BlockPos p = entry.getKey();
|
||||
BlockState original = entry.getValue(), current = level.getBlockState(p);
|
||||
if (original.equals(current)) continue;
|
||||
if (!carvableOriginal(level, sampler, p, original)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean carvableOriginal(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler,
|
||||
BlockPos p, BlockState original) {
|
||||
return original != null && original.isCollisionShapeFullBlock(level, p)
|
||||
&& !original.hasBlockEntity() && original.getFluidState().isEmpty()
|
||||
&& !original.is(BlockTags.LOGS) && !original.is(BlockTags.LEAVES)
|
||||
&& !original.is(Blocks.BUDDING_AMETHYST) && !original.is(Blocks.MAGMA_BLOCK)
|
||||
&& sampler.sample(p.getX(), p.getY(), p.getZ()) > 0;
|
||||
}
|
||||
|
||||
private static List<BlockPos> geodeEntrance(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler,
|
||||
BlockPos start, BlockPos origin, Map<BlockPos, BlockState> before) {
|
||||
var previous = new java.util.HashMap<BlockPos, BlockPos>();
|
||||
var distances = new java.util.HashMap<BlockPos, Integer>();
|
||||
var queue = new java.util.ArrayDeque<BlockPos>();
|
||||
queue.add(start); distances.put(start, 0);
|
||||
BlockPos destination = null;
|
||||
while (!queue.isEmpty()) {
|
||||
BlockPos p = queue.removeFirst();
|
||||
int distance = distances.get(p);
|
||||
boolean adjacentBudding = false;
|
||||
for (Direction direction : Direction.values())
|
||||
adjacentBudding |= level.getBlockState(p.relative(direction)).is(Blocks.BUDDING_AMETHYST)
|
||||
|| level.getBlockState(p.above().relative(direction)).is(Blocks.BUDDING_AMETHYST);
|
||||
if (p.getY() <= start.getY() - 3 && level.getBlockState(p).isAir()
|
||||
&& level.getBlockState(p.above()).isAir() && adjacentBudding) { destination = p; break; }
|
||||
if (distance >= 16) continue;
|
||||
for (Direction direction : Direction.values()) {
|
||||
BlockPos next = p.relative(direction);
|
||||
if (Math.abs(next.getX() - start.getX()) > 6 || Math.abs(next.getZ() - start.getZ()) > 6
|
||||
|| next.getY() < origin.getY() - 5 || next.getY() > start.getY()
|
||||
|| distances.containsKey(next) || !before.containsKey(next) || !before.containsKey(next.above())) continue;
|
||||
if (!entranceBlock(level, sampler, next, before) || !entranceBlock(level, sampler, next.above(), before)) continue;
|
||||
distances.put(next, distance + 1); previous.put(next, p); queue.addLast(next);
|
||||
}
|
||||
}
|
||||
if (destination == null) return List.of();
|
||||
var passage = new java.util.LinkedHashSet<BlockPos>();
|
||||
for (BlockPos p = destination; p != null; p = previous.get(p)) { passage.add(p); passage.add(p.above()); }
|
||||
return List.copyOf(passage);
|
||||
}
|
||||
|
||||
private static boolean entranceBlock(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler,
|
||||
BlockPos p, Map<BlockPos, BlockState> before) {
|
||||
BlockState current = level.getBlockState(p);
|
||||
if (current.isAir()) return true;
|
||||
// Keep every budding block and attached crystal. Search around them instead.
|
||||
if (current.is(Blocks.BUDDING_AMETHYST) || current.is(Blocks.AMETHYST_CLUSTER)
|
||||
|| current.is(Blocks.SMALL_AMETHYST_BUD) || current.is(Blocks.MEDIUM_AMETHYST_BUD)
|
||||
|| current.is(Blocks.LARGE_AMETHYST_BUD)) return false;
|
||||
for (Direction direction : Direction.values()) {
|
||||
BlockState neighbour = level.getBlockState(p.relative(direction));
|
||||
if (neighbour.getBlock() instanceof AmethystClusterBlock
|
||||
&& neighbour.getValue(AmethystClusterBlock.FACING) == direction) return false;
|
||||
}
|
||||
return carvableOriginal(level, sampler, p, before.get(p));
|
||||
}
|
||||
|
||||
private static boolean stable(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler, BlockPos p) {
|
||||
BlockState state = level.getBlockState(p);
|
||||
return sampler.sample(p.getX(), p.getY(), p.getZ()) > 0 && state.isCollisionShapeFullBlock(level, p)
|
||||
&& state.getFluidState().isEmpty() && !state.hasBlockEntity() && !state.is(BlockTags.LOGS)
|
||||
&& !state.is(BlockTags.LEAVES) && !state.is(Blocks.BUDDING_AMETHYST) && !state.is(Blocks.MAGMA_BLOCK);
|
||||
}
|
||||
|
||||
private static long chunkSeed(long seed, int x, int z, long salt) {
|
||||
return WoodlandGroveGeometry.seed(seed, x * 16, 0, z * 16, salt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.WeakHashMap;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.data.worldgen.features.TreeFeatures;
|
||||
import net.minecraft.data.worldgen.features.VegetationFeatures;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.VegetationBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.feature.Feature;
|
||||
import net.minecraft.world.level.levelgen.feature.AbstractHugeMushroomFeature;
|
||||
|
||||
/** Alpha.9: sheltered vanilla mushroom trees and dark oak on natural lower floors,
|
||||
* plus one globally selected remarkable tree.
|
||||
* Placement runs only in new generation. No world journal or mutable placement counter is retained. */
|
||||
public final class CavernGrovesFeature implements Feature {
|
||||
public static final MapCodec<CavernGrovesFeature> CODEC = MapCodec.unit(CavernGrovesFeature::new);
|
||||
private static final Map<RandomState, Optional<RemarkableTree>> REMARKABLE =
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
|
||||
public record RemarkableTree(BlockPos base, String featureId, int radius, int height) {
|
||||
public RemarkableTree { base = base.immutable(); }
|
||||
}
|
||||
private record Species(ResourceKey<Feature> key, int width, int radius, int height) {}
|
||||
private record Candidate(int x, int z, long rank) {}
|
||||
@FunctionalInterface
|
||||
private interface Protection {
|
||||
boolean intersects(BlockPos base, int radius, int height, int rootDepth);
|
||||
}
|
||||
private static final Species CHERRY = new Species(TreeFeatures.CHERRY, 1, 9, 15);
|
||||
private static final Species[] SPECIES = {CHERRY, CHERRY, CHERRY, CHERRY,
|
||||
new Species(TreeFeatures.SPRUCE, 1, 5, 15), new Species(TreeFeatures.JUNGLE_TREE_NO_VINE, 1, 5, 14),
|
||||
new Species(TreeFeatures.ACACIA, 1, 7, 14), new Species(TreeFeatures.MANGROVE, 1, 10, 20),
|
||||
new Species(TreeFeatures.PALE_OAK_BONEMEAL, 2, 6, 12)};
|
||||
|
||||
@Override public MapCodec<CavernGrovesFeature> codec() { return CODEC; }
|
||||
|
||||
public static Optional<RemarkableTree> remarkablePlan(ServerLevel level) {
|
||||
if (!(level.getChunkSource().getGenerator() instanceof NoiseBasedChunkGenerator noise)) return Optional.empty();
|
||||
return remarkablePlan(noise, level.getChunkSource().randomState());
|
||||
}
|
||||
|
||||
public static Optional<RemarkableTree> remarkablePlan(NoiseBasedChunkGenerator noise, RandomState state) {
|
||||
if (!CavernHydrologyRuntime.enabled(noise)) return Optional.empty();
|
||||
return REMARKABLE.computeIfAbsent(state, ignored -> {
|
||||
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
|
||||
WoodlandGroveGeometry.Sampler sampler = (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z);
|
||||
Protection protection = (base, radius, height, rootDepth) ->
|
||||
CavernHydrologyRuntime.protects(noise, state, base, radius, height, rootDepth);
|
||||
Species species = SPECIES[Math.floorMod(WoodlandGroveGeometry.seed(state.seed(), 0, 0, 0, 0x70EEL), SPECIES.length)];
|
||||
var candidates = new ArrayList<Candidate>();
|
||||
for (int x = -152; x <= 152; x += 16) for (int z = -152; z <= 152; z += 16) {
|
||||
if (Math.hypot(x, z) <= 168) candidates.add(new Candidate(x, z,
|
||||
WoodlandGroveGeometry.seed(state.seed(), x, 0, z, 0x71AFL)));
|
||||
}
|
||||
candidates.sort(Comparator.comparingLong(Candidate::rank).thenComparingInt(Candidate::x).thenComparingInt(Candidate::z));
|
||||
for (Candidate candidate : candidates) {
|
||||
int bed = WoodlandGroveGeometry.highestSurface(sampler, candidate.x(), candidate.z());
|
||||
if (!WoodlandGroveGeometry.naturalFootprint(sampler, candidate.x(), bed, candidate.z(), species.width())) continue;
|
||||
BlockPos base = new BlockPos(candidate.x(), bed + 1, candidate.z());
|
||||
if (blocked(protection, base, species.radius() + 1, species.height(), rootDepth(species))) continue;
|
||||
if (!WoodlandGroveGeometry.treeRoom(sampler, base.getX(), base.getY(), base.getZ(),
|
||||
species.width(), species.radius(), species.height())) continue;
|
||||
return Optional.of(new RemarkableTree(base, species.key().identifier().toString(), species.radius(), species.height()));
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource ignored, BlockPos origin) {
|
||||
if (!(generator instanceof NoiseBasedChunkGenerator noise) || !CavernHydrologyRuntime.enabled(noise)) return false;
|
||||
int minX = (origin.getX() >> 4) * 16, minZ = (origin.getZ() >> 4) * 16;
|
||||
if (Math.abs((long) minX) > 272 || Math.abs((long) minZ) > 272) return false;
|
||||
var state = level.getLevel().getChunkSource().randomState();
|
||||
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
|
||||
WoodlandGroveGeometry.Sampler sampler = (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z);
|
||||
Protection protection = (base, radius, height, rootDepth) ->
|
||||
CavernHydrologyRuntime.protects(noise, state, base, radius, height, rootDepth);
|
||||
boolean changed = false;
|
||||
int[] heights = new int[256];
|
||||
Arrays.fill(heights, Integer.MIN_VALUE);
|
||||
var remarkable = remarkablePlan(noise, state);
|
||||
if (remarkable.isPresent()) {
|
||||
var tree = remarkable.get();
|
||||
BlockPos base = tree.base();
|
||||
if ((base.getX() >> 4) == (minX >> 4) && (base.getZ() >> 4) == (minZ >> 4)) {
|
||||
for (Species species : SPECIES) if (species.key().identifier().toString().equals(tree.featureId())) {
|
||||
changed |= tree(level, generator, sampler, state.seed(), base, species, protection, 0x723BL);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// The scarce tall rooms usually go to dark oak first; one quarter of chunks
|
||||
// give mushroom trees first choice. This changes priority, never attempt budgets
|
||||
// or natural fit requirements, and uses a separate deterministic chunk seed.
|
||||
boolean mushroomsFirst = Math.floorMod(WoodlandGroveGeometry.seed(state.seed(), minX, 0, minZ, 0x8503L), 4) == 0;
|
||||
if (mushroomsFirst) changed |= mushroomTrees(level, generator, sampler, state.seed(), protection, minX, minZ, heights);
|
||||
// Four spaced origins per chunk; the 2x2 trunk and full canopy still have to fit the cave.
|
||||
for (int x = minX + 4; x < minX + 16; x += 8) for (int z = minZ + 4; z < minZ + 16; z += 8) {
|
||||
int highest = localTop(level, sampler, protection, minX, minZ, x, z, heights), placed = 0;
|
||||
for (int bed = highest - 8; bed >= 40; bed--) {
|
||||
BlockPos base = new BlockPos(x, bed + 1, z);
|
||||
if (!ground(level, base.below()) || !level.getBlockState(base).isAir()) continue;
|
||||
var biome = level.getBiome(base);
|
||||
boolean dark = biome.is(CavernIslandBiomeSource.DARK_GROVE);
|
||||
boolean bamboo = biome.is(CavernIslandBiomeSource.BAMBOO_GROVE);
|
||||
if (!dark && !bamboo) continue;
|
||||
long placementSeed = WoodlandGroveGeometry.seed(state.seed(), x, bed, z, 0x730DL);
|
||||
if (dark) {
|
||||
if (tree(level, generator, sampler, state.seed(), base,
|
||||
new Species(TreeFeatures.DARK_OAK, 2, 6, 12), protection, 0x730DL)) { changed = true; placed++; }
|
||||
} else if (Math.floorMod(placementSeed, 8) == 0) {
|
||||
if (tree(level, generator, sampler, state.seed(), base,
|
||||
new Species(TreeFeatures.JUNGLE_TREE_NO_VINE, 1, 5, 14), protection, 0x735BL)) { changed = true; placed++; }
|
||||
} else if (Math.floorMod(placementSeed, 3) == 0
|
||||
&& WoodlandGroveGeometry.naturalFootprint(sampler, x, bed, z, 1)
|
||||
&& WoodlandGroveGeometry.treeRoom(sampler, x, bed + 1, z, 1, 3, 16)
|
||||
&& !blocked(protection, base, 4, 17) && actualRoom(level, base, 1, 3, 16)) {
|
||||
setBlock(level, base.below(), Blocks.PODZOL.defaultBlockState());
|
||||
changed |= level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(VegetationFeatures.BAMBOO_NO_PODZOL)
|
||||
.value().place(level, generator, RandomSource.create(placementSeed), base);
|
||||
placed++;
|
||||
}
|
||||
if (placed >= 2) break;
|
||||
}
|
||||
}
|
||||
if (!mushroomsFirst) changed |= mushroomTrees(level, generator, sampler, state.seed(), protection, minX, minZ, heights);
|
||||
// Coherent ground patches: about 40% moss, 40% podzol, 20% mycelium.
|
||||
// Small mushrooms are accents (one opportunity per hundred columns), not the canopy.
|
||||
for (int x = minX; x < minX + 16; x++) for (int z = minZ; z < minZ + 16; z++) {
|
||||
int highest = localTop(level, sampler, protection, minX, minZ, x, z, heights), floors = 0;
|
||||
long detail = WoodlandGroveGeometry.seed(state.seed(), x, 0, z, 0x843EL);
|
||||
boolean smallMushroomPlaced = false;
|
||||
for (int bed = highest - 8; bed >= 32; bed--) {
|
||||
BlockPos floor = new BlockPos(x, bed, z), plant = floor.above();
|
||||
if (!ground(level, floor) || !level.getBlockState(plant).isAir()) continue;
|
||||
var biome = level.getBiome(plant);
|
||||
if (!biome.is(CavernIslandBiomeSource.DARK_GROVE) && !biome.is(CavernIslandBiomeSource.BAMBOO_GROVE)) continue;
|
||||
if (!WoodlandGroveGeometry.naturalFootprint(sampler, x, bed, z, 1) || blocked(protection, plant, 1, 2)) continue;
|
||||
long patch = WoodlandGroveGeometry.seed(state.seed(), Math.floorDiv(x, 7), 0, Math.floorDiv(z, 7), 0x742DL);
|
||||
int soilKind = Math.floorMod(patch, 5);
|
||||
BlockState soil = (soilKind < 2 ? Blocks.MOSS_BLOCK : soilKind < 4 ? Blocks.PODZOL : Blocks.MYCELIUM).defaultBlockState();
|
||||
setBlock(level, floor, soil);
|
||||
if (!smallMushroomPlaced && Math.floorMod(detail, 100) == 0) {
|
||||
BlockState mushroom = (Math.floorMod(detail >>> 8, 2) == 0 ? Blocks.BROWN_MUSHROOM : Blocks.RED_MUSHROOM).defaultBlockState();
|
||||
if (mushroom.canSurvive(level, plant)) {
|
||||
setBlock(level, plant, mushroom);
|
||||
smallMushroomPlaced = true;
|
||||
}
|
||||
}
|
||||
changed = true;
|
||||
if (++floors >= 3) break;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private boolean mushroomTrees(WorldGenLevel level, ChunkGenerator generator, WoodlandGroveGeometry.Sampler sampler,
|
||||
long seed, Protection protection, int minX, int minZ, int[] heights) {
|
||||
boolean changed = false;
|
||||
RandomSource mushroomSites = RandomSource.create(WoodlandGroveGeometry.seed(seed, minX, 0, minZ, 0x8501L));
|
||||
int mushroomAttempts = 1 + mushroomSites.nextInt(3);
|
||||
for (int attempt = 0; attempt < mushroomAttempts; attempt++) {
|
||||
int x = minX + 3 + mushroomSites.nextInt(10), z = minZ + 3 + mushroomSites.nextInt(10);
|
||||
int highest = localTop(level, sampler, protection, minX, minZ, x, z, heights);
|
||||
for (int bed = highest - 8; bed >= 32; bed--) {
|
||||
BlockPos base = new BlockPos(x, bed + 1, z);
|
||||
if (!ground(level, base.below()) || !level.getBlockState(base).isAir()
|
||||
|| !level.getBiome(base).is(CavernIslandBiomeSource.DARK_GROVE)) continue;
|
||||
long mushroomSeed = WoodlandGroveGeometry.seed(seed, x, bed, z, 0x8502L + attempt);
|
||||
var key = Math.floorMod(mushroomSeed, 2) == 0 ? TreeFeatures.HUGE_BROWN_MUSHROOM : TreeFeatures.HUGE_RED_MUSHROOM;
|
||||
Feature feature = level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(key).value();
|
||||
if (!(feature instanceof AbstractHugeMushroomFeature mushroom)) break;
|
||||
int height = mushroom.getTreeHeight(RandomSource.create(mushroomSeed));
|
||||
int radius = mushroom.foliageRadius();
|
||||
if (!mushroomRoom(level, sampler, protection, base, radius, height)) continue;
|
||||
BlockState originalFloor = level.getBlockState(base.below());
|
||||
setBlock(level, base.below(), (Math.floorMod(mushroomSeed, 5) == 0 ? Blocks.MYCELIUM : Blocks.PODZOL).defaultBlockState());
|
||||
// Vanilla consumes its height draw first. Replaying the same random seed
|
||||
// gives precisely the height whose natural envelope was checked above.
|
||||
boolean placed = mushroom.place(level, generator, RandomSource.create(mushroomSeed), base);
|
||||
if (!placed) setBlock(level, base.below(), originalFloor);
|
||||
changed |= placed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private boolean tree(WorldGenLevel level, ChunkGenerator generator, WoodlandGroveGeometry.Sampler sampler, long seed,
|
||||
BlockPos base, Species species, Protection protection, long salt) {
|
||||
if (blocked(protection, base, species.radius() + 1, species.height(), rootDepth(species))
|
||||
|| !WoodlandGroveGeometry.naturalFootprint(sampler, base.getX(), base.getY() - 1, base.getZ(), species.width())
|
||||
|| !actualRoom(level, base, species.width(), species.radius(), species.height())
|
||||
|| !WoodlandGroveGeometry.treeRoom(sampler, base.getX(), base.getY(), base.getZ(), species.width(), species.radius(), species.height())) return false;
|
||||
for (int dx = 0; dx < species.width(); dx++) for (int dz = 0; dz < species.width(); dz++) {
|
||||
BlockPos floor = base.offset(dx, -1, dz);
|
||||
if (!ground(level, floor)) return false;
|
||||
}
|
||||
for (int dx = 0; dx < species.width(); dx++) for (int dz = 0; dz < species.width(); dz++) {
|
||||
setBlock(level, base.offset(dx, -1, dz), Blocks.PODZOL.defaultBlockState());
|
||||
}
|
||||
return level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(species.key()).value()
|
||||
.place(level, generator, RandomSource.create(WoodlandGroveGeometry.seed(seed, base.getX(), base.getY(), base.getZ(), salt)), base);
|
||||
}
|
||||
|
||||
private static boolean actualRoom(WorldGenLevel level, BlockPos base, int width, int radius, int height) {
|
||||
for (int y = 0; y <= height; y++) {
|
||||
int from = y < 3 ? 0 : -radius, to = y < 3 ? width - 1 : radius;
|
||||
for (int dx = from; dx <= to; dx++) for (int dz = from; dz <= to; dz++) {
|
||||
BlockState current = level.getBlockState(base.offset(dx, y, dz));
|
||||
if (!current.getFluidState().isEmpty() || !(current.isAir() || current.is(BlockTags.LEAVES)
|
||||
|| current.getBlock() instanceof VegetationBlock)) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean ground(WorldGenLevel level, BlockPos floor) {
|
||||
BlockState state = level.getBlockState(floor);
|
||||
if (state.is(Blocks.DEEPSLATE)) {
|
||||
// Deep humid ledges can support organic soil. Check every tree foot rather
|
||||
// than admitting this rock globally, including for the remarkable tree.
|
||||
var biome = level.getBiome(floor.above());
|
||||
return biome.is(CavernIslandBiomeSource.DARK_GROVE) || biome.is(CavernIslandBiomeSource.BAMBOO_GROVE);
|
||||
}
|
||||
return state.is(Blocks.STONE) || state.is(Blocks.DIRT) || state.is(Blocks.GRASS_BLOCK) || state.is(Blocks.PODZOL)
|
||||
|| state.is(Blocks.MYCELIUM) || state.is(Blocks.ROOTED_DIRT) || state.is(Blocks.COARSE_DIRT)
|
||||
|| state.is(Blocks.GRANITE) || state.is(Blocks.DIORITE) || state.is(Blocks.ANDESITE) || state.is(Blocks.TUFF)
|
||||
|| state.is(Blocks.COBBLESTONE) || state.is(Blocks.MOSSY_COBBLESTONE) || state.is(Blocks.PACKED_MUD);
|
||||
}
|
||||
|
||||
private static int localTop(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler, Protection protection,
|
||||
int minX, int minZ, int x, int z, int[] heights) {
|
||||
int index = (x - minX) * 16 + z - minZ;
|
||||
if (heights[index] != Integer.MIN_VALUE) return heights[index];
|
||||
// In an untouched column the heightmap bounds the original surface cheaply.
|
||||
// A protected water/lava column may have been carved: recover its raw top
|
||||
// without depending on the current hydrology plan's representation.
|
||||
int ceiling = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z) - 1;
|
||||
if (protection.intersects(new BlockPos(x, level.getMinY(), z), 0, level.getHeight() - 1, 0))
|
||||
ceiling = level.getMaxY();
|
||||
return heights[index] = WoodlandGroveGeometry.highestSurfaceBelow(sampler, x, z, ceiling);
|
||||
}
|
||||
|
||||
private static boolean mushroomRoom(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler, Protection protection,
|
||||
BlockPos base, int radius, int height) {
|
||||
if (blocked(protection, base, radius + 1, height + 1, 4)
|
||||
|| !WoodlandGroveGeometry.naturalFootprint(sampler, base.getX(), base.getY() - 1, base.getZ(), 1)) return false;
|
||||
for (int depth = 1; depth <= 3; depth++) {
|
||||
BlockPos floor = base.below(depth);
|
||||
if (!level.getBlockState(floor).isCollisionShapeFullBlock(level, floor)) return false;
|
||||
}
|
||||
// A complete conservative column around the cap handles both brown umbrellas
|
||||
// and the lower rounded red cap. Neither natural rock nor existing logs move.
|
||||
for (int y = 0; y <= height + 1; y++) for (int dx = -radius; dx <= radius; dx++) for (int dz = -radius; dz <= radius; dz++) {
|
||||
BlockPos p = base.offset(dx, y, dz);
|
||||
BlockState current = level.getBlockState(p);
|
||||
if (sampler.sample(p.getX(), p.getY(), p.getZ()) > 0 || !current.getFluidState().isEmpty()
|
||||
|| !(current.isAir() || current.is(BlockTags.LEAVES))) return false;
|
||||
}
|
||||
// Being below another surface is not enough: the same column needs an actual
|
||||
// original rock ceiling above the whole mushroom, not an open hillside.
|
||||
for (int y = base.getY() + height + 2; y <= level.getMaxY(); y++)
|
||||
if (sampler.sample(base.getX(), y, base.getZ()) > 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int rootDepth(Species species) { return species.key().equals(TreeFeatures.MANGROVE) ? 16 : 4; }
|
||||
|
||||
private static boolean blocked(Protection protection, BlockPos base, int radius, int height) {
|
||||
return blocked(protection, base, radius, height, 4);
|
||||
}
|
||||
|
||||
private static boolean blocked(Protection protection, BlockPos base, int radius, int height, int rootDepth) {
|
||||
return protection.intersects(base, radius, height, rootDepth);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,966 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.PriorityQueue;
|
||||
import java.util.Set;
|
||||
|
||||
/** Alpha.9 river catchments, wider basins and rocky wall springs.
|
||||
* This version is separate from NaturalHydrology so previously created worlds keep their original terrain.
|
||||
* Results retain no chunks, sampler, random state or Minecraft objects. */
|
||||
public final class CavernHydrology {
|
||||
public static final int RADIUS = 192;
|
||||
public static final int MAX_CARVE = 16;
|
||||
public static final int MAX_DEPTH = 8;
|
||||
public static final int MAX_LAKE_CARVE = 12;
|
||||
public static final int MAX_RIVER_BANK_CARVE = 12;
|
||||
private static final int GRID = 8;
|
||||
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
private static final int[][] NEIGHBORS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}};
|
||||
|
||||
private CavernHydrology() {}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Sampler { float sample(int x, int y, int z); }
|
||||
|
||||
public enum Kind { POND, LAKE, RIVER, TERRACE }
|
||||
public enum ShoreMaterial { SAND, GRAVEL, CLAY, STONE, GRASS }
|
||||
public record Point(int x, int z) {}
|
||||
public record Bounds(int minX, int minZ, int maxX, int maxZ) {}
|
||||
|
||||
public record Position(int x, int y, int z) {}
|
||||
public record FlowBounds(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {}
|
||||
public record Spring(long id, Position source, Position outlet, List<Position> flowPath, FlowBounds flowBounds) {
|
||||
public Spring { flowPath = List.copyOf(flowPath); }
|
||||
}
|
||||
public record Terrace(long id, List<Long> basinIds) {
|
||||
public Terrace { basinIds = List.copyOf(basinIds); }
|
||||
}
|
||||
/** A retained upper basin supplies this outlet. toFeatureId=-1 denotes an open terminal fall. */
|
||||
public record Spill(long id, long fromFeatureId, long toFeatureId, Position source, Position outlet,
|
||||
List<Position> flowPath, FlowBounds flowBounds) {
|
||||
public Spill { flowPath = List.copyOf(flowPath); }
|
||||
}
|
||||
|
||||
/** Replaces existing solids bedY-sedimentDepth+1..bedY, with two intact natural supports below.
|
||||
* Water occupies bedY+1..waterY; everything above it through carveTop is removed. For a dry
|
||||
* terrace waterY=-1, and bedY+1..carveTop is removed (at most two original blocks). */
|
||||
public record Cell(int x, int z, int waterY, int bedY, int carveTop,
|
||||
ShoreMaterial material, long featureId, int sedimentDepth) {
|
||||
public boolean hasWater() { return waterY >= 0; }
|
||||
}
|
||||
|
||||
public record Feature(long id, Kind kind, int waterY, List<Point> path, int waterCells, Bounds bounds) {
|
||||
public Feature { path = List.copyOf(path); }
|
||||
}
|
||||
|
||||
public static final class Plan {
|
||||
private final Map<Long, Cell> cells;
|
||||
private final List<Cell> allCells;
|
||||
private final Map<Long, List<Cell>> layers;
|
||||
private final Map<Long, List<Cell>> chunks;
|
||||
private final List<Feature> features;
|
||||
private final List<Spring> springs;
|
||||
private final Map<Long, List<Spring>> springChunks;
|
||||
private final Set<Position> springFlow;
|
||||
private final int sampledColumns;
|
||||
private final long densitySamples;
|
||||
private final List<Terrace> terraces;
|
||||
private final List<Spill> spills;
|
||||
private final Set<Position> spillOpenings;
|
||||
|
||||
private Plan(Map<Long, Cell> source, List<Feature> features, List<Spring> springs, Set<Position> springFlow, int columns, long samples) {
|
||||
this(source, features, springs, springFlow, columns, samples, CavernTerraces.Result.EMPTY);
|
||||
}
|
||||
|
||||
private Plan(Map<Long, Cell> source, List<Feature> features, List<Spring> springs, Set<Position> springFlow,
|
||||
int columns, long samples, CavernTerraces.Result lower) {
|
||||
List<Cell> combined = new ArrayList<>(source.values());
|
||||
combined.addAll(lower.cells());
|
||||
List<Cell> sorted = combined.stream()
|
||||
.sorted(Comparator.comparingInt(Cell::x).thenComparingInt(Cell::z).thenComparingInt(Cell::bedY)).toList();
|
||||
allCells = List.copyOf(sorted);
|
||||
Map<Long, Cell> ordered = new LinkedHashMap<>();
|
||||
Map<Long, List<Cell>> stacked = new HashMap<>();
|
||||
Map<Long, List<Cell>> byChunk = new HashMap<>();
|
||||
for (Cell cell : sorted) {
|
||||
ordered.put(key(cell.x(), cell.z()), cell);
|
||||
stacked.computeIfAbsent(key(cell.x(), cell.z()), ignored -> new ArrayList<>()).add(cell);
|
||||
byChunk.computeIfAbsent(key(cell.x() >> 4, cell.z() >> 4), ignored -> new ArrayList<>()).add(cell);
|
||||
}
|
||||
byChunk.replaceAll((ignored, values) -> List.copyOf(values));
|
||||
cells = Collections.unmodifiableMap(ordered);
|
||||
stacked.replaceAll((ignored, values) -> List.copyOf(values));
|
||||
layers = Map.copyOf(stacked);
|
||||
chunks = Map.copyOf(byChunk);
|
||||
List<Feature> allFeatures = new ArrayList<>(features);
|
||||
allFeatures.addAll(lower.features());
|
||||
this.features = List.copyOf(allFeatures);
|
||||
this.springs = List.copyOf(springs);
|
||||
Set<Position> allFlow = new HashSet<>(springFlow);
|
||||
allFlow.addAll(lower.predictedFlow());
|
||||
this.springFlow = Set.copyOf(allFlow);
|
||||
terraces = List.copyOf(lower.terraces());
|
||||
spills = List.copyOf(lower.spills());
|
||||
spillOpenings = spills.stream().map(Spill::outlet).collect(java.util.stream.Collectors.toUnmodifiableSet());
|
||||
Map<Long, List<Spring>> groupedSprings = new HashMap<>();
|
||||
for (Spring spring : springs) groupedSprings.computeIfAbsent(
|
||||
key(spring.source().x() >> 4, spring.source().z() >> 4), ignored -> new ArrayList<>()).add(spring);
|
||||
groupedSprings.replaceAll((ignored, values) -> List.copyOf(values));
|
||||
springChunks = Map.copyOf(groupedSprings);
|
||||
sampledColumns = columns;
|
||||
densitySamples = samples;
|
||||
}
|
||||
|
||||
public Cell cellAt(int x, int z) { return cells.get(key(x, z)); }
|
||||
public List<Cell> cellsAt(int x, int z) { return layers.getOrDefault(key(x, z), List.of()); }
|
||||
/** Return the modified vertical interval, including its two unchanged support layers. */
|
||||
public Cell cellAt(int x, int y, int z) {
|
||||
for (Cell cell : cellsAt(x, z)) if (y >= cell.bedY() - cell.sedimentDepth() - 1
|
||||
&& y <= Math.max(cell.waterY(), cell.carveTop())) return cell;
|
||||
return null;
|
||||
}
|
||||
public Collection<Cell> cells() { return allCells; }
|
||||
public List<Cell> cellsInChunk(int x, int z) { return chunks.getOrDefault(key(x, z), List.of()); }
|
||||
public List<Feature> features() { return features; }
|
||||
public boolean isRiverCell(Cell cell) { return features.stream().anyMatch(f -> f.kind() == Kind.RIVER && f.id() == cell.featureId()); }
|
||||
public boolean isTerraceCell(Cell cell) { return features.stream().anyMatch(f -> f.kind() == Kind.TERRACE && f.id() == cell.featureId()); }
|
||||
public List<Terrace> terraces() { return terraces; }
|
||||
public List<Spill> spills() { return spills; }
|
||||
public boolean isSpillOpening(int x, int y, int z) { return spillOpenings.contains(new Position(x, y, z)); }
|
||||
public List<Spring> springs() { return springs; }
|
||||
public List<Spring> springsInChunk(int x, int z) { return springChunks.getOrDefault(key(x, z), List.of()); }
|
||||
/** Prediction in undecorated density, not a restriction on Minecraft fluid simulation. */
|
||||
public boolean allowsSpringFlow(int x, int y, int z) { return springFlow.contains(new Position(x, y, z)); }
|
||||
public int sampledColumns() { return sampledColumns; }
|
||||
public long densitySamples() { return densitySamples; }
|
||||
}
|
||||
|
||||
public static Plan create(long seed, Sampler sampler) {
|
||||
return new Planner(seed, sampler).build();
|
||||
}
|
||||
|
||||
private record Column(int top, int solidBottom) {}
|
||||
private record Candidate(int x, int z, double score) {}
|
||||
private record Node(int x, int z, double cost) {}
|
||||
private static final Comparator<Node> NODE_ORDER = Comparator.comparingDouble(Node::cost)
|
||||
.thenComparingInt(Node::x).thenComparingInt(Node::z);
|
||||
|
||||
private static final class Planner {
|
||||
private final long seed;
|
||||
private final Sampler sampler;
|
||||
private final Map<Long, Column> columns = new HashMap<>();
|
||||
private final Map<Long, Integer> coarse = new HashMap<>();
|
||||
private final Map<Long, Cell> cells = new HashMap<>();
|
||||
private final List<Feature> features = new ArrayList<>();
|
||||
private final List<Spring> springs = new ArrayList<>();
|
||||
private final Set<Position> springFlow = new HashSet<>();
|
||||
private long samples;
|
||||
|
||||
Planner(long seed, Sampler sampler) { this.seed = seed; this.sampler = sampler; }
|
||||
|
||||
Plan build() {
|
||||
List<Candidate> candidates = candidates();
|
||||
addRiver();
|
||||
int attempts = 0;
|
||||
for (Candidate candidate : candidates) {
|
||||
if (features.size() >= 4 || attempts++ >= 96) break;
|
||||
Point center = new Point(candidate.x(), candidate.z());
|
||||
if (nearWater(center.x(), center.z(), 38)) continue;
|
||||
Column column = column(center.x(), center.z());
|
||||
if (column.top() < 64 || column.top() - column.solidBottom() < 7) continue;
|
||||
Kind kind = features.size() < 2 ? Kind.LAKE : Kind.POND;
|
||||
int target = kind == Kind.LAKE ? 700 + (int) (random(seed, center.x(), center.z()) * 400)
|
||||
: 100 + (int) (random(seed + 1, center.x(), center.z()) * 140);
|
||||
// Prefer an existing low spot; a shallow cut is the fallback on level terrain.
|
||||
for (int level : new int[]{column.top() + 1, column.top(), column.top() - 1, column.top() - 2}) {
|
||||
Set<Long> footprint = growBasin(center, level, target);
|
||||
if (footprint == null) continue;
|
||||
long id = featureId(features.size());
|
||||
Map<Long, Cell> wet = kind == Kind.LAKE ? lakeCells(footprint, level, id) : waterCells(footprint, level, id);
|
||||
if (wet == null || !contained(wet)) continue;
|
||||
addFeature(id, kind, level, List.of(center), wet);
|
||||
break;
|
||||
}
|
||||
}
|
||||
addRiverSpring();
|
||||
addSedimentSheets();
|
||||
addLowerWallSprings();
|
||||
addSprings();
|
||||
Plan surface = new Plan(cells, features, springs, springFlow, columns.size(), samples);
|
||||
var lower = CavernTerraces.create(seed, sampler, surface);
|
||||
return new Plan(cells, features, springs, springFlow, columns.size(), samples, lower);
|
||||
}
|
||||
|
||||
/** A connected catchment is excavated only in a thick natural plateau. The route is
|
||||
* selected on the actual fissured density, not painted across missing rock. Its two
|
||||
* terminal pools share the same retained waterline; a separate rock spring feeds it. */
|
||||
private void addRiver() {
|
||||
List<Integer> heights = coarse.values().stream().filter(y -> y >= 160).sorted().toList();
|
||||
if (heights.isEmpty()) return;
|
||||
Set<Integer> levels = new java.util.LinkedHashSet<>();
|
||||
for (double quantile : new double[]{.55, .35, .75, .15, .90}) {
|
||||
int top = heights.get(Math.min(heights.size() - 1, (int) (heights.size() * quantile)));
|
||||
for (int cut : new int[]{4, 8, 0}) levels.add(top - cut);
|
||||
}
|
||||
for (int level : levels) {
|
||||
Set<Long> allowed = new HashSet<>();
|
||||
for (int x = -168; x <= 168; x += 4) for (int z = -168; z <= 168; z += 4) {
|
||||
if (!inside(x, z, 170)) continue;
|
||||
boolean safe = riverRock(x, z, level);
|
||||
for (int dx : new int[]{-3, 3}) for (int dz : new int[]{-3, 3}) {
|
||||
if (safe && !riverRock(x + dx, z + dz, level)) safe = false;
|
||||
}
|
||||
if (safe) allowed.add(key(x, z));
|
||||
}
|
||||
for (int attempt = 0; attempt < 4 && allowed.size() > 100; attempt++) {
|
||||
List<Point> route = longestRoute(allowed, level);
|
||||
if (route.size() < 36) break;
|
||||
Point first = route.getFirst(), last = route.getLast();
|
||||
if (Math.hypot(last.x() - first.x(), last.z() - first.z()) < 100) break;
|
||||
List<Point> path = smoothRoute(route);
|
||||
Set<Long> wet = new HashSet<>();
|
||||
Set<Long> bad = new HashSet<>();
|
||||
for (Point at : path) {
|
||||
int radius = field(at.x(), at.z(), 23, 8119) > .52 ? 3 : 2;
|
||||
for (int dx = -radius - 1; dx <= radius + 1; dx++) for (int dz = -radius - 1; dz <= radius + 1; dz++) {
|
||||
if (!riverRock(at.x() + dx, at.z() + dz, level)) {
|
||||
for (int gx = -1; gx <= 1; gx++) for (int gz = -1; gz <= 1; gz++)
|
||||
bad.add(key(Math.floorDiv(at.x(), 4) * 4 + gx * 4, Math.floorDiv(at.z(), 4) * 4 + gz * 4));
|
||||
}
|
||||
if (dx * dx + dz * dz <= radius * radius + 1) wet.add(key(at.x() + dx, at.z() + dz));
|
||||
}
|
||||
}
|
||||
if (!bad.isEmpty()) { allowed.removeAll(bad); continue; }
|
||||
// Both end pools are required and remain connected to the channel.
|
||||
boolean pools = true;
|
||||
for (Point endpoint : List.of(first, last)) {
|
||||
Set<Long> pool = growRiverPool(endpoint, level);
|
||||
if (pool.size() < 90) { pools = false; break; }
|
||||
wet.addAll(pool);
|
||||
}
|
||||
if (!pools) { allowed.remove(key(first.x(), first.z())); allowed.remove(key(last.x(), last.z())); continue; }
|
||||
long id = featureId(features.size());
|
||||
Map<Long, Cell> river = new HashMap<>();
|
||||
boolean safe = true;
|
||||
for (long k : wet) {
|
||||
Point at = point(k);
|
||||
Column rock = column(at.x(), at.z());
|
||||
int depth = 2;
|
||||
boolean edge = false;
|
||||
for (int[] d : CARDINALS) edge |= !wet.contains(key(at.x() + d[0], at.z() + d[1]));
|
||||
if (edge) depth = 1;
|
||||
int bed = level - depth;
|
||||
if (rock.top() < bed || rock.top() - bed > MAX_CARVE || rock.solidBottom() > bed - 6) { safe = false; break; }
|
||||
river.put(k, new Cell(at.x(), at.z(), level, bed, Math.max(level, rock.top()),
|
||||
wetMaterial(at.x(), at.z(), edge), id, 3));
|
||||
}
|
||||
if (!safe || !contained(river)) break;
|
||||
addFeature(id, Kind.RIVER, level, path, river);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Set<Long> growRiverPool(Point center, int level) {
|
||||
Set<Long> result = new HashSet<>();
|
||||
PriorityQueue<Node> queue = new PriorityQueue<>(NODE_ORDER);
|
||||
Set<Long> seen = new HashSet<>();
|
||||
queue.add(new Node(center.x(), center.z(), 0));
|
||||
while (!queue.isEmpty() && result.size() < 240) {
|
||||
Node at = queue.remove();
|
||||
if (!seen.add(key(at.x(), at.z())) || Math.hypot(at.x() - center.x(), at.z() - center.z()) > 12
|
||||
|| !riverRock(at.x(), at.z(), level)) continue;
|
||||
boolean retained = true;
|
||||
for (int[] d : CARDINALS) retained &= solid(at.x() + d[0], level, at.z() + d[1]);
|
||||
if (!retained) continue;
|
||||
result.add(key(at.x(), at.z()));
|
||||
for (int[] d : CARDINALS) {
|
||||
int x = at.x() + d[0], z = at.z() + d[1];
|
||||
queue.add(new Node(x, z, Math.hypot(x - center.x(), z - center.z())
|
||||
+ field(x, z, 8, 411) * 6 + Math.max(0, column(x,z).top() - level) * .4));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean riverRock(int x, int z, int level) {
|
||||
Column rock = column(x, z);
|
||||
return rock.top() >= level && rock.top() <= level + 12 && rock.solidBottom() <= level - 8;
|
||||
}
|
||||
|
||||
private List<Point> longestRoute(Set<Long> allowed, int level) {
|
||||
Set<Long> remaining = new HashSet<>(allowed);
|
||||
List<Point> longest = List.of();
|
||||
while (!remaining.isEmpty()) {
|
||||
long start = remaining.stream().min(Long::compare).orElseThrow();
|
||||
Map<Long, Long> first = routeTree(start, allowed);
|
||||
remaining.removeAll(first.keySet());
|
||||
if (first.size() < 36) continue;
|
||||
long far = lastKey(first);
|
||||
Map<Long, Long> second = routeTree(far, first.keySet());
|
||||
long end = lastKey(second);
|
||||
Map<Long, Long> shaped = riverRouteTree(far, end, first.keySet(), level);
|
||||
List<Point> path = new ArrayList<>();
|
||||
for (long at = end; ; at = shaped.get(at)) {
|
||||
path.add(point(at));
|
||||
if (at == far) break;
|
||||
}
|
||||
if (path.size() > longest.size()) longest = path;
|
||||
}
|
||||
return longest;
|
||||
}
|
||||
|
||||
private Map<Long, Long> riverRouteTree(long start, long end, Set<Long> allowed, int level) {
|
||||
Map<Long, Long> parent = new HashMap<>();
|
||||
Map<Long, Double> costs = new HashMap<>();
|
||||
PriorityQueue<Node> queue = new PriorityQueue<>(NODE_ORDER);
|
||||
Point origin = point(start);
|
||||
parent.put(start, start); costs.put(start, 0.0); queue.add(new Node(origin.x(), origin.z(), 0));
|
||||
while (!queue.isEmpty()) {
|
||||
Node at = queue.remove();
|
||||
long current = key(at.x(), at.z());
|
||||
if (at.cost() > costs.getOrDefault(current, Double.POSITIVE_INFINITY)) continue;
|
||||
if (current == end) break;
|
||||
for (int[] d : NEIGHBORS) {
|
||||
int x = at.x() + d[0] * 4, z = at.z() + d[1] * 4;
|
||||
long next = key(x, z);
|
||||
if (!allowed.contains(next)) continue;
|
||||
double cost = at.cost() + Math.hypot(d[0], d[1]) * (1 + Math.max(0, column(x,z).top() - level) * .14
|
||||
+ field(x, z, 27, 8113) * 1.8);
|
||||
if (cost >= costs.getOrDefault(next, Double.POSITIVE_INFINITY)) continue;
|
||||
parent.put(next, current); costs.put(next, cost); queue.add(new Node(x,z,cost));
|
||||
}
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
|
||||
private List<Point> smoothRoute(List<Point> coarsePath) {
|
||||
record Smooth(double x, double z) {}
|
||||
List<Smooth> values = coarsePath.stream().map(p -> new Smooth(p.x(), p.z())).toList();
|
||||
// Bend over several grid steps rather than rounding each voxel-sized corner.
|
||||
for (int pass = 0; pass < 2; pass++) {
|
||||
List<Smooth> next = new ArrayList<>();
|
||||
for (int i = 0; i < values.size(); i++) {
|
||||
if (i == 0 || i == values.size() - 1) { next.add(values.get(i)); continue; }
|
||||
double x = 0, z = 0, sum = 0;
|
||||
for (int offset = -4; offset <= 4; offset++) {
|
||||
int weight = 5 - Math.abs(offset);
|
||||
Smooth p = values.get(Math.clamp(i + offset, 0, values.size() - 1));
|
||||
x += p.x() * weight; z += p.z() * weight; sum += weight;
|
||||
}
|
||||
next.add(new Smooth(x / sum, z / sum));
|
||||
}
|
||||
values = next;
|
||||
}
|
||||
for (int pass = 0; pass < 2; pass++) {
|
||||
List<Smooth> next = new ArrayList<>(); next.add(values.getFirst());
|
||||
for (int i = 1; i < values.size(); i++) {
|
||||
Smooth a = values.get(i - 1), b = values.get(i);
|
||||
next.add(new Smooth(a.x() * .75 + b.x() * .25, a.z() * .75 + b.z() * .25));
|
||||
next.add(new Smooth(a.x() * .25 + b.x() * .75, a.z() * .25 + b.z() * .75));
|
||||
}
|
||||
next.add(values.getLast()); values = next;
|
||||
}
|
||||
List<Point> path = new ArrayList<>();
|
||||
for (int i = 1; i < values.size(); i++) {
|
||||
Smooth a = values.get(i - 1), b = values.get(i);
|
||||
int steps = Math.max(1, (int) Math.ceil(Math.max(Math.abs(b.x() - a.x()), Math.abs(b.z() - a.z()))));
|
||||
for (int step = 0; step <= steps; step++) {
|
||||
double t = step / (double) steps;
|
||||
Point p = new Point((int) Math.round(a.x() + (b.x() - a.x()) * t), (int) Math.round(a.z() + (b.z() - a.z()) * t));
|
||||
if (path.isEmpty() || !path.getLast().equals(p)) path.add(p);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private Map<Long, Long> routeTree(long start, Set<Long> allowed) {
|
||||
Map<Long, Long> parent = new LinkedHashMap<>();
|
||||
ArrayDeque<Long> queue = new ArrayDeque<>();
|
||||
parent.put(start, start); queue.add(start);
|
||||
while (!queue.isEmpty()) {
|
||||
long current = queue.removeFirst();
|
||||
Point at = point(current);
|
||||
for (int[] d : NEIGHBORS) {
|
||||
long next = key(at.x() + d[0] * 4, at.z() + d[1] * 4);
|
||||
if (allowed.contains(next) && !parent.containsKey(next)) { parent.put(next, current); queue.addLast(next); }
|
||||
}
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
|
||||
private long lastKey(Map<Long, Long> values) {
|
||||
long result = 0;
|
||||
for (long key : values.keySet()) result = key;
|
||||
return result;
|
||||
}
|
||||
|
||||
/** A real spring emerges from one of the river's taller rock banks and drops into the
|
||||
* connected retained channel. The source replaces rock; its mouth is the carved valley. */
|
||||
private void addRiverSpring() {
|
||||
Feature river = features.stream().filter(f -> f.kind() == Kind.RIVER).findFirst().orElse(null);
|
||||
if (river == null) return;
|
||||
List<Cell> bank = cells.values().stream().filter(c -> c.featureId() == river.id())
|
||||
.sorted(Comparator.comparingInt(Cell::carveTop).reversed().thenComparingInt(Cell::x).thenComparingInt(Cell::z)).toList();
|
||||
for (Cell wet : bank) for (int[] d : CARDINALS) {
|
||||
int x = wet.x() + d[0], z = wet.z() + d[1];
|
||||
if (cells.containsKey(key(x, z))) continue;
|
||||
for (int height : new int[]{8, 7, 6, 5}) {
|
||||
int y = wet.waterY() + height;
|
||||
if (wet.carveTop() < y) continue;
|
||||
Position source = new Position(x, y, z), outlet = new Position(wet.x(), y, wet.z());
|
||||
boolean safe = true;
|
||||
for (int sy = y - 3; sy <= y + 1; sy++) safe &= flowSolid(new Position(x, sy, z), outlet);
|
||||
for (int[] side : CARDINALS) {
|
||||
if (side[0] == -d[0] && side[1] == -d[1]) continue;
|
||||
safe &= flowSolid(new Position(x + side[0], y, z + side[1]), outlet);
|
||||
}
|
||||
if (!safe) continue;
|
||||
Flow flow = traceSpring(source, outlet);
|
||||
if (flow == null || flow.volume().stream().noneMatch(p -> {
|
||||
Cell c = cells.get(key(p.x(), p.z()));
|
||||
return c != null && c.featureId() == river.id() && p.y() <= c.waterY() && p.y() > c.bedY();
|
||||
})) continue;
|
||||
long id = mix(seed ^ 0xA81BE2L);
|
||||
springs.add(new Spring(id, source, outlet, flow.path(), flow.bounds()));
|
||||
springFlow.addAll(flow.volume());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<Candidate> candidates() {
|
||||
for (int x = -RADIUS; x <= RADIUS; x += GRID) {
|
||||
for (int z = -RADIUS; z <= RADIUS; z += GRID) {
|
||||
if (inside(x, z, RADIUS)) coarse.put(key(x, z), coarseTop(x, z));
|
||||
}
|
||||
}
|
||||
List<Candidate> result = new ArrayList<>();
|
||||
for (int x = -RADIUS + GRID; x < RADIUS; x += GRID) {
|
||||
for (int z = -RADIUS + GRID; z < RADIUS; z += GRID) {
|
||||
int height = coarse.getOrDefault(key(x, z), -1);
|
||||
if (height < 64 || !inside(x, z, RADIUS - 24)) continue;
|
||||
double average = 0;
|
||||
boolean safe = true;
|
||||
for (int[] d : CARDINALS) {
|
||||
int neighbor = coarse.getOrDefault(key(x + d[0] * GRID, z + d[1] * GRID), -1);
|
||||
if (neighbor < 64 || Math.abs(neighbor - height) > 16) safe = false;
|
||||
average += neighbor * 0.25;
|
||||
}
|
||||
if (safe) result.add(new Candidate(x, z,
|
||||
(average - height) * 2.0 + random(seed + 2, x, z) * 3.0));
|
||||
}
|
||||
}
|
||||
result.sort(Comparator.comparingDouble(Candidate::score).reversed()
|
||||
.thenComparingInt(Candidate::x).thenComparingInt(Candidate::z));
|
||||
return result;
|
||||
}
|
||||
|
||||
private int coarseTop(int x, int z) {
|
||||
for (int y = 383; y >= 32; y -= 4) if (solid(x, y, z)) return y;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private Column column(int x, int z) {
|
||||
long key = key(x, z);
|
||||
Column old = columns.get(key);
|
||||
if (old != null) return old;
|
||||
int top = -1;
|
||||
// Exact scan, including everything above the planned water: a cave roof cannot be
|
||||
// mistaken for a surface. Only candidates and their nearby shore need this refinement.
|
||||
for (int y = 383; y >= 24; y--) {
|
||||
if (solid(x, y, z)) { top = y; break; }
|
||||
}
|
||||
int bottom = top;
|
||||
if (top >= 0) {
|
||||
while (bottom > Math.max(16, top - 40) && solid(x, bottom - 1, z)) bottom--;
|
||||
}
|
||||
Column created = new Column(top, bottom);
|
||||
columns.put(key, created);
|
||||
return created;
|
||||
}
|
||||
|
||||
private boolean eligible(int x, int z, int level) {
|
||||
if (level < 32 || level >= 383 || !inside(x, z, RADIUS - 2)) return false;
|
||||
Cell old = cells.get(key(x, z));
|
||||
if (old != null && old.hasWater() && old.waterY() != level) return false;
|
||||
Column column = column(x, z);
|
||||
return column.top() >= level - 3 && column.top() < level + 6
|
||||
&& column.solidBottom() <= Math.min(column.top(), level - 1) - 4;
|
||||
}
|
||||
|
||||
private Set<Long> growBasin(Point center, int level, int target) {
|
||||
if (!eligible(center.x(), center.z(), level)) return null;
|
||||
Set<Long> wet = new HashSet<>();
|
||||
Map<Long, Double> costs = new HashMap<>();
|
||||
PriorityQueue<Node> queue = new PriorityQueue<>(NODE_ORDER);
|
||||
queue.add(new Node(center.x(), center.z(), 0));
|
||||
costs.put(key(center.x(), center.z()), 0.0);
|
||||
while (!queue.isEmpty() && wet.size() < target) {
|
||||
Node current = queue.remove();
|
||||
if (current.cost() > costs.getOrDefault(key(current.x(), current.z()), Double.POSITIVE_INFINITY)) continue;
|
||||
if (!eligible(current.x(), current.z(), level)) continue;
|
||||
wet.add(key(current.x(), current.z()));
|
||||
for (int[] d : NEIGHBORS) {
|
||||
int x = current.x() + d[0], z = current.z() + d[1];
|
||||
if (Math.hypot(x - center.x(), z - center.z()) > 40 || wet.contains(key(x, z))) continue;
|
||||
if (!eligible(x, z, level)) continue;
|
||||
double step = Math.hypot(d[0], d[1]);
|
||||
double cost = current.cost() + step * (1 + Math.max(0, column(x, z).top() - level) * 0.8
|
||||
+ terrainNoise(x, z) * 4.5 + field(x + 71, z - 93, 21, 107) * 3.5);
|
||||
if (cost >= costs.getOrDefault(key(x, z), Double.POSITIVE_INFINITY)) continue;
|
||||
costs.put(key(x, z), cost);
|
||||
queue.add(new Node(x, z, cost));
|
||||
}
|
||||
}
|
||||
wet = connectedTo(wet, key(center.x(), center.z()));
|
||||
if (wet.size() < Math.min(target, 30)) return null;
|
||||
// Filling a hollow requires every lower connected outlet to be included. Abort rather
|
||||
// than place a dam if this natural basin runs to a cliff, cave or an oversized catchment.
|
||||
List<Long> work = new ArrayList<>(wet);
|
||||
for (int i = 0; i < work.size(); i++) {
|
||||
Point point = point(work.get(i));
|
||||
for (int[] d : CARDINALS) {
|
||||
int x = point.x() + d[0], z = point.z() + d[1];
|
||||
long neighbor = key(x, z);
|
||||
if (wet.contains(neighbor) || column(x, z).top() >= level) continue;
|
||||
if (wet.size() >= 1200 || Math.hypot(x - center.x(), z - center.z()) > 48
|
||||
|| !eligible(x, z, level)) return null;
|
||||
wet.add(neighbor);
|
||||
work.add(neighbor);
|
||||
}
|
||||
}
|
||||
return wet;
|
||||
}
|
||||
|
||||
private Map<Long, Cell> waterCells(Set<Long> footprint, int level, long id) {
|
||||
Map<Long, Cell> result = new HashMap<>();
|
||||
for (long key : footprint) {
|
||||
Point p = point(key);
|
||||
if (!eligible(p.x(), p.z(), level)) return null;
|
||||
boolean edge = false;
|
||||
for (int[] d : CARDINALS) edge |= !footprint.contains(key(p.x() + d[0], p.z() + d[1]));
|
||||
Column column = column(p.x(), p.z());
|
||||
int depth = Math.max(level - column.top() + 1, edge ? 1 : 2 + (terrainNoise(p.x(), p.z()) > 0.55 ? 1 : 0));
|
||||
depth = Math.min(depth, 8 - Math.max(0, column.top() - level));
|
||||
if (depth < 1 || depth > MAX_DEPTH) return null;
|
||||
int bed = level - depth;
|
||||
if (column.top() < bed || column.solidBottom() > bed - 2) return null;
|
||||
ShoreMaterial material = wetMaterial(p.x(), p.z(), edge);
|
||||
int sedimentDepth = sedimentDepth(p.x(), p.z());
|
||||
if (column.solidBottom() > bed - sedimentDepth - 1) return null;
|
||||
result.put(key, new Cell(p.x(), p.z(), level, bed, Math.max(level, column.top()),
|
||||
material, id, sedimentDepth));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Deepen only sufficiently thick interiors, with one-block shore steps. Natural weak
|
||||
* columns keep a shallower bed instead of receiving a manufactured lake foundation. */
|
||||
private Map<Long, Cell> lakeCells(Set<Long> footprint, int level, long id) {
|
||||
Map<Long, Integer> distance = new HashMap<>();
|
||||
ArrayDeque<Long> queue = new ArrayDeque<>();
|
||||
for (long k : footprint) {
|
||||
Point at = point(k);
|
||||
for (int[] d : CARDINALS) if (!footprint.contains(key(at.x() + d[0], at.z() + d[1]))) {
|
||||
distance.put(k, 0); queue.addLast(k); break;
|
||||
}
|
||||
}
|
||||
while (!queue.isEmpty()) {
|
||||
long k = queue.removeFirst(); Point at = point(k);
|
||||
for (int[] d : CARDINALS) {
|
||||
long next = key(at.x() + d[0], at.z() + d[1]);
|
||||
if (footprint.contains(next) && !distance.containsKey(next)) {
|
||||
distance.put(next, distance.get(k) + 1); queue.addLast(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
Map<Long, Cell> result = new HashMap<>();
|
||||
for (long k : footprint) {
|
||||
Point at = point(k);
|
||||
if (!eligible(at.x(), at.z(), level)) return null;
|
||||
Column rock = column(at.x(), at.z());
|
||||
int sediment = sedimentDepth(at.x(), at.z());
|
||||
int wanted = Math.max(level - rock.top() + 1, Math.min(MAX_DEPTH, 1 + distance.get(k)));
|
||||
int depth = Math.min(wanted, Math.min(level - rock.solidBottom() - sediment - 1,
|
||||
MAX_LAKE_CARVE - Math.max(0, rock.top() - level)));
|
||||
if (depth < 1 || rock.top() < level - depth) return null;
|
||||
int bed = level - depth;
|
||||
result.put(k, new Cell(at.x(), at.z(), level, bed, Math.max(level, rock.top()),
|
||||
wetMaterial(at.x(), at.z(), distance.get(k) == 0), id, sediment));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean contained(Map<Long, Cell> wet) {
|
||||
for (Cell cell : wet.values()) {
|
||||
for (int y = cell.bedY() - 2; y <= cell.bedY(); y++) {
|
||||
if (!solid(cell.x(), y, cell.z())) return false;
|
||||
}
|
||||
for (int[] d : CARDINALS) {
|
||||
int x = cell.x() + d[0], z = cell.z() + d[1];
|
||||
Cell neighbor = wet.get(key(x, z));
|
||||
if (neighbor == null) neighbor = cells.get(key(x, z));
|
||||
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) {
|
||||
if (neighbor != null && neighbor.hasWater()) {
|
||||
if (neighbor.waterY() != cell.waterY()) return false;
|
||||
if (y > neighbor.bedY()) continue;
|
||||
}
|
||||
if (!solid(x, y, z)) return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void addFeature(long id, Kind kind, int level, List<Point> path, Map<Long, Cell> wet) {
|
||||
int minX = Integer.MAX_VALUE, minZ = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE, maxZ = Integer.MIN_VALUE;
|
||||
int count = 0;
|
||||
for (Cell cell : wet.values()) {
|
||||
Cell old = cells.get(key(cell.x(), cell.z()));
|
||||
if (old == null || !old.hasWater()) { cells.put(key(cell.x(), cell.z()), cell); count++; }
|
||||
minX = Math.min(minX, cell.x()); minZ = Math.min(minZ, cell.z());
|
||||
maxX = Math.max(maxX, cell.x()); maxZ = Math.max(maxZ, cell.z());
|
||||
}
|
||||
features.add(new Feature(id, kind, level, path, count, new Bounds(minX, minZ, maxX, maxZ)));
|
||||
}
|
||||
|
||||
private static Set<Long> connectedTo(Set<Long> footprint, long start) {
|
||||
Set<Long> connected = new HashSet<>();
|
||||
List<Long> work = new ArrayList<>();
|
||||
connected.add(start); work.add(start);
|
||||
for (int i = 0; i < work.size(); i++) {
|
||||
Point point = point(work.get(i));
|
||||
for (int[] d : CARDINALS) {
|
||||
long next = key(point.x() + d[0], point.z() + d[1]);
|
||||
if (footprint.contains(next) && connected.add(next)) work.add(next);
|
||||
}
|
||||
}
|
||||
return connected;
|
||||
}
|
||||
|
||||
private record ShoreNode(int x, int z, int level, long feature, double cost) {}
|
||||
|
||||
/** Sediment follows shallow topography in broad, discontinuous sheets. A second nearby
|
||||
* pocket can share the same sheet; the result is not a one-block material ring. */
|
||||
private void addSedimentSheets() {
|
||||
Comparator<ShoreNode> order = Comparator.comparingDouble(ShoreNode::cost)
|
||||
.thenComparingInt(ShoreNode::x).thenComparingInt(ShoreNode::z).thenComparingLong(ShoreNode::feature);
|
||||
Set<Long> rivers = features.stream().filter(f -> f.kind() == Kind.RIVER).map(Feature::id)
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
PriorityQueue<ShoreNode> queue = new PriorityQueue<>(order);
|
||||
Map<Long, Double> best = new HashMap<>();
|
||||
for (Cell cell : cells.values()) if (cell.hasWater()) {
|
||||
queue.add(new ShoreNode(cell.x(), cell.z(), cell.waterY(), cell.featureId(), 0));
|
||||
best.put(key(cell.x(), cell.z()), 0.0);
|
||||
}
|
||||
while (!queue.isEmpty()) {
|
||||
ShoreNode node = queue.remove();
|
||||
if (node.cost() > best.getOrDefault(key(node.x(), node.z()), Double.POSITIVE_INFINITY)) continue;
|
||||
boolean riverBank = rivers.contains(node.feature());
|
||||
Column column = column(node.x(), node.z());
|
||||
Cell existing = cells.get(key(node.x(), node.z()));
|
||||
if (existing == null) {
|
||||
boolean springRock = springs.stream().anyMatch(spring ->
|
||||
Math.abs(spring.source().x() - node.x()) <= 4 && Math.abs(spring.source().z() - node.z()) <= 4);
|
||||
if (springRock) continue;
|
||||
double extent = riverBank ? 18 + field(node.x(), node.z(), 25, 131) * 6
|
||||
: 9 + field(node.x(), node.z(), 25, 131) * 10;
|
||||
if (node.cost() > extent) continue;
|
||||
int delta = column.top() - node.level();
|
||||
if (delta < 0 || delta > (riverBank ? 14 : 5)) continue;
|
||||
// Low shore: waterline, then one-block steps. Cut at most two existing blocks;
|
||||
// far deposits keep the original landform and feather back into grass/stone.
|
||||
int terrace = node.level() + (int) Math.floor(node.cost() / (riverBank ? 2.5 : 4.5));
|
||||
int cut = riverBank ? MAX_RIVER_BANK_CARVE : 2;
|
||||
int bed = Math.max(node.level(), Math.max(column.top() - cut, Math.min(column.top(), terrace)));
|
||||
int depth = sedimentDepth(node.x(), node.z());
|
||||
if (column.solidBottom() > bed - depth - 1) continue;
|
||||
boolean safe = true;
|
||||
for (int[] d : CARDINALS) {
|
||||
Cell next = cells.get(key(node.x() + d[0], node.z() + d[1]));
|
||||
if (next != null && next.hasWater() && bed < next.waterY()) safe = false;
|
||||
}
|
||||
if (!safe) continue;
|
||||
ShoreMaterial material = dryMaterial(node.x(), node.z(), node.cost() / extent);
|
||||
cells.put(key(node.x(), node.z()), new Cell(node.x(), node.z(), -1, bed,
|
||||
column.top(), material, node.feature(), depth));
|
||||
}
|
||||
for (int[] d : NEIGHBORS) {
|
||||
int x = node.x() + d[0], z = node.z() + d[1];
|
||||
if (!inside(x, z, RADIUS - 2)) continue;
|
||||
Cell water = cells.get(key(x, z));
|
||||
if (water != null && water.hasWater()) continue;
|
||||
Column next = column(x, z);
|
||||
if (next.top() < node.level() || next.top() > node.level() + (riverBank ? 14 : 5)) continue;
|
||||
double cost = node.cost() + Math.hypot(d[0], d[1])
|
||||
* (0.7 + Math.abs(next.top() - column.top()) * 0.32 + field(x, z, 12, 211) * 0.6);
|
||||
if (cost > (riverBank ? 24 : 19) || cost >= best.getOrDefault(key(x, z), Double.POSITIVE_INFINITY)) continue;
|
||||
best.put(key(x, z), cost);
|
||||
queue.add(new ShoreNode(x, z, node.level(), node.feature(), cost));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int sedimentDepth(int x, int z) { return 3 + (int) (field(x, z, 15, 319) * 2.999); }
|
||||
|
||||
private ShoreMaterial wetMaterial(int x, int z, boolean edge) {
|
||||
double geology = field(x, z, 24, 503) * 0.75 + field(x, z, 7, 509) * 0.25;
|
||||
if (geology < 0.46) return ShoreMaterial.STONE;
|
||||
if (geology < 0.65) return ShoreMaterial.GRAVEL;
|
||||
if (!edge && geology > 0.78) return ShoreMaterial.CLAY;
|
||||
return ShoreMaterial.SAND;
|
||||
}
|
||||
|
||||
private ShoreMaterial dryMaterial(int x, int z, double distance) {
|
||||
double geology = field(x, z, 24, 503) * 0.75 + field(x, z, 7, 509) * 0.25;
|
||||
double grass = field(x + 103, z - 137, 19, 521);
|
||||
if (grass > 0.62 || distance > 0.60 + grass * 0.65) return ShoreMaterial.GRASS;
|
||||
if (geology < 0.46) return ShoreMaterial.STONE;
|
||||
if (geology < 0.65) return ShoreMaterial.GRAVEL;
|
||||
return ShoreMaterial.SAND;
|
||||
}
|
||||
|
||||
private record Cliff(int x, int z, int dx, int dz, double score) {}
|
||||
private record FlowNode(Position position, int reach) {}
|
||||
private record Flow(Set<Position> volume, List<Position> path, FlowBounds bounds) {}
|
||||
|
||||
/** Pick rare exposed rock niches at the edges of real upper surfaces. The source replaces
|
||||
* one rock block under an existing roof. Its outlet is already air over an eight-block drop;
|
||||
* no source, shelf, dam, rock column or ocean is ever placed in empty terrain. */
|
||||
private void addSprings() {
|
||||
List<Cliff> cliffs = new ArrayList<>();
|
||||
for (int x = -264; x <= 264; x += GRID) for (int z = -264; z <= 264; z += GRID) {
|
||||
if (!inside(x, z, 264)) continue;
|
||||
int top = coarse.computeIfAbsent(key(x, z), ignored -> coarseTop(point(ignored).x(), point(ignored).z()));
|
||||
if (top < 88) continue;
|
||||
for (int[] d : CARDINALS) {
|
||||
int nx = x + d[0] * GRID, nz = z + d[1] * GRID;
|
||||
int lower = coarse.computeIfAbsent(key(nx, nz), ignored -> coarseTop(point(ignored).x(), point(ignored).z()));
|
||||
if (top - lower < 16) continue;
|
||||
cliffs.add(new Cliff(x, z, d[0], d[1],
|
||||
Math.min(64, top - lower) * 0.06 + random(seed + 811, x + d[0], z + d[1]) * 3));
|
||||
}
|
||||
}
|
||||
cliffs.sort(Comparator.comparingDouble(Cliff::score).reversed()
|
||||
.thenComparingInt(Cliff::x).thenComparingInt(Cliff::z)
|
||||
.thenComparingInt(Cliff::dx).thenComparingInt(Cliff::dz));
|
||||
int attempts = 0;
|
||||
for (Cliff cliff : cliffs) {
|
||||
if (springs.size() >= 3 || attempts++ >= 160) break;
|
||||
boolean placed = false;
|
||||
for (int tangent : new int[]{0, -2, 2, -4, 4}) {
|
||||
for (int along = 0; along < GRID; along++) {
|
||||
int x = cliff.x() + cliff.dx() * along - cliff.dz() * tangent;
|
||||
int z = cliff.z() + cliff.dz() * along + cliff.dx() * tangent;
|
||||
if (!inside(x, z, 266)) continue;
|
||||
int ox = x + cliff.dx(), oz = z + cliff.dz();
|
||||
Column rock = column(x, z), outletColumn = column(ox, oz);
|
||||
if (rock.top() < 80 || rock.top() - outletColumn.top() < 14) continue;
|
||||
// Five blocks below the highest natural surface is below the soil cap.
|
||||
int floor = Math.max(64, Math.max(rock.top() - 20, outletColumn.top() + 8));
|
||||
for (int y = rock.top() - 5; y >= floor; y -= 3) {
|
||||
Position source = new Position(x, y, z), outlet = new Position(ox, y, oz);
|
||||
if (!separatedSpring(source) || !springNiche(source, cliff.dx(), cliff.dz()) || nearModifiedGround(source, 32)) continue;
|
||||
Flow flow = traceSpring(source, outlet);
|
||||
if (flow == null || flow.bounds().minY() > y - 12) continue;
|
||||
long id = mix(seed ^ (0x5F81A6L + springs.size() * 0x9E3779B97F4A7C15L));
|
||||
springs.add(new Spring(id, source, outlet, flow.path(), flow.bounds()));
|
||||
springFlow.addAll(flow.volume());
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
if (placed) break;
|
||||
}
|
||||
if (placed) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean nearModifiedGround(Position source, int distance) {
|
||||
for (Cell cell : cells.values()) {
|
||||
int dy = Math.max(Math.max(cell.bedY() - cell.sedimentDepth() - source.y(), 0),
|
||||
source.y() - cell.carveTop());
|
||||
long dx = source.x() - cell.x(), dz = source.z() - cell.z();
|
||||
if (dx * dx + dz * dz + (long) dy * dy < (long) distance * distance) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean separatedSpring(Position source) {
|
||||
for (Spring spring : springs) {
|
||||
double horizontal = Math.hypot(source.x() - spring.source().x(), source.z() - spring.source().z());
|
||||
int dy = Math.abs(source.y() - spring.source().y());
|
||||
if (Math.hypot(horizontal, dy) < 64 || dy < 12) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private record Wall(int x, int y, int z, int dx, int dz, double score) {}
|
||||
|
||||
/** Probe a few exposed faces below the highest surface as well. This finds overhangs and
|
||||
* lower strata that a highest-column heightmap cannot represent, without inventing a ledge. */
|
||||
private void addLowerWallSprings() {
|
||||
List<Wall> candidates = new ArrayList<>();
|
||||
for (int x = -240; x <= 240; x += 16) for (int z = -240; z <= 240; z += 16) {
|
||||
if (!inside(x, z, 240)) continue;
|
||||
int top = coarse.computeIfAbsent(key(x, z), ignored -> coarseTop(point(ignored).x(), point(ignored).z()));
|
||||
for (int y = top - 32; y >= 64; y -= 16) {
|
||||
if (!solid(x, y, z)) continue;
|
||||
for (int[] direction : CARDINALS) {
|
||||
if (solid(x + direction[0] * 4, y, z + direction[1] * 4)) continue;
|
||||
candidates.add(new Wall(x, y, z, direction[0], direction[1],
|
||||
random(seed + 1217 + y, x + direction[0], z + direction[1]) * 3
|
||||
+ Math.min(100, top - y) * 0.015));
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates.sort(Comparator.comparingDouble(Wall::score).reversed()
|
||||
.thenComparingInt(Wall::x).thenComparingInt(Wall::z).thenComparingInt(Wall::y)
|
||||
.thenComparingInt(Wall::dx).thenComparingInt(Wall::dz));
|
||||
int attempts = 0;
|
||||
for (Wall candidate : candidates) {
|
||||
if (springs.size() >= 2 || attempts++ >= 96) break;
|
||||
boolean placed = false;
|
||||
for (int tangent : new int[]{0, -2, 2}) {
|
||||
for (int along = 0; along < 4; along++) {
|
||||
int x = candidate.x() + candidate.dx() * along - candidate.dz() * tangent;
|
||||
int z = candidate.z() + candidate.dz() * along + candidate.dx() * tangent;
|
||||
for (int dy : new int[]{0, 3, -3, 6, -6}) {
|
||||
int y = candidate.y() + dy;
|
||||
if (y < 56 || column(x, z).top() - y < 24) continue;
|
||||
Position source = new Position(x, y, z);
|
||||
if (!separatedSpring(source) || !springNiche(source, candidate.dx(), candidate.dz())
|
||||
|| nearModifiedGround(source, 32)) continue;
|
||||
Position outlet = new Position(x + candidate.dx(), y, z + candidate.dz());
|
||||
Flow flow = traceSpring(source, outlet);
|
||||
if (flow == null || flow.bounds().minY() > y - 12) continue;
|
||||
long id = mix(seed ^ (0x5F81A6L + springs.size() * 0x9E3779B97F4A7C15L));
|
||||
springs.add(new Spring(id, source, outlet, flow.path(), flow.bounds()));
|
||||
springFlow.addAll(flow.volume());
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
if (placed) break;
|
||||
}
|
||||
if (placed) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean springNiche(Position source, int dx, int dz) {
|
||||
for (int y = source.y() - 3; y <= source.y() + 1; y++) {
|
||||
if (!solid(source.x(), y, source.z())) return false;
|
||||
}
|
||||
for (int[] d : CARDINALS) {
|
||||
if (d[0] == dx && d[1] == dz) continue;
|
||||
if (!solid(source.x() + d[0], source.y(), source.z() + d[1])) return false;
|
||||
}
|
||||
for (int y = source.y(); y >= source.y() - 8; y--) {
|
||||
if (solid(source.x() + dx, y, source.z() + dz)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Predict drainage through undecorated rock to select springs and inspection bounds.
|
||||
* Descend through air first, then explore seven horizontal steps on a shelf. Later trees
|
||||
* can divert the actual fluid: this prediction is not a bound on decorated-world physics.
|
||||
* Integration tests trace actual water back to its source within the inspected chunk halo. */
|
||||
private Flow traceSpring(Position source, Position outlet) {
|
||||
Map<Position, Integer> reach = new HashMap<>();
|
||||
Map<Position, Position> parent = new HashMap<>();
|
||||
ArrayDeque<FlowNode> queue = new ArrayDeque<>();
|
||||
reach.put(source, 7);
|
||||
queue.add(new FlowNode(source, 7));
|
||||
Position lowest = source;
|
||||
while (!queue.isEmpty()) {
|
||||
FlowNode node = queue.removeFirst();
|
||||
Position at = node.position();
|
||||
if (node.reach() < reach.getOrDefault(at, -1)) continue;
|
||||
if (at.y() < lowest.y()) lowest = at;
|
||||
if (reach.size() > 24000) return null;
|
||||
if (at.y() <= 0) continue;
|
||||
Position below = new Position(at.x(), at.y() - 1, at.z());
|
||||
if (!flowSolid(below, source)) {
|
||||
enqueueFlow(below, 7, at, queue, reach, parent);
|
||||
continue;
|
||||
}
|
||||
if (node.reach() <= 0) continue;
|
||||
for (int[] d : CARDINALS) {
|
||||
Position next = new Position(at.x() + d[0], at.y(), at.z() + d[1]);
|
||||
if (flowSolid(next, source)) continue;
|
||||
enqueueFlow(next, node.reach() - 1, at, queue, reach, parent);
|
||||
}
|
||||
}
|
||||
if (!reach.containsKey(outlet)) return null;
|
||||
int minX = source.x(), maxX = minX, minY = source.y(), maxY = minY;
|
||||
int minZ = source.z(), maxZ = minZ;
|
||||
for (Position position : reach.keySet()) {
|
||||
minX = Math.min(minX, position.x()); maxX = Math.max(maxX, position.x());
|
||||
minY = Math.min(minY, position.y()); maxY = Math.max(maxY, position.y());
|
||||
minZ = Math.min(minZ, position.z()); maxZ = Math.max(maxZ, position.z());
|
||||
}
|
||||
List<Position> path = new ArrayList<>();
|
||||
for (Position at = lowest; at != null; at = parent.get(at)) path.add(at);
|
||||
Collections.reverse(path);
|
||||
return new Flow(Set.copyOf(reach.keySet()), path, new FlowBounds(minX, minY, minZ, maxX, maxY, maxZ));
|
||||
}
|
||||
|
||||
private boolean flowSolid(Position at, Position source) {
|
||||
if (at.equals(source)) return false;
|
||||
Cell cell = cells.get(key(at.x(), at.z()));
|
||||
if (cell != null && at.y() > cell.bedY() && at.y() <= cell.carveTop()) return false;
|
||||
return solid(at.x(), at.y(), at.z());
|
||||
}
|
||||
|
||||
private static void enqueueFlow(Position next, int available, Position from, ArrayDeque<FlowNode> queue,
|
||||
Map<Position, Integer> reach, Map<Position, Position> parent) {
|
||||
if (available <= reach.getOrDefault(next, -1)) return;
|
||||
if (!reach.containsKey(next)) parent.put(next, from);
|
||||
reach.put(next, available);
|
||||
queue.addLast(new FlowNode(next, available));
|
||||
}
|
||||
|
||||
private boolean nearWater(int x, int z, int distance) {
|
||||
for (Feature feature : features) {
|
||||
Point point = feature.path().getFirst();
|
||||
if (Math.hypot(x - point.x(), z - point.z()) < distance) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean solid(int x, int y, int z) { samples++; return sampler.sample(x, y, z) > 0; }
|
||||
private long featureId(int index) { return mix(seed ^ (0x51A7E7L + index * 0x9E3779B97F4A7C15L)); }
|
||||
|
||||
private double terrainNoise(int x, int z) { return field(x, z, 8, 31); }
|
||||
|
||||
private double field(int x, int z, int scale, long salt) {
|
||||
int gx = Math.floorDiv(x, scale), gz = Math.floorDiv(z, scale);
|
||||
double tx = smooth(Math.floorMod(x, scale) / (double) scale);
|
||||
double tz = smooth(Math.floorMod(z, scale) / (double) scale);
|
||||
double a = random(seed + salt, gx, gz) * (1 - tx) + random(seed + salt, gx + 1, gz) * tx;
|
||||
double b = random(seed + salt, gx, gz + 1) * (1 - tx) + random(seed + salt, gx + 1, gz + 1) * tx;
|
||||
return a * (1 - tz) + b * tz;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean inside(int x, int z, int radius) { return Math.hypot((double) x, z) < radius; }
|
||||
private static long key(int x, int z) { return ((long) x << 32) | (z & 0xffffffffL); }
|
||||
private static Point point(long key) { return new Point((int) (key >> 32), (int) key); }
|
||||
private static double smooth(double t) { return t * t * (3 - 2 * t); }
|
||||
private static double random(long seed, int x, int z) { return (mix(seed ^ key(x, z)) >>> 11) * 0x1.0p-53; }
|
||||
private static long mix(long value) {
|
||||
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
|
||||
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
|
||||
return value ^ (value >>> 31);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
|
||||
/** Alpha.9 only: immutable plans cached by world state, applied to fresh terrain before decoration. */
|
||||
public final class CavernHydrologyRuntime {
|
||||
private static final Map<RandomState, CavernHydrology.Plan> PLANS =
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
private static final Map<RandomState, CavernLavaDeposit.Plan> LAVA_PLANS =
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
|
||||
private CavernHydrologyRuntime() {}
|
||||
|
||||
public static boolean enabled(NoiseBasedChunkGenerator generator) {
|
||||
return generator.generatorSettings().is(SanctuarySpawn.CAVERN_SETTINGS);
|
||||
}
|
||||
|
||||
public static CavernHydrology.Plan cavernPlan(ServerLevel level) {
|
||||
return plan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(),
|
||||
level.getChunkSource().randomState());
|
||||
}
|
||||
|
||||
public static CavernHydrology.Plan plan(NoiseBasedChunkGenerator generator, RandomState randomState) {
|
||||
if (!enabled(generator)) throw new IllegalArgumentException("Cavern hydrology is not enabled for this generator");
|
||||
return PLANS.computeIfAbsent(randomState, state -> {
|
||||
long start = System.nanoTime();
|
||||
var density = generator.generatorSettings().value().noiseRouter().finalDensity();
|
||||
var plan = CavernHydrology.create(state.seed(),
|
||||
(x, y, z) -> state.sampleBlockValueUncached(density, x, y, z));
|
||||
SanctuaryMod.LOGGER.info("Sanctuary cavern hydrology: seed {}, {} features, {} columns, {} springs, {} ms",
|
||||
state.seed(), plan.features().size(), plan.cells().size(), plan.springs().size(),
|
||||
(System.nanoTime() - start) / 1_000_000);
|
||||
SanctuaryMod.LOGGER.info("Sanctuary cavern catchments: {}", plan.features().stream().map(f -> f.kind() + " Y" + f.waterY() + " water=" + f.waterCells() + " path=" + f.path().size() + " " + f.bounds()).toList());
|
||||
return plan;
|
||||
});
|
||||
}
|
||||
|
||||
public static CavernLavaDeposit.Plan lavaPlan(ServerLevel level) {
|
||||
return lavaPlan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(),
|
||||
level.getChunkSource().randomState());
|
||||
}
|
||||
|
||||
public static CavernLavaDeposit.Plan lavaPlan(NoiseBasedChunkGenerator generator, RandomState randomState) {
|
||||
// Acquire the water plan before the lava cache lock; the water planner never acquires it.
|
||||
var water = plan(generator, randomState);
|
||||
return LAVA_PLANS.computeIfAbsent(randomState, state -> {
|
||||
long start = System.nanoTime();
|
||||
var density = generator.generatorSettings().value().noiseRouter().finalDensity();
|
||||
var lava = CavernLavaDeposit.create(state.seed(),
|
||||
(x, y, z) -> state.sampleBlockValueUncached(density, x, y, z), water);
|
||||
SanctuaryMod.LOGGER.info("Sanctuary starter lava: seed {}, {} sources, access {}, {} ms",
|
||||
state.seed(), lava.lavaCells().size(), lava.access(), (System.nanoTime() - start) / 1_000_000);
|
||||
return lava;
|
||||
});
|
||||
}
|
||||
|
||||
public static void apply(NoiseBasedChunkGenerator generator, RandomState randomState, ChunkAccess chunk) {
|
||||
if (!enabled(generator)) return;
|
||||
var chunkPos = chunk.getPos();
|
||||
if (Math.abs((long) chunkPos.x() * 16) > IslandShape.TERRAIN_LIMIT + 16L
|
||||
|| Math.abs((long) chunkPos.z() * 16) > IslandShape.TERRAIN_LIMIT + 16L) return;
|
||||
var plan = plan(generator, randomState);
|
||||
var cells = plan.cellsInChunk(chunkPos.x(), chunkPos.z());
|
||||
var springs = plan.springsInChunk(chunkPos.x(), chunkPos.z());
|
||||
var outlets = new java.util.ArrayList<>(plan.springs().stream().map(CavernHydrology.Spring::outlet)
|
||||
.filter(outlet -> (outlet.x() >> 4) == chunkPos.x() && (outlet.z() >> 4) == chunkPos.z()).toList());
|
||||
for (var spill : plan.spills()) {
|
||||
var outlet = spill.outlet();
|
||||
if ((outlet.x() >> 4) == chunkPos.x() && (outlet.z() >> 4) == chunkPos.z()) outlets.add(outlet);
|
||||
}
|
||||
var lava = lavaPlan(generator, randomState);
|
||||
for (var fall : lava.falls()) {
|
||||
var outlet = fall.outlet();
|
||||
if ((outlet.x() >> 4) == chunkPos.x() && (outlet.z() >> 4) == chunkPos.z())
|
||||
outlets.add(new CavernHydrology.Position(outlet.x(), outlet.y(), outlet.z()));
|
||||
}
|
||||
var lavaCells = lava.cellsInChunk(chunkPos.x(), chunkPos.z());
|
||||
var lavaSupports = lava.supportsInChunk(chunkPos.x(), chunkPos.z());
|
||||
if (cells.isEmpty() && springs.isEmpty() && outlets.isEmpty() && lavaCells.isEmpty() && lavaSupports.isEmpty()) return;
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
// Validate all replacements and two intact support layers before modifying this chunk.
|
||||
for (var cell : cells) {
|
||||
for (int y = cell.bedY() - cell.sedimentDepth() - 1; y <= cell.bedY(); y++) {
|
||||
requireSolid(chunk, pos.set(cell.x(), y, cell.z()), randomState.seed());
|
||||
}
|
||||
}
|
||||
for (var spring : springs) {
|
||||
var source = spring.source();
|
||||
requireSolid(chunk, pos.set(source.x(), source.y(), source.z()), randomState.seed());
|
||||
}
|
||||
for (var support : lavaSupports) {
|
||||
requireSolid(chunk, pos.set(support.x(), support.y(), support.z()), randomState.seed());
|
||||
if (chunk.getBlockState(pos).ignitedByLava()) {
|
||||
throw new IllegalStateException("Lava pocket requires nonflammable natural support at " + pos);
|
||||
}
|
||||
}
|
||||
for (var cell : lavaCells) {
|
||||
requireSolid(chunk, pos.set(cell.x(), cell.y(), cell.z()), randomState.seed());
|
||||
}
|
||||
for (var cell : cells) {
|
||||
for (int depth = 0; depth < cell.sedimentDepth(); depth++) {
|
||||
pos.set(cell.x(), cell.bedY() - depth, cell.z());
|
||||
BlockState material = CavernMaterials.sedimentBlock(cell, randomState.seed(), depth);
|
||||
chunk.setBlockState(pos, material, 0);
|
||||
}
|
||||
for (int y = cell.bedY() + 1; y <= Math.max(cell.waterY(), cell.carveTop()); y++) {
|
||||
pos.set(cell.x(), y, cell.z());
|
||||
chunk.setBlockState(pos, cell.hasWater() && y <= cell.waterY()
|
||||
? Blocks.WATER.defaultBlockState() : Blocks.AIR.defaultBlockState(), 0);
|
||||
if (cell.hasWater() && y <= cell.waterY()) chunk.markPosForPostProcessing(pos);
|
||||
}
|
||||
}
|
||||
for (var spring : springs) {
|
||||
var source = spring.source();
|
||||
pos.set(source.x(), source.y(), source.z());
|
||||
chunk.setBlockState(pos, Blocks.WATER.defaultBlockState(), 0);
|
||||
// Only the source is generated. Vanilla fluid ticks create the descending waterfall.
|
||||
chunk.markPosForPostProcessing(pos);
|
||||
}
|
||||
for (var cell : lavaCells) {
|
||||
pos.set(cell.x(), cell.y(), cell.z());
|
||||
chunk.setBlockState(pos, (cell.lava() ? Blocks.LAVA : Blocks.AIR).defaultBlockState(), 0);
|
||||
if (cell.lava()) chunk.markPosForPostProcessing(pos);
|
||||
}
|
||||
// This existing vanilla queue is consumed after neighbouring chunks finish decoration.
|
||||
// Mark the outlet in its own chunk, including when its source belongs to a neighbour.
|
||||
for (var outlet : outlets) {
|
||||
pos.set(outlet.x(), outlet.y(), outlet.z());
|
||||
chunk.markPosForPostProcessing(pos);
|
||||
}
|
||||
Heightmap.primeHeightmaps(chunk, EnumSet.of(Heightmap.Types.WORLD_SURFACE_WG,
|
||||
Heightmap.Types.OCEAN_FLOOR_WG));
|
||||
}
|
||||
|
||||
/** Reserve water, sediment and hot-rock volumes before placing vegetation. */
|
||||
public static boolean protects(NoiseBasedChunkGenerator generator, RandomState randomState, BlockPos base,
|
||||
int radius, int height, int rootDepth) {
|
||||
var water = plan(generator, randomState);
|
||||
var lava = lavaPlan(generator, randomState);
|
||||
int low = base.getY() - rootDepth, high = base.getY() + height;
|
||||
for (int x = base.getX() - radius; x <= base.getX() + radius; x++) {
|
||||
for (int z = base.getZ() - radius; z <= base.getZ() + radius; z++) {
|
||||
for (var cell : water.cellsAt(x, z))
|
||||
if (low <= cell.carveTop() && high >= cell.bedY() - cell.sedimentDepth() - 1) return true;
|
||||
}
|
||||
}
|
||||
for (var spring : water.springs()) {
|
||||
var bounds = spring.flowBounds();
|
||||
if (base.getX() + radius >= bounds.minX() - 2 && base.getX() - radius <= bounds.maxX() + 2
|
||||
&& base.getZ() + radius >= bounds.minZ() - 2 && base.getZ() - radius <= bounds.maxZ() + 2
|
||||
&& low <= bounds.maxY() + 2 && high >= bounds.minY() - 2) return true;
|
||||
var source = spring.source();
|
||||
if (Math.abs(base.getX() - source.x()) <= radius + 3 && Math.abs(base.getZ() - source.z()) <= radius + 3
|
||||
&& low <= source.y() + 3 && high >= source.y() - 3) return true;
|
||||
}
|
||||
for (var bounds : lava.flowBounds()) {
|
||||
if (base.getX() + radius >= bounds.minX() - 8 && base.getX() - radius <= bounds.maxX() + 8
|
||||
&& base.getZ() + radius >= bounds.minZ() - 8 && base.getZ() - radius <= bounds.maxZ() + 8
|
||||
&& low <= bounds.maxY() + 8 && high >= bounds.minY() - 8) return true;
|
||||
}
|
||||
for (var spill : water.spills()) {
|
||||
var bounds = spill.flowBounds();
|
||||
if (base.getX() + radius >= bounds.minX() - 3 && base.getX() - radius <= bounds.maxX() + 3
|
||||
&& base.getZ() + radius >= bounds.minZ() - 3 && base.getZ() - radius <= bounds.maxZ() + 3
|
||||
&& low <= bounds.maxY() + 3 && high >= bounds.minY() - 3) return true;
|
||||
}
|
||||
for (var cell : lava.cells()) if (Math.abs(base.getX() - cell.x()) <= radius + 2 && Math.abs(base.getZ() - cell.z()) <= radius + 2
|
||||
&& low <= cell.y() + 2 && high >= cell.y() - 2) return true;
|
||||
for (var support : lava.supports()) if (Math.abs(base.getX() - support.x()) <= radius + 2 && Math.abs(base.getZ() - support.z()) <= radius + 2
|
||||
&& low <= support.y() + 2 && high >= support.y() - 2) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void requireSolid(ChunkAccess chunk, BlockPos pos, long seed) {
|
||||
if (!chunk.getBlockState(pos).isCollisionShapeFullBlock(chunk, pos)) {
|
||||
throw new IllegalStateException("Cavern hydrology differs from terrain at " + pos + " for seed " + seed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.util.stream.Stream;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeResolver;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Climate;
|
||||
|
||||
/** Seeded alpha.9 rift climate, separate from all saved earlier biome-source codecs. */
|
||||
public final class CavernIslandBiomeSource extends BiomeSource {
|
||||
public static final ResourceKey<Biome> OAK_FOREST = key("cavern_oak_forest");
|
||||
public static final ResourceKey<Biome> BIRCH_FOREST = key("cavern_birch_forest");
|
||||
public static final ResourceKey<Biome> CLEARING = key("cavern_clearing");
|
||||
public static final ResourceKey<Biome> DRY_WOODLAND = key("cavern_dry_woodland");
|
||||
public static final ResourceKey<Biome> ROCKY_HEATH = key("cavern_rocky_heath");
|
||||
public static final ResourceKey<Biome> DARK_GROVE = key("cavern_dark_grove");
|
||||
public static final ResourceKey<Biome> BAMBOO_GROVE = key("cavern_bamboo_grove");
|
||||
public static final ResourceKey<Biome> SULFUR_DEPTHS = key("cavern_sulfur_depths");
|
||||
public static final ResourceKey<Biome> LUSH_CAVES = key("cavern_lush_caves");
|
||||
public static final ResourceKey<Biome> DRIPSTONE_CAVES = key("cavern_dripstone_caves");
|
||||
|
||||
public static final MapCodec<CavernIslandBiomeSource> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||
Biome.CODEC.fieldOf("oak_forest").forGetter(source -> source.oakForest),
|
||||
Biome.CODEC.fieldOf("birch_forest").forGetter(source -> source.birchForest),
|
||||
Biome.CODEC.fieldOf("clearing").forGetter(source -> source.clearing),
|
||||
Biome.CODEC.fieldOf("dry_woodland").forGetter(source -> source.dryWoodland),
|
||||
Biome.CODEC.fieldOf("rocky_heath").forGetter(source -> source.rockyHeath),
|
||||
Biome.CODEC.fieldOf("dark_grove").forGetter(source -> source.darkGrove),
|
||||
Biome.CODEC.fieldOf("bamboo_grove").forGetter(source -> source.bambooGrove),
|
||||
Biome.CODEC.fieldOf("sulfur_depths").forGetter(source -> source.sulfurDepths),
|
||||
Biome.CODEC.fieldOf("lush_caves").forGetter(source -> source.lushCaves),
|
||||
Biome.CODEC.fieldOf("dripstone_caves").forGetter(source -> source.dripstoneCaves)
|
||||
).apply(instance, CavernIslandBiomeSource::new));
|
||||
|
||||
private final Holder<Biome> oakForest;
|
||||
private final Holder<Biome> birchForest;
|
||||
private final Holder<Biome> clearing;
|
||||
private final Holder<Biome> dryWoodland;
|
||||
private final Holder<Biome> rockyHeath;
|
||||
private final Holder<Biome> darkGrove;
|
||||
private final Holder<Biome> bambooGrove;
|
||||
private final Holder<Biome> sulfurDepths;
|
||||
private final Holder<Biome> lushCaves;
|
||||
private final Holder<Biome> dripstoneCaves;
|
||||
|
||||
public CavernIslandBiomeSource(Holder<Biome> oakForest, Holder<Biome> birchForest, Holder<Biome> clearing,
|
||||
Holder<Biome> dryWoodland, Holder<Biome> rockyHeath, Holder<Biome> darkGrove,
|
||||
Holder<Biome> bambooGrove, Holder<Biome> sulfurDepths, Holder<Biome> lushCaves, Holder<Biome> dripstoneCaves) {
|
||||
this.oakForest = oakForest;
|
||||
this.birchForest = birchForest;
|
||||
this.clearing = clearing;
|
||||
this.dryWoodland = dryWoodland;
|
||||
this.rockyHeath = rockyHeath;
|
||||
this.darkGrove = darkGrove;
|
||||
this.bambooGrove = bambooGrove;
|
||||
this.sulfurDepths = sulfurDepths;
|
||||
this.lushCaves = lushCaves;
|
||||
this.dripstoneCaves = dripstoneCaves;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<CavernIslandBiomeSource> codec() { return CODEC; }
|
||||
|
||||
@Override
|
||||
protected Stream<Holder<Biome>> collectPossibleBiomes() {
|
||||
return Stream.of(oakForest, birchForest, clearing, dryWoodland, rockyHeath, darkGrove, bambooGrove, sulfurDepths, lushCaves, dripstoneCaves);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiomeResolver createResolver(Climate.Sampler sampler) {
|
||||
return (quartX, quartY, quartZ) -> {
|
||||
int x = quartX * 4, y = quartY * 4, z = quartZ * 4;
|
||||
float variation = sampler.temperature().sampleValue(x, 0, z);
|
||||
float moisture = sampler.humidity().sampleValue(x, 0, z);
|
||||
return switch (CavernBiomePalette.select(y, variation, moisture)) {
|
||||
case OAK_FOREST -> oakForest;
|
||||
case BIRCH_FOREST -> birchForest;
|
||||
case CLEARING -> clearing;
|
||||
case DRY_WOODLAND -> dryWoodland;
|
||||
case ROCKY_HEATH -> rockyHeath;
|
||||
case DARK_GROVE -> darkGrove;
|
||||
case BAMBOO_GROVE -> bambooGrove;
|
||||
case SULFUR_DEPTHS -> sulfurDepths;
|
||||
case LUSH_CAVES -> lushCaves;
|
||||
case DRIPSTONE_CAVES -> dripstoneCaves;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
private static ResourceKey<Biome> key(String path) {
|
||||
return ResourceKey.create(Registries.BIOME, SanctuaryMod.id(path));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/** Alpha.9 lava: an optional covered pocket and up to two deep rock outlets.
|
||||
* All sources replace natural rock; Minecraft alone produces the falling lava. */
|
||||
public final class CavernLavaDeposit {
|
||||
public static final int RADIUS = 192;
|
||||
public static final int WATER_CLEARANCE = 32;
|
||||
public static final int LAVA_VOLUME = 18;
|
||||
private static final int ENTRY_DISTANCE = 8;
|
||||
private static final int[][] DIRECTIONS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
private static final Comparator<Position> POSITION_ORDER = Comparator.comparingInt(Position::x)
|
||||
.thenComparingInt(Position::z).thenComparingInt(Position::y);
|
||||
private static final Comparator<Cell> CELL_ORDER = Comparator.comparingInt(Cell::x)
|
||||
.thenComparingInt(Cell::z).thenComparingInt(Cell::y);
|
||||
|
||||
private CavernLavaDeposit() {}
|
||||
|
||||
public record Position(int x, int y, int z) {}
|
||||
/** lava=false removes existing rock to open the two-block-high chamber or approach. */
|
||||
public record Cell(int x, int y, int z, boolean lava) {}
|
||||
|
||||
public static final class Plan {
|
||||
private final List<Cell> cells;
|
||||
private final List<Cell> lavaCells;
|
||||
private final List<Position> supports;
|
||||
private final Optional<Position> access;
|
||||
private final Map<Long, List<Cell>> chunks;
|
||||
private final Map<Long, List<Position>> supportChunks;
|
||||
private final List<CavernLavaFlows.Fall> falls;
|
||||
private final Map<Long, List<CavernLavaFlows.Fall>> fallChunks;
|
||||
|
||||
private Plan(List<Cell> cells, Set<Position> supports, Position access) {
|
||||
this(cells, supports, access, List.of());
|
||||
}
|
||||
|
||||
private Plan(List<Cell> cells, Set<Position> supports, Position access, List<CavernLavaFlows.Fall> falls) {
|
||||
this.falls = List.copyOf(falls);
|
||||
Map<Long, List<CavernLavaFlows.Fall>> groupedFalls = new HashMap<>();
|
||||
for (var fall : falls) groupedFalls.computeIfAbsent(
|
||||
key(fall.source().x() >> 4, fall.source().z() >> 4), ignored -> new ArrayList<>()).add(fall);
|
||||
groupedFalls.replaceAll((ignored, values) -> List.copyOf(values));
|
||||
fallChunks = Map.copyOf(groupedFalls);
|
||||
this.cells = cells.stream().sorted(CELL_ORDER).toList();
|
||||
lavaCells = this.cells.stream().filter(Cell::lava).toList();
|
||||
this.supports = supports.stream().sorted(POSITION_ORDER).toList();
|
||||
this.access = Optional.ofNullable(access);
|
||||
Map<Long, List<Cell>> byChunk = new HashMap<>();
|
||||
for (Cell cell : this.cells) byChunk.computeIfAbsent(key(cell.x() >> 4, cell.z() >> 4),
|
||||
ignored -> new ArrayList<>()).add(cell);
|
||||
byChunk.replaceAll((ignored, values) -> List.copyOf(values));
|
||||
chunks = Map.copyOf(byChunk);
|
||||
Map<Long, List<Position>> bySupportChunk = new HashMap<>();
|
||||
for (Position support : this.supports) bySupportChunk.computeIfAbsent(key(support.x() >> 4, support.z() >> 4),
|
||||
ignored -> new ArrayList<>()).add(support);
|
||||
bySupportChunk.replaceAll((ignored, values) -> List.copyOf(values));
|
||||
supportChunks = Map.copyOf(bySupportChunk);
|
||||
}
|
||||
|
||||
public List<Cell> cells() { return cells; }
|
||||
public List<Cell> lavaCells() { return lavaCells; }
|
||||
public List<Position> supports() { return supports; }
|
||||
public List<CavernLavaFlows.Fall> falls() { return falls; }
|
||||
public List<CavernLavaFlows.Fall> fallsInChunk(int x, int z) { return fallChunks.getOrDefault(key(x, z), List.of()); }
|
||||
public List<Position> flowSources() { return falls.stream().map(CavernLavaFlows.Fall::source).toList(); }
|
||||
public List<CavernHydrology.FlowBounds> flowBounds() { return falls.stream().map(CavernLavaFlows.Fall::flowBounds).toList(); }
|
||||
/** Player feet on a natural ledge, with three blocks of headroom below a roof or sky. */
|
||||
public Optional<Position> access() { return access; }
|
||||
public List<Cell> cellsInChunk(int x, int z) { return chunks.getOrDefault(key(x, z), List.of()); }
|
||||
public List<Position> supportsInChunk(int x, int z) { return supportChunks.getOrDefault(key(x, z), List.of()); }
|
||||
}
|
||||
|
||||
public static Plan create(long seed, CavernHydrology.Sampler sampler, CavernHydrology.Plan hydrology) {
|
||||
Plan pocket = new Search(seed, sampler, hydrology).find();
|
||||
List<CavernLavaFlows.Fall> falls = CavernLavaFlows.create(seed, sampler, hydrology, pocket);
|
||||
List<Cell> cells = new ArrayList<>(pocket.cells());
|
||||
Set<Position> supports = new HashSet<>(pocket.supports());
|
||||
for (var fall : falls) {
|
||||
Position source = fall.source();
|
||||
cells.add(new Cell(source.x(), source.y(), source.z(), true));
|
||||
supports.addAll(fall.supports());
|
||||
}
|
||||
return new Plan(cells, supports, pocket.access().orElse(null), falls);
|
||||
}
|
||||
|
||||
private record Candidate(int x, int z, int dx, int dz, double score) {}
|
||||
private record LowerCandidate(int x, int y, int z, int dx, int dz, double score) {}
|
||||
|
||||
private static final class Search {
|
||||
private final long seed;
|
||||
private final CavernHydrology.Sampler sampler;
|
||||
private final CavernHydrology.Plan hydrology;
|
||||
private final Map<Long, Integer> heights = new HashMap<>();
|
||||
|
||||
Search(long seed, CavernHydrology.Sampler sampler, CavernHydrology.Plan hydrology) {
|
||||
this.seed = seed;
|
||||
this.sampler = sampler;
|
||||
this.hydrology = hydrology;
|
||||
}
|
||||
|
||||
Plan find() {
|
||||
Plan lower = findLowerPocket();
|
||||
if (lower != null) return lower;
|
||||
List<Candidate> candidates = new ArrayList<>();
|
||||
for (int x = -184; x <= 184; x += 8) for (int z = -184; z <= 184; z += 8) {
|
||||
if (!inside(x, z, RADIUS - 8)) continue;
|
||||
int ledge = top(x, z);
|
||||
if (ledge < 64 || ledge > 300) continue;
|
||||
for (int[] direction : DIRECTIONS) {
|
||||
int cx = x - direction[0] * ENTRY_DISTANCE, cz = z - direction[1] * ENTRY_DISTANCE;
|
||||
if (!inside(cx, cz, RADIUS - 12) || !clearOfWater(cx, cz)) continue;
|
||||
int rise = top(cx, cz) - ledge;
|
||||
if (rise < 12) continue;
|
||||
candidates.add(new Candidate(x, z, direction[0], direction[1],
|
||||
random(seed, cx, cz) + Math.min(rise, 40) * 0.015));
|
||||
}
|
||||
}
|
||||
candidates.sort(Comparator.comparingDouble(Candidate::score).reversed()
|
||||
.thenComparingInt(Candidate::x).thenComparingInt(Candidate::z)
|
||||
.thenComparingInt(Candidate::dx).thenComparingInt(Candidate::dz));
|
||||
int attempts = 0;
|
||||
for (Candidate candidate : candidates) {
|
||||
if (attempts++ >= 192) break;
|
||||
// Refine the coarse edge, including a ledge lying between two eight-block samples.
|
||||
for (int along = 0; along < 8; along++) for (int tangent : new int[]{0, -3, 3}) {
|
||||
int x = candidate.x() - candidate.dx() * along - candidate.dz() * tangent;
|
||||
int z = candidate.z() - candidate.dz() * along + candidate.dx() * tangent;
|
||||
Plan plan = tryNiche(x, z, candidate.dx(), candidate.dz());
|
||||
if (plan != null) return plan;
|
||||
}
|
||||
}
|
||||
return new Plan(List.of(), Set.of(), null);
|
||||
}
|
||||
|
||||
/** Prefer a real lower ledge below an overhang. The chamber uses exactly the same rock
|
||||
* shell as the outdoor fallback; only the entrance may now have a natural ceiling. */
|
||||
private Plan findLowerPocket() {
|
||||
List<LowerCandidate> candidates = new ArrayList<>();
|
||||
for (int x = -176; x <= 176; x += 16) for (int z = -176; z <= 176; z += 16) {
|
||||
if (!inside(x, z, RADIUS - 8)) continue;
|
||||
for (int coarseY = 64; coarseY <= 160; coarseY += 8) {
|
||||
if (!solid(x, coarseY, z) || solid(x, coarseY + 8, z)) continue;
|
||||
int level = coarseY;
|
||||
for (int y = coarseY + 7; y > coarseY; y--) {
|
||||
if (solid(x, y, z)) { level = y; break; }
|
||||
}
|
||||
if (level > 160 || top(x, z) < level + 12) continue;
|
||||
for (int[] direction : DIRECTIONS) {
|
||||
int inwardX = x - direction[0] * 16, inwardZ = z - direction[1] * 16;
|
||||
if (!inside(inwardX, inwardZ, RADIUS - 12)
|
||||
|| !solid(inwardX, level + 2, inwardZ) || !solid(inwardX, level + 10, inwardZ)) continue;
|
||||
candidates.add(new LowerCandidate(x, level, z, direction[0], direction[1],
|
||||
random(seed + level + 0x108E2L, x - direction[0], z - direction[1])
|
||||
+ (160 - level) * .002));
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates.sort(Comparator.comparingDouble(LowerCandidate::score).reversed()
|
||||
.thenComparingInt(LowerCandidate::x).thenComparingInt(LowerCandidate::z)
|
||||
.thenComparingInt(LowerCandidate::y).thenComparingInt(LowerCandidate::dx).thenComparingInt(LowerCandidate::dz));
|
||||
int attempts = 0;
|
||||
for (LowerCandidate candidate : candidates) {
|
||||
if (attempts++ >= 128) break;
|
||||
for (int along = 0; along < 16; along++) for (int tangent : new int[]{0, -3, 3}) {
|
||||
int x = candidate.x() - candidate.dx() * along - candidate.dz() * tangent;
|
||||
int z = candidate.z() - candidate.dz() * along + candidate.dx() * tangent;
|
||||
for (int level = Math.min(160, candidate.y() + 5); level >= Math.max(64, candidate.y() - 5); level--) {
|
||||
// Reject solid cave walls and missing floors before expensive shell probes.
|
||||
if (!solid(x, level, z) || solid(x, level + 1, z)) continue;
|
||||
Plan plan = tryNiche(x, z, candidate.dx(), candidate.dz(), level, true);
|
||||
if (plan != null) return plan;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Plan tryNiche(int entryX, int entryZ, int dx, int dz) {
|
||||
return tryNiche(entryX, entryZ, dx, dz, top(entryX, entryZ), false);
|
||||
}
|
||||
|
||||
private Plan tryNiche(int entryX, int entryZ, int dx, int dz, int level, boolean underground) {
|
||||
int cx = entryX - dx * ENTRY_DISTANCE, cz = entryZ - dz * ENTRY_DISTANCE;
|
||||
if (!inside(cx, cz, RADIUS - 12) || level < 64 || level > (underground ? 160 : 300)
|
||||
|| top(cx, cz) < level + 12) return null;
|
||||
// A lower entrance has genuine overhead island rock, but does not need a sky view.
|
||||
if (underground && top(entryX, entryZ) < level + 12) return null;
|
||||
int airLimit = underground ? level + 3 : 383;
|
||||
for (int y = level + 1; y <= airLimit; y++) if (solid(entryX, y, entryZ)) return null;
|
||||
for (int y = level - 2; y <= level; y++) if (!solid(entryX, y, entryZ)) return null;
|
||||
if (underground ? !clearOfWater(cx, level, cz) : !clearOfWater(cx, cz)) return null;
|
||||
|
||||
List<Cell> cells = new ArrayList<>();
|
||||
Set<Position> changed = new HashSet<>();
|
||||
Set<Position> supports = new HashSet<>();
|
||||
for (int x = cx - 1; x <= cx + 1; x++) for (int z = cz - 1; z <= cz + 1; z++) {
|
||||
for (int y = level - 1; y <= level + 2; y++) {
|
||||
if (!solid(x, y, z)) return null;
|
||||
add(cells, changed, x, y, z, y <= level);
|
||||
}
|
||||
}
|
||||
// A dry lip separates the ledge from the pool. Only the air above it is opened.
|
||||
for (int distance = 2; distance < ENTRY_DISTANCE; distance++) {
|
||||
int x = cx + dx * distance, z = cz + dz * distance;
|
||||
for (int y = level + 1; y <= level + 2; y++) {
|
||||
if (solid(x, y, z)) add(cells, changed, x, y, z, false);
|
||||
}
|
||||
}
|
||||
// Every carved column must lie under eight continuous natural rock blocks. This
|
||||
// excludes soil-cap excavation at the entrance as well as exposed surface lava.
|
||||
Map<Long, Integer> highest = new HashMap<>();
|
||||
for (Cell cell : cells) highest.merge(key(cell.x(), cell.z()), cell.y(), Math::max);
|
||||
for (var column : highest.entrySet()) {
|
||||
int x = (int) (column.getKey() >> 32), z = (int) (long) column.getKey();
|
||||
for (int y = column.getValue() + 1; y <= column.getValue() + 8; y++) {
|
||||
if (!requireRock(supports, x, y, z)) return null;
|
||||
}
|
||||
}
|
||||
// Three intact blocks under the pool, two-block-thick side walls, and a solid chamber
|
||||
// surround prevent escape through an adjacent cave. No part of this shell is created.
|
||||
for (int x = cx - 3; x <= cx + 3; x++) for (int z = cz - 3; z <= cz + 3; z++) {
|
||||
for (int y = level - 4; y <= level + 2; y++) {
|
||||
if (changed.contains(new Position(x, y, z))) continue;
|
||||
if (!requireRock(supports, x, y, z)) return null;
|
||||
}
|
||||
}
|
||||
// The natural roof also covers a conservative envelope around local lava fire spread.
|
||||
// Its thickness keeps later surface vegetation well above the lava chamber.
|
||||
for (int x = cx - 4; x <= cx + 4; x++) for (int z = cz - 4; z <= cz + 4; z++) {
|
||||
for (int y = level + 3; y <= level + 10; y++) {
|
||||
if (!requireRock(supports, x, y, z)) return null;
|
||||
}
|
||||
}
|
||||
for (int distance = 2; distance <= ENTRY_DISTANCE; distance++) {
|
||||
int x = cx + dx * distance, z = cz + dz * distance;
|
||||
for (int y = level - 2; y <= level; y++) {
|
||||
if (!requireRock(supports, x, y, z)) return null;
|
||||
}
|
||||
}
|
||||
supports.removeAll(changed);
|
||||
return new Plan(cells, supports, new Position(entryX, level + 1, entryZ));
|
||||
}
|
||||
|
||||
private boolean requireRock(Set<Position> supports, int x, int y, int z) {
|
||||
if (!solid(x, y, z)) return false;
|
||||
supports.add(new Position(x, y, z));
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean clearOfWater(int x, int z) {
|
||||
// The extra ten blocks include both the entrance and the complete protected shell.
|
||||
int clearance = WATER_CLEARANCE + 10;
|
||||
for (var cell : hydrology.cells()) {
|
||||
long dx = x - cell.x(), dz = z - cell.z();
|
||||
if (dx * dx + dz * dz < (long) clearance * clearance) return false;
|
||||
}
|
||||
for (var spring : hydrology.springs()) {
|
||||
var bounds = spring.flowBounds();
|
||||
long dx = Math.max(Math.max((long) bounds.minX() - x, 0), (long) x - bounds.maxX());
|
||||
long dz = Math.max(Math.max((long) bounds.minZ() - z, 0), (long) z - bounds.maxZ());
|
||||
if (dx * dx + dz * dz < (long) clearance * clearance) return false;
|
||||
}
|
||||
for (var spill : hydrology.spills()) {
|
||||
var bounds = spill.flowBounds();
|
||||
long dx = Math.max(Math.max((long) bounds.minX() - x, 0), (long) x - bounds.maxX());
|
||||
long dz = Math.max(Math.max((long) bounds.minZ() - z, 0), (long) z - bounds.maxZ());
|
||||
if (dx * dx + dz * dz < (long) clearance * clearance) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean clearOfWater(int x, int level, int z) {
|
||||
// This box covers chamber, dry approach, intact shell and roof. A lake far above is
|
||||
// allowed; an actual waterfall crossing this level still excludes the entire pocket.
|
||||
var pocket = new CavernHydrology.FlowBounds(x - 10, level - 4, z - 10, x + 10, level + 10, z + 10);
|
||||
for (var cell : hydrology.cells()) {
|
||||
var ground = new CavernHydrology.FlowBounds(cell.x(), cell.bedY() - cell.sedimentDepth() - 1,
|
||||
cell.z(), cell.x(), Math.max(cell.carveTop(), cell.waterY()), cell.z());
|
||||
if (distanceSquared(pocket, ground) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
|
||||
}
|
||||
for (var spring : hydrology.springs()) {
|
||||
if (distanceSquared(pocket, spring.flowBounds()) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
|
||||
}
|
||||
for (var spill : hydrology.spills()) {
|
||||
if (distanceSquared(pocket, spill.flowBounds()) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static long distanceSquared(CavernHydrology.FlowBounds a, CavernHydrology.FlowBounds b) {
|
||||
long dx = Math.max(0, Math.max((long) a.minX() - b.maxX(), (long) b.minX() - a.maxX()));
|
||||
long dy = Math.max(0, Math.max((long) a.minY() - b.maxY(), (long) b.minY() - a.maxY()));
|
||||
long dz = Math.max(0, Math.max((long) a.minZ() - b.maxZ(), (long) b.minZ() - a.maxZ()));
|
||||
return dx * dx + dy * dy + dz * dz;
|
||||
}
|
||||
|
||||
private int top(int x, int z) {
|
||||
return heights.computeIfAbsent(key(x, z), ignored -> {
|
||||
for (int y = 383; y >= 32; y -= 4) {
|
||||
if (!solid(x, y, z)) continue;
|
||||
for (int exact = Math.min(383, y + 3); exact > y; exact--) if (solid(x, exact, z)) return exact;
|
||||
return y;
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
}
|
||||
|
||||
private boolean solid(int x, int y, int z) { return sampler.sample(x, y, z) > 0; }
|
||||
}
|
||||
|
||||
private static void add(List<Cell> cells, Set<Position> changed, int x, int y, int z, boolean lava) {
|
||||
if (changed.add(new Position(x, y, z))) cells.add(new Cell(x, y, z, lava));
|
||||
}
|
||||
|
||||
private static boolean inside(int x, int z, int radius) { return (long) x * x + (long) z * z < (long) radius * radius; }
|
||||
private static long key(int x, int z) { return ((long) x << 32) | (z & 0xffffffffL); }
|
||||
private static double random(long seed, int x, int z) {
|
||||
long value = seed ^ key(x, z) ^ 0x1A7A5EEDL;
|
||||
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
|
||||
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
|
||||
return ((value ^ (value >>> 31)) >>> 11) * 0x1.0p-53;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.CavernLavaDeposit.Position;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/** Selects a few volcanic seeps in deep exposed rock. Sources replace rock; their outlets and
|
||||
* entire falling columns are left empty for vanilla lava ticks. No retaining wall is generated. */
|
||||
public final class CavernLavaFlows {
|
||||
public static final int MIN_SOURCE_Y = 40;
|
||||
public static final int MAX_SOURCE_Y = 160;
|
||||
public static final int MAX_FALLS = 2;
|
||||
public static final int HORIZONTAL_REACH = 3;
|
||||
public static final int FIRE_CLEARANCE = 8;
|
||||
public static final int WATER_CLEARANCE = 24;
|
||||
private static final int RADIUS = 224;
|
||||
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
private static final Comparator<Position> POSITION_ORDER = Comparator.comparingInt(Position::x)
|
||||
.thenComparingInt(Position::z).thenComparingInt(Position::y);
|
||||
|
||||
private CavernLavaFlows() {}
|
||||
|
||||
/** flowBounds predicts undecorated rock drainage; it does not override Minecraft physics.
|
||||
* supports contains only unchanged natural rock around the source, never the source itself. */
|
||||
public record Fall(long id, Position source, Position outlet, List<Position> flowPath,
|
||||
CavernHydrology.FlowBounds flowBounds, List<Position> supports) {
|
||||
public Fall { flowPath = List.copyOf(flowPath); supports = List.copyOf(supports); }
|
||||
}
|
||||
|
||||
public static List<Fall> create(long seed, CavernHydrology.Sampler sampler,
|
||||
CavernHydrology.Plan water, CavernLavaDeposit.Plan pocket) {
|
||||
return new Search(seed, sampler, water, pocket).find();
|
||||
}
|
||||
|
||||
private record Candidate(int x, int y, int z, int dx, int dz, double score) {}
|
||||
private record Node(Position at, int reach) {}
|
||||
private record Trace(List<Position> path, CavernHydrology.FlowBounds bounds) {}
|
||||
|
||||
private static final class Search {
|
||||
private final long seed;
|
||||
private final CavernHydrology.Sampler sampler;
|
||||
private final CavernHydrology.Plan water;
|
||||
private final CavernLavaDeposit.Plan pocket;
|
||||
private final List<Fall> falls = new ArrayList<>();
|
||||
|
||||
Search(long seed, CavernHydrology.Sampler sampler, CavernHydrology.Plan water, CavernLavaDeposit.Plan pocket) {
|
||||
this.seed = seed; this.sampler = sampler; this.water = water; this.pocket = pocket;
|
||||
}
|
||||
|
||||
List<Fall> find() {
|
||||
List<Candidate> candidates = new ArrayList<>();
|
||||
// Coarse probes below the surface can see a lower overhang that a heightmap hides.
|
||||
for (int x = -216; x <= 216; x += 8) for (int z = -216; z <= 216; z += 8) {
|
||||
if ((long) x * x + (long) z * z >= (long) RADIUS * RADIUS) continue;
|
||||
for (int y = MIN_SOURCE_Y; y <= MAX_SOURCE_Y; y += 8) {
|
||||
if (!solid(x, y, z)) continue;
|
||||
for (int[] d : CARDINALS) {
|
||||
if (solid(x + d[0] * 4, y, z + d[1] * 4)) continue;
|
||||
double score = random(seed + y, x + d[0], z + d[1])
|
||||
+ (MAX_SOURCE_Y - y) * .002;
|
||||
candidates.add(new Candidate(x, y, z, d[0], d[1], score));
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates.sort(Comparator.comparingDouble(Candidate::score).reversed()
|
||||
.thenComparingInt(Candidate::x).thenComparingInt(Candidate::z)
|
||||
.thenComparingInt(Candidate::y).thenComparingInt(Candidate::dx).thenComparingInt(Candidate::dz));
|
||||
int attempts = 0;
|
||||
for (Candidate candidate : candidates) {
|
||||
if (falls.size() >= MAX_FALLS || attempts++ >= 160) break;
|
||||
boolean placed = false;
|
||||
for (int tangent : new int[]{0, -2, 2}) {
|
||||
for (int along = 0; along < 4; along++) {
|
||||
int x = candidate.x() + candidate.dx() * along - candidate.dz() * tangent;
|
||||
int z = candidate.z() + candidate.dz() * along + candidate.dx() * tangent;
|
||||
for (int dy : new int[]{0, 2, -2, 4, -4}) {
|
||||
Position source = new Position(x, candidate.y() + dy, z);
|
||||
if (source.y() < MIN_SOURCE_Y || source.y() > MAX_SOURCE_Y || !separated(source)) continue;
|
||||
List<Position> supports = niche(source, candidate.dx(), candidate.dz());
|
||||
if (supports == null) continue;
|
||||
Position outlet = new Position(x + candidate.dx(), source.y(), z + candidate.dz());
|
||||
Trace flow = trace(source, outlet);
|
||||
if (flow == null || flow.bounds().minY() > source.y() - 12 || !clearOfWater(flow.bounds())) continue;
|
||||
long id = mix(seed ^ (0x1A7AFA11L + falls.size() * 0x9E3779B97F4A7C15L));
|
||||
falls.add(new Fall(id, source, outlet, flow.path(), flow.bounds(), supports));
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
if (placed) break;
|
||||
}
|
||||
if (placed) break;
|
||||
}
|
||||
}
|
||||
return List.copyOf(falls);
|
||||
}
|
||||
|
||||
private boolean separated(Position source) {
|
||||
CavernHydrology.FlowBounds point = bounds(source);
|
||||
if (!clearOfWater(point)) return false;
|
||||
for (Fall fall : falls) {
|
||||
if (distanceSquared(source, fall.source()) < 80L * 80
|
||||
|| distanceSquared(point, fall.flowBounds()) < 40L * 40) return false;
|
||||
}
|
||||
for (var cell : pocket.cells()) {
|
||||
if (distanceSquared(source, new Position(cell.x(), cell.y(), cell.z())) < 32L * 32) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private List<Position> niche(Position source, int dx, int dz) {
|
||||
if (!solid(source)) return null;
|
||||
Set<Position> supports = new HashSet<>();
|
||||
// The source sits in rock, with a short intact roof, floor and three back/side faces.
|
||||
// The spill column needs no floor: its absence is precisely what makes a cascade.
|
||||
for (int dy = -3; dy <= 8; dy++) {
|
||||
if (dy == 0) continue;
|
||||
Position at = new Position(source.x(), source.y() + dy, source.z());
|
||||
if (!solid(at)) return null;
|
||||
supports.add(at);
|
||||
}
|
||||
for (int[] d : CARDINALS) {
|
||||
if (d[0] == dx && d[1] == dz) continue;
|
||||
for (int depth = 1; depth <= 2; depth++) {
|
||||
Position at = new Position(source.x() + d[0] * depth, source.y(), source.z() + d[1] * depth);
|
||||
if (!solid(at)) return null;
|
||||
supports.add(at);
|
||||
}
|
||||
}
|
||||
for (int dy = 0; dy >= -8; dy--) {
|
||||
if (solid(source.x() + dx, source.y() + dy, source.z() + dz)) return null;
|
||||
}
|
||||
return supports.stream().sorted(POSITION_ORDER).toList();
|
||||
}
|
||||
|
||||
private boolean clearOfWater(CavernHydrology.FlowBounds bounds) {
|
||||
for (var cell : water.cells()) {
|
||||
var column = new CavernHydrology.FlowBounds(cell.x(), cell.bedY() - cell.sedimentDepth() - 1,
|
||||
cell.z(), cell.x(), Math.max(cell.carveTop(), cell.waterY()), cell.z());
|
||||
if (distanceSquared(bounds, column) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
|
||||
}
|
||||
for (var spring : water.springs()) {
|
||||
if (distanceSquared(bounds, spring.flowBounds()) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
|
||||
}
|
||||
for (var spill : water.spills()) {
|
||||
if (distanceSquared(bounds, spill.flowBounds()) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Descending lava resets its horizontal reach to three in this non-ultrawarm dimension.
|
||||
* Explore all downhill branches through rock, so selection does not depend on chunk order. */
|
||||
private Trace trace(Position source, Position outlet) {
|
||||
Map<Position, Integer> reach = new HashMap<>();
|
||||
Map<Position, Position> parent = new HashMap<>();
|
||||
ArrayDeque<Node> queue = new ArrayDeque<>();
|
||||
reach.put(source, HORIZONTAL_REACH);
|
||||
queue.addLast(new Node(source, HORIZONTAL_REACH));
|
||||
Position lowest = source;
|
||||
while (!queue.isEmpty()) {
|
||||
Node node = queue.removeFirst();
|
||||
Position at = node.at();
|
||||
if (node.reach() < reach.getOrDefault(at, -1)) continue;
|
||||
if (at.y() < lowest.y()) lowest = at;
|
||||
if (reach.size() > 12000 || Math.abs(at.x() - source.x()) > 64 || Math.abs(at.z() - source.z()) > 64) return null;
|
||||
if (at.y() <= 0) continue;
|
||||
Position below = new Position(at.x(), at.y() - 1, at.z());
|
||||
if (!flowSolid(below, source)) {
|
||||
enqueue(below, HORIZONTAL_REACH, at, reach, parent, queue);
|
||||
continue;
|
||||
}
|
||||
if (node.reach() <= 0) continue;
|
||||
for (int[] d : CARDINALS) {
|
||||
Position next = new Position(at.x() + d[0], at.y(), at.z() + d[1]);
|
||||
if (!flowSolid(next, source)) enqueue(next, node.reach() - 1, at, reach, parent, queue);
|
||||
}
|
||||
}
|
||||
if (!reach.containsKey(outlet)) return null;
|
||||
int minX = source.x(), maxX = minX, minY = source.y(), maxY = minY, minZ = source.z(), maxZ = minZ;
|
||||
for (Position at : reach.keySet()) {
|
||||
minX = Math.min(minX, at.x()); maxX = Math.max(maxX, at.x());
|
||||
minY = Math.min(minY, at.y()); maxY = Math.max(maxY, at.y());
|
||||
minZ = Math.min(minZ, at.z()); maxZ = Math.max(maxZ, at.z());
|
||||
}
|
||||
List<Position> path = new ArrayList<>();
|
||||
for (Position at = lowest; at != null; at = parent.get(at)) path.add(at);
|
||||
Collections.reverse(path);
|
||||
return new Trace(List.copyOf(path), new CavernHydrology.FlowBounds(minX, minY, minZ, maxX, maxY, maxZ));
|
||||
}
|
||||
|
||||
private boolean flowSolid(Position at, Position source) {
|
||||
if (at.equals(source)) return false;
|
||||
var cell = water.cellAt(at.x(), at.y(), at.z());
|
||||
if (cell != null && at.y() > cell.bedY() && at.y() <= cell.carveTop()) return false;
|
||||
return solid(at);
|
||||
}
|
||||
|
||||
private boolean solid(Position at) { return solid(at.x(), at.y(), at.z()); }
|
||||
private boolean solid(int x, int y, int z) { return sampler.sample(x, y, z) > 0; }
|
||||
}
|
||||
|
||||
private static void enqueue(Position next, int available, Position from, Map<Position, Integer> reach,
|
||||
Map<Position, Position> parent, ArrayDeque<Node> queue) {
|
||||
if (available <= reach.getOrDefault(next, -1)) return;
|
||||
if (!reach.containsKey(next)) parent.put(next, from);
|
||||
reach.put(next, available);
|
||||
queue.addLast(new Node(next, available));
|
||||
}
|
||||
|
||||
private static CavernHydrology.FlowBounds bounds(Position at) {
|
||||
return new CavernHydrology.FlowBounds(at.x(), at.y(), at.z(), at.x(), at.y(), at.z());
|
||||
}
|
||||
|
||||
private static long distanceSquared(Position a, Position b) {
|
||||
long dx = a.x() - b.x(), dy = a.y() - b.y(), dz = a.z() - b.z();
|
||||
return dx * dx + dy * dy + dz * dz;
|
||||
}
|
||||
|
||||
private static long distanceSquared(CavernHydrology.FlowBounds a, CavernHydrology.FlowBounds b) {
|
||||
long dx = Math.max(0, Math.max((long) a.minX() - b.maxX(), (long) b.minX() - a.maxX()));
|
||||
long dy = Math.max(0, Math.max((long) a.minY() - b.maxY(), (long) b.minY() - a.maxY()));
|
||||
long dz = Math.max(0, Math.max((long) a.minZ() - b.maxZ(), (long) b.minZ() - a.maxZ()));
|
||||
return dx * dx + dy * dy + dz * dz;
|
||||
}
|
||||
|
||||
private static double random(long seed, int x, int z) {
|
||||
return (mix(seed ^ ((long) x << 32) ^ (z & 0xffffffffL)) >>> 11) * 0x1.0p-53;
|
||||
}
|
||||
|
||||
private static long mix(long value) {
|
||||
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
|
||||
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
|
||||
return value ^ (value >>> 31);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/** Alpha.9 shore deposits: broad continuous patches through the existing sediment thickness. */
|
||||
public final class CavernMaterials {
|
||||
private CavernMaterials() {}
|
||||
|
||||
public static BlockState sedimentBlock(CavernHydrology.Cell cell, long seed, int depth) {
|
||||
if (cell.material() == CavernHydrology.ShoreMaterial.GRASS) {
|
||||
return (depth == 0 ? Blocks.GRASS_BLOCK : Blocks.DIRT).defaultBlockState();
|
||||
}
|
||||
if (cell.material() == CavernHydrology.ShoreMaterial.CLAY) return Blocks.CLAY.defaultBlockState();
|
||||
double patch = patch(seed, cell.x(), cell.z(), 32);
|
||||
Block material;
|
||||
if (cell.hasWater() && patch < 0.24) material = Blocks.CLAY;
|
||||
else if (patch < 0.53) material = Blocks.SAND;
|
||||
else if (patch < 0.68) material = Blocks.GRAVEL;
|
||||
else {
|
||||
double rock = patch(seed ^ 0x4c6179657273L, cell.x(), cell.z(), 40);
|
||||
material = rock < 0.28 ? Blocks.GRANITE : rock < 0.45 ? Blocks.DIORITE
|
||||
: rock < 0.65 ? Blocks.ANDESITE : Blocks.STONE;
|
||||
}
|
||||
return material.defaultBlockState();
|
||||
}
|
||||
|
||||
private static double patch(long seed, int x, int z, int scale) {
|
||||
int gx = Math.floorDiv(x, scale), gz = Math.floorDiv(z, scale);
|
||||
double tx = smooth(Math.floorMod(x, scale) / (double) scale);
|
||||
double tz = smooth(Math.floorMod(z, scale) / (double) scale);
|
||||
double near = lerp(value(seed, gx, gz), value(seed, gx + 1, gz), tx);
|
||||
double far = lerp(value(seed, gx, gz + 1), value(seed, gx + 1, gz + 1), tx);
|
||||
return lerp(near, far, tz);
|
||||
}
|
||||
|
||||
private static double value(long seed, int x, int z) {
|
||||
return (WoodlandGroveGeometry.seed(seed, x, 0, z, 0x53484f5245L) >>> 11) * 0x1.0p-53;
|
||||
}
|
||||
|
||||
private static double smooth(double t) { return t * t * (3 - 2 * t); }
|
||||
private static double lerp(double a, double b, double t) { return a + (b - a) * t; }
|
||||
}
|
||||
+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 CavernShoreSugarCaneFeature implements Feature {
|
||||
public static final MapCodec<CavernShoreSugarCaneFeature> CODEC = MapCodec.unit(CavernShoreSugarCaneFeature::new);
|
||||
|
||||
@Override
|
||||
public MapCodec<CavernShoreSugarCaneFeature> codec() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource random, BlockPos origin) {
|
||||
if (!(generator instanceof NoiseBasedChunkGenerator noise) || !CavernHydrologyRuntime.enabled(noise)) return false;
|
||||
var plan = CavernHydrologyRuntime.plan(noise, level.getLevel().getChunkSource().randomState());
|
||||
var cane = Blocks.SUGAR_CANE.defaultBlockState();
|
||||
boolean placed = false;
|
||||
for (var cell : plan.cellsInChunk(origin.getX() >> 4, origin.getZ() >> 4)) {
|
||||
if (cell.hasWater() || random.nextInt(7) != 0) continue;
|
||||
BlockPos base = new BlockPos(cell.x(), cell.bedY() + 1, cell.z());
|
||||
if (!level.getBlockState(base).isAir() || !cane.canSurvive(level, base)) continue;
|
||||
int height = 1 + random.nextInt(3);
|
||||
for (int y = 0; y < height; y++) {
|
||||
BlockPos pos = base.above(y);
|
||||
if (!level.getBlockState(pos).isAir() || !cane.canSurvive(level, pos)) break;
|
||||
setBlock(level, pos, cane);
|
||||
placed = true;
|
||||
}
|
||||
}
|
||||
return placed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.VegetationBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.material.Fluids;
|
||||
|
||||
/** One-time alpha.9 outlet finishing, using vanilla's persisted and consumed generation queue. */
|
||||
public final class CavernSpringOutlets {
|
||||
public record Pending(List<CavernHydrology.Position> sources, List<CavernHydrology.Position> outlets) {
|
||||
public static final Pending EMPTY = new Pending(List.of(), List.of());
|
||||
public Pending { sources = List.copyOf(sources); outlets = List.copyOf(outlets); }
|
||||
public boolean isEmpty() { return sources.isEmpty() && outlets.isEmpty(); }
|
||||
}
|
||||
|
||||
private CavernSpringOutlets() {}
|
||||
|
||||
/** Called before vanilla consumes the queue; a fully processed/reloaded chunk has no work. */
|
||||
public static Pending capturePending(ServerLevel level, LevelChunk chunk) {
|
||||
if (!(level.getChunkSource().getGenerator() instanceof NoiseBasedChunkGenerator noise)
|
||||
|| !CavernHydrologyRuntime.enabled(noise)) return Pending.EMPTY;
|
||||
boolean queued = false;
|
||||
for (var section : chunk.getPostProcessing()) if (section != null && !section.isEmpty()) {
|
||||
queued = true;
|
||||
break;
|
||||
}
|
||||
if (!queued) return Pending.EMPTY;
|
||||
var plan = CavernHydrologyRuntime.plan(noise, level.getChunkSource().randomState());
|
||||
List<CavernHydrology.Position> sources = new ArrayList<>(), outlets = new ArrayList<>();
|
||||
for (var spring : plan.springs()) {
|
||||
if (markedHere(chunk, spring.source())) sources.add(spring.source());
|
||||
if (markedHere(chunk, spring.outlet())) outlets.add(spring.outlet());
|
||||
}
|
||||
for (var spill : plan.spills()) {
|
||||
if (markedHere(chunk, spill.source())) sources.add(spill.source());
|
||||
if (markedHere(chunk, spill.outlet())) outlets.add(spill.outlet());
|
||||
}
|
||||
for (var fall : CavernHydrologyRuntime.lavaPlan(noise, level.getChunkSource().randomState()).falls()) {
|
||||
var source = new CavernHydrology.Position(fall.source().x(), fall.source().y(), fall.source().z());
|
||||
var outlet = new CavernHydrology.Position(fall.outlet().x(), fall.outlet().y(), fall.outlet().z());
|
||||
if (markedHere(chunk, source)) sources.add(source);
|
||||
if (markedHere(chunk, outlet)) outlets.add(outlet);
|
||||
}
|
||||
return new Pending(sources, outlets);
|
||||
}
|
||||
|
||||
/** All neighbouring FEATURES have finished before a chunk reaches this stage. */
|
||||
public static void finish(ServerLevel level, LevelChunk chunk, Pending pending) {
|
||||
if (pending.isEmpty() || !(level.getChunkSource().getGenerator() instanceof NoiseBasedChunkGenerator noise)
|
||||
|| !CavernHydrologyRuntime.enabled(noise)) return;
|
||||
var random = level.getChunkSource().randomState();
|
||||
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
|
||||
for (var outlet : pending.outlets()) {
|
||||
if (!owns(chunk, outlet)) throw new IllegalArgumentException("An outlet belongs to another chunk: " + outlet);
|
||||
var modified = CavernHydrologyRuntime.plan(noise, random).cellAt(outlet.x(), outlet.y(), outlet.z());
|
||||
boolean carved = modified != null && outlet.y() > modified.waterY() && outlet.y() > modified.bedY()
|
||||
&& outlet.y() <= modified.carveTop();
|
||||
if (!carved && random.sampleBlockValueUncached(density, outlet.x(), outlet.y(), outlet.z()) > 0) {
|
||||
throw new IllegalStateException("A spring outlet must already be natural air: " + outlet);
|
||||
}
|
||||
BlockPos pos = blockPos(outlet);
|
||||
BlockState current = chunk.getBlockState(pos);
|
||||
if (removableDecoration(current)) {
|
||||
// Only this declared air cell is reopened. Neighbour notification wakes the
|
||||
// adjacent source even if its own chunk was post-processed first.
|
||||
level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
|
||||
}
|
||||
}
|
||||
for (var source : pending.sources()) {
|
||||
if (!owns(chunk, source)) throw new IllegalArgumentException("A source belongs to another chunk: " + source);
|
||||
BlockPos pos = blockPos(source);
|
||||
BlockState current = chunk.getBlockState(pos);
|
||||
if (current.getFluidState().isSource()) {
|
||||
if (current.is(Blocks.WATER)) level.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(level));
|
||||
else if (current.is(Blocks.LAVA)) level.scheduleTick(pos, Fluids.LAVA, Fluids.LAVA.getTickDelay(level));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean markedHere(LevelChunk chunk, CavernHydrology.Position position) {
|
||||
if (!owns(chunk, position)) return false;
|
||||
var queue = chunk.getPostProcessing()[chunk.getSectionIndex(position.y())];
|
||||
return queue != null && queue.contains(ProtoChunk.packOffsetCoordinates(blockPos(position)));
|
||||
}
|
||||
|
||||
private static boolean owns(LevelChunk chunk, CavernHydrology.Position position) {
|
||||
return (position.x() >> 4) == chunk.getPos().x() && (position.z() >> 4) == chunk.getPos().z();
|
||||
}
|
||||
|
||||
private static BlockPos blockPos(CavernHydrology.Position position) {
|
||||
return new BlockPos(position.x(), position.y(), position.z());
|
||||
}
|
||||
|
||||
private static boolean removableDecoration(BlockState state) {
|
||||
if (state.hasBlockEntity()) return false;
|
||||
return state.is(Blocks.GLOW_LICHEN) || state.is(Blocks.VINE) || state.is(Blocks.MOSS_CARPET)
|
||||
|| state.getBlock() instanceof VegetationBlock || state.is(BlockTags.LEAVES) || state.is(BlockTags.LOGS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.CavernHydrology.*;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.PriorityQueue;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/** Small connected water terraces inside existing cave ledges. Each retained pool is excavated
|
||||
* in rock; one declared edge may overflow. No dam, shelf, cave fill or fluid column is generated. */
|
||||
public final class CavernTerraces {
|
||||
public static final int MIN_Y = 56, MAX_Y = 208;
|
||||
public static final int MAX_GROUPS = 2, MAX_DEPTH = 3, MAX_CARVE = 5;
|
||||
private static final int RADIUS = 216, SEDIMENT = 3;
|
||||
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
private static final int[][] NEIGHBORS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}};
|
||||
|
||||
private CavernTerraces() {}
|
||||
|
||||
public record Result(List<Cell> cells, List<Feature> features, List<Terrace> terraces,
|
||||
List<Spill> spills, Set<Position> predictedFlow) {
|
||||
public static final Result EMPTY = new Result(List.of(), List.of(), List.of(), List.of(), Set.of());
|
||||
public Result {
|
||||
cells = List.copyOf(cells); features = List.copyOf(features); terraces = List.copyOf(terraces);
|
||||
spills = List.copyOf(spills); predictedFlow = Set.copyOf(predictedFlow);
|
||||
}
|
||||
}
|
||||
|
||||
public static Result create(long seed, Sampler sampler, CavernHydrology.Plan surface) {
|
||||
return new Search(seed, sampler, surface).build();
|
||||
}
|
||||
|
||||
private record Candidate(int x, int y, int z, int dx, int dz, double score) {}
|
||||
private record Floor(int top, int bottom, int ceiling) {}
|
||||
private record Node(int x, int z, double cost) {}
|
||||
private record FlowNode(Position at, int reach) {}
|
||||
private record Flow(Set<Position> volume, List<Position> path, FlowBounds bounds) {}
|
||||
private record Pool(Map<Long, Cell> cells, Position source, Position outlet) {}
|
||||
private static final Floor NO_FLOOR = new Floor(-1, -1, -1);
|
||||
|
||||
private static final class Search {
|
||||
private final long seed;
|
||||
private final Sampler sampler;
|
||||
private final CavernHydrology.Plan surface;
|
||||
private final Map<Position, Floor> floors = new HashMap<>();
|
||||
private final List<Cell> acceptedCells = new ArrayList<>();
|
||||
private final List<Feature> features = new ArrayList<>();
|
||||
private final List<Terrace> terraces = new ArrayList<>();
|
||||
private final List<Spill> spills = new ArrayList<>();
|
||||
private final Set<Position> predicted = new HashSet<>();
|
||||
|
||||
Search(long seed, Sampler sampler, CavernHydrology.Plan surface) {
|
||||
this.seed = seed; this.sampler = sampler; this.surface = surface;
|
||||
}
|
||||
|
||||
Result build() {
|
||||
List<Candidate> candidates = new ArrayList<>();
|
||||
for (int x = -208; x <= 208; x += 8) for (int z = -208; z <= 208; z += 8) {
|
||||
if (!inside(x, z)) continue;
|
||||
for (int y = 64; y <= MAX_Y; y += 8) {
|
||||
if (!solid(x, y, z) || solid(x, y + 8, z)) continue;
|
||||
int level = y;
|
||||
for (int exact = y + 7; exact > y; exact--) if (solid(x, exact, z)) { level = exact; break; }
|
||||
if (level > MAX_Y || floor(x, z, level) == NO_FLOOR) continue;
|
||||
for (int[] d : CARDINALS) if (!solid(x + d[0] * 4, level, z + d[1] * 4)) {
|
||||
candidates.add(new Candidate(x, level, z, d[0], d[1], random(seed + level, x + d[0], z + d[1])));
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates.sort(Comparator.comparingDouble(Candidate::score).reversed()
|
||||
.thenComparingInt(Candidate::x).thenComparingInt(Candidate::z).thenComparingInt(Candidate::y)
|
||||
.thenComparingInt(Candidate::dx).thenComparingInt(Candidate::dz));
|
||||
int attempts = 0;
|
||||
for (Candidate candidate : candidates) {
|
||||
if (terraces.size() >= MAX_GROUPS || attempts++ >= 160) break;
|
||||
boolean added = false;
|
||||
for (int tangent : new int[]{0, -2, 2}) {
|
||||
for (int along = 0; along < 4; along++) {
|
||||
int x = candidate.x() + candidate.dx() * along - candidate.dz() * tangent;
|
||||
int z = candidate.z() + candidate.dz() * along + candidate.dx() * tangent;
|
||||
for (int dy : new int[]{0, -1, 1, -2, 2}) {
|
||||
Position source = new Position(x, candidate.y() + dy, z);
|
||||
Position outlet = new Position(x + candidate.dx(), source.y(), z + candidate.dz());
|
||||
if (!mouth(source, outlet) || !separated(source)) continue;
|
||||
if (addGroup(source, outlet)) { added = true; break; }
|
||||
}
|
||||
if (added) break;
|
||||
}
|
||||
if (added) break;
|
||||
}
|
||||
}
|
||||
return new Result(acceptedCells, features, terraces, spills, predicted);
|
||||
}
|
||||
|
||||
private boolean addGroup(Position source, Position outlet) {
|
||||
long groupId = mix(seed ^ (0xA9CA7E2L + terraces.size() * 0x9E3779B97F4A7C15L));
|
||||
long upperId = mix(groupId + 1), lowerId = mix(groupId + 2);
|
||||
Pool upper = grow(source, source.y(), source, outlet, upperId);
|
||||
if (upper == null) return false;
|
||||
|
||||
// The first impact on a slope is often a thin lip. Follow natural drainage across
|
||||
// its ledges, then find a supported receiving pool at a genuinely lower level.
|
||||
// The same bounded fluid trace is used before and after carving the receiving pool.
|
||||
Flow drainage = trace(source, outlet, new ArrayList<>(upper.cells().values()));
|
||||
if (drainage == null) return false;
|
||||
List<Position> contacts = new ArrayList<>();
|
||||
for (Position at : drainage.volume()) {
|
||||
int level = at.y() - 1;
|
||||
if (level < MIN_Y || level > source.y() - 4 || !solid(at.x(), level, at.z())) continue;
|
||||
Floor ground = floor(at.x(), at.z(), level);
|
||||
if (ground != NO_FLOOR && ground.bottom() <= level - 7)
|
||||
contacts.add(new Position(at.x(), level, at.z()));
|
||||
}
|
||||
contacts.sort(Comparator.comparingInt(Position::y).reversed()
|
||||
.thenComparingInt(Position::x).thenComparingInt(Position::z));
|
||||
Pool lower = null;
|
||||
Position landing = null;
|
||||
int attempts = 0;
|
||||
for (Position contact : contacts) {
|
||||
if (attempts++ >= 48) break;
|
||||
lower = grow(contact, contact.y(), null, null, lowerId);
|
||||
if (lower != null) { landing = contact; break; }
|
||||
}
|
||||
if (lower == null) return false;
|
||||
List<Cell> groupCells = new ArrayList<>(upper.cells().values());
|
||||
groupCells.addAll(lower.cells().values());
|
||||
if (overlapping(groupCells)) return false;
|
||||
Flow inlet = trace(source, outlet, groupCells);
|
||||
if (inlet == null || !reaches(inlet, lower)) return false;
|
||||
|
||||
// If the same lower ledge has another natural edge, connect it to an open terminal
|
||||
// fall. Otherwise its second pool stays closed; the first inter-pool cascade remains.
|
||||
Pool drained = terminalPool(landing, lowerId);
|
||||
Flow terminal = null;
|
||||
if (drained != null) {
|
||||
List<Cell> replacement = new ArrayList<>(upper.cells().values());
|
||||
replacement.addAll(drained.cells().values());
|
||||
if (!overlapping(replacement)) {
|
||||
Flow in = trace(source, outlet, replacement);
|
||||
Flow out = trace(drained.source(), drained.outlet(), replacement);
|
||||
if (in != null && reaches(in, drained) && out != null
|
||||
&& out.bounds().minY() <= drained.source().y() - 12) {
|
||||
lower = drained; inlet = in; terminal = out; groupCells = replacement;
|
||||
}
|
||||
}
|
||||
}
|
||||
acceptedCells.addAll(groupCells);
|
||||
features.add(feature(upperId, upper, source));
|
||||
features.add(feature(lowerId, lower, landing));
|
||||
terraces.add(new Terrace(groupId, List.of(upperId, lowerId)));
|
||||
spills.add(new Spill(mix(groupId + 3), upperId, lowerId, source, outlet, inlet.path(), inlet.bounds()));
|
||||
predicted.addAll(inlet.volume());
|
||||
if (terminal != null) {
|
||||
spills.add(new Spill(mix(groupId + 4), lowerId, -1, lower.source(), lower.outlet(), terminal.path(), terminal.bounds()));
|
||||
predicted.addAll(terminal.volume());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Pool terminalPool(Position center, long id) {
|
||||
List<Position> candidates = new ArrayList<>();
|
||||
for (int dx = -12; dx <= 12; dx++) for (int dz = -12; dz <= 12; dz++) {
|
||||
if (dx * dx + dz * dz > 144) continue;
|
||||
candidates.add(new Position(center.x() + dx, center.y(), center.z() + dz));
|
||||
}
|
||||
candidates.sort(Comparator.comparingDouble((Position p) -> Math.hypot(p.x() - center.x(), p.z() - center.z()))
|
||||
.thenComparingInt(Position::x).thenComparingInt(Position::z));
|
||||
int tried = 0;
|
||||
for (Position source : candidates) for (int[] d : CARDINALS) {
|
||||
Position outlet = new Position(source.x() + d[0], source.y(), source.z() + d[1]);
|
||||
if (!mouth(source, outlet)) continue;
|
||||
if (tried++ >= 12) return null;
|
||||
Pool pool = grow(center, center.y(), source, outlet, id);
|
||||
if (pool != null) return pool;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Pool grow(Position center, int level, Position source, Position outlet, long id) {
|
||||
PriorityQueue<Node> queue = new PriorityQueue<>(Comparator.comparingDouble(Node::cost)
|
||||
.thenComparingInt(Node::x).thenComparingInt(Node::z));
|
||||
Map<Long, Double> costs = new HashMap<>();
|
||||
Set<Long> wet = new HashSet<>();
|
||||
long origin = key(center.x(), center.z());
|
||||
queue.add(new Node(center.x(), center.z(), 0)); costs.put(origin, 0.0);
|
||||
int target = 100 + (int) (random(seed + level, center.x(), center.z()) * 80);
|
||||
while (!queue.isEmpty() && wet.size() < target) {
|
||||
Node node = queue.remove(); long k = key(node.x(), node.z());
|
||||
if (wet.contains(k) || node.cost() > costs.getOrDefault(k, Double.POSITIVE_INFINITY)
|
||||
|| !poolColumn(node.x(), node.z(), level, source, outlet)) continue;
|
||||
wet.add(k);
|
||||
for (int[] d : NEIGHBORS) {
|
||||
int x = node.x() + d[0], z = node.z() + d[1]; long next = key(x, z);
|
||||
if (Math.hypot(x - center.x(), z - center.z()) > 16 || wet.contains(next)) continue;
|
||||
double cost = node.cost() + Math.hypot(d[0], d[1]) * (1 + noise(x, z, 11) * 1.8);
|
||||
if (cost >= costs.getOrDefault(next, Double.POSITIVE_INFINITY)) continue;
|
||||
costs.put(next, cost); queue.add(new Node(x, z, cost));
|
||||
}
|
||||
}
|
||||
wet = connected(wet, origin);
|
||||
if (wet.size() < 40 || source != null && !wet.contains(key(source.x(), source.z()))) return null;
|
||||
Map<Long, Cell> cells = new HashMap<>();
|
||||
for (long k : wet) {
|
||||
int x = (int) (k >> 32), z = (int) k;
|
||||
boolean edge = false;
|
||||
for (int[] d : CARDINALS) edge |= !wet.contains(key(x + d[0], z + d[1]));
|
||||
int depth = edge ? 1 : 2 + (noise(x, z, 9) > .6 ? 1 : 0);
|
||||
Floor floor = floor(x, z, level);
|
||||
int bed = level - depth;
|
||||
if (floor == NO_FLOOR || floor.top() - bed > MAX_CARVE || floor.bottom() > bed - SEDIMENT - 1) return null;
|
||||
ShoreMaterial material = noise(x + 101, z - 87, 19) > .42 ? ShoreMaterial.CLAY : ShoreMaterial.STONE;
|
||||
cells.put(k, new Cell(x, z, level, bed, floor.top(), material, id, SEDIMENT));
|
||||
}
|
||||
for (Cell cell : cells.values()) for (int[] d : CARDINALS) {
|
||||
int x = cell.x() + d[0], z = cell.z() + d[1];
|
||||
Cell next = cells.get(key(x, z));
|
||||
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) {
|
||||
if (next != null && y > next.bedY()) continue;
|
||||
if (source != null && cell.x() == source.x() && cell.z() == source.z()
|
||||
&& x == outlet.x() && z == outlet.z() && y == outlet.y()) continue;
|
||||
if (!solid(x, y, z)) return null;
|
||||
}
|
||||
}
|
||||
return new Pool(Map.copyOf(cells), source, outlet);
|
||||
}
|
||||
|
||||
private boolean poolColumn(int x, int z, int level, Position source, Position outlet) {
|
||||
if (!inside(x, z)) return false;
|
||||
Floor floor = floor(x, z, level);
|
||||
if (floor == NO_FLOOR || floor.bottom() > level - 7 || !available(x, z, level - 8, floor.top())) return false;
|
||||
for (int[] d : CARDINALS) {
|
||||
if (source != null && x == source.x() && z == source.z()
|
||||
&& x + d[0] == outlet.x() && z + d[1] == outlet.z()) continue;
|
||||
if (!solid(x + d[0], level, z + d[1])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Floor floor(int x, int z, int level) {
|
||||
return floors.computeIfAbsent(new Position(x, level, z), ignored -> {
|
||||
if (level < MIN_Y || level > MAX_Y) return NO_FLOOR;
|
||||
int top = -1;
|
||||
for (int y = level + 2; y >= level; y--) {
|
||||
if (solid(x, y, z)) { top = y; break; }
|
||||
}
|
||||
if (top < 0) return NO_FLOOR;
|
||||
for (int y = top + 1; y <= top + 4; y++) if (solid(x, y, z)) return NO_FLOOR;
|
||||
int ceiling = top + 5;
|
||||
while (ceiling < 384 && !solid(x, ceiling, z)) ceiling++;
|
||||
if (ceiling >= 384) return NO_FLOOR;
|
||||
int bottom = top;
|
||||
while (bottom > top - 16 && solid(x, bottom - 1, z)) bottom--;
|
||||
return new Floor(top, bottom, ceiling);
|
||||
});
|
||||
}
|
||||
|
||||
private boolean mouth(Position source, Position outlet) {
|
||||
if (source.y() < MIN_Y || source.y() > MAX_Y || !solid(source.x(), source.y(), source.z())) return false;
|
||||
// An adjacent natural step may receive the water before it descends again. Requiring
|
||||
// a vertical shaft here would discard the gentle ledges that form cave terraces.
|
||||
if (solid(outlet.x(), outlet.y(), outlet.z())) return false;
|
||||
return floor(source.x(), source.z(), source.y()) != NO_FLOOR;
|
||||
}
|
||||
|
||||
private boolean available(int x, int z, int low, int high) {
|
||||
for (Cell cell : surface.cellsAt(x, z)) if (low <= cell.carveTop() + 4
|
||||
&& high >= cell.bedY() - cell.sedimentDepth() - 5) return false;
|
||||
for (Cell cell : acceptedCells) if (cell.x() == x && cell.z() == z && low <= cell.carveTop() + 4
|
||||
&& high >= cell.bedY() - cell.sedimentDepth() - 5) return false;
|
||||
for (Spring spring : surface.springs()) if (inBounds(x, low, high, z, spring.flowBounds(), 6)) return false;
|
||||
for (Spill spill : spills) if (inBounds(x, low, high, z, spill.flowBounds(), 6)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean separated(Position source) {
|
||||
for (Spill spill : spills) if (Math.hypot(source.x() - spill.source().x(), source.z() - spill.source().z()) < 64) return false;
|
||||
return available(source.x(), source.z(), source.y() - 8, source.y() + 4);
|
||||
}
|
||||
|
||||
private static boolean overlapping(List<Cell> cells) {
|
||||
Map<Long, List<Cell>> columns = new HashMap<>();
|
||||
for (Cell cell : cells) {
|
||||
var existing = columns.computeIfAbsent(key(cell.x(), cell.z()), ignored -> new ArrayList<>());
|
||||
for (Cell old : existing) if (cell.bedY() - cell.sedimentDepth() - 1 <= old.carveTop()
|
||||
&& old.bedY() - old.sedimentDepth() - 1 <= cell.carveTop()) return true;
|
||||
existing.add(cell);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Flow trace(Position source, Position outlet, List<Cell> group) {
|
||||
Map<Long, List<Cell>> changes = new HashMap<>();
|
||||
Set<Position> retained = new HashSet<>();
|
||||
for (Cell cell : group) {
|
||||
changes.computeIfAbsent(key(cell.x(), cell.z()), ignored -> new ArrayList<>()).add(cell);
|
||||
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) retained.add(new Position(cell.x(), y, cell.z()));
|
||||
}
|
||||
Map<Position, Boolean> solidCache = new HashMap<>();
|
||||
Predicate<Position> open = at -> !solidCache.computeIfAbsent(at, p -> flowSolid(p, changes));
|
||||
Predicate<Position> canEnter = at -> !retained.contains(at) && open.test(at);
|
||||
Predicate<Position> hole = at -> open.test(new Position(at.x(), at.y() - 1, at.z()));
|
||||
Map<Position, Integer> reach = new HashMap<>(); Map<Position, Position> parent = new HashMap<>();
|
||||
ArrayDeque<FlowNode> queue = new ArrayDeque<>();
|
||||
reach.put(source, 7); queue.addLast(new FlowNode(source, 7)); Position lowest = source;
|
||||
while (!queue.isEmpty()) {
|
||||
FlowNode node = queue.removeFirst(); Position at = node.at();
|
||||
if (node.reach() < reach.getOrDefault(at, -1)) continue;
|
||||
if (at.y() < lowest.y()) lowest = at;
|
||||
if (reach.size() > 16000 || Math.abs(at.x() - source.x()) > 64 || Math.abs(at.z() - source.z()) > 64) return null;
|
||||
// Vanilla never replaces an existing source. Record the arriving contact,
|
||||
// but do not manufacture a route through the separately retained basin.
|
||||
if (retained.contains(at) && !at.equals(source)) continue;
|
||||
if (at.y() <= 0) continue;
|
||||
Position below = new Position(at.x(), at.y() - 1, at.z());
|
||||
if (open.test(below)) { enqueue(below, 7, at, reach, parent, queue); continue; }
|
||||
if (node.reach() <= 0) continue;
|
||||
for (Position next : downhillChoices(at, canEnter, hole))
|
||||
enqueue(next, node.reach() - 1, at, reach, parent, queue);
|
||||
}
|
||||
if (!reach.containsKey(outlet)) return null;
|
||||
int minX = source.x(), maxX = minX, minY = source.y(), maxY = minY, minZ = source.z(), maxZ = minZ;
|
||||
for (Position at : reach.keySet()) {
|
||||
minX = Math.min(minX, at.x()); maxX = Math.max(maxX, at.x()); minY = Math.min(minY, at.y()); maxY = Math.max(maxY, at.y());
|
||||
minZ = Math.min(minZ, at.z()); maxZ = Math.max(maxZ, at.z());
|
||||
}
|
||||
List<Position> path = new ArrayList<>();
|
||||
for (Position at = lowest; at != null; at = parent.get(at)) path.add(at);
|
||||
Collections.reverse(path);
|
||||
return new Flow(Set.copyOf(reach.keySet()), List.copyOf(path), new FlowBounds(minX, minY, minZ, maxX, maxY, maxZ));
|
||||
}
|
||||
|
||||
private boolean flowSolid(Position at, Map<Long, List<Cell>> changes) {
|
||||
for (Cell cell : changes.getOrDefault(key(at.x(), at.z()), List.of()))
|
||||
if (at.y() > cell.bedY() && at.y() <= cell.carveTop()) return false;
|
||||
Cell surfaceCell = surface.cellAt(at.x(), at.y(), at.z());
|
||||
if (surfaceCell != null && at.y() > surfaceCell.bedY() && at.y() <= surfaceCell.carveTop()) return false;
|
||||
return solid(at.x(), at.y(), at.z());
|
||||
}
|
||||
|
||||
private static boolean reaches(Flow flow, Pool pool) {
|
||||
return flow.volume().stream().anyMatch(at -> {
|
||||
Cell cell = pool.cells().get(key(at.x(), at.z()));
|
||||
return cell != null && at.y() > cell.bedY() && at.y() <= cell.waterY();
|
||||
});
|
||||
}
|
||||
|
||||
private static Feature feature(long id, Pool pool, Position center) {
|
||||
int minX = Integer.MAX_VALUE, minZ = minX, maxX = Integer.MIN_VALUE, maxZ = maxX;
|
||||
for (Cell cell : pool.cells().values()) {
|
||||
minX = Math.min(minX, cell.x()); maxX = Math.max(maxX, cell.x()); minZ = Math.min(minZ, cell.z()); maxZ = Math.max(maxZ, cell.z());
|
||||
}
|
||||
return new Feature(id, Kind.TERRACE, center.y(), List.of(new Point(center.x(), center.z())), pool.cells().size(),
|
||||
new Bounds(minX, minZ, maxX, maxZ));
|
||||
}
|
||||
|
||||
private boolean solid(int x, int y, int z) { return sampler.sample(x, y, z) > 0; }
|
||||
private double noise(int x, int z, int scale) {
|
||||
int gx = Math.floorDiv(x, scale), gz = Math.floorDiv(z, scale);
|
||||
double tx = smooth(Math.floorMod(x, scale) / (double) scale), tz = smooth(Math.floorMod(z, scale) / (double) scale);
|
||||
double a = random(seed, gx, gz) * (1 - tx) + random(seed, gx + 1, gz) * tx;
|
||||
double b = random(seed, gx, gz + 1) * (1 - tx) + random(seed, gx + 1, gz + 1) * tx;
|
||||
return a * (1 - tz) + b * tz;
|
||||
}
|
||||
}
|
||||
|
||||
/** WaterFluid's bare-rock downhill choice: consider holes through four recursive
|
||||
* steps, keep only the nearest directions (including ties), spread everywhere
|
||||
* only when no direction finds a hole. Retained sources cannot be entered. */
|
||||
static List<Position> downhillChoices(Position at, Predicate<Position> canEnter, Predicate<Position> hole) {
|
||||
List<Position> selected = new ArrayList<>();
|
||||
int best = 1000;
|
||||
for (int[] d : CARDINALS) {
|
||||
Position next = new Position(at.x() + d[0], at.y(), at.z() + d[1]);
|
||||
if (!canEnter.test(next)) continue;
|
||||
int cost = hole.test(next) ? 0 : slopeDistance(next, 1, -d[0], -d[1], canEnter, hole);
|
||||
if (cost < best) selected.clear();
|
||||
if (cost <= best) { selected.add(next); best = cost; }
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
private static int slopeDistance(Position at, int distance, int backX, int backZ,
|
||||
Predicate<Position> canEnter, Predicate<Position> hole) {
|
||||
int best = 1000;
|
||||
for (int[] d : CARDINALS) {
|
||||
if (d[0] == backX && d[1] == backZ) continue;
|
||||
Position next = new Position(at.x() + d[0], at.y(), at.z() + d[1]);
|
||||
if (!canEnter.test(next)) continue;
|
||||
if (hole.test(next)) return distance;
|
||||
if (distance < 4) best = Math.min(best,
|
||||
slopeDistance(next, distance + 1, -d[0], -d[1], canEnter, hole));
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private static void enqueue(Position next, int amount, Position from, Map<Position, Integer> reach,
|
||||
Map<Position, Position> parent, ArrayDeque<FlowNode> queue) {
|
||||
if (amount <= reach.getOrDefault(next, -1)) return;
|
||||
if (!reach.containsKey(next)) parent.put(next, from);
|
||||
reach.put(next, amount); queue.addLast(new FlowNode(next, amount));
|
||||
}
|
||||
private static Set<Long> connected(Set<Long> footprint, long origin) {
|
||||
if (!footprint.contains(origin)) return Set.of();
|
||||
Set<Long> seen = new HashSet<>(); ArrayDeque<Long> queue = new ArrayDeque<>();
|
||||
seen.add(origin); queue.add(origin);
|
||||
while (!queue.isEmpty()) {
|
||||
long at = queue.removeFirst(); int x = (int) (at >> 32), z = (int) at;
|
||||
for (int[] d : CARDINALS) {
|
||||
long next = key(x + d[0], z + d[1]);
|
||||
if (footprint.contains(next) && seen.add(next)) queue.addLast(next);
|
||||
}
|
||||
}
|
||||
return seen;
|
||||
}
|
||||
private static boolean inBounds(int x, int low, int high, int z, FlowBounds b, int margin) {
|
||||
return x >= b.minX() - margin && x <= b.maxX() + margin && z >= b.minZ() - margin && z <= b.maxZ() + margin
|
||||
&& low <= b.maxY() + margin && high >= b.minY() - margin;
|
||||
}
|
||||
private static boolean inside(int x, int z) { return (long) x * x + (long) z * z < (long) RADIUS * RADIUS; }
|
||||
private static long key(int x, int z) { return ((long) x << 32) | (z & 0xffffffffL); }
|
||||
private static double smooth(double t) { return t * t * (3 - 2 * t); }
|
||||
private static double random(long seed, int x, int z) { return (mix(seed ^ key(x, z)) >>> 11) * 0x1.0p-53; }
|
||||
private static long mix(long value) {
|
||||
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
|
||||
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
|
||||
return value ^ (value >>> 31);
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,9 @@ public final class SanctuarySpawn {
|
||||
public static final ResourceKey<NoiseGeneratorSettings> RIFT_SETTINGS =
|
||||
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("sanctuary_rift"));
|
||||
|
||||
public static final ResourceKey<NoiseGeneratorSettings> CAVERN_SETTINGS =
|
||||
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("sanctuary_cavern"));
|
||||
|
||||
private SanctuarySpawn() {}
|
||||
|
||||
public static boolean usesSanctuaryGenerator(ServerLevel level) {
|
||||
@@ -43,7 +46,8 @@ public final class SanctuarySpawn {
|
||||
|| generator.generatorSettings().is(NATURAL_SETTINGS)
|
||||
|| generator.generatorSettings().is(LAYERED_SETTINGS)
|
||||
|| generator.generatorSettings().is(WOODLAND_SETTINGS)
|
||||
|| generator.generatorSettings().is(RIFT_SETTINGS));
|
||||
|| generator.generatorSettings().is(RIFT_SETTINGS)
|
||||
|| generator.generatorSettings().is(CAVERN_SETTINGS));
|
||||
}
|
||||
|
||||
/** Called only during first creation; saved spawns and /setworldspawn survive subsequent loads. */
|
||||
|
||||
@@ -27,5 +27,15 @@
|
||||
"biome.sanctuary.rift_rocky_heath": "Rocky Heath",
|
||||
"biome.sanctuary.rift_dark_grove": "Dark Undergrowth",
|
||||
"biome.sanctuary.rift_bamboo_grove": "Bamboo Grove",
|
||||
"biome.sanctuary.rift_sulfur_depths": "Sulfur Depths"
|
||||
"biome.sanctuary.rift_sulfur_depths": "Sulfur Depths",
|
||||
"biome.sanctuary.cavern_oak_forest": "Sanctuary Oak Woodland",
|
||||
"biome.sanctuary.cavern_birch_forest": "Sanctuary Birch Woodland",
|
||||
"biome.sanctuary.cavern_clearing": "Flowered Clearing",
|
||||
"biome.sanctuary.cavern_dry_woodland": "Dry Woodland",
|
||||
"biome.sanctuary.cavern_rocky_heath": "Rocky Heath",
|
||||
"biome.sanctuary.cavern_dark_grove": "Dark Undergrowth",
|
||||
"biome.sanctuary.cavern_bamboo_grove": "Bamboo Grove",
|
||||
"biome.sanctuary.cavern_sulfur_depths": "Sulfur Depths",
|
||||
"biome.sanctuary.cavern_lush_caves": "Sanctuary Lush Caves",
|
||||
"biome.sanctuary.cavern_dripstone_caves": "Sanctuary Dripstone Caves"
|
||||
}
|
||||
|
||||
@@ -27,5 +27,15 @@
|
||||
"biome.sanctuary.rift_rocky_heath": "Lande rocheuse",
|
||||
"biome.sanctuary.rift_dark_grove": "Sous-bois sombre",
|
||||
"biome.sanctuary.rift_bamboo_grove": "Bosquet de bambous",
|
||||
"biome.sanctuary.rift_sulfur_depths": "Profondeurs soufrées"
|
||||
"biome.sanctuary.rift_sulfur_depths": "Profondeurs soufrées",
|
||||
"biome.sanctuary.cavern_oak_forest": "Chênaie de Sanctuary",
|
||||
"biome.sanctuary.cavern_birch_forest": "Bétulaie de Sanctuary",
|
||||
"biome.sanctuary.cavern_clearing": "Clairière fleurie",
|
||||
"biome.sanctuary.cavern_dry_woodland": "Bois sec",
|
||||
"biome.sanctuary.cavern_rocky_heath": "Lande rocheuse",
|
||||
"biome.sanctuary.cavern_dark_grove": "Sous-bois sombre",
|
||||
"biome.sanctuary.cavern_bamboo_grove": "Bosquet de bambous",
|
||||
"biome.sanctuary.cavern_sulfur_depths": "Profondeurs soufrées",
|
||||
"biome.sanctuary.cavern_lush_caves": "Grottes luxuriantes de Sanctuary",
|
||||
"biome.sanctuary.cavern_dripstone_caves": "Grottes de spéléothèmes de Sanctuary"
|
||||
}
|
||||
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.sparse_jungle"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:wolf",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 2
|
||||
},
|
||||
"weight": 8
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.95,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:brown_mushroom_normal",
|
||||
"minecraft:red_mushroom_normal",
|
||||
"sanctuary:cavern_groves",
|
||||
"sanctuary:cavern_decorations",
|
||||
"sanctuary:cavern_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.8
|
||||
}
|
||||
+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.7,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:forest_flowers",
|
||||
"minecraft:wildflowers_birch_forest",
|
||||
"minecraft:trees_birch",
|
||||
"minecraft:patch_bush",
|
||||
"minecraft:flower_default",
|
||||
"minecraft:patch_grass_forest",
|
||||
"minecraft:brown_mushroom_normal",
|
||||
"minecraft:red_mushroom_normal",
|
||||
"minecraft:patch_pumpkin",
|
||||
"minecraft:patch_sugar_cane",
|
||||
"minecraft:patch_firefly_bush_near_water",
|
||||
"sanctuary:cavern_groves",
|
||||
"sanctuary:cavern_decorations",
|
||||
"sanctuary:cavern_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.7
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.meadow"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:donkey",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 2,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 1
|
||||
},
|
||||
{
|
||||
"type": "minecraft:rabbit",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 2
|
||||
},
|
||||
"weight": 2
|
||||
},
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 2
|
||||
},
|
||||
"weight": 2
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.75,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:patch_tall_grass_2",
|
||||
"minecraft:patch_grass_meadow",
|
||||
"minecraft:flower_meadow",
|
||||
"minecraft:trees_meadow",
|
||||
"minecraft:wildflowers_meadow",
|
||||
"minecraft:flower_flower_forest",
|
||||
"sanctuary:cavern_groves",
|
||||
"sanctuary:cavern_decorations",
|
||||
"sanctuary:cavern_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.7
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.forest"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.9,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:brown_mushroom_normal",
|
||||
"minecraft:red_mushroom_normal",
|
||||
"sanctuary:cavern_groves",
|
||||
"sanctuary:cavern_decorations",
|
||||
"sanctuary:cavern_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.7
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.forest"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.55,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:brown_mushroom_normal",
|
||||
"minecraft:red_mushroom_normal",
|
||||
"sanctuary:cavern_groves",
|
||||
"sanctuary:cavern_decorations",
|
||||
"sanctuary:cavern_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.5
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.forest"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
},
|
||||
{
|
||||
"type": "minecraft:wolf",
|
||||
"count": 4,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.4,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:forest_flowers",
|
||||
"sanctuary:rift_sparse_trees",
|
||||
"minecraft:patch_bush",
|
||||
"minecraft:flower_default",
|
||||
"minecraft:patch_grass_forest",
|
||||
"minecraft:brown_mushroom_normal",
|
||||
"minecraft:red_mushroom_normal",
|
||||
"sanctuary:cavern_groves",
|
||||
"sanctuary:cavern_decorations",
|
||||
"sanctuary:cavern_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.75
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.forest"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.9,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:brown_mushroom_normal",
|
||||
"minecraft:red_mushroom_normal",
|
||||
"sanctuary:cavern_groves",
|
||||
"sanctuary:cavern_decorations",
|
||||
"sanctuary:cavern_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.5
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.forest"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
},
|
||||
{
|
||||
"type": "minecraft:wolf",
|
||||
"count": 4,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.8,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:forest_flowers",
|
||||
"minecraft:trees_birch_and_oak_leaf_litter",
|
||||
"minecraft:patch_bush",
|
||||
"minecraft:flower_default",
|
||||
"minecraft:patch_grass_forest",
|
||||
"minecraft:brown_mushroom_normal",
|
||||
"minecraft:red_mushroom_normal",
|
||||
"minecraft:patch_pumpkin",
|
||||
"minecraft:patch_sugar_cane",
|
||||
"minecraft:patch_firefly_bush_near_water",
|
||||
"sanctuary:cavern_groves",
|
||||
"sanctuary:cavern_decorations",
|
||||
"sanctuary:cavern_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.7
|
||||
}
|
||||
+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.65,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"sanctuary:cavern_groves",
|
||||
"sanctuary:cavern_decorations",
|
||||
"sanctuary:cavern_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.7
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.forest"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
},
|
||||
{
|
||||
"type": "minecraft:wolf",
|
||||
"count": 4,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.5,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"sanctuary:cavern_groves",
|
||||
"sanctuary:cavern_decorations",
|
||||
"sanctuary:cavern_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.8
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"type": "minecraft:geode",
|
||||
"blocks": {
|
||||
"alternate_inner_layer_provider": {
|
||||
"id": "minecraft:budding_amethyst"
|
||||
},
|
||||
"cannot_replace": "#minecraft:features_cannot_replace",
|
||||
"filling_provider": {
|
||||
"id": "minecraft:air"
|
||||
},
|
||||
"inner_layer_provider": {
|
||||
"id": "minecraft:amethyst_block"
|
||||
},
|
||||
"inner_placements": [
|
||||
"minecraft:small_amethyst_bud",
|
||||
"minecraft:medium_amethyst_bud",
|
||||
"minecraft:large_amethyst_bud",
|
||||
"minecraft:amethyst_cluster"
|
||||
],
|
||||
"invalid_blocks": "#minecraft:geode_invalid_blocks",
|
||||
"middle_layer_provider": {
|
||||
"id": "minecraft:calcite"
|
||||
},
|
||||
"outer_layer_provider": {
|
||||
"id": "minecraft:smooth_basalt"
|
||||
}
|
||||
},
|
||||
"crack": {
|
||||
"generate_crack_chance": 1.0
|
||||
},
|
||||
"invalid_blocks_threshold": 1,
|
||||
"layers": {},
|
||||
"outer_wall_distance": 1,
|
||||
"use_alternate_layer0_chance": 0.2,
|
||||
"distribution_points": 2,
|
||||
"point_offset": 1,
|
||||
"min_gen_offset": -5,
|
||||
"max_gen_offset": 7
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"type": "minecraft:sequence",
|
||||
"sequence": [
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:cavern_strata",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": -0.3,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:blackstone"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:cavern_strata",
|
||||
"min_threshold": 0.3,
|
||||
"max_threshold": 1000,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:basalt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:deepslate"
|
||||
}
|
||||
]
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"type": "minecraft:sequence",
|
||||
"sequence": [
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 136
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": "sanctuary:cavern_deep_strata"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 152
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:cavern_depth_boundary",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": 0.45,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": "sanctuary:cavern_deep_strata"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 168
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:cavern_depth_boundary",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": -0.45,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": "sanctuary:cavern_deep_strata"
|
||||
}
|
||||
},
|
||||
"sanctuary:cavern_upper_strata"
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:sequence",
|
||||
"sequence": [
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:cavern_strata",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": -0.36,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:cobblestone"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:cavern_strata",
|
||||
"min_threshold": 0.36,
|
||||
"max_threshold": 1000,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:packed_mud"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:cavern_strata",
|
||||
"min_threshold": -0.14,
|
||||
"max_threshold": 0.14,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:mossy_cobblestone"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:tuff"
|
||||
}
|
||||
]
|
||||
}
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
{
|
||||
"type": "minecraft:sequence",
|
||||
"sequence": [
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:biome",
|
||||
"biome_is": "sanctuary:cavern_sulfur_depths"
|
||||
},
|
||||
"then_run": "sanctuary:cavern_sulfur_strata"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 136
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": "sanctuary:cavern_deep_strata"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 152
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:cavern_depth_boundary",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": 0.45,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": "sanctuary:cavern_deep_strata"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 168
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:cavern_depth_boundary",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": -0.45,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": "sanctuary:cavern_deep_strata"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:biome",
|
||||
"biome_is": "sanctuary:cavern_lush_caves"
|
||||
},
|
||||
"then_run": "sanctuary:cavern_humid_strata"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:biome",
|
||||
"biome_is": "sanctuary:cavern_dark_grove"
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:sequence",
|
||||
"sequence": [
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 192
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": "sanctuary:cavern_humid_strata"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 208
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:cavern_depth_boundary",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": 0.45,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": "sanctuary:cavern_humid_strata"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 224
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:cavern_depth_boundary",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": -0.45,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": "sanctuary:cavern_humid_strata"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:biome",
|
||||
"biome_is": "sanctuary:cavern_bamboo_grove"
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:sequence",
|
||||
"sequence": [
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 192
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": "sanctuary:cavern_humid_strata"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 208
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:cavern_depth_boundary",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": 0.45,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": "sanctuary:cavern_humid_strata"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 224
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:cavern_depth_boundary",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": -0.45,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": "sanctuary:cavern_humid_strata"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:biome",
|
||||
"biome_is": "sanctuary:cavern_rocky_heath"
|
||||
},
|
||||
"then_run": "sanctuary:cavern_upper_strata"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:cavern_exposure",
|
||||
"min_threshold": 0.85,
|
||||
"max_threshold": 1000,
|
||||
"is_3d": false
|
||||
},
|
||||
"then_run": "sanctuary:cavern_upper_strata"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:biome",
|
||||
"biome_is": "sanctuary:cavern_dry_woodland"
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:cavern_exposure",
|
||||
"min_threshold": 0.15,
|
||||
"max_threshold": 1000,
|
||||
"is_3d": false
|
||||
},
|
||||
"then_run": "sanctuary:cavern_upper_strata"
|
||||
}
|
||||
},
|
||||
"sanctuary:starter_island",
|
||||
"sanctuary:cavern_upper_strata"
|
||||
]
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"type": "minecraft:sequence",
|
||||
"sequence": [
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:cavern_strata",
|
||||
"min_threshold": -0.15,
|
||||
"max_threshold": 0.55,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:sulfur"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:cavern_strata",
|
||||
"min_threshold": -0.55,
|
||||
"max_threshold": -0.3,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:cinnabar"
|
||||
}
|
||||
},
|
||||
"sanctuary:cavern_depth_strata"
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:sequence",
|
||||
"sequence": [
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:cavern_strata",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": -0.36,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:granite"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:cavern_strata",
|
||||
"min_threshold": 0.36,
|
||||
"max_threshold": 1000,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:diorite"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:cavern_strata",
|
||||
"min_threshold": -0.14,
|
||||
"max_threshold": 0.14,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:andesite"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:stone"
|
||||
}
|
||||
]
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"base_amplitude": 1.0,
|
||||
"base_octave": -6,
|
||||
"octave_count": 2
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"base_amplitude": 1.0,
|
||||
"base_octave": -7,
|
||||
"octave_count": 2
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"base_amplitude": 1.0,
|
||||
"base_octave": -6,
|
||||
"octave_count": 2
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"default_block": "minecraft:stone",
|
||||
"default_fluid": "minecraft:air",
|
||||
"disable_mob_generation": false,
|
||||
"legacy_random_source": true,
|
||||
"material_rule": "sanctuary:cavern_island",
|
||||
"noise": {
|
||||
"height": 384,
|
||||
"min_y": 0
|
||||
},
|
||||
"noise_router": {
|
||||
"chunk_surface_level": 0.0,
|
||||
"continents": 0.0,
|
||||
"depth": 0.0,
|
||||
"erosion": 0.0,
|
||||
"final_density": "sanctuary:final_density_rift",
|
||||
"ridges": 0.0,
|
||||
"temperature": "sanctuary:temperate_variation",
|
||||
"vegetation": "sanctuary:temperate_moisture"
|
||||
},
|
||||
"sea_level": -64,
|
||||
"spawn_target": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"feature": {
|
||||
"type": "sanctuary:cavern_decorations"
|
||||
},
|
||||
"placement": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"feature": {
|
||||
"type": "sanctuary:cavern_groves"
|
||||
},
|
||||
"placement": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"feature": {
|
||||
"type": "sanctuary:cavern_shore_sugar_cane"
|
||||
},
|
||||
"placement": []
|
||||
}
|
||||
+12
-10
@@ -5,17 +5,19 @@
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": {
|
||||
"type": "sanctuary:rift_island",
|
||||
"oak_forest": "sanctuary:rift_oak_forest",
|
||||
"birch_forest": "sanctuary:rift_birch_forest",
|
||||
"clearing": "sanctuary:rift_clearing",
|
||||
"dry_woodland": "sanctuary:rift_dry_woodland",
|
||||
"dark_grove": "sanctuary:rift_dark_grove",
|
||||
"bamboo_grove": "sanctuary:rift_bamboo_grove",
|
||||
"sulfur_depths": "sanctuary:rift_sulfur_depths",
|
||||
"rocky_heath": "sanctuary:rift_rocky_heath"
|
||||
"type": "sanctuary:cavern_island",
|
||||
"oak_forest": "sanctuary:cavern_oak_forest",
|
||||
"birch_forest": "sanctuary:cavern_birch_forest",
|
||||
"clearing": "sanctuary:cavern_clearing",
|
||||
"dry_woodland": "sanctuary:cavern_dry_woodland",
|
||||
"dark_grove": "sanctuary:cavern_dark_grove",
|
||||
"bamboo_grove": "sanctuary:cavern_bamboo_grove",
|
||||
"sulfur_depths": "sanctuary:cavern_sulfur_depths",
|
||||
"rocky_heath": "sanctuary:cavern_rocky_heath",
|
||||
"lush_caves": "sanctuary:cavern_lush_caves",
|
||||
"dripstone_caves": "sanctuary:cavern_dripstone_caves"
|
||||
},
|
||||
"settings": "sanctuary:sanctuary_rift"
|
||||
"settings": "sanctuary:sanctuary_cavern"
|
||||
}
|
||||
},
|
||||
"minecraft:the_end": {
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"SurfaceHydrologyMixin",
|
||||
"LayeredSpringOutletMixin",
|
||||
"WoodlandSpringOutletMixin",
|
||||
"RiftSpringOutletMixin"
|
||||
"RiftSpringOutletMixin",
|
||||
"CavernSpringOutletMixin"
|
||||
],
|
||||
"client": [
|
||||
"client.SanctuarySkyMixin",
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.CavernHydrology.*;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
|
||||
/** Geometry tests complement engine checks of generated fluids, plants and post-processing. */
|
||||
public final class CavernHydrologySmoke {
|
||||
private CavernHydrologySmoke() {}
|
||||
|
||||
public static void main(String[] args) {
|
||||
verifyDownhillChoice();
|
||||
var empty = CavernHydrology.create(0, (x, y, z) -> -1);
|
||||
check(empty.cells().isEmpty() && empty.terraces().isEmpty() && empty.spills().isEmpty(), "void must stay empty");
|
||||
Sampler ledges = (x, y, z) -> {
|
||||
if (Math.hypot(x, z) >= 160) return -1;
|
||||
int floor = x <= 0 ? 144 : x <= 24 ? 120 : 96;
|
||||
return y >= 40 && y <= floor || y >= 200 && y <= 220 ? 1 : -1;
|
||||
};
|
||||
var first = CavernTerraces.create(0, ledges, empty);
|
||||
check(!first.terraces().isEmpty(), "natural reference ledges must create connected terraces");
|
||||
check(first.equals(CavernTerraces.create(0, ledges, empty)), "same seed must replay immutable terrace geometry");
|
||||
check(!first.equals(CavernTerraces.create(42, ledges, empty)), "seed must affect terrace positions");
|
||||
verify(first, ledges);
|
||||
Sampler steppedSlope = (x, y, z) -> {
|
||||
if (Math.hypot(x, z) >= 160) return -1;
|
||||
int floor = 144 - Math.min(4, Math.max(0, Math.floorDiv(x + 2, 3)));
|
||||
return y >= 40 && y <= floor || y >= 200 && y <= 220 ? 1 : -1;
|
||||
};
|
||||
var stepped = CavernTerraces.create(0, steppedSlope, empty);
|
||||
check(!stepped.terraces().isEmpty(), "gentle natural steps must lead to a lower receiving pool");
|
||||
verify(stepped, steppedSlope);
|
||||
check(stepped.spills().stream().filter(s -> s.toFeatureId() != -1).anyMatch(s ->
|
||||
steppedSlope.sample(s.outlet().x(), s.outlet().y() - 1, s.outlet().z()) > 0),
|
||||
"reference drainage must cross a shallow first impact, not require an immediate shaft");
|
||||
var noRoof = CavernTerraces.create(0, (x, y, z) -> y >= 200 ? -1 : ledges.sample(x, y, z), empty);
|
||||
check(noRoof.terraces().isEmpty(), "lower terraces must have a natural cave roof");
|
||||
var thin = CavernTerraces.create(0, (x, y, z) -> {
|
||||
int floor = x <= 0 ? 144 : x <= 24 ? 120 : 96;
|
||||
return y < floor && y > floor - 8 ? -1 : ledges.sample(x, y, z);
|
||||
}, empty);
|
||||
check(thin.terraces().isEmpty(), "perforated foundations cannot receive terraced pools");
|
||||
|
||||
// This complete plan contains surface water on the roof above the cavity. Lookups must
|
||||
// retain both levels in the same X/Z column and must never carve the natural roof away.
|
||||
Sampler stackedLedge = (x, y, z) -> {
|
||||
if (Math.hypot(x, z) >= 100 || y < 40) return -1;
|
||||
int roofTop = Math.abs(x) < 28 && Math.abs(z) < 28 ? 216 : 220;
|
||||
int floor = x <= 0 ? 144 : x <= 24 ? 120 : 96;
|
||||
boolean cavern = Math.abs(z) <= 12 && x >= -48 && x <= 48;
|
||||
return y <= roofTop && (!cavern || y <= floor || y >= 196) ? 1 : -1;
|
||||
};
|
||||
var stacked = CavernHydrology.create(0, stackedLedge);
|
||||
check(!stacked.terraces().isEmpty(), "surface water must not suppress safe lower layers");
|
||||
boolean shared = false;
|
||||
for (var cell : stacked.cells()) {
|
||||
check(stacked.cellAt(cell.x(), cell.bedY(), cell.z()).equals(cell), "height-aware lookup loses a layer");
|
||||
check(stacked.cellsInChunk(cell.x() >> 4, cell.z() >> 4).contains(cell), "chunk slice loses a layer");
|
||||
var layers = stacked.cellsAt(cell.x(), cell.z());
|
||||
shared |= layers.size() > 1;
|
||||
for (var other : layers) if (!other.equals(cell)) check(other.carveTop() < cell.bedY() - cell.sedimentDepth() - 1
|
||||
|| cell.carveTop() < other.bedY() - other.sedimentDepth() - 1, "vertical edits overlap");
|
||||
}
|
||||
check(shared, "reference world must exercise two modified layers in one X/Z column");
|
||||
for (var spill : stacked.spills()) {
|
||||
check(stacked.isSpillOpening(spill.outlet().x(), spill.outlet().y(), spill.outlet().z()), "declared outlet missing");
|
||||
check(stacked.allowsSpringFlow(spill.outlet().x(), spill.outlet().y(), spill.outlet().z()), "flow prediction omits spill");
|
||||
}
|
||||
int maxLakeDepth = stacked.cells().stream().filter(c -> stacked.features().stream()
|
||||
.anyMatch(f -> f.id() == c.featureId() && f.kind() == Kind.LAKE))
|
||||
.mapToInt(c -> c.waterY() - c.bedY()).max().orElse(0);
|
||||
check(maxLakeDepth >= 6 && maxLakeDepth <= CavernHydrology.MAX_DEPTH, "surface lake must deepen within natural support budget");
|
||||
System.out.println("CavernHydrologySmoke: linked cave terraces, gravity paths, natural shells, stacked layers and deeper surface lakes passed.");
|
||||
}
|
||||
|
||||
private static void verifyDownhillChoice() {
|
||||
Position start = new Position(0, 100, 0);
|
||||
// Two open branches have identical floors at the junction, but water must
|
||||
// choose the east drop two steps away over the west drop four steps away.
|
||||
var nearer = CavernTerraces.downhillChoices(start, p -> p.z() == 0 && Math.abs(p.x()) <= 5,
|
||||
p -> p.x() == 2 || p.x() == -4);
|
||||
check(nearer.equals(java.util.List.of(new Position(1, 100, 0))),
|
||||
"a longer competing drop must not invent a water route into its receiving basin");
|
||||
var tied = CavernTerraces.downhillChoices(start, p -> p.z() == 0 && Math.abs(p.x()) <= 5,
|
||||
p -> Math.abs(p.x()) == 2);
|
||||
check(new HashSet<>(tied).equals(java.util.Set.of(new Position(1, 100, 0), new Position(-1, 100, 0))),
|
||||
"equally near drops must retain both vanilla branches");
|
||||
var closed = CavernTerraces.downhillChoices(start, p -> p.z() == 0 && Math.abs(p.x()) <= 5, p -> false);
|
||||
check(new HashSet<>(closed).equals(new HashSet<>(tied)), "flat floor without a nearby drop spreads to both sides");
|
||||
}
|
||||
|
||||
private static void verify(CavernTerraces.Result result, Sampler density) {
|
||||
Map<Long, Feature> features = new HashMap<>();
|
||||
result.features().forEach(feature -> features.put(feature.id(), feature));
|
||||
for (var group : result.terraces()) {
|
||||
check(group.basinIds().size() == 2, "two retained steps per group");
|
||||
var upper = features.get(group.basinIds().get(0)); var lower = features.get(group.basinIds().get(1));
|
||||
check(upper.waterY() - lower.waterY() >= 4, "terraces require distinct gravity levels");
|
||||
var spill = result.spills().stream().filter(s -> s.fromFeatureId() == upper.id() && s.toFeatureId() == lower.id())
|
||||
.findFirst().orElseThrow(() -> new AssertionError("missing inter-pool cascade"));
|
||||
check(spill.flowPath().getFirst().equals(spill.source()), "cascade root must be retained basin water");
|
||||
check(spill.flowPath().stream().anyMatch(p -> result.cells().stream().anyMatch(c ->
|
||||
c.featureId() == lower.id() && c.x() == p.x() && c.z() == p.z() && p.y() > c.bedY() && p.y() <= c.waterY())),
|
||||
"cascade must actually reach the lower retained pool");
|
||||
}
|
||||
var openings = new HashSet<Position>(); result.spills().forEach(spill -> openings.add(spill.outlet()));
|
||||
for (var cell : result.cells()) {
|
||||
check(cell.waterY() - cell.bedY() >= 1 && cell.waterY() - cell.bedY() <= CavernTerraces.MAX_DEPTH, "shallow lower depth");
|
||||
check(cell.carveTop() - cell.bedY() <= CavernTerraces.MAX_CARVE, "limited excavation");
|
||||
for (int y = cell.bedY() - cell.sedimentDepth() - 1; y <= cell.bedY(); y++)
|
||||
check(density.sample(cell.x(), y, cell.z()) > 0, "natural foundation and sediment support");
|
||||
boolean roof = false;
|
||||
for (int y = cell.carveTop() + 5; y < 384; y++) roof |= density.sample(cell.x(), y, cell.z()) > 0;
|
||||
check(roof, "cavity roof must remain natural");
|
||||
for (int[] d : new int[][]{{1,0},{-1,0},{0,1},{0,-1}}) {
|
||||
int x = cell.x() + d[0], z = cell.z() + d[1];
|
||||
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) {
|
||||
final int sy = y;
|
||||
boolean adjacentWater = result.cells().stream().anyMatch(c -> c.x() == x && c.z() == z
|
||||
&& c.waterY() == cell.waterY() && sy > c.bedY() && sy <= c.waterY());
|
||||
check(adjacentWater || density.sample(x, y, z) > 0 || openings.contains(new Position(x,y,z)),
|
||||
"every exterior fluid face must be natural rock or an explicitly declared outlet");
|
||||
}
|
||||
}
|
||||
}
|
||||
for (var spill : result.spills()) {
|
||||
var path = spill.flowPath();
|
||||
for (int i = 1; i < path.size(); i++) {
|
||||
var a = path.get(i - 1); var b = path.get(i);
|
||||
check(Math.abs(a.x() - b.x()) + Math.abs(a.y() - b.y()) + Math.abs(a.z() - b.z()) == 1 && b.y() <= a.y(),
|
||||
"gravity connection must traverse adjacent non-ascending voxels");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void check(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.HashSet;
|
||||
|
||||
/** Pure geometry regressions; actual fluid propagation is covered separately in Minecraft. */
|
||||
public final class CavernLavaSmoke {
|
||||
private CavernLavaSmoke() {}
|
||||
|
||||
public static void main(String[] args) {
|
||||
CavernHydrology.Plan water = CavernHydrology.create(0, (x, y, z) -> -1);
|
||||
CavernLavaDeposit.Plan empty = CavernLavaDeposit.create(0, (x, y, z) -> -1, water);
|
||||
check(empty.cells().isEmpty() && empty.falls().isEmpty(), "void cannot create lava sites");
|
||||
check(CavernLavaFlows.create(0, (x, y, z) -> 1, water, empty).isEmpty(),
|
||||
"sealed rock cannot create an outlet");
|
||||
|
||||
CavernHydrology.Sampler wall = (x, y, z) ->
|
||||
x <= -32 && x >= -176 && Math.abs(z) <= 152 && y >= 48 && y <= 200 ? 1 : -1;
|
||||
var plan = CavernLavaDeposit.create(42, wall, water);
|
||||
check(plan.falls().size() == 2, "two natural wall outlets expected on reference wall");
|
||||
check(plan.falls().equals(CavernLavaDeposit.create(42, wall, water).falls()), "same seed must reproduce all sites");
|
||||
check(!plan.falls().equals(CavernLavaDeposit.create(43, wall, water).falls()), "seed must affect selection");
|
||||
check(plan.lavaCells().size() == 2 && plan.cells().size() == 2,
|
||||
"each seep writes only its rock source, never a filled falling column");
|
||||
for (var cell : plan.cells()) check(wall.sample(cell.x(), cell.y(), cell.z()) > 0,
|
||||
"every changed cell must start as natural rock");
|
||||
var supports = new HashSet<>(plan.supports());
|
||||
for (var support : supports) check(wall.sample(support.x(), support.y(), support.z()) > 0,
|
||||
"source protection must be existing rock");
|
||||
for (var fall : plan.falls()) {
|
||||
validate(fall, wall);
|
||||
check(fall.flowBounds().minY() == 0, "reference wall must allow a cascade into the void");
|
||||
check(!supports.contains(fall.source()), "source itself must not be an unchanged support");
|
||||
check(plan.fallsInChunk(fall.source().x() >> 4, fall.source().z() >> 4).contains(fall),
|
||||
"each source belongs to its own chunk");
|
||||
check(plan.cellsInChunk(fall.source().x() >> 4, fall.source().z() >> 4).stream()
|
||||
.anyMatch(c -> c.x() == fall.source().x() && c.y() == fall.source().y() && c.z() == fall.source().z()),
|
||||
"source chunk must receive its lava cell");
|
||||
}
|
||||
check(plan.flowSources().size() == 2 && plan.flowBounds().size() == 2, "flow metadata must cover every source");
|
||||
|
||||
// The shelf extends well beyond the three-block lava reach. A water-like reach of seven
|
||||
// would incorrectly predict a spill over its edge, rather than the small retained puddle.
|
||||
CavernHydrology.Sampler shelf = (x, y, z) -> {
|
||||
boolean upperWall = x <= -32 && x >= -176 && Math.abs(z) <= 152 && y >= 120 && y <= 200;
|
||||
boolean lowerShelf = x >= -216 && x <= -24 && Math.abs(z) <= 216 && y == 100;
|
||||
return upperWall || lowerShelf ? 1 : -1;
|
||||
};
|
||||
var shelfFalls = CavernLavaFlows.create(42, shelf, water, empty);
|
||||
check(!shelfFalls.isEmpty(), "reference shelf must receive lava");
|
||||
for (var fall : shelfFalls) {
|
||||
validate(fall, shelf);
|
||||
check(fall.flowBounds().minY() == 101, "lava must stop on a wide natural shelf within reach three");
|
||||
}
|
||||
check(CavernLavaFlows.HORIZONTAL_REACH == 3, "non-ultrawarm lava reach");
|
||||
lowerPocketPriority(water);
|
||||
System.out.println("CavernLavaSmoke: seeded deep rock sources, existing supports, chunk ownership and three-block lava reach passed.");
|
||||
}
|
||||
|
||||
private static void lowerPocketPriority(CavernHydrology.Plan water) {
|
||||
CavernHydrology.Sampler layeredCliff = (x, y, z) -> {
|
||||
if (Math.hypot(x, z) >= 220 || y < 64) return -1;
|
||||
if (x <= 0) return y <= 260 ? 1 : -1;
|
||||
return y <= 120 || y >= 200 && y <= 220 ? 1 : -1;
|
||||
};
|
||||
var plan = CavernLavaDeposit.create(0, layeredCliff, water);
|
||||
var entry = plan.access().orElseThrow(() -> new AssertionError("reference cave should contain a pocket"));
|
||||
check(entry.y() == 121, "lower cave pocket must take priority over the available upper ledge");
|
||||
check(layeredCliff.sample(entry.x(), 220, entry.z()) > 0, "lower access must retain its natural ceiling");
|
||||
check(plan.lavaCells().size() - plan.falls().size() == 18, "lower pocket keeps exactly eighteen sources");
|
||||
for (int y = entry.y(); y < entry.y() + 3; y++) check(layeredCliff.sample(entry.x(), y, entry.z()) <= 0,
|
||||
"lower pocket entrance must have three blocks of natural headroom");
|
||||
for (int y = entry.y() - 3; y < entry.y(); y++) check(layeredCliff.sample(entry.x(), y, entry.z()) > 0,
|
||||
"lower pocket entrance must stand on three natural blocks");
|
||||
var changed = new HashSet<CavernLavaDeposit.Position>();
|
||||
for (var cell : plan.cells()) {
|
||||
check(layeredCliff.sample(cell.x(), cell.y(), cell.z()) > 0, "lower chamber only removes existing rock");
|
||||
changed.add(new CavernLavaDeposit.Position(cell.x(), cell.y(), cell.z()));
|
||||
}
|
||||
for (var support : plan.supports()) {
|
||||
check(layeredCliff.sample(support.x(), support.y(), support.z()) > 0 && !changed.contains(support),
|
||||
"lower pocket shell must remain natural and unchanged");
|
||||
}
|
||||
CavernHydrology.Sampler upperOnly = (x, y, z) -> Math.hypot(x, z) < 220
|
||||
&& y >= 100 && y <= (x <= 0 ? 240 : 200) ? 1 : -1;
|
||||
var fallback = CavernLavaDeposit.create(0, upperOnly, water);
|
||||
check(fallback.access().orElseThrow().y() == 201, "existing upper ledge remains a fallback when no lower site fits");
|
||||
check(fallback.lavaCells().size() - fallback.falls().size() == 18, "fallback keeps its original pocket volume");
|
||||
}
|
||||
|
||||
private static void validate(CavernLavaFlows.Fall fall, CavernHydrology.Sampler sampler) {
|
||||
check(fall.source().y() >= 40 && fall.source().y() <= 160, "source altitude");
|
||||
check(sampler.sample(fall.source().x(), fall.source().y(), fall.source().z()) > 0, "rock source");
|
||||
check(sampler.sample(fall.outlet().x(), fall.outlet().y(), fall.outlet().z()) <= 0, "naturally open outlet");
|
||||
var path = fall.flowPath();
|
||||
check(path.getFirst().equals(fall.source()) && path.get(1).equals(fall.outlet()), "explicit source/outlet path");
|
||||
check(fall.source().y() - path.getLast().y() >= 12, "genuine falling path");
|
||||
for (int i = 1; i < path.size(); i++) {
|
||||
var a = path.get(i - 1); var b = path.get(i);
|
||||
check(Math.abs(a.x() - b.x()) + Math.abs(a.y() - b.y()) + Math.abs(a.z() - b.z()) == 1,
|
||||
"flow path must use adjacent voxels");
|
||||
check(b.y() <= a.y(), "lava cannot ascend");
|
||||
check(sampler.sample(b.x(), b.y(), b.z()) <= 0, "flow must stay outside natural rock");
|
||||
var bounds = fall.flowBounds();
|
||||
check(b.x() >= bounds.minX() && b.x() <= bounds.maxX() && b.y() >= bounds.minY()
|
||||
&& b.y() <= bounds.maxY() && b.z() >= bounds.minZ() && b.z() <= bounds.maxZ(), "path within declared bounds");
|
||||
}
|
||||
}
|
||||
|
||||
private static void check(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
/** Pure climate invariants; actual decor and abundance are measured in engine diagnostics. */
|
||||
public final class CavernPaletteSmoke {
|
||||
public static void main(String[] args) {
|
||||
int oldSulfur = 0, newSulfur = 0, lush = 0, dripstone = 0;
|
||||
for (int y : new int[]{64, 112, 160, 176, 184, 216, 260}) {
|
||||
for (int v = -100; v <= 100; v += 2) for (int m = -100; m <= 100; m += 2) {
|
||||
float variation = v / 100F, moisture = m / 100F;
|
||||
var old = RiftBiomePalette.select(y, variation, moisture);
|
||||
var current = CavernBiomePalette.select(y, variation, moisture);
|
||||
if (old == RiftBiomePalette.Kind.SULFUR_DEPTHS) oldSulfur++;
|
||||
if (current == CavernBiomePalette.Kind.SULFUR_DEPTHS) {
|
||||
newSulfur++;
|
||||
require(y < 176, "Sulfur must remain below the upper woodlands");
|
||||
}
|
||||
if (current == CavernBiomePalette.Kind.LUSH_CAVES) lush++;
|
||||
if (current == CavernBiomePalette.Kind.DRIPSTONE_CAVES) dripstone++;
|
||||
if (y >= 184 && old != RiftBiomePalette.Kind.SULFUR_DEPTHS)
|
||||
require(current.name().equals(old.name()), "Existing upper forest climate must remain unchanged");
|
||||
}
|
||||
}
|
||||
require(newSulfur > 0 && newSulfur * 8 < oldSulfur, "The broad sulfur province must become localized pockets");
|
||||
require(lush > 0 && dripstone > 0, "Both lower cave climates must have a real selection domain");
|
||||
require(CavernBiomePalette.select(112, 0.1F, 0.5F) == CavernBiomePalette.Kind.LUSH_CAVES,
|
||||
"Moist lower cavities must select lush caves");
|
||||
require(CavernBiomePalette.select(112, -0.1F, -0.5F) == CavernBiomePalette.Kind.DRIPSTONE_CAVES,
|
||||
"Drier lower cavities must select dripstone caves");
|
||||
System.out.println("CavernPaletteSmoke passed: sulfur climate cells " + oldSulfur + " -> " + newSulfur
|
||||
+ "; lush=" + lush + ", dripstone=" + dripstone + " (uniform climate grid, not island abundance)");
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -51,11 +51,14 @@ 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.7.mrpack
|
||||
packwiz modrinth export --output ../Sanctuary-0.1.0-alpha.9.mrpack
|
||||
```
|
||||
|
||||
L'export contient le JAR Sanctuary local et la référence vérifiée de Fabric API.
|
||||
Il ne publie rien sur Modrinth.
|
||||
L’alpha.9 reste en préparation : produire cet export après la réussite du
|
||||
build de cette version, sans réimporter une nouvelle instance pour mettre
|
||||
à jour l’instance Sanctuary Beta existante.
|
||||
|
||||
Utiliser l'URL HTTP affichée de `pack.toml` dans packwiz-installer. Le serveur
|
||||
local doit rester actif pendant l'installation. Ce n'est pas une publication
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "0.1.0-alpha.8"
|
||||
version = "0.1.0-alpha.9"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
Reference in New Issue
Block a user