Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1e48fa969 | ||
|
|
f621f921a1 | ||
|
|
6e94c1c539 |
@@ -1,5 +1,71 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.0-alpha.6 — 2026-09-08
|
||||
|
||||
- Palette tempérée plus douce, avec des couleurs communes pour l’herbe, le
|
||||
feuillage et l’eau. Dappled Forest quitte l’île initiale et reste réservé
|
||||
aux futurs continents ; les sauvegardes alpha.5 le conservent.
|
||||
- Prairies fleuries et affleurements rocheux plus étendus, avec une répartition
|
||||
des biomes selon l’altitude. Les corniches inférieures peuvent porter de la
|
||||
mousse, des fougères, des fleurs et des petits arbustes.
|
||||
- Sources recherchées aussi dans les parois des strates inférieures. Les
|
||||
cascades peuvent rejoindre un palier ou descendre dans le vide.
|
||||
- Retrait des courts ruisseaux à niveau constant : l’île conserve ses étangs
|
||||
et petits lacs. La grande rivière avec source, chute et bassin reste à
|
||||
développer ; elle n’est pas activée dans cette livraison.
|
||||
- Nouvelle clé `sanctuary:sanctuary_layered`, avec les anciennes générations
|
||||
conservées. Créer un **nouveau monde Sanctuary** pour cet incrément.
|
||||
|
||||
Validation : dix tests serveur réussis sur chacune des graines `0`, `42` et
|
||||
`8675309`, avec 1 800 ticks de fluides, contrôles des corniches et respect des
|
||||
modifications ultérieures aux sources. `check build assemblePack` réussi.
|
||||
|
||||
## 0.1.0-alpha.5 — 2026-09-08
|
||||
|
||||
- Plages plus larges et irrégulières, en nappes de sable, gravier et roche, avec
|
||||
des transitions herbeuses. Dépôts de 3 à 5 blocs au lieu d’une seule couche.
|
||||
- Étangs et lacs moins creusés, rives variées et petites terrasses ; la canne
|
||||
à sucre reste soumise à ses conditions vanilla.
|
||||
- Quelques sources sortent de niches rocheuses naturelles à différentes
|
||||
altitudes. Leurs cascades peuvent descendre dans le vide et servir à circuler
|
||||
verticalement ; seuls les blocs sources sont posés par la génération.
|
||||
- Petite réserve de lave dans une niche rocheuse accessible lorsque le relief
|
||||
fournit un fond, des parois et une voûte adaptés.
|
||||
- Mosaïque de cinq biomes tempérés, dont Dappled Forest de Minecraft 26.3.
|
||||
- Petits filons de charbon, fer, cuivre, or, redstone, lapis et diamant à des
|
||||
altitudes adaptées à l’île. Aucun quota ajouté après comptage des ressources.
|
||||
- Nouvelle clé de génération `sanctuary:sanctuary_natural` ; anciennes versions
|
||||
de terrain et d’hydrologie conservées pour les sauvegardes existantes.
|
||||
|
||||
Créer un **nouveau monde Sanctuary** pour tester cette génération. La hauteur
|
||||
et le ciel validés en alpha.3/4 sont conservés. L’accès à l’End, les expansions
|
||||
et les grandes rivières en pente restent à développer.
|
||||
|
||||
Validation : dix tests serveur réussis sur chacune des graines 0, 42 et 8675309,
|
||||
avec 1 800 ticks réels de fluides. Les bassins et les réserves de lave restent
|
||||
stables ; les neuf cascades atteignent Y=0. `check build assemblePack` et les
|
||||
contrôles des couches de sédiments, des anciens générateurs et du pack réussissent.
|
||||
|
||||
## 0.1.0-alpha.4 — 2026-09-08
|
||||
|
||||
- Première hydrologie de surface : étangs, petits lacs et cours d’eau calmes
|
||||
adaptés au relief, avec vérification du fond et des berges.
|
||||
- Fonds de sable, gravier et argile, berges sableuses et canne à sucre
|
||||
placée après la végétation lorsque ses conditions vanilla sont réunies.
|
||||
- Hauteur, ciel et densité du terrain alpha.3 conservés ; nouveaux paramètres
|
||||
dédiés pour préserver la génération des anciennes sauvegardes.
|
||||
- Plan déterministe indépendant de l’ordre de génération des chunks, sans
|
||||
digue ajoutée, remplissage de cavité souterraine ni mer globale.
|
||||
|
||||
Créer un **nouveau monde Sanctuary** pour tester l’hydrologie. Le pack continue
|
||||
à se mettre à jour dans la même instance Prism. Les grandes rivières en pente,
|
||||
cascades, océans et eaux des futurs continents restent à développer.
|
||||
|
||||
Validation : 10/10 tests serveur réussis sur chacune des graines 0, 42 et
|
||||
8675309, avec eau contenue après simulation réelle, continuité du cours d’eau
|
||||
et canne à sucre valide. Tests de forme et de déterminisme également réussis.
|
||||
Les cartes de blocs sont inspectées ; l’aspect dans le client reste à tester.
|
||||
|
||||
## 0.1.0-alpha.3 — 2026-09-08
|
||||
|
||||
- Nouveaux mondes Sanctuary sur 384 blocs de hauteur, avec l'île relevée de
|
||||
|
||||
@@ -22,20 +22,37 @@ décrits dans la vision ne sont pas encore implémentés.
|
||||
- Mod `sanctuary` indépendant et pack Sanctuary construit avec packwiz.
|
||||
- Preset **Sanctuary** sélectionnable à la création d'un monde, avec une île
|
||||
flottante finie, un terrain naturel et un extérieur vide.
|
||||
- Forêt de départ avec végétation, minerais et animaux vanilla. Les formes de
|
||||
terrain reprennent l'approche du générateur 26.2, adaptée à l'API 26.3.
|
||||
- Île tempérée aux teintes douces : prairies fleuries, bosquets, mousse et
|
||||
affleurements rocheux, avec des biomes qui varient aussi selon l’altitude. Les
|
||||
formes de terrain reprennent le générateur 26.2, adapté à l'API 26.3.
|
||||
- Petits filons de charbon, fer, cuivre, or, redstone, lapis et diamant à des
|
||||
altitudes adaptées au terrain relevé. Les stocks se constatent après génération.
|
||||
- Spawn recherché sur le sol de l'île lors de la création. Les mondes ordinaires
|
||||
et les choix ultérieurs de spawn ne sont pas remplacés.
|
||||
- Nouveaux mondes sur 384 blocs (`Y=0..383`), île relevée de 64 blocs,
|
||||
dessous sculpté et nuages au-dessus du terrain. Les anciennes sauvegardes
|
||||
conservent leurs paramètres de génération.
|
||||
- Étangs, petits lacs, plages plus larges et dépôts de
|
||||
trois à cinq blocs de sable, gravier, pierre ou terre enherbée. La canne à
|
||||
sucre apparaît lorsque les rives respectent ses règles vanilla.
|
||||
- Les courts ruisseaux imposés sont retirés ; l’île garde ses étangs et petits
|
||||
lacs. La grande rivière traversante reste à développer.
|
||||
- Quelques sources dans des niches rocheuses naturelles, y compris aux étages
|
||||
inférieurs : Minecraft fait couler leur eau en cascades, qui peuvent descendre jusque dans le vide.
|
||||
- Une petite poche de lave couverte, accessible depuis une corniche naturelle,
|
||||
peut fournir la matière nécessaire à l’obsidienne lorsque le relief convient.
|
||||
|
||||
L'île a une emprise nominale de 512 blocs de diamètre, avec une bordure sculptée
|
||||
par des bruits liés à la seed. Aucune masse centrale n'est ajoutée pour forcer
|
||||
le terrain : le spawn cherche un emplacement naturel. Le catalogue
|
||||
TerraMix, les continents déverrouillables, les lacs et rivières, les ruines et
|
||||
TerraMix, les continents déverrouillables, les océans, les ruines et
|
||||
les autres fonctionnalités restent à construire par tickets.
|
||||
|
||||
L’alpha.6 utilise de nouveaux paramètres de génération ; créer un **nouveau monde**
|
||||
pour découvrir ses biomes par altitude et ses eaux. Les sauvegardes alpha.2
|
||||
à alpha.5 conservent leur génération. Dappled Forest est désormais réservé aux
|
||||
futurs continents et reste présent dans les anciennes sauvegardes alpha.5. La validation de cet incrément est suivie dans [Validation](docs/testing.md).
|
||||
|
||||
Le premier champ de recherche climatique est défini : nord froid, sud chaud,
|
||||
ouest sec et est humide, avec leurs combinaisons diagonales. Un outil de
|
||||
développement relève les ressources après génération ; le système d'expansion
|
||||
@@ -58,7 +75,7 @@ sera une mise à jour explicite, avec vérification des API et des sauvegardes.
|
||||
| Fabric API | 0.160.0+26.3 |
|
||||
| Fabric Loom | 1.17.20 |
|
||||
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
|
||||
| Sanctuary / pack | 0.1.0-alpha.3 |
|
||||
| Sanctuary / pack | 0.1.0-alpha.6 |
|
||||
|
||||
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
|
||||
uniquement la bibliothèque standard et repère aussi une installation Python
|
||||
@@ -85,7 +102,7 @@ décrits dans [Validation](docs/testing.md).
|
||||
|
||||
Résultats :
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.3.jar` : mod à installer avec
|
||||
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.6.jar` : mod à installer avec
|
||||
Fabric API sur la version Minecraft indiquée.
|
||||
- `build/packwiz/` : pack de développement complet, avec le mod construit et
|
||||
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
|
||||
|
||||
@@ -7,9 +7,13 @@ provenance détaillée et le périmètre du portage.
|
||||
Minecraft appartient à Mojang Studios / Microsoft. Sanctuary est un projet
|
||||
communautaire indépendant. Le dépôt ne redistribue pas le jeu.
|
||||
|
||||
Le biome de départ reprend la configuration de la forêt vanilla 26.3-pre-2,
|
||||
adaptée pour une île finie. Les références de blocs, végétation et minerais
|
||||
sont résolues par Minecraft ; leurs textures et modèles ne sont pas inclus.
|
||||
Les biomes de départ reprennent les configurations de forêt, plaine, forêt de
|
||||
bouleaux, forêt fleurie et Dappled Forest de Minecraft 26.3-pre-2, adaptées à
|
||||
une île finie. Les variantes alpha.6 réutilisent les végétations vanilla pour
|
||||
les prairies fleuries et les bosquets ; Dappled Forest reste disponible pour
|
||||
les sauvegardes alpha.5. Les règles des petits minerais sont adaptées à partir des
|
||||
features vanilla de cette même version. Les références de blocs, végétation et
|
||||
minerais sont résolues par Minecraft ; leurs textures et modèles ne sont pas inclus.
|
||||
|
||||
Fabric Loader : Apache-2.0. Fabric API : Apache-2.0. Fabric Loom et le wrapper
|
||||
Gradle : Apache-2.0. Ces projets conservent leurs auteurs, notices et licences.
|
||||
|
||||
+38
-1
@@ -118,10 +118,34 @@ Ce jalon fournit un outil de développement du terrain. L'interface d'expansion
|
||||
- Pour l'île initiale tempérée et légèrement humide : privilégier petits étangs,
|
||||
lacs de surface, rivière et berges propices à la canne à sucre, avec quelques
|
||||
plages plus sèches. Pas de grands bassins souterrains remplis d'eau.
|
||||
- L'eau ne se répand pas de manière incontrôlée dans le vide lors du chargement et des mises à jour de blocs du scénario testé.
|
||||
- Les étangs et les lacs retiennent leur eau après les mises à jour de blocs.
|
||||
Des sources rocheuses déclarées peuvent former des cascades jusqu’au vide ;
|
||||
leurs écoulements restent attribuables à ces sources, sans inondation globale.
|
||||
- Les profils du dessous et les bords du continent sont inspectés visuellement depuis les airs.
|
||||
- La génération est mesurée sur une emprise et une machine indiquées ; les valeurs observées sont consignées, sans annoncer un objectif de performance non mesuré.
|
||||
|
||||
**Incrément île initiale — alpha.4 :** première hydrologie de surface retenue,
|
||||
conservée pour les sauvegardes de cette version.
|
||||
|
||||
**Incrément île initiale — alpha.5 :** plages plus larges,
|
||||
dépôts de trois à cinq couches sur support naturel, excavation limitée,
|
||||
rives variées et sources rocheuses pouvant former des cascades jusque dans le
|
||||
vide. Le contrat et l’isolation des anciennes générations sont décrits dans
|
||||
[Génération](worldgen.md). Ce travail prépare les eaux des continents sans clore
|
||||
WG-06 : océans, grandes rivières et ouverture des continents restent à développer.
|
||||
|
||||
**Incrément île initiale — alpha.6 :** retrait des courts ruisseaux, conservation
|
||||
des bassins, présence de roche élargie et sources de paroi dans les strates
|
||||
inférieures. Les écoulements sont contrôlés dans le moteur et les anciennes
|
||||
générations sont préservées.
|
||||
|
||||
**Ticket suivant — grande rivière facultative :** chercher un long parcours qui
|
||||
épouse l’île, avec source, chute et bassin, rives de sable, gravier et argile.
|
||||
Le démontrer sur la vraie densité Minecraft puis après décoration et ticks de
|
||||
fluide, avec une graine et des coordonnées reproductibles. Les essais du
|
||||
prototype alpha.6 n’ont fourni aucun parcours retenu sur les graines de référence ;
|
||||
ce prototype n’est pas distribué. Ne pas imposer de rivière si le relief ne s’y prête pas.
|
||||
|
||||
### WG-07 — Introduire un premier biome distinct et une structure
|
||||
|
||||
**Dépendance :** WG-06.
|
||||
@@ -136,6 +160,19 @@ Ce jalon fournit un outil de développement du terrain. L'interface d'expansion
|
||||
- La reprise du catalogue TerraMix natif d'Another World 26.2 est évaluée séparément ; « plus de cent biomes » reste une ambition tant que son adaptation n'est pas vérifiée.
|
||||
- Les limites des futures zones Lost Cities et des structures uniques sont identifiées sans imposer leur livraison dans ce ticket.
|
||||
|
||||
**Incrément île initiale — alpha.5 :** cinq variantes
|
||||
tempérées à dominante forestière, dont Dappled Forest vanilla 26.3, et petits
|
||||
filons de sept minerais à des altitudes adaptées. Leurs stocks sont observés
|
||||
après génération. Cela ne clôture pas WG-07 : aucune structure de référence ni
|
||||
reprise du catalogue TerraMix n’est encore fournie. La survie initiale doit
|
||||
permettre de produire et progresser au-delà du bois et de la pierre ; l’accès
|
||||
à toute la progression Minecraft n’est pas encore garanti pour chaque seed.
|
||||
|
||||
**Ticket palette — alpha.6 :** réserver les biomes très contrastés de la 26.3,
|
||||
dont Dappled Forest, aux futurs continents. L’île combine prairies fleuries,
|
||||
bosquets et zones rocheuses, avec des teintes cohérentes et une végétation
|
||||
effective sur les corniches inférieures. Cela ne livre pas encore les continents.
|
||||
|
||||
## Réserve de thèmes futurs
|
||||
|
||||
Ces thèmes servent à retrouver la vision, pas à demander leur implémentation immédiate. On en extrait un ticket seulement lorsqu'il devient utile au prochain incrément jouable.
|
||||
|
||||
+31
-6
@@ -30,18 +30,27 @@ de sélection des biomes feront partie du ticket de génération des continents.
|
||||
Les ressources et cultures futures, notamment celles d'It's Alive !, pourront
|
||||
ainsi demander l'exploration de plusieurs climats.
|
||||
|
||||
L'île initiale utilise encore un biome de départ spécifique. Une température
|
||||
ou une humidité du biome ne constitue pas à elle seule une carte climatique
|
||||
directionnelle : cette carte et la sélection des biomes des continents restent
|
||||
des travaux distincts.
|
||||
L’alpha.5 proposait cinq biomes tempérés dont Dappled Forest. L’alpha.6 réserve
|
||||
ce dernier aux futurs continents et donne à l’île une palette plus douce de
|
||||
prairies, prairies fleuries, bosquets, corniches moussues et zones rocheuses.
|
||||
La répartition tient compte de l’altitude, y compris dans les strates inférieures.
|
||||
La variation locale et l’humidité dépendent de la graine ; elles ne
|
||||
constituent pas encore le champ de recherche directionnel des continents.
|
||||
|
||||
## Eau de surface et exploration
|
||||
|
||||
L'île de départ doit pouvoir accueillir naturellement des étangs et lacs de
|
||||
surface, des berges propices à la canne à sucre et, lorsque le relief s'y prête,
|
||||
des rivières. L'eau s'inscrit dans les creux et les chemins du terrain existant.
|
||||
Les rivières flottantes et les cascades doivent tenir compte de la continuité
|
||||
de leur lit et du vide sous les îles.
|
||||
Les bassins gardent un fond et des berges fermés. Une grande rivière reste
|
||||
facultative : elle doit suivre le relief et former un parcours lisible avec
|
||||
source, chute et bassin. Ses exutoires peuvent laisser couler l’eau dans le vide.
|
||||
Sans parcours suffisamment long, l’île conserve seulement ses petits bassins.
|
||||
L’alpha.6 livre cette variante à bassins seuls ; la grande rivière reste à développer.
|
||||
L’alpha.5 ajoute séparément quelques sources sortant d’une niche rocheuse
|
||||
naturelle : leurs cascades peuvent descendre dans le vide et servir à remonter
|
||||
verticalement. Cet écoulement est volontaire ; il ne doit pas entraîner des
|
||||
fuites sur toutes les rives.
|
||||
|
||||
Le contrat ne demande ni un bassin souterrain imposé pour obtenir un stock
|
||||
d'eau, ni une cuvette ajoutée systématiquement à une position fixe. Une surface
|
||||
@@ -76,6 +85,22 @@ rapports de continents devront conserver leur propre graine, version, emprise
|
||||
et état de génération, afin d'additionner seulement des régions connues et
|
||||
disjointes lorsque le besoin économique sera défini.
|
||||
|
||||
## Ressources de départ — depuis l’alpha.5
|
||||
|
||||
L’île vise les premières étapes de survie, les outils, l’enchantement et la
|
||||
redstone. Les sept minerais (charbon, fer, cuivre, or, redstone, lapis et diamant)
|
||||
utilisent des petites veines à des altitudes compatibles avec le terrain relevé.
|
||||
Le charbon et le fer ne reprennent plus leurs fréquences vanilla beaucoup trop
|
||||
généreuses dans cette géométrie. Une petite réserve de lave peut apparaître dans une niche rocheuse accessible
|
||||
pour les premières productions et l’accès au Nether. Le bois, la pierre, les animaux et les cultures
|
||||
renouvelables conservent leur fonctionnement Minecraft : ce réglage des minerais
|
||||
ne constitue pas une interdiction des grandes constructions.
|
||||
|
||||
Les placements restent probabilistes, sans quota de rattrapage après comptage.
|
||||
Une graine peut donc manquer d’une ressource ; la présence de tout le parcours
|
||||
Minecraft n’est pas garantie. Les structures donnant accès à l’End et leur lien
|
||||
avec l’expansion restent à concevoir.
|
||||
|
||||
## Relevé de développement disponible
|
||||
|
||||
`ResourceSurveyDiagnostics`, dans `src/gametest`, lit les chunks au statut
|
||||
|
||||
+11
-3
@@ -19,7 +19,7 @@ Après les vérifications, le commit et le push de la branche du ticket, le scri
|
||||
de publication réalise les étapes de release et de canal ci-dessous :
|
||||
|
||||
```sh
|
||||
python3 scripts/publish_pack.py --notes-file chemin/vers/notes.md --asset build/Sanctuary-0.1.0-alpha.3.mrpack
|
||||
python3 scripts/publish_pack.py --notes-file chemin/vers/notes.md --asset build/Sanctuary-0.1.0-alpha.6.mrpack
|
||||
```
|
||||
|
||||
Les options sont facultatives. Ce script **publie** sur le Git configuré dans
|
||||
@@ -38,7 +38,7 @@ La procédure complète, également utilisable manuellement :
|
||||
3. Préparer les métadonnées de la livraison, avec l'URL exacte du futur JAR :
|
||||
|
||||
```sh
|
||||
python3 scripts/pack.py release https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.3/sanctuary-0.1.0-alpha.3.jar
|
||||
python3 scripts/pack.py release https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.6/sanctuary-0.1.0-alpha.6.jar
|
||||
```
|
||||
|
||||
4. Pousser le commit source vérifié. Créer une release Gitea correspondant à ce
|
||||
@@ -115,10 +115,18 @@ n'autorisent pas à supprimer ou régénérer un monde.
|
||||
|
||||
La hauteur de 384 blocs, le dessous sculpté et le ciel de l'alpha.3 se testent
|
||||
dans un **nouveau monde** avec le preset **Sanctuary**. Les anciens mondes gardent
|
||||
leurs identifiants de génération et leurs hauteurs. Les détails des versions de génération sont dans
|
||||
leurs identifiants de génération et leurs hauteurs. L’hydrologie de l’alpha.4
|
||||
nécessite elle aussi un nouveau monde Sanctuary ; la mise à jour de l’instance
|
||||
n’ajoute pas d’eau aux sauvegardes existantes. Les nouvelles plages, cascades,
|
||||
biomes et petits filons de l’alpha.5 utilisent à leur tour un identifiant
|
||||
séparé : créer un nouveau monde Sanctuary pour les découvrir. Les détails des versions de génération sont dans
|
||||
[Génération](worldgen.md). Les changements de version Minecraft ou de format
|
||||
de sauvegarde demandent leur propre ticket et contrat de migration.
|
||||
|
||||
L’alpha.6 utilise à son tour `sanctuary:sanctuary_layered` pour ses biomes par
|
||||
altitude et sa nouvelle hydrologie. Créer un nouveau monde Sanctuary ; les
|
||||
sauvegardes alpha.5 conservent notamment leur palette avec Dappled Forest.
|
||||
|
||||
Références officielles : [installation packwiz](https://packwiz.infra.link/tutorials/installing/packwiz-installer/),
|
||||
[commandes Prism](https://prismlauncher.org/wiki/help-pages/custom-commands/),
|
||||
[bootstrap v0.0.3](https://github.com/packwiz/packwiz-installer-bootstrap/releases/tag/v0.0.3).
|
||||
|
||||
+274
-2
@@ -27,6 +27,18 @@ sculpture du bord, limite extérieure, hauteur et stabilité de la fonction
|
||||
géométrique. Elle complète les tests en jeu ; elle ne charge pas à elle
|
||||
seule les ressources de génération de Minecraft.
|
||||
|
||||
La tâche `:sanctuary:hydrologySmoke`, également incluse dans `check`, vérifie
|
||||
le planificateur sur des reliefs contrôlés : reproductibilité, variation de la
|
||||
graine, limites de profondeur et d’excavation, support naturel, parois, ruisseau
|
||||
sur plateau compatible et partition indépendante de l’ordre des chunks. Les
|
||||
scénarios vides, percés ou trop minces ne reçoivent aucun bassin de secours.
|
||||
|
||||
Les tâches `:sanctuary:naturalHydrologySmoke` et `:sanctuary:starterLavaSmoke`
|
||||
complètent ces contrôles pour l’alpha.5 : dépôts de trois à cinq couches sur
|
||||
support naturel, terrasses peu creusées, niches de source, parcours de cascade
|
||||
et petite réserve de lave avec accès sec. Elles vérifient le déterminisme et
|
||||
l’absence de remplissage de secours lorsque la géométrie ne convient pas.
|
||||
|
||||
## Tests dans le moteur Minecraft
|
||||
|
||||
`runGameTest` démarre le serveur de test headless officiel de Fabric dans
|
||||
@@ -52,7 +64,7 @@ Le framework déplace aussi le spawn vers une grille de tests située loin du ce
|
||||
Le test capture donc le spawn juste avant ce déplacement, après l'initialisation
|
||||
normale du monde, et examine les coordonnées absolues de l'île.
|
||||
|
||||
Les sept tests Sanctuary vérifient :
|
||||
Les neuf tests Sanctuary vérifient :
|
||||
|
||||
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
|
||||
@@ -68,10 +80,18 @@ Les sept tests Sanctuary vérifient :
|
||||
5. Le nouveau monde et son bruit utilisent bien 384 blocs, à partir de Y=0,
|
||||
tandis que les anciens paramètres restent à 256 blocs. Les nuages sont
|
||||
au-dessus de l'île, à Y=352,33.
|
||||
6. Les chunks complets inspectés gardent une marge vide sous l'île, à Y=0..15.
|
||||
6. Aucun plancher solide n’apparaît sous l’île, à Y=0..15. Dans les générations
|
||||
alpha.5 et alpha.6, seules les eaux des cascades déclarées peuvent traverser cette marge.
|
||||
7. Le relevé après génération compte exactement tous les états des blocs des
|
||||
chunks demandés, avec des totaux cohérents et une complétude explicite.
|
||||
Voir [Expansion et ressources](expansion.md) pour le mode complet facultatif.
|
||||
8. Le plan hydrologique se retrouve dans les vrais chunks après décoration :
|
||||
profils épais de sédiments, bassins retenus, raccords entre chunks, canne à
|
||||
sucre valide, niches rocheuses et descente réelle des cascades. Les cinq
|
||||
biomes et les minerais sont inspectés sur le terrain. Une réserve de lave
|
||||
présente conserve ses 18 sources, ses supports et son accès après simulation.
|
||||
9. Les anciennes clés gardent leur traitement d’origine. Seuls les nouveaux
|
||||
paramètres activent les biomes par altitude et les eaux de l’alpha.6.
|
||||
|
||||
La recherche de spawn parcourt l'île finie, par anneaux de quatre blocs jusqu'à
|
||||
288 blocs du centre. Elle examine d'abord la hauteur brute pour éviter de
|
||||
@@ -109,6 +129,47 @@ Référence du workflow : [tests automatiques Fabric](https://docs.fabricmc.net/
|
||||
Les signatures propres à `26.3-pre-2` ont été vérifiées dans les dépendances locales,
|
||||
car la documentation publiée vise actuellement `26.2`.
|
||||
|
||||
## Diagnostic hydrologique
|
||||
|
||||
Les essais hydrologiques demandent les chunks contenant tous les sites du plan
|
||||
et un halo d’un chunk. Ils inspectent des chunks `FULL`, avec minerais, arbres
|
||||
et plantes déjà générés, et comparent tous les blocs d’eau observés au volume
|
||||
annoncé. Leur carte montre les vrais blocs de surface ; le feuillage et les
|
||||
troncs sont masqués pour lire les berges. Il ne s’agit pas d’une capture du jeu.
|
||||
|
||||
Pour l’alpha.5, les chunks de l’île sont maintenus actifs, puis le test attend
|
||||
`ServerLevel.isPositionTickingWithEntitiesLoaded` sur chaque chunk de fluide.
|
||||
Ce prédicat, réellement utilisé par le planificateur des ticks en 26.3, exige
|
||||
à la fois le statut de simulation et le chargement des entités. `FULL` et
|
||||
`shouldTickBlocksAt` seuls ne prouvent pas que les ticks programmés s’exécutent.
|
||||
Un témoin d’eau temporaire est vérifié dans chaque chunk de source et de lave,
|
||||
puis retiré entièrement. La simulation des sites dure **1 800 ticks** après cette
|
||||
activation. La graine 0 doit notamment démontrer une cascade qui atteint `Y=0`.
|
||||
|
||||
Les fichiers `natural-hydrology-seed-<graine>-generated.json/.png` et
|
||||
`natural-hydrology-seed-<graine>-after-1800-ticks.json/.png` sont écrits dans
|
||||
`mods/sanctuary/build/run/gameTest/diagnostics/`, avec des profils PNG des
|
||||
cascades. Ils décrivent les sites, dépôts, fluides, biomes et ressources des
|
||||
chunks inspectés, ainsi que la graine et la version. Ce relevé de ressources
|
||||
reste local à ces chunks et ne représente pas le stock total de l’île.
|
||||
|
||||
Le diagnostic alpha.4 est conservé séparément, avec ses fichiers
|
||||
`surface-hydrology-…-after-200-ticks` et ses eaux entièrement retenues.
|
||||
Le nombre de blocs d’eau ne mesure pas un débit ; un résultat sans site
|
||||
`STREAM` ne valide pas une rivière, même s’il contient des lacs.
|
||||
|
||||
Pour itérer sur l’eau sans recalculer les trois grandes cartes de densité :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryDensityMaps=false -PsanctuaryResourceSurvey=off --console=plain
|
||||
```
|
||||
|
||||
Ces options désactivent seulement les exports de densité et le relevé de
|
||||
ressources général (neuf chunks ou île entière). Le diagnostic hydrologique
|
||||
garde ses propres comptes locaux. Les options gardent les assertions de densité et tous les tests de
|
||||
comportement, y compris les cartes et contrôles hydrologiques. Le parcours
|
||||
standard conserve les exports complets de densité pour la graine 0.
|
||||
|
||||
## Vérification client facultative
|
||||
|
||||
```sh
|
||||
@@ -127,6 +188,217 @@ Pour cet essai visuel en monde de développement créatif, comparer la même vue
|
||||
Y=63 ni assombrissement d'altitude au passage de Y=32. Les brouillards de l'eau,
|
||||
de la météo et des effets d'aveuglement ou d'obscurité doivent rester présents.
|
||||
|
||||
## Contrôles alpha.6
|
||||
|
||||
`layeredHydrologySmoke` vérifie les bassins sans ruisseau de secours, les
|
||||
supports des dépôts, leur déterminisme et les sources sur les faces des
|
||||
strates inférieures. Les reliefs vides, trop minces ou percés ne sont pas comblés.
|
||||
`layeredEcologySmoke` vérifie les sols et le dégagement des corniches,
|
||||
l’exclusion du sommet et du vide, ainsi que la variation verticale des biomes.
|
||||
|
||||
`LayeredHydrologyDiagnostics` inspecte les nouveaux paramètres et refuse
|
||||
Dappled Forest dans leur palette. Il vérifie les couleurs communes, les biomes
|
||||
des surfaces hautes et basses, la végétation réelle et les surfaces rocheuses.
|
||||
Les plantes vanilla sur une corniche mince sont relevées séparément : elles
|
||||
ne constituent pas une preuve des trois supports exigés par la nouvelle
|
||||
décoration moussue. Leurs coordonnées et leurs supports sont exportés.
|
||||
|
||||
Les tickets de toute l’emprise sont posés avant le chargement des chunks `FULL`.
|
||||
Le test appelle ensuite `ServerLevel.waitForEntities(chunk, 0)` sur cette même
|
||||
emprise : le serveur GameTest accélère ses ticks et un délai en ticks seul ne
|
||||
prouve pas que les lectures asynchrones d’entités sont terminées. Le prédicat
|
||||
`isPositionTickingWithEntitiesLoaded` et les témoins d’écoulement restent requis.
|
||||
|
||||
Le diagnostic simule 1 800 ticks dans les chunks actifs. Les bassins doivent
|
||||
retenir leur eau. Les écoulements extérieurs doivent être reliés à une source
|
||||
rocheuse déclarée par des connexions d’eau réelles, sans eau orpheline ni voisin
|
||||
horizontal en dehors de l’emprise inspectée. Les seuls sites de surface sont
|
||||
des étangs et des lacs ; aucune rivière n’est activée dans cette livraison.
|
||||
Les ouvertures des sources sont vérifiées après toute la décoration ; une
|
||||
source bouchée par du lichen ne peut pas satisfaire le contrôle de descente.
|
||||
Après ces mesures, un test de réentrée pose temporairement un lichen à une
|
||||
bouche dont les marqueurs de génération ont été consommés. Un nouvel appel au
|
||||
post-traitement doit le conserver ; le test restaure ensuite le bloc d’origine.
|
||||
La preuve est dans `layered-hydrology-seed-<seed>-postprocessing-reentry.json`.
|
||||
|
||||
Les cartes `layered-hydrology-seed-<seed>-<phase>*.png` montrent les surfaces,
|
||||
les corniches et les profils de sources. Les JSON associés contiennent les
|
||||
résultats et leur emprise. Ces lectures de blocs ne remplacent pas l’examen
|
||||
du paysage dans le client.
|
||||
|
||||
## Validation alpha.6 — 8 septembre 2026
|
||||
|
||||
Les dix tests serveur passent sur chacune des graines `0`, `42` et `8675309`,
|
||||
dans des mondes neufs Minecraft 26.3-pre-2 avec Java 25, sur Apple M1 à 8 Go.
|
||||
Chaque emprise est réellement active pendant 1 800 ticks mesurés. La commande
|
||||
finale `check build assemblePack`, avec les exports de densité et le relevé
|
||||
ressources général facultatifs désactivés, passe en **3 min 30 s** sur la graine 0.
|
||||
Les six tests de forme, hydrologie, lave et écologie passent également.
|
||||
|
||||
| Graine | Spawn naturel | Eau des bassins | Eau des cascades | Canne | Lave | Chunks FULL | Plantes sur corniches épaisses |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| `0` | `(0, 252, 0)` | 1 533 | 414 | 20 | 18 | 151 | 11 021 |
|
||||
| `42` | `(0, 249, 0)` | 1 376 | 913 | 4 | 18 | 149 | 6 334 |
|
||||
| `8675309` | `(0, 247, 0)` | 1 590 | 2 350 | 10 | 18 | 160 | 9 532 |
|
||||
|
||||
Ces nombres décrivent uniquement les chunks inspectés, après décoration et
|
||||
simulation. Les plantes incluent la végétation vanilla et la nouvelle décoration,
|
||||
sur des corniches avec trois supports naturels ; les plantes de corniches
|
||||
plus minces sont comptées séparément. Les cinq variantes de biome sont observées
|
||||
sur les surfaces inférieures, avec respectivement cinq, trois et deux changements
|
||||
verticaux contrôlés. Les surfaces inspectées comportent 5 805, 4 765 et 10 045
|
||||
colonnes de pierre au sommet ; il ne s’agit pas d’un quota pour l’île entière.
|
||||
|
||||
Sur la graine 0, les sources `(-112, 103, 0)` et `(-108, 227, -96)` atteignent
|
||||
Y=0 ; celle de `(-110, 148, -46)` rejoint un palier à Y=130. La première traverse
|
||||
une frontière de chunks. Sur la graine 42, la source `(-62, 151, -82)` dégage
|
||||
correctement son ouverture autrefois obstruée par du lichen et descend de
|
||||
49 blocs ; les deux autres descendent de 103 et 236 blocs. Les trois cascades
|
||||
de la graine 8675309 atteignent Y=0. Tous les écoulements observés sont reliés
|
||||
à leur source, sans eau orpheline ni dépassement horizontal de l’emprise.
|
||||
|
||||
Les bassins conservent leur eau ; les sédiments et leurs supports résistent aux
|
||||
ticks de gravité. Les trois niches de lave gardent leurs 18 sources, leur coque
|
||||
et leur accès sec. Le test de réentrée après consommation des marqueurs réussit
|
||||
sur les trois graines : une modification ultérieure de la bouche reste intacte.
|
||||
Les 52 fichiers historiques de génération suivis pour cette livraison gardent
|
||||
leur SHA-256 d’avant modification.
|
||||
|
||||
Les cartes de surface, de corniches et les profils de cascade ont été examinés.
|
||||
Ce sont des lectures de blocs ; le rendu dans le client reste à apprécier en
|
||||
jeu. La grande rivière traversante n’est pas activée : cette livraison adopte
|
||||
les petits bassins et les sources de paroi. Les ressources restent observées
|
||||
après génération, sans garantie de toute la progression Minecraft pour chaque graine.
|
||||
|
||||
## Validation alpha.5 — 8 septembre 2026
|
||||
|
||||
Les essais utilisent Minecraft 26.3-pre-2, Java 25 et le preset de production
|
||||
sur des mondes neufs, sur Apple M1 à 8 Go. Le diagnostic attend les chunks
|
||||
réellement simulables avant de mesurer 1 800 ticks de fluides.
|
||||
|
||||
Les dix tests serveur réussissent sur chacune des graines `0`, `42` et
|
||||
`8675309`. La vérification finale `check build assemblePack`, avec les cartes de
|
||||
densité et le relevé général facultatifs désactivés, réussit en **3 min 11 s**
|
||||
sur la graine 0. Les contrôles ciblés de ressources, de biomes et de fluides
|
||||
restent actifs. Les tâches de forme, d’hydrologie et de lave réussissent aussi.
|
||||
|
||||
| Graine | Spawn naturel | Eau des bassins | Eau des cascades | Canne à sucre | Lave | Chunks FULL inspectés |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| `0` | `(0, 252, 0)` | 1 930 | 1 649 | 52 | 18 | 189 |
|
||||
| `42` | `(0, 249, 0)` | 1 859 | 1 058 | 25 | 18 | 177 |
|
||||
| `8675309` | `(0, 247, 0)` | 1 830 | 3 960 | 16 | 18 | 195 |
|
||||
|
||||
Sur la graine 0, les trois sources partent de `(-108, 227, -96)`,
|
||||
`(-26, 203, -141)` et `(-82, 176, 141)` et atteignent toutes **Y=0**. Les trois
|
||||
sources des graines 42 et 8675309 atteignent également Y=0. Les bassins conservent leur
|
||||
volume ; la niche de lave conserve 18 sources et son accès praticable, sans
|
||||
incendie détecté. Le cours d’eau reste continu à travers les frontières de chunks.
|
||||
|
||||
Les dépôts ont trois à cinq couches et deux supports naturels en dessous.
|
||||
La rive sèche la plus éloignée de l’eau atteint une distance de Manhattan de
|
||||
20 blocs sur la graine 0, 28 sur la graine 42 et 27 sur la graine 8675309. Les cartes montrent des plages
|
||||
larges et asymétriques, des bandes de gravier et des interruptions par la roche
|
||||
ou l’herbe. Quelques petites poches restent anguleuses à l’échelle du bloc.
|
||||
Les profils montrent les cascades réelles ; ces lectures de blocs ne remplacent
|
||||
pas l’appréciation du relief et des couleurs dans le client.
|
||||
|
||||
Les cinq biomes sont vérifiés dans des chunks réels. Dans les **189 chunks
|
||||
inspectés de la graine 0**, le relevé compte 754 minerais de charbon, 464 de fer,
|
||||
197 de cuivre, 215 d’or, 210 de redstone, 23 de lapis et 14 de diamant. Ce sont
|
||||
des blocs présents, pas des rendements de minage ni un total pour l’île entière.
|
||||
Le bois, la pierre et les ressources renouvelables n’ont pas de plafond de
|
||||
construction ajouté. La progression complète et l’équilibrage coopératif restent
|
||||
à éprouver en partie.
|
||||
|
||||
Le calcul initial du plan hydrologique prend 23 623 ms dans la vérification
|
||||
finale de la graine 0 et 24 475 ms dans l’essai de la graine 42 ; la recherche
|
||||
de lave ajoute respectivement 1 075 ms et 1 568 ms. Le plan est mis en cache par état aléatoire de monde,
|
||||
sans charger de chunks voisins. Ces durées sont distinctes du chargement des
|
||||
chunks de diagnostic.
|
||||
|
||||
Le premier essai court a révélé un défaut du banc de test : un chunk FULL muni
|
||||
d’un ticket pouvait encore attendre ses entités et ne pas exécuter ses ticks de
|
||||
fluide. Le contrôle utilise désormais le prédicat exact du moteur et un témoin
|
||||
indépendant par chunk de source. Un autre essai a rencontré trois blocs de chêne couché vanilla au niveau
|
||||
supérieur d’un étang. Le diagnostic conserve cette décoration et distingue le
|
||||
volume d’eau prévu des sources restantes : seuls des troncs horizontaux de
|
||||
surface jouxtant une source, avec de l’eau dessous ou le fond peu profond
|
||||
plein, sont acceptés. Leur position et leur état exacts, l’appui ou l’eau sous
|
||||
le bois, les parois et le reste des sources sont vérifiés
|
||||
après simulation. Le contrôle de continuité du cours d’eau reste strict.
|
||||
Le dernier essai de la graine 8675309 conserve les 1 830 sources prévues sans
|
||||
tronc dans l’étang ; le décor vanilla peut varier avec l’ordre de génération
|
||||
des chunks, tandis que le plan Sanctuary reste identique.
|
||||
|
||||
Deux arbres sur des paliers de la troisième graine dévient aussi les cascades.
|
||||
Le trajet sur densité nue ne prévoit pas ces feuilles. Le diagnostic final
|
||||
compare cette prévision aux blocs réels, puis vérifie les composantes d’eau
|
||||
par connexions de face : toute composante hors bassin doit rejoindre une source
|
||||
déclarée. Aucune eau ne doit toucher un chunk horizontal non inspecté. L’emprise
|
||||
reste celle prévue initialement avec son halo d’un chunk, sans élargissement
|
||||
après observation des écoulements. Sur la graine 8675309, les trois composantes
|
||||
comptent 2 445, 1 152 et 363 blocs d’eau après simulation ; toutes rejoignent
|
||||
leur source et restent entièrement dans les chunks inspectés.
|
||||
Les anciennes classes et ressources de terrain et d’hydrologie sont également
|
||||
comparées octet par octet avec l’alpha.4.
|
||||
|
||||
Journaux : `build/alpha5-build.log`, `build/alpha5-seed42-final.log` et
|
||||
`build/alpha5-seed8675309-provenance.log`. Les exports JSON/PNG sont dans le dossier
|
||||
`diagnostics` décrit plus haut. Le dernier contrôle par composantes d’eau est
|
||||
exécuté sur les graines 8675309 et 0 ; l’essai réussi de la graine 42 utilisait
|
||||
encore le contrôle plus restrictif du trajet prévu sur densité nue. Le code de
|
||||
génération est identique entre ces essais.
|
||||
|
||||
## Validation alpha.4 — 8 septembre 2026
|
||||
|
||||
Les tests serveur utilisent le preset de production et des mondes neufs. Sur
|
||||
la graine `0`, le scénario exige un véritable site `STREAM` ; les autres graines
|
||||
vérifient la continuité de chaque cours d’eau qu’elles produisent, sans exiger
|
||||
qu’une graine arbitraire soit artificiellement complétée.
|
||||
|
||||
| Graine | Spawn naturel | Eau après 200 ticks | Canne à sucre | Chunks FULL inspectés | Raccords d’eau aux frontières |
|
||||
| --- | --- | ---: | ---: | ---: | ---: |
|
||||
| `0` | `(0, 251, -4)` | 1 938 | 15 | 94 | 253 |
|
||||
| `42` | `(-4, 249, -4)` | 1 501 | 24 | 94 | 213 |
|
||||
| `8675309` | `(0, 247, 0)` | 1 551 | 12 | 90 | 190 |
|
||||
|
||||
Pour la graine `0`, le cours d’eau est à **Y=250**, de **X=−48, Z=48** à
|
||||
**X=−20, Z=−20**. Son chemin suit 18 points et relie deux petites poches d’eau.
|
||||
Deux petits lacs et quatre étangs sont également présents, en comptant la poche
|
||||
de départ du cours d’eau. La graine `42` produit elle aussi un cours d’eau, à
|
||||
Y=221, de `(−72, 24)` à `(−60, −40)` en X/Z. Ce sont des observations de ces
|
||||
graines, pas un minimum promis pour toutes les îles.
|
||||
|
||||
**10/10 tests requis réussissent sur chacune des trois graines**, avec un cours
|
||||
d’eau observé sur chacune. Les contrôles comparent chaque bloc d’eau avant et après une simulation réelle
|
||||
confirmée par un témoin d’écoulement et l’activité de tous les chunks concernés.
|
||||
Les quantités d’eau restent identiques. Les cartes des vrais blocs ont été
|
||||
inspectées : les lacs sont arrondis et les chenaux restent continus. Le tracé
|
||||
des cours d’eau comporte encore des segments géométriques ; l’appréciation du
|
||||
relief, des berges et de leur aspect dans le client reste à faire par le joueur.
|
||||
Les cartes masquent la canopée et ne remplacent pas une vue en jeu.
|
||||
|
||||
Le plan initial prend **12 060 ms** pour la graine `0` et **8 743 ms** pour `42`
|
||||
et **9 755 ms** pour `8675309`, sur le poste Apple M1 à 8 Go utilisé pendant ces
|
||||
essais. Il est calculé une seule
|
||||
fois par état aléatoire de monde, sans charger les chunks voisins. La préparation
|
||||
des 94 chunks FULL de chaque diagnostic est distincte du calcul du plan et
|
||||
ne représente pas le travail requis à chaque lancement de partie.
|
||||
|
||||
Les trois exécutions serveur réussies ont été suivies de
|
||||
`./gradlew check build assemblePack -x :sanctuary:runGameTest` : le serveur déjà
|
||||
validé séparément n’est pas relancé à l’identique. Les tests de forme et
|
||||
d’hydrologie pure, la compilation et l’assemblage du pack réussissent en 2 s.
|
||||
Le JAR ne contient aucune classe de test, et le `.mrpack` contient exactement
|
||||
ce même JAR, avec les versions Minecraft et Fabric attendues.
|
||||
|
||||
Journaux : `build/alpha4-hydrology-v2-seed0.log` et
|
||||
`build/alpha4-validation-seed42.log`, `build/alpha4-validation-seed8675309.log`
|
||||
et `build/alpha4-build.log`. Les exports correspondants sont dans
|
||||
le dossier `diagnostics` décrit plus haut. Les cartes de densité, inchangées
|
||||
par l’hydrologie, ont été exportées lors des premières exécutions de ce ticket ;
|
||||
les assertions de densité restent actives pendant chaque validation.
|
||||
|
||||
## Validation alpha.3 — 8 septembre 2026
|
||||
|
||||
Sur Java 25 / Minecraft 26.3-pre-2 / Fabric Loader 0.19.5 :
|
||||
|
||||
+29
-2
@@ -51,12 +51,39 @@ Les intégrations communautaires envisagées comprennent Fabric API, Sodium, Iri
|
||||
L'île de Sanctuary est le point de départ commun. Elle flotte dans un vide qui ne doit pas être rempli par la génération habituelle du monde. Les continents suivants sont de vastes terres suspendues, créées progressivement selon une taille et une direction maîtrisées.
|
||||
|
||||
L'île initiale doit être **tempérée et légèrement humide**, avec quelques zones
|
||||
plus sèches, notamment des plages. Une rivière et de petits étangs ou lacs de
|
||||
surface peuvent accueillir de la canne à sucre. Les grands bassins souterrains
|
||||
plus sèches, notamment de larges plages et dépôts irréguliers de sable, gravier,
|
||||
pierre et terre enherbée. De petits étangs ou lacs de surface
|
||||
peuvent accueillir de la canne à sucre. Lorsque le relief le permet, une grande
|
||||
rivière suit la forme de l’île, depuis une source jusqu’à une cascade et un
|
||||
bassin, avec des rives de sable, de gravier et d’argile. Ce parcours doit former
|
||||
un lieu reconnaissable ; il reste facultatif. À défaut, l’île conserve ses
|
||||
petits bassins, sans imposer de courts ruisseaux. Le terrain est traité près de la surface,
|
||||
avec des dépôts épais et des rives variées. De rares sources sortent de niches
|
||||
rocheuses naturelles et leurs cascades peuvent descendre jusque dans le vide ;
|
||||
les bassins restent retenus et ne doivent pas provoquer une inondation globale.
|
||||
Une rivière peut avoir des échappements et des chutes déclarés. Les grands bassins souterrains
|
||||
remplis d'eau ne sont pas retenus pour cette île. Le terrain doit laisser voir un
|
||||
dessous sculpté et dégagé, dans un monde de **384 blocs de hauteur**, avec des
|
||||
nuages relevés et sans disque noir artificiel apparaissant sous un seuil d'altitude.
|
||||
|
||||
L’ambiance de départ doit rester douce et tempérée, avec des bosquets,
|
||||
des prairies fleuries et de larges zones rocheuses. Les biomes très contrastés
|
||||
de Minecraft 26.3, dont Dappled Forest, sont réservés aux futurs continents.
|
||||
Les biomes et la végétation varient aussi en hauteur : les corniches inférieures
|
||||
peuvent porter des bosquets, de la mousse et des fleurs lorsque leur surface
|
||||
s’y prête. Quelques sources de paroi et ruissellements relient ces étages.
|
||||
Les plages s’inspirent des nappes de
|
||||
sable des générations Alpha/Beta : elles peuvent relier des poches d’eau, tandis
|
||||
que d’autres rives restent rocheuses ou enherbées. Les quelques cascades servent
|
||||
également d’ascenseurs d’eau pour circuler verticalement.
|
||||
|
||||
L’île doit offrir les ressources nécessaires aux premières étapes de Minecraft,
|
||||
en quantités modestes pour pousser à explorer les expansions lors des grands
|
||||
projets. Les minerais doivent être trouvables aux altitudes du terrain. Les
|
||||
stocks sont observés après génération et les ressources renouvelables gardent
|
||||
leurs règles Minecraft. Le réglage précis de ces quantités reste à éprouver en
|
||||
partie coopérative. L’état livré figure dans [Génération](worldgen.md).
|
||||
|
||||
Le premier champ de recherche des expansions associe les **huit directions**
|
||||
autour de Sanctuary à des signatures de température et d'humidité. En regardant
|
||||
la carte avec le nord en haut : **nord froid, sud chaud, ouest sec et est humide**.
|
||||
|
||||
+245
-16
@@ -34,30 +34,35 @@ de test et les sauvegardes personnelles doivent rester distincts.
|
||||
- Le terrain naturel reste libre de laisser un trou à l'origine, de former des
|
||||
surplombs ou de se séparer en fragments. **Aucun noyau ellipsoïdal, cylindre ou
|
||||
plateforme de départ n'est ajouté.** Au-dessus de `Y=144`, le relief de la
|
||||
génération précédente est conservé, simplement relevé de 64 blocs.
|
||||
génération précédente est conservé dans la densité, simplement relevé de 64
|
||||
blocs. L’hydrologie traite ensuite les lits et les rives près de la surface.
|
||||
- Le pourtour est sculpté par retrait de matière à partir du rayon **128 blocs**.
|
||||
Le contour de référence de 256 blocs est déformé par les bruits, entre 204 et
|
||||
288 blocs ; il indique la fin de l'érosion, pas un mur de roche.
|
||||
La côte réelle apparaît plus tôt, selon la densité du terrain et la seed.
|
||||
Il reste du vide permanent au-delà de **288 blocs**, sans répétition d'îles.
|
||||
Aucun terrain ne réapparaît au-delà de **288 blocs**, sans répétition d'îles.
|
||||
Les arbres peuvent dépasser la côte rocheuse de quelques blocs.
|
||||
- Les nouveaux mondes utilisent **384 blocs de hauteur**, de `Y=0` à `Y=383`,
|
||||
pour la dimension et le générateur. La roche disparaît avant leurs limites.
|
||||
Il n'y a ni plancher de bedrock,
|
||||
ni mer globale, ni reprise du terrain à distance. Le Nether et l'End restent
|
||||
ceux de Minecraft.
|
||||
- `sanctuary:starter_forest` reprend végétation, minerais et animaux de la forêt
|
||||
vanilla 26.3. Ses blocs sont exclusivement vanilla. C'est un biome distinct
|
||||
pour ne pas hériter des tags de structures vanilla et de leurs apparitions
|
||||
possibles dans le vide.
|
||||
- Les carvers, géodes, donjons, lacs de lave et sources sont retirés de cette
|
||||
forêt initiale. Les minerais remplacent la roche existante et les arbres ont
|
||||
besoin de sol. Aucun mod de biomes externe n'est nécessaire.
|
||||
- Les nouveaux mondes utilisent une palette tempérée de prairies, de bosquets,
|
||||
de corniches moussues et de zones rocheuses, répartie aussi en hauteur.
|
||||
Dappled Forest est réservé aux futurs continents. Leur végétation et leurs animaux proviennent
|
||||
de Minecraft 26.3-pre-2. Les nouveaux identifiants ne reprennent pas les tags
|
||||
de structures vanilla, afin de ne pas placer leurs structures dans le vide.
|
||||
- Les carvers, géodes, donjons, grands lacs de lave et sources vanilla sont
|
||||
retirés. 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.
|
||||
|
||||
Ce premier prototype ne fournit pas encore d'eau : hydrologie, répartition
|
||||
complète des ressources de survie et variété de biomes constituent le prochain
|
||||
ticket de terrain. Les minerais vanilla conservent leurs plages d'altitude ;
|
||||
leur abondance n'est donc pas encore équilibrée pour une île flottante.
|
||||
L’alpha.6 associe ces biomes aux plages, aux eaux et aux petits filons décrits
|
||||
ci-dessous. Les ressources de survie ne se limitent pas au bois et à la pierre,
|
||||
mais cet incrément ne garantit pas encore une partie Minecraft complète sur
|
||||
chaque seed. L’accès à chaque progression vanilla et les ressources renouvelables
|
||||
nécessitent des vérifications concrètes. Les stocks réels sont relevés après la
|
||||
génération, sans corriger une île pour atteindre un quota.
|
||||
|
||||
## Sculpture du pourtour — conservée depuis la génération v2
|
||||
|
||||
@@ -118,8 +123,9 @@ ce gradient inférieur. Une nouvelle érosion travaille sur une transition de
|
||||
deux bruits de sculpture existants. Elle laisse les lobes naturels se poursuivre
|
||||
vers le bas, puis les amincit à différentes hauteurs ; elle n'ajoute ni socle ni
|
||||
surface garantie. La transition n'affecte plus le terrain à partir de `Y=144`.
|
||||
Une marge vide protège le bas de la dimension ; le contrôle des chunks terminés
|
||||
vérifie que la tranche `Y=0..15` ne contient aucun bloc.
|
||||
Une marge vide protège le bas de la dimension : aucun plancher rocheux ne doit
|
||||
apparaître dans `Y=0..15`. À partir de l’alpha.5, seules les eaux descendantes de
|
||||
sources explicitement déclarées peuvent traverser cette marge.
|
||||
|
||||
Les champs optionnels du codec `vertical_offset`, `height` et `underside` valent
|
||||
respectivement `0`, `256` et un bruit nul par défaut. Seule la nouvelle fonction
|
||||
@@ -134,6 +140,227 @@ ou régénéré. Il n'existe pas de migration automatique des anciens mondes ver
|
||||
dimension de 384 blocs. Changer manuellement leurs références de générateur ou
|
||||
de dimension sort de ce contrat de compatibilité.
|
||||
|
||||
## Hydrologie de surface — génération v4
|
||||
|
||||
La version **0.1.0-alpha.4** conserve la densité, la hauteur et le ciel de
|
||||
l’alpha.3. Les nouveaux mondes sélectionnent les paramètres
|
||||
`sanctuary:sanctuary_hydrology` et le biome `sanctuary:starter_forest_hydrology`.
|
||||
Le type de dimension reste `sanctuary:sanctuary_384`. Les anciens paramètres et
|
||||
le biome `sanctuary:starter_forest` restent disponibles et inchangés : ils
|
||||
n’activent pas cette hydrologie dans les sauvegardes antérieures.
|
||||
|
||||
Un plan calculé depuis le relief et la graine recherche des emplacements de
|
||||
surface pour des étangs, des petits lacs et un premier cours d’eau calme.
|
||||
Le lit peut être légèrement creusé dans la roche existante. Avant d’accepter
|
||||
un emplacement, le plan vérifie le fond et les parois en trois dimensions ainsi
|
||||
que l’ouverture vers le ciel. Un trou profond dans la carte de hauteur ne suffit
|
||||
pas à constituer un lac. Aucune digue ni masse de roche n’est ajoutée pour
|
||||
boucher une fuite, et aucun bassin souterrain n’est rempli.
|
||||
|
||||
Les fonds alternent sable, gravier et argile ; les berges basses peuvent recevoir
|
||||
du sable. Dans ce nouveau biome, les disques de sédiments vanilla et les veines
|
||||
de gravier sont remplacés par ces dépôts de rive et de fond : une feature
|
||||
ultérieure ne doit pas transformer leur support en gravier instable. Les
|
||||
sources de magma sous-marines sont également absentes. Après les arbres et la
|
||||
végétation, de la canne à sucre peut pousser sur
|
||||
les rives qui respectent ses règles de placement vanilla. Le nombre de sites,
|
||||
leur eau et leurs ressources dépendent du terrain : le générateur ne complète
|
||||
pas une graine pour atteindre un quota économique.
|
||||
|
||||
Le plan est indépendant des chunks chargés. Chaque chunk applique uniquement
|
||||
ses propres colonnes après le terrain et avant la décoration, ce qui permet de
|
||||
raccorder un même plan d’eau aux frontières des chunks. Les anciens chunks ne
|
||||
sont jamais retouchés, et aucun travail hydrologique ne se déclenche au simple
|
||||
chargement d’une sauvegarde.
|
||||
|
||||
Cette première version vise de petites eaux retenues et des cours d’eau calmes
|
||||
sur les plateaux. Le cours d’eau est un chenal à niveau constant reliant des
|
||||
plans d’eau ; il ne simule pas encore une pente vers l’aval ou un débit. Les
|
||||
tracés peuvent passer par les diagonales, mais leur eau reste connectée de
|
||||
bloc en bloc. L’excavation est limitée à six blocs, l’eau à quatre blocs de
|
||||
profondeur, avec un fond plein sur trois blocs d’épaisseur, dont deux blocs naturels sous
|
||||
le sédiment. Ce contrat alpha.4 reste celui de ses sauvegardes. Les sources en cascade sont
|
||||
ajoutées séparément dans la génération v5 ; les grandes rivières en pente, océans,
|
||||
aquifères, climats multiples et continents restent des incréments distincts.
|
||||
|
||||
## Plages, sources et mosaïque tempérée — génération v5
|
||||
|
||||
L’implémentation **0.1.0-alpha.5** sélectionne la nouvelle clé de paramètres
|
||||
`sanctuary:sanctuary_natural` (`SanctuarySpawn.NATURAL_SETTINGS`). Elle conserve
|
||||
la densité relevée, les 384 blocs de hauteur et le ciel de l’alpha.3. La classe
|
||||
`NaturalHydrology` et son application sont séparées de `SurfaceHydrology` et
|
||||
`HydrologyRuntime` : le code, les paramètres `sanctuary:sanctuary_hydrology` et
|
||||
le biome `sanctuary:starter_forest_hydrology` de l’alpha.4 restent inchangés.
|
||||
Les clés alpha.2 et alpha.3 restent également disponibles avec leur comportement
|
||||
antérieur. **Ces changements concernent uniquement les nouveaux mondes** : aucune
|
||||
conversion de sauvegarde, régénération de chunk ou expansion n’est effectuée.
|
||||
|
||||
### Dépôts et eau de surface
|
||||
|
||||
Le plan recherche des étangs, de petits lacs et un cours d’eau calme dans le
|
||||
relief naturel. L’excavation maximale passe à **quatre blocs**, pour une eau de
|
||||
**trois blocs de profondeur** au plus. Les bassins gardent un fond et des parois
|
||||
naturels pleins ; aucune digue n’est ajoutée et aucune chambre souterraine n’est
|
||||
remplie. Le cours d’eau reste à niveau constant : il ne modélise pas un débit
|
||||
ni une pente continue vers l’aval.
|
||||
|
||||
Les plages sont des nappes irrégulières qui suivent les faibles variations du
|
||||
relief autour des plans d’eau. Elles peuvent s’étendre sur plusieurs blocs,
|
||||
se rapprocher d’une autre poche d’eau et retrouver progressivement l’herbe ou
|
||||
la pierre. Les matériaux varient entre sable, gravier, pierre et terre enherbée ;
|
||||
l’argile peut apparaître dans les fonds. Le traitement d’une terrasse sèche
|
||||
retire au plus **deux blocs** du relief existant.
|
||||
|
||||
Chaque dépôt remplace **trois à cinq couches** de terrain déjà plein et garde
|
||||
**deux couches naturelles de support** en dessous. Ce support permet notamment
|
||||
au sable et au gravier de rester en place après les mises à jour de gravité.
|
||||
Les veines de gravier, disques de sédiments et magma sous-marin vanilla ne sont
|
||||
pas réintroduits dans ces nouveaux biomes. Les autres décorations rocheuses et
|
||||
les minerais peuvent encore remplacer la pierre ; ils ne remplacent pas les
|
||||
couches de sable, gravier ou argile. Les rives compatibles peuvent porter de la
|
||||
canne à sucre après la végétation. Les troncs couchés vanilla peuvent occuper
|
||||
quelques blocs de la surface d’un étang ; les contrôles distinguent cette
|
||||
décoration de l’eau restante et vérifient leur fond ou l’eau sous les troncs,
|
||||
les sources voisines et la rétention du bassin.
|
||||
|
||||
### Sources rocheuses et cascades
|
||||
|
||||
Le plan peut retenir jusqu’à **trois sources** dans des niches rocheuses
|
||||
naturelles, séparées des dépôts modifiés. Une source remplace un seul bloc de
|
||||
roche, sous un toit, sur un fond et entre trois côtés déjà pleins. Son unique
|
||||
sortie horizontale est naturellement ouverte sur une chute d’au moins huit
|
||||
blocs. Le générateur ne construit ni pylône, ni corniche, ni tunnel pour obtenir
|
||||
ce résultat.
|
||||
|
||||
Seul le bloc source est généré. Les ticks de fluide de Minecraft forment la
|
||||
cascade et ses éventuels passages sur des paliers. Ces écoulements peuvent
|
||||
descendre **jusque dans le vide**, conformément au comportement demandé. Cette
|
||||
exception concerne les sources déclarées. Le trajet calculé dans la roche nue
|
||||
est une prévision : les arbres générés ensuite peuvent dévier une chute. Les
|
||||
diagnostics vérifient donc les connexions réelles de chaque bloc d’eau vers une
|
||||
source, dans l’emprise des chunks inspectés et de leur halo. Ils échouent si de
|
||||
l’eau touche une frontière non inspectée ou si un bassin perd sa coque. Les
|
||||
déviations par rapport à la prévision restent mesurées dans le rapport. Le
|
||||
comportement des fluides reste celui de Minecraft, y compris lorsque les joueurs
|
||||
modifient une rive.
|
||||
|
||||
### Petite réserve de lave
|
||||
|
||||
`StarterLavaDeposit` recherche au plus une niche de **18 blocs sources** dans
|
||||
la roche existante, accessible par un passage sec de deux blocs de haut depuis
|
||||
une corniche naturelle. Le fond garde trois blocs pleins, les parois deux et
|
||||
la couverture huit ; ces supports sont vérifiés avant les écritures. La niche
|
||||
reste éloignée des eaux et de leurs parcours. Elle fournit de la lave prélevable
|
||||
pour les premières productions et un portail du Nether. Si aucun site ne
|
||||
convient, le plan reste vide : aucun bassin artificiel n’est ajouté dans le vide.
|
||||
|
||||
Les tests vérifient l’accès praticable, la rétention de la lave et l’absence
|
||||
d’incendie après simulation.
|
||||
|
||||
### Biomes et minerais
|
||||
|
||||
Le biome source `sanctuary:temperate_island` utilise deux champs de bruit
|
||||
initialisés par la seed pour répartir cinq variantes horizontales. Une rive et
|
||||
la roche sous elle restent dans la même variante :
|
||||
|
||||
| Identifiant | Variante |
|
||||
| --- | --- |
|
||||
| `sanctuary:temperate_forest` | Forêt dominante |
|
||||
| `sanctuary:temperate_plains` | Clairières et plaines tempérées |
|
||||
| `sanctuary:temperate_birch_forest` | Forêt de bouleaux |
|
||||
| `sanctuary:temperate_flower_forest` | Forêt fleurie |
|
||||
| `sanctuary:temperate_dappled_forest` | Dappled Forest, peupliers et végétation associée |
|
||||
|
||||
Dappled Forest est un biome vanilla introduit dans
|
||||
[Minecraft 26.3 Snapshot 1](https://www.minecraft.net/en-us/article/minecraft-26-3-snapshot-1).
|
||||
Cette variante reprend ses données de **26.3-pre-2**, avec une humidité légèrement
|
||||
relevée pour Sanctuary. Elle n’ajoute ni dépendance externe ni structure de camp.
|
||||
La palette tempérée ne constitue pas encore le catalogue TerraMix ni le champ
|
||||
climatique des futurs continents.
|
||||
|
||||
Les nouveaux biomes remplacent les filons vanilla de métaux par de petites
|
||||
features `sanctuary:island_ore_*`, à des altitudes adaptées à l’île relevée :
|
||||
|
||||
| Ressource | Taille du filon | Tentatives par chunk | Plage Y |
|
||||
| --- | --- | --- | --- |
|
||||
| Charbon | 6 | 2 | 96–288 |
|
||||
| Fer | 4 | 2 | 88–248 |
|
||||
| Cuivre | 4 | 1 | 96–248 |
|
||||
| Or | 4 | 1 | 104–224 |
|
||||
| Redstone | 4 | 1 | 96–224 |
|
||||
| Lapis | 3 | Une tentative, chance 1/2 | 112–232 |
|
||||
| Diamant | 3 | Une tentative, chance 1/4 | 104–208 |
|
||||
|
||||
Ces paramètres décrivent des tentatives, pas des quantités promises : le filon
|
||||
doit rencontrer la roche existante, et sa forme dépend du hasard de génération.
|
||||
La sélection des biomes et des eaux ne cherche aucun stock cible. Un relevé
|
||||
partiel reste un relevé partiel ; la présence des sept ressources sur une seed
|
||||
de référence ne garantit pas leur présence sur toutes les seeds.
|
||||
|
||||
La validation de l’alpha.5 est suivie dans [Validation](testing.md). Ses diagnostics
|
||||
sont conçus pour inspecter les blocs décorés, les profils des dépôts, les
|
||||
jointures de chunks, les biomes et les ressources, puis simuler réellement les
|
||||
fluides dans les chunks des sites. Les cartes et profils PNG montrent des
|
||||
lectures de blocs, pas un rendu Minecraft. La descente des
|
||||
cascades est contrôlée après activation effective des chunks qui les simulent.
|
||||
|
||||
## Prairies et corniches — génération v6
|
||||
|
||||
L’alpha.6 utilise `sanctuary:sanctuary_layered` et le biome source
|
||||
`sanctuary:layered_island`. Les formes et le ciel restent ceux de la génération
|
||||
relevée. Les anciennes classes, biomes et règles de matériaux restent disponibles
|
||||
pour les sauvegardes alpha.2 à alpha.5 ; seul le preset de création choisit v6.
|
||||
|
||||
### Une palette qui varie aussi en hauteur
|
||||
|
||||
Les cinq variantes partagent leurs teintes d’herbe et de feuillage. Les champs
|
||||
de climat liés à la seed déforment de larges bandes d’altitude, autour de Y154
|
||||
et Y214 : prairies et prairies fleuries sur le dessus, bosquets et corniches
|
||||
moussues plus bas. Des zones rocheuses traversent ces bandes et remplacent la
|
||||
couverture de terre par de la pierre sur le relief existant. Les prairies gardent
|
||||
quelques arbres épars et leur végétation vanilla. Dappled Forest n’appartient
|
||||
plus à cette palette ; son utilisation sur de futurs continents reste à développer.
|
||||
|
||||
| Identifiant | Ambiance |
|
||||
| --- | --- |
|
||||
| `sanctuary:layered_meadow` | Prairie tempérée |
|
||||
| `sanctuary:layered_flower_meadow` | Prairie fleurie |
|
||||
| `sanctuary:layered_lower_grove` | Bosquet inférieur |
|
||||
| `sanctuary:layered_mossy_terraces` | Corniches moussues |
|
||||
| `sanctuary:layered_rocky_terraces` | Affleurements rocheux |
|
||||
|
||||
`LowerTerraceVegetationFeature` parcourt aussi les sols naturels sous les
|
||||
surplombs, au moins huit blocs sous une surface supérieure. Elle demande trois
|
||||
blocs pleins de support et deux blocs libres au-dessus. Elle peut remplacer la
|
||||
surface existante par de la mousse et planter fougères, fleurs ou petits arbustes
|
||||
lorsque leurs règles de survie le permettent. Les cellules hydrologiques, les
|
||||
sources, les parcours d’eau prévus et les supports de lave sont protégés.
|
||||
Il n’y a ni corniche construite dans l’air ni ajout d’éclairage artificiel.
|
||||
|
||||
### Bassins et sources de paroi
|
||||
|
||||
`LayeredHydrology` conserve les petits bassins et les dépôts épais, avec une
|
||||
plus grande présence de pierre. Le court `STREAM` à niveau constant de l’alpha.5
|
||||
n’est pas repris. Cette version retient uniquement les étangs et les petits
|
||||
lacs ; elle ne cherche pas de grande rivière. Le parcours traversant avec
|
||||
source, cascade et bassin reste un ticket distinct, à démontrer sur le relief
|
||||
Minecraft réel. Aucun chenal de secours n’est creusé.
|
||||
|
||||
La profondeur reste limitée à trois blocs et l’incision à quatre. Le plan
|
||||
vérifie le fond et les rives avant d’appliquer ses écritures, dans les nouveaux
|
||||
chunks seulement. Le sable, le gravier, l’argile et la roche suivent les rives.
|
||||
Des niches de source sont également recherchées dans les strates inférieures,
|
||||
et pas seulement près de la surface la plus haute. Leurs chutes sont produites
|
||||
par les ticks de Minecraft à partir d’un seul bloc source. Le post-traitement
|
||||
des nouveaux chunks libère uniquement leur ouverture, déjà vide dans la densité
|
||||
naturelle, pour qu’un lichen ou une plante ne bouche pas la source. Les positions
|
||||
sont marquées dans la file vanilla pendant la génération, puis consommées une
|
||||
seule fois. Un rechargement ne dégage pas une bouche modifiée par un joueur.
|
||||
Aucune chute n’est préremplie et aucun nouveau passage n’est creusé dans la roche.
|
||||
|
||||
Les parcours, les écoulements et la végétation réellement observés, ainsi que
|
||||
les limites du modèle, sont consignés dans [Validation](testing.md).
|
||||
|
||||
## Apparition commune
|
||||
|
||||
Une injection limitée à la création initiale cherche du sol naturel depuis
|
||||
@@ -142,7 +369,9 @@ une surface stable de 3 × 3 blocs, avec deux blocs libres en hauteur. En
|
||||
l'absence d'une telle surface, elle retient une colonne de sol praticable ; si
|
||||
aucun emplacement naturel sûr n'existe, la création signale la seed concernée
|
||||
plutôt que d'ajouter une plateforme. Elle ne s'applique qu'à l'Overworld
|
||||
utilisant les paramètres `sanctuary:sanctuary`.
|
||||
utilisant les paramètres `sanctuary:sanctuary`, `sanctuary:sanctuary_384` ou
|
||||
`sanctuary:sanctuary_hydrology`, `sanctuary:sanctuary_natural` ou
|
||||
`sanctuary:sanctuary_layered`.
|
||||
Elle enregistre le spawn partagé dans la sauvegarde et ne le réinitialise pas
|
||||
au redémarrage : les modifications administratives de `/setworldspawn` restent
|
||||
conservées. Les lits et la dispersion habituelle du spawn suivent encore les
|
||||
|
||||
+2
-2
@@ -8,6 +8,6 @@ loader_version=0.19.5
|
||||
loom_version=1.17.20
|
||||
fabric_api_version=0.160.0+26.3
|
||||
|
||||
mod_version=0.1.0-alpha.3
|
||||
pack_version=0.1.0-alpha.3
|
||||
mod_version=0.1.0-alpha.6
|
||||
pack_version=0.1.0-alpha.6
|
||||
maven_group=fr.koka.sanctuary
|
||||
|
||||
@@ -34,6 +34,7 @@ tasks.named('runGameTest') {
|
||||
dependsOn('cleanGameTestWorld')
|
||||
systemProperty('sanctuary.test.seed', providers.gradleProperty('sanctuaryTestSeed').getOrElse('0'))
|
||||
systemProperty('sanctuary.test.resourceSurvey', providers.gradleProperty('sanctuaryResourceSurvey').getOrElse('sample'))
|
||||
systemProperty('sanctuary.test.densityMaps', providers.gradleProperty('sanctuaryDensityMaps').getOrElse('true'))
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -76,5 +77,56 @@ tasks.register('worldgenSmoke', JavaExec) {
|
||||
|
||||
tasks.named('check') { dependsOn('worldgenSmoke') }
|
||||
|
||||
tasks.register('hydrologySmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Check deterministic surface drainage, natural support and bounded excavation.'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.SurfaceHydrologySmoke'
|
||||
}
|
||||
|
||||
tasks.named('check') { dependsOn('hydrologySmoke') }
|
||||
|
||||
// src/test contains a JavaExec assertion harness; engine tests use Fabric's gametest source set.
|
||||
tasks.named('test') { failOnNoDiscoveredTests = false }
|
||||
|
||||
tasks.register('naturalHydrologySmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Check broad shores, supported sediments and natural rock springs.'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.NaturalHydrologySmoke'
|
||||
}
|
||||
|
||||
tasks.named('check') { dependsOn('naturalHydrologySmoke') }
|
||||
|
||||
tasks.register('starterLavaSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Check a small contained lava niche in naturally supported rock.'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.StarterLavaDepositSmoke'
|
||||
}
|
||||
|
||||
tasks.named('check') { dependsOn('starterLavaSmoke') }
|
||||
|
||||
// Alpha.6 has separate contracts so saved alpha.5 worlds keep their generation.
|
||||
tasks.register('layeredHydrologySmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Check retained basins, broad rock shores and lower wall springs.'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.LayeredHydrologySmoke'
|
||||
}
|
||||
|
||||
tasks.named('check') { dependsOn('layeredHydrologySmoke') }
|
||||
|
||||
tasks.register('layeredEcologySmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Check altitude biome variation and naturally supported lower terraces.'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.LayeredEcologySmoke'
|
||||
}
|
||||
|
||||
tasks.named('check') { dependsOn('layeredEcologySmoke') }
|
||||
|
||||
@@ -44,7 +44,7 @@ final class DensityDiagnostics {
|
||||
var noises = registries.lookupOrThrow(Registries.NOISE);
|
||||
// The default run exports all three seeds once. Additional complete-world runs only
|
||||
// repeat the sparse assertions, avoiding redundant image generation.
|
||||
boolean export = level.getSeed() == 0L;
|
||||
boolean export = level.getSeed() == 0L && Boolean.parseBoolean(System.getProperty("sanctuary.test.densityMaps", "true"));
|
||||
boolean foundUnforcedCentralVoid = false;
|
||||
for (long seed : new long[]{0L, 42L, 8675309L}) {
|
||||
RandomState state = RandomState.create(noises, seed, settings);
|
||||
|
||||
+448
@@ -0,0 +1,448 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.HydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.SanctuarySpawn;
|
||||
import fr.koka.sanctuary.worldgen.ShoreSugarCaneFeature;
|
||||
import fr.koka.sanctuary.worldgen.SurfaceHydrology;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.tags.FluidTags;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DensityFunction;
|
||||
import net.minecraft.world.level.material.Fluids;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
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.Set;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/** Checks the actual decorated terrain and fluid simulation in a disposable world, far from test fixtures. */
|
||||
public final class HydrologyDiagnostics {
|
||||
private static final List<Direction> HORIZONTAL = List.of(Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST);
|
||||
|
||||
private HydrologyDiagnostics() {}
|
||||
|
||||
public static void start(GameTestHelper helper) throws IOException {
|
||||
Context context = new Context(helper);
|
||||
context.prepare();
|
||||
context.verifyNaturalShellAndFinishedWater();
|
||||
context.verifyAllObservedWaterIsPlanned();
|
||||
context.export("generated");
|
||||
helper.assertTrue(context.caneBlocks > 0,
|
||||
"Regression seed must provide at least one naturally valid shore sugar cane plant; inspect the exported site map");
|
||||
// Other synchronous generation diagnostics finish before this tick. The temporary fluid
|
||||
// witness therefore cannot be counted by the initial resource survey.
|
||||
helper.runAtTickTime(20, context::startFluidWitness);
|
||||
helper.runAtTickTime(50, context::verifyFluidWitnessAndRemoveIt);
|
||||
helper.runAtTickTime(200, () -> {
|
||||
try {
|
||||
context.verifyNaturalShellAndFinishedWater();
|
||||
context.verifyAllObservedWaterIsPlanned();
|
||||
context.export("after-200-ticks");
|
||||
SanctuaryMod.LOGGER.info("Hydrology seed {}: {} water blocks retained after real fluid ticks, {} chunk seams checked",
|
||||
context.level.getSeed(), context.expectedWater.size(), context.seams);
|
||||
helper.succeed();
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("Could not write hydrology diagnostics", exception);
|
||||
} finally {
|
||||
context.cleanup();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void verifyLegacyIsolation(GameTestHelper helper) {
|
||||
ServerLevel level = helper.getLevel();
|
||||
var active = (NoiseBasedChunkGenerator) level.getChunkSource().getGenerator();
|
||||
helper.assertTrue(HydrologyRuntime.enabled(active), "The current preset must enable surface hydrology");
|
||||
helper.assertTrue(active.generatorSettings().is(SanctuarySpawn.HYDROLOGY_SETTINGS),
|
||||
"The active generator must use the new hydrology settings, not alter a legacy settings key");
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS);
|
||||
for (var key : List.of(SanctuarySpawn.SETTINGS, SanctuarySpawn.RAISED_SETTINGS)) {
|
||||
var legacy = new NoiseBasedChunkGenerator(active.getBiomeSource(), registry.getOrThrow(key));
|
||||
helper.assertFalse(HydrologyRuntime.enabled(legacy), "Saved legacy generator must remain dry: " + key.identifier());
|
||||
helper.assertFalse(new ShoreSugarCaneFeature().place(level, legacy, RandomSource.create(0), BlockPos.ZERO),
|
||||
"Even an explicitly invoked shoreline feature must ignore legacy generators");
|
||||
}
|
||||
var biomes = level.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
var oldBiome = biomes.getOrThrow(ResourceKey.create(Registries.BIOME, SanctuaryMod.id("starter_forest"))).value();
|
||||
var wetBiome = biomes.getOrThrow(ResourceKey.create(Registries.BIOME, SanctuaryMod.id("starter_forest_hydrology"))).value();
|
||||
var shore = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE).getOrThrow(
|
||||
ResourceKey.create(Registries.PLACED_FEATURE, SanctuaryMod.id("shore_sugar_cane"))).value();
|
||||
helper.assertFalse(oldBiome.getGenerationSettings().hasFeature(shore),
|
||||
"The old biome referenced by saved worlds must not acquire the new shoreline decoration");
|
||||
helper.assertTrue(wetBiome.getGenerationSettings().hasFeature(shore),
|
||||
"The new hydrology biome must include its shoreline decoration");
|
||||
helper.assertTrue(active.getBiomeSource().possibleBiomes().stream().allMatch(holder -> holder.is(
|
||||
ResourceKey.create(Registries.BIOME, SanctuaryMod.id("starter_forest_hydrology")))),
|
||||
"The current preset must select the separate hydrology biome");
|
||||
}
|
||||
|
||||
private static final class Context {
|
||||
final GameTestHelper helper;
|
||||
final ServerLevel level;
|
||||
final SurfaceHydrology.Plan plan;
|
||||
final RandomState random;
|
||||
final DensityFunction natural;
|
||||
final Map<Long, SurfaceHydrology.Cell> cells = new HashMap<>();
|
||||
final Set<BlockPos> expectedWater = new HashSet<>();
|
||||
final Map<Long, LevelChunk> chunks = new TreeMap<>();
|
||||
final Set<Long> newlyForced = new HashSet<>();
|
||||
BlockPos witness;
|
||||
boolean witnessPlaced;
|
||||
boolean allWaterChunksTicking;
|
||||
int seams;
|
||||
int caneBlocks;
|
||||
|
||||
Context(GameTestHelper helper) {
|
||||
this.helper = helper;
|
||||
level = helper.getLevel();
|
||||
var generator = (NoiseBasedChunkGenerator) level.getChunkSource().getGenerator();
|
||||
helper.assertTrue(HydrologyRuntime.enabled(generator), "Hydrology test requires the production wet preset");
|
||||
plan = HydrologyRuntime.plan(level);
|
||||
random = level.getChunkSource().randomState();
|
||||
natural = generator.generatorSettings().value().noiseRouter().finalDensity();
|
||||
}
|
||||
|
||||
void prepare() {
|
||||
helper.assertTrue(!plan.features().isEmpty(), "Regression seed must exercise real surface water");
|
||||
for (var feature : plan.features()) {
|
||||
SanctuaryMod.LOGGER.info("Hydrology planned site: seed {}, {}, Y{}, start {}, {} water columns, {} path points",
|
||||
level.getSeed(), feature.kind(), feature.waterY(), feature.path().getFirst(), feature.waterCells(), feature.path().size());
|
||||
}
|
||||
if (level.getSeed() == 0L) {
|
||||
helper.assertTrue(plan.features().stream().anyMatch(feature -> feature.kind() == SurfaceHydrology.Kind.STREAM),
|
||||
"The reference seed 0 must demonstrate a real stream, not only isolated ponds; see the logged planned sites");
|
||||
}
|
||||
Set<Long> requested = new HashSet<>();
|
||||
for (var cell : plan.cells()) {
|
||||
helper.assertTrue(cells.put(ChunkPos.pack(cell.x(), cell.z()), cell) == null,
|
||||
"A plan must own each horizontal column exactly once");
|
||||
if (cell.hasWater()) {
|
||||
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) {
|
||||
expectedWater.add(new BlockPos(cell.x(), y, cell.z()));
|
||||
}
|
||||
}
|
||||
int cx = cell.x() >> 4;
|
||||
int cz = cell.z() >> 4;
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
for (int dz = -1; dz <= 1; dz++) requested.add(ChunkPos.pack(cx + dx, cz + dz));
|
||||
}
|
||||
}
|
||||
helper.assertTrue(!expectedWater.isEmpty(), "At least one generated water volume must be tested");
|
||||
SanctuaryMod.LOGGER.info("Hydrology seed {}: preparing {} site and neighbour chunks for actual water inspection",
|
||||
level.getSeed(), requested.size());
|
||||
for (long packed : requested.stream().sorted().toList()) {
|
||||
ChunkPos pos = ChunkPos.unpack(packed);
|
||||
if (level.setChunkForced(pos.x(), pos.z(), true)) newlyForced.add(packed);
|
||||
var chunk = level.getChunkSource().getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true);
|
||||
helper.assertTrue(chunk instanceof LevelChunk, "Hydrology inspection requires a decorated FULL chunk");
|
||||
chunks.put(packed, (LevelChunk) chunk);
|
||||
}
|
||||
// Use one contained witness in the SAME simulation chunk as actual water. It proves
|
||||
// that the island's fluids tick, rather than merely waiting beside the remote test grid.
|
||||
BlockPos first = expectedWater.stream().min(Comparator.comparingInt((BlockPos pos) -> pos.getX())
|
||||
.thenComparingInt(BlockPos::getZ).thenComparingInt(BlockPos::getY)).orElseThrow();
|
||||
witness = new BlockPos((first.getX() >> 4) * 16 + 8, level.getMaxY() - 20,
|
||||
(first.getZ() >> 4) * 16 + 8);
|
||||
for (BlockPos position : expectedWater) {
|
||||
for (Direction direction : List.of(Direction.EAST, Direction.SOUTH)) {
|
||||
BlockPos next = position.relative(direction);
|
||||
if (expectedWater.contains(next)
|
||||
&& (position.getX() >> 4 != next.getX() >> 4 || position.getZ() >> 4 != next.getZ() >> 4)) seams++;
|
||||
}
|
||||
}
|
||||
helper.assertTrue(seams > 0, "Regression seed must exercise a continuous water volume across a chunk seam");
|
||||
SanctuaryMod.LOGGER.info("Hydrology seed {}: {} features, {} water blocks, {} FULL chunks including halo, {} seam adjacencies",
|
||||
level.getSeed(), plan.features().size(), expectedWater.size(), chunks.size(), seams);
|
||||
}
|
||||
|
||||
void verifyNaturalShellAndFinishedWater() {
|
||||
for (var cell : plan.cells()) {
|
||||
if (!cell.hasWater()) continue;
|
||||
helper.assertTrue(cell.waterY() > cell.bedY() && cell.waterY() - cell.bedY() <= SurfaceHydrology.MAX_DEPTH,
|
||||
"A surface basin must keep the declared shallow water depth");
|
||||
helper.assertTrue(cell.carveTop() - cell.bedY() <= SurfaceHydrology.MAX_CARVE,
|
||||
"Hydrology must not excavate a deep artificial chamber beneath the surface");
|
||||
for (int depth = 0; depth < 3; depth++) {
|
||||
int y = cell.bedY() - depth;
|
||||
helper.assertTrue(sample(cell.x(), y, cell.z()) > 0,
|
||||
"Water must retain three blocks of natural floor, without adding a liner at " + cell.x() + "," + y + "," + cell.z());
|
||||
BlockPos floor = new BlockPos(cell.x(), y, cell.z());
|
||||
helper.assertTrue(level.getBlockState(floor).isCollisionShapeFullBlock(level, floor),
|
||||
"Generated water floor must remain solid after decoration and fluid ticks: " + floor);
|
||||
}
|
||||
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) {
|
||||
BlockPos water = new BlockPos(cell.x(), y, cell.z());
|
||||
helper.assertTrue(level.getFluidState(water).is(FluidTags.WATER) && level.getFluidState(water).isSource(),
|
||||
"The planned water volume must exist as retained source water: " + water);
|
||||
for (Direction direction : HORIZONTAL) {
|
||||
BlockPos side = water.relative(direction);
|
||||
if (expectedWater.contains(side)) continue;
|
||||
helper.assertTrue(sample(side.getX(), side.getY(), side.getZ()) > 0,
|
||||
"Every external underwater side must be supported by natural terrain: " + side);
|
||||
helper.assertTrue(level.getBlockState(side).isCollisionShapeFullBlock(level, side),
|
||||
"A decorated shore must not leave a hole in the three-dimensional water shell: " + side);
|
||||
}
|
||||
}
|
||||
// The original density must have no roof above the explicitly carved surface.
|
||||
// Leaves and branches from neighbouring trees may shade the water afterwards.
|
||||
for (int y = cell.carveTop() + 1; y <= level.getMaxY(); y++) {
|
||||
helper.assertTrue(sample(cell.x(), y, cell.z()) <= 0,
|
||||
"A surface basin must not be a flooded underground chamber: " + cell.x() + "," + y + "," + cell.z());
|
||||
}
|
||||
}
|
||||
for (var feature : plan.features()) {
|
||||
if (feature.kind() != SurfaceHydrology.Kind.STREAM) continue;
|
||||
helper.assertTrue(feature.path().size() > 1, "A stream needs a route connecting distinct surface locations");
|
||||
double travelled = 0;
|
||||
for (int i = 1; i < feature.path().size(); i++) {
|
||||
var from = feature.path().get(i - 1);
|
||||
var to = feature.path().get(i);
|
||||
int length = Math.max(Math.abs(to.x() - from.x()), Math.abs(to.z() - from.z()));
|
||||
helper.assertTrue(length > 0, "A stream route must advance between consecutive points");
|
||||
helper.assertTrue(from.x() == to.x() || from.z() == to.z()
|
||||
|| Math.abs(to.x() - from.x()) == Math.abs(to.z() - from.z()),
|
||||
"Stream control points must be joined by cardinal or diagonal steps");
|
||||
travelled += Math.hypot(to.x() - from.x(), to.z() - from.z());
|
||||
BlockPos previous = null;
|
||||
for (int step = 0; step <= length; step++) {
|
||||
BlockPos p = new BlockPos(from.x() + (to.x() - from.x()) * step / length,
|
||||
feature.waterY(), from.z() + (to.z() - from.z()) * step / length);
|
||||
helper.assertTrue(expectedWater.contains(p) && level.getFluidState(p).is(FluidTags.WATER),
|
||||
"The real stream must form an uninterrupted water route across chunk boundaries: " + p);
|
||||
var column = plan.cellAt(p.getX(), p.getZ());
|
||||
helper.assertTrue(column != null && column.waterY() == feature.waterY(),
|
||||
"This stream contract has one constant water level along its whole route");
|
||||
if (previous != null && p.getX() != previous.getX() && p.getZ() != previous.getZ()) {
|
||||
BlockPos bridgeX = new BlockPos(p.getX(), p.getY(), previous.getZ());
|
||||
BlockPos bridgeZ = new BlockPos(previous.getX(), p.getY(), p.getZ());
|
||||
helper.assertTrue(expectedWater.contains(bridgeX) && level.getFluidState(bridgeX).is(FluidTags.WATER)
|
||||
|| expectedWater.contains(bridgeZ) && level.getFluidState(bridgeZ).is(FluidTags.WATER),
|
||||
"Diagonal route steps need actual cardinal water connectivity, not only touching corners");
|
||||
}
|
||||
previous = p;
|
||||
}
|
||||
}
|
||||
var first = feature.path().getFirst();
|
||||
var last = feature.path().getLast();
|
||||
helper.assertTrue(travelled >= 24 && Math.hypot(last.x() - first.x(), last.z() - first.z()) >= 16,
|
||||
"A demonstrated stream must have at least 24 blocks of route between locations at least 16 blocks apart");
|
||||
}
|
||||
}
|
||||
|
||||
void verifyAllObservedWaterIsPlanned() {
|
||||
int[] observed = {0};
|
||||
int[] cane = {0};
|
||||
for (LevelChunk chunk : chunks.values()) {
|
||||
chunk.findBlocks(state -> state.getFluidState().is(FluidTags.WATER) || state.is(Blocks.SUGAR_CANE), (pos, state) -> {
|
||||
if (state.getFluidState().is(FluidTags.WATER)) {
|
||||
helper.assertTrue(expectedWater.contains(pos),
|
||||
"Water escaped its declared surface volume, possibly through a chunk seam or toward the void: " + pos);
|
||||
observed[0]++;
|
||||
}
|
||||
if (state.is(Blocks.SUGAR_CANE)) {
|
||||
helper.assertTrue(state.canSurvive(level, pos), "Generated shore sugar cane must survive at " + pos);
|
||||
cane[0]++;
|
||||
}
|
||||
});
|
||||
}
|
||||
helper.assertTrue(observed[0] == expectedWater.size(), "Observed water must account for every planned water block");
|
||||
caneBlocks = cane[0];
|
||||
}
|
||||
|
||||
void startFluidWitness() {
|
||||
helper.assertTrue(level.shouldTickBlocksAt(witness), "The witness and its water-site chunk must actually tick");
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
for (int dz = -1; dz <= 1; dz++) {
|
||||
for (int dy = -4; dy <= 0; dy++) {
|
||||
BlockPos p = witness.offset(dx, dy, dz);
|
||||
helper.assertTrue(level.getBlockState(p).isAir(), "The temporary simulation witness must use empty test space");
|
||||
}
|
||||
}
|
||||
}
|
||||
witnessPlaced = true;
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
for (int dz = -1; dz <= 1; dz++) {
|
||||
for (int dy = -4; dy <= 0; dy++) {
|
||||
if (dx != 0 || dz != 0 || dy == -4) level.setBlock(witness.offset(dx, dy, dz), Blocks.STONE.defaultBlockState(), 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
level.setBlock(witness, Blocks.WATER.defaultBlockState(), 3);
|
||||
helper.assertTrue(level.getFluidState(witness.below()).isEmpty(), "Witness starts with an empty cell below its source");
|
||||
level.scheduleTick(witness, Fluids.WATER, 1);
|
||||
for (BlockPos water : expectedWater) {
|
||||
helper.assertTrue(level.shouldTickBlocksAt(water), "Every actual water-site chunk must be simulated: " + water);
|
||||
level.scheduleTick(water, Fluids.WATER, 1);
|
||||
}
|
||||
allWaterChunksTicking = true;
|
||||
}
|
||||
|
||||
void verifyFluidWitnessAndRemoveIt() {
|
||||
try {
|
||||
helper.assertTrue(level.getFluidState(witness.below()).is(FluidTags.WATER),
|
||||
"Real fluid simulation must move the witness water down inside the forced island chunk");
|
||||
SanctuaryMod.LOGGER.info("Hydrology seed {}: fluid tick witness passed at {}", level.getSeed(), witness);
|
||||
} finally {
|
||||
removeWitness();
|
||||
}
|
||||
}
|
||||
|
||||
void removeWitness() {
|
||||
if (!witnessPlaced) return;
|
||||
// Empty the fluid column before opening its walls, keeping the test fixture contained.
|
||||
for (int dy = 0; dy >= -3; dy--) level.setBlock(witness.offset(0, dy, 0), Blocks.AIR.defaultBlockState(), 3);
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
for (int dz = -1; dz <= 1; dz++) {
|
||||
for (int dy = -4; dy <= 0; dy++) level.setBlock(witness.offset(dx, dy, dz), Blocks.AIR.defaultBlockState(), 3);
|
||||
}
|
||||
}
|
||||
witnessPlaced = false;
|
||||
}
|
||||
|
||||
void cleanup() {
|
||||
removeWitness();
|
||||
for (long packed : newlyForced) {
|
||||
ChunkPos chunk = ChunkPos.unpack(packed);
|
||||
level.setChunkForced(chunk.x(), chunk.z(), false);
|
||||
}
|
||||
}
|
||||
|
||||
float sample(int x, int y, int z) {
|
||||
return random.sampleBlockValueUncached(natural, x, y, z);
|
||||
}
|
||||
|
||||
void export(String phase) throws IOException {
|
||||
Path directory = Path.of("diagnostics");
|
||||
Files.createDirectories(directory);
|
||||
String stem = "surface-hydrology-seed-" + level.getSeed() + "-" + phase;
|
||||
Map<String, Object> report = new LinkedHashMap<>();
|
||||
report.put("seed", level.getSeed());
|
||||
report.put("phase", phase);
|
||||
report.put("actual_water_blocks", expectedWater.size());
|
||||
report.put("actual_cane_blocks", caneBlocks);
|
||||
report.put("checked_chunk_seam_adjacencies", seams);
|
||||
report.put("all_water_site_chunks_confirmed_ticking", allWaterChunksTicking);
|
||||
report.put("inspected_full_chunks", chunks.values().stream().map(chunk ->
|
||||
Map.of("x", chunk.getPos().x(), "z", chunk.getPos().z())).toList());
|
||||
report.put("features", plan.features());
|
||||
report.put("shore_diagnostics_by_feature", shoreDiagnostics());
|
||||
report.put("planned_columns", plan.cells());
|
||||
report.put("scope", "actual blocks in completed site chunks and a one-chunk halo; uninspected map cells remain blank");
|
||||
Files.writeString(directory.resolve(stem + ".json"), new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n");
|
||||
exportMap(directory.resolve(stem + ".png"), phase);
|
||||
}
|
||||
|
||||
Map<Long, Map<String, Object>> shoreDiagnostics() {
|
||||
Map<Long, Map<String, Object>> result = new LinkedHashMap<>();
|
||||
for (var feature : plan.features()) {
|
||||
int dryBanks = 0;
|
||||
int banksAtWaterLevel = 0;
|
||||
int baseCanSurvive = 0;
|
||||
int unobstructedBases = 0;
|
||||
int viableUnobstructedBases = 0;
|
||||
int actualCane = 0;
|
||||
for (var cell : plan.cells()) {
|
||||
if (cell.featureId() != feature.id() || cell.hasWater()) continue;
|
||||
dryBanks++;
|
||||
if (cell.bedY() == feature.waterY()) banksAtWaterLevel++;
|
||||
BlockPos base = new BlockPos(cell.x(), cell.bedY() + 1, cell.z());
|
||||
boolean viable = Blocks.SUGAR_CANE.defaultBlockState().canSurvive(level, base);
|
||||
boolean clear = level.getBlockState(base).isAir() || level.getBlockState(base).is(Blocks.SUGAR_CANE);
|
||||
if (viable) baseCanSurvive++;
|
||||
if (clear) unobstructedBases++;
|
||||
if (viable && clear) viableUnobstructedBases++;
|
||||
for (int height = 0; height < 4; height++) {
|
||||
if (level.getBlockState(base.above(height)).is(Blocks.SUGAR_CANE)) actualCane++;
|
||||
else break;
|
||||
}
|
||||
}
|
||||
result.put(feature.id(), Map.of("kind", feature.kind().name(), "water_y", feature.waterY(),
|
||||
"dry_bank_columns", dryBanks, "bank_surface_at_water_level", banksAtWaterLevel,
|
||||
"bases_where_cane_can_survive", baseCanSurvive, "unobstructed_bases", unobstructedBases,
|
||||
"viable_unobstructed_bases", viableUnobstructedBases,
|
||||
"actual_cane_blocks_on_planned_banks", actualCane));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void exportMap(Path output, String phase) throws IOException {
|
||||
int minX = chunks.values().stream().mapToInt(chunk -> chunk.getPos().getMinBlockX()).min().orElseThrow();
|
||||
int maxX = chunks.values().stream().mapToInt(chunk -> chunk.getPos().getMaxBlockX()).max().orElseThrow();
|
||||
int minZ = chunks.values().stream().mapToInt(chunk -> chunk.getPos().getMinBlockZ()).min().orElseThrow();
|
||||
int maxZ = chunks.values().stream().mapToInt(chunk -> chunk.getPos().getMaxBlockZ()).max().orElseThrow();
|
||||
int scale = Math.max(1, Math.min(3, 1000 / Math.max(maxX - minX + 1, maxZ - minZ + 1)));
|
||||
int width = (maxX - minX + 1) * scale;
|
||||
int height = (maxZ - minZ + 1) * scale;
|
||||
BufferedImage image = new BufferedImage(Math.max(width + 40, 920), height + 110, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D graphics = image.createGraphics();
|
||||
graphics.setColor(new Color(0xF0F0EC));
|
||||
graphics.fillRect(0, 0, image.getWidth(), image.getHeight());
|
||||
graphics.setColor(new Color(0x17222C));
|
||||
graphics.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 17));
|
||||
graphics.drawString("Actual FULL chunks | seed " + level.getSeed() + " | " + phase, 20, 24);
|
||||
graphics.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
|
||||
graphics.drawString("Water blue; sand gold; clay teal; gravel brown; cane bright green; terrain height grey. Canopy skipped.", 20, 45);
|
||||
graphics.drawString("Not a Minecraft render. X=" + minX + ".." + maxX + ", Z=" + minZ + ".." + maxZ
|
||||
+ ". Dark blue = void; dark brown = not inspected; faint lines = chunk seams.", 20, 63);
|
||||
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
|
||||
for (int x = minX; x <= maxX; x++) {
|
||||
for (int z = minZ; z <= maxZ; z++) {
|
||||
LevelChunk chunk = chunks.get(ChunkPos.pack(x >> 4, z >> 4));
|
||||
int color = 0x3A3030;
|
||||
if (chunk != null) {
|
||||
color = 0x142738;
|
||||
for (int y = chunk.getHeight(Heightmap.Types.WORLD_SURFACE, x, z); y >= level.getMinY(); y--) {
|
||||
position.set(x, y, z);
|
||||
BlockState state = chunk.getBlockState(position);
|
||||
if (state.getFluidState().is(FluidTags.WATER)) { color = 0x207BD4; break; }
|
||||
if (state.is(Blocks.SUGAR_CANE)) { color = 0x7AFF38; break; }
|
||||
if (state.is(Blocks.SAND)) { color = 0xD5B668; break; }
|
||||
if (state.is(Blocks.CLAY)) { color = 0x6D9FAD; break; }
|
||||
if (state.is(Blocks.GRAVEL)) { color = 0x9B8878; break; }
|
||||
if (state.isAir() || state.is(BlockTags.LEAVES) || state.is(BlockTags.LOGS)
|
||||
|| state.getCollisionShape(level, position).isEmpty()) continue;
|
||||
int value = 40 + Math.clamp(y, 0, level.getHeight()) * 180 / level.getHeight();
|
||||
color = value << 16 | value << 8 | value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
graphics.setColor(new Color(color));
|
||||
graphics.fillRect(20 + (x - minX) * scale, 80 + (z - minZ) * scale, scale, scale);
|
||||
}
|
||||
}
|
||||
graphics.setColor(new Color(255, 255, 255, 35));
|
||||
for (int x = minX; x <= maxX; x += 16) graphics.drawLine(20 + (x - minX) * scale, 80, 20 + (x - minX) * scale, 80 + height);
|
||||
for (int z = minZ; z <= maxZ; z += 16) graphics.drawLine(20, 80 + (z - minZ) * scale, 20 + width, 80 + (z - minZ) * scale);
|
||||
graphics.dispose();
|
||||
ImageIO.write(image, "png", output.toFile());
|
||||
}
|
||||
}
|
||||
}
|
||||
+1308
File diff suppressed because it is too large
Load Diff
+1030
File diff suppressed because it is too large
Load Diff
+39
-2
@@ -1,6 +1,10 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.SanctuarySpawn;
|
||||
import fr.koka.sanctuary.worldgen.NaturalHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.LayeredHydrologyRuntime;
|
||||
import net.minecraft.tags.FluidTags;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.IslandShape;
|
||||
import fr.koka.sanctuary.worldgen.MainIslandDensity;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
@@ -23,6 +27,23 @@ import net.minecraft.world.level.storage.LevelData;
|
||||
public final class SanctuaryWorldGameTests {
|
||||
public static LevelData.RespawnData initialSpawn;
|
||||
|
||||
@GameTest(maxTicks = 3000)
|
||||
public void generatedSurfaceWaterRemainsContained(GameTestHelper helper) throws java.io.IOException {
|
||||
var generator = (NoiseBasedChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
if (LayeredHydrologyRuntime.enabled(generator)) LayeredHydrologyDiagnostics.start(helper);
|
||||
else if (NaturalHydrologyRuntime.enabled(generator)) NaturalHydrologyDiagnostics.start(helper);
|
||||
else HydrologyDiagnostics.start(helper);
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 200)
|
||||
public void hydrologyDoesNotActivateForSavedLegacySettings(GameTestHelper helper) {
|
||||
var generator = (NoiseBasedChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
if (LayeredHydrologyRuntime.enabled(generator)) LayeredHydrologyDiagnostics.verifyLegacyIsolation(helper);
|
||||
else if (NaturalHydrologyRuntime.enabled(generator)) NaturalHydrologyDiagnostics.verifyLegacyIsolation(helper);
|
||||
else HydrologyDiagnostics.verifyLegacyIsolation(helper);
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 200)
|
||||
public void generatedIslandAndInitialSpawn(GameTestHelper helper) {
|
||||
ServerLevel level = helper.getLevel();
|
||||
@@ -93,8 +114,24 @@ public final class SanctuaryWorldGameTests {
|
||||
for (int[] coordinate : new int[][]{{0, 0}, {-8, 0}, {8, 0}, {0, -8}, {0, 8}}) {
|
||||
ChunkAccess chunk = helper.getLevel().getChunkSource().getChunk(
|
||||
coordinate[0], coordinate[1], ChunkStatus.FULL, true);
|
||||
helper.assertTrue(chunk != null && chunk.getSection(0).hasOnlyAir(),
|
||||
"The fully generated island must leave Y0..15 empty, without a floor cut at the dimension bound");
|
||||
helper.assertTrue(chunk != null, "The lower-margin test needs a fully generated chunk");
|
||||
if (chunk.getSection(0).hasOnlyAir()) continue;
|
||||
var generator = (NoiseBasedChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
helper.assertTrue(NaturalHydrologyRuntime.enabled(generator) || LayeredHydrologyRuntime.enabled(generator),
|
||||
"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);
|
||||
helper.assertTrue(state.isAir() || state.getFluidState().is(FluidTags.WATER)
|
||||
&& (LayeredHydrologyRuntime.enabled(generator)
|
||||
? LayeredHydrologyDiagnostics.isWithinSpringInspectionFootprint(LayeredHydrologyRuntime.layeredPlan(helper.getLevel()), x, z)
|
||||
: NaturalHydrologyDiagnostics.isWithinSpringInspectionFootprint(NaturalHydrologyRuntime.naturalPlan(helper.getLevel()), x, z)),
|
||||
"The lower margin has no artificial floor; water must lie in the finite spring inspection footprint, with provenance checked by the full fluid diagnostic: " + p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
package fr.koka.sanctuary;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.MainIslandDensity;
|
||||
import fr.koka.sanctuary.worldgen.NaturalShoreSugarCaneFeature;
|
||||
import fr.koka.sanctuary.worldgen.TemperateIslandBiomeSource;
|
||||
import fr.koka.sanctuary.worldgen.LayeredIslandBiomeSource;
|
||||
import fr.koka.sanctuary.worldgen.LayeredShoreSugarCaneFeature;
|
||||
import fr.koka.sanctuary.worldgen.LowerTerraceVegetationFeature;
|
||||
import fr.koka.sanctuary.worldgen.ShoreSugarCaneFeature;
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
@@ -19,6 +25,12 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("main_island"), MainIslandDensity.CODEC);
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("shore_sugar_cane"), ShoreSugarCaneFeature.CODEC);
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("natural_shore_sugar_cane"), NaturalShoreSugarCaneFeature.CODEC);
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("temperate_island"), TemperateIslandBiomeSource.CODEC);
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("layered_shore_sugar_cane"), LayeredShoreSugarCaneFeature.CODEC);
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("lower_terrace_vegetation"), LowerTerraceVegetationFeature.CODEC);
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("layered_island"), LayeredIslandBiomeSource.CODEC);
|
||||
LOGGER.info("Sanctuary initialized: the Sanctuary world preset is available.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package fr.koka.sanctuary.mixin;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.LayeredSpringOutlets;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/** Finish only pending generation markers; completed saved chunks are never repaired on reload. */
|
||||
@Mixin(LevelChunk.class)
|
||||
public abstract class LayeredSpringOutletMixin {
|
||||
@Unique
|
||||
private LayeredSpringOutlets.Pending sanctuary$pendingSpringOutlets = LayeredSpringOutlets.Pending.EMPTY;
|
||||
|
||||
@Inject(method = "postProcessGeneration", at = @At("HEAD"))
|
||||
private void sanctuary$captureSpringOutlets(ServerLevel level, CallbackInfo callback) {
|
||||
sanctuary$pendingSpringOutlets = LayeredSpringOutlets.capturePending(level, (LevelChunk) (Object) this);
|
||||
}
|
||||
|
||||
@Inject(method = "postProcessGeneration", at = @At("TAIL"))
|
||||
private void sanctuary$finishSpringOutlets(ServerLevel level, CallbackInfo callback) {
|
||||
var pending = sanctuary$pendingSpringOutlets;
|
||||
sanctuary$pendingSpringOutlets = LayeredSpringOutlets.Pending.EMPTY;
|
||||
LayeredSpringOutlets.finish(level, (LevelChunk) (Object) this, pending);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package fr.koka.sanctuary.mixin;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.HydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.NaturalHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.LayeredHydrologyRuntime;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeManager;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
/** The terrain future completes before decoration starts; never reads or writes another chunk. */
|
||||
@Mixin(NoiseBasedChunkGenerator.class)
|
||||
public abstract class SurfaceHydrologyMixin {
|
||||
@Inject(method = "buildTerrain", at = @At("RETURN"), cancellable = true)
|
||||
private void sanctuary$surfaceWater(ChunkAccess chunk, Blender blender, RandomState randomState,
|
||||
StructureManager structures, BiomeManager biomeManager, WorldGenRegion region,
|
||||
Set<Holder<Biome>> biomes, CallbackInfoReturnable<CompletableFuture<ChunkAccess>> callback) {
|
||||
var generator = (NoiseBasedChunkGenerator) (Object) this;
|
||||
if (LayeredHydrologyRuntime.enabled(generator)) {
|
||||
callback.setReturnValue(callback.getReturnValue().thenApply(generated -> {
|
||||
LayeredHydrologyRuntime.apply(generator, randomState, generated);
|
||||
return generated;
|
||||
}));
|
||||
} else if (NaturalHydrologyRuntime.enabled(generator)) {
|
||||
callback.setReturnValue(callback.getReturnValue().thenApply(generated -> {
|
||||
NaturalHydrologyRuntime.apply(generator, randomState, generated);
|
||||
return generated;
|
||||
}));
|
||||
} else if (HydrologyRuntime.enabled(generator)) {
|
||||
callback.setReturnValue(callback.getReturnValue().thenApply(generated -> {
|
||||
HydrologyRuntime.apply(generator, randomState, generated);
|
||||
return generated;
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
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;
|
||||
|
||||
/** One immutable plan per world random state. The value retains neither that state nor its sampler. */
|
||||
public final class HydrologyRuntime {
|
||||
private static final Map<RandomState, SurfaceHydrology.Plan> PLANS =
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
|
||||
private HydrologyRuntime() {}
|
||||
|
||||
public static boolean enabled(NoiseBasedChunkGenerator generator) {
|
||||
return generator.generatorSettings().is(SanctuarySpawn.HYDROLOGY_SETTINGS);
|
||||
}
|
||||
|
||||
public static SurfaceHydrology.Plan plan(ServerLevel level) {
|
||||
return plan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(),
|
||||
level.getChunkSource().randomState());
|
||||
}
|
||||
|
||||
public static SurfaceHydrology.Plan plan(NoiseBasedChunkGenerator generator, RandomState randomState) {
|
||||
if (!enabled(generator)) throw new IllegalArgumentException("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 = SurfaceHydrology.create(state.seed(),
|
||||
(x, y, z) -> state.sampleBlockValueUncached(density, x, y, z));
|
||||
SanctuaryMod.LOGGER.info("Sanctuary surface hydrology: seed {}, {} features, {} columns, {} ms",
|
||||
state.seed(), plan.features().size(), plan.cells().size(), (System.nanoTime() - start) / 1_000_000);
|
||||
return plan;
|
||||
});
|
||||
}
|
||||
|
||||
/** Executed during fresh terrain generation only, before ores, trees and shoreline plants. */
|
||||
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 cells = plan(generator, randomState).cellsInChunk(chunkPos.x(), chunkPos.z());
|
||||
if (cells.isEmpty()) return;
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
// Validate this chunk's entire foundation before the first write. The planner may only
|
||||
// replace existing terrain; a mismatch must fail rather than leave a partially applied bed.
|
||||
for (var cell : cells) {
|
||||
for (int y = cell.bedY() - 2; y <= cell.bedY(); y++) {
|
||||
pos.set(cell.x(), y, cell.z());
|
||||
if (!chunk.getBlockState(pos).isCollisionShapeFullBlock(chunk, pos)) {
|
||||
throw new IllegalStateException("Hydrology bed differs from natural terrain at " + pos
|
||||
+ " for seed " + randomState.seed());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (var cell : cells) {
|
||||
pos.set(cell.x(), cell.bedY(), cell.z());
|
||||
BlockState material = switch (cell.material()) {
|
||||
case SAND -> Blocks.SAND.defaultBlockState();
|
||||
case GRAVEL -> Blocks.GRAVEL.defaultBlockState();
|
||||
case CLAY -> Blocks.CLAY.defaultBlockState();
|
||||
};
|
||||
chunk.setBlockState(pos, material, 0);
|
||||
if (!cell.hasWater()) continue;
|
||||
for (int y = cell.bedY() + 1; y <= Math.max(cell.waterY(), cell.carveTop()); y++) {
|
||||
pos.set(cell.x(), y, cell.z());
|
||||
chunk.setBlockState(pos, y <= cell.waterY()
|
||||
? Blocks.WATER.defaultBlockState() : Blocks.AIR.defaultBlockState(), 0);
|
||||
if (y <= cell.waterY()) chunk.markPosForPostProcessing(pos);
|
||||
}
|
||||
}
|
||||
// buildTerrain's incoming proto status may not yet maintain the WG heightmaps. Decoration
|
||||
// must see the new bed and waterline so trees are placed on dry shores, not below the lake.
|
||||
Heightmap.primeHeightmaps(chunk, EnumSet.of(Heightmap.Types.WORLD_SURFACE_WG,
|
||||
Heightmap.Types.OCEAN_FLOOR_WG));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
/** Broad, seed-warped vertical bands; none changes terrain density or guarantees a resource. */
|
||||
public final class LayeredBiomePalette {
|
||||
public enum Kind { MEADOW, FLOWER_MEADOW, LOWER_GROVE, MOSSY_TERRACES, ROCKY_TERRACES }
|
||||
|
||||
private LayeredBiomePalette() {}
|
||||
|
||||
public static Kind select(int blockY, float variation, float moisture) {
|
||||
double elevation = blockY - Math.clamp(variation, -1.0F, 1.0F) * 28
|
||||
- Math.clamp(moisture, -1.0F, 1.0F) * 12;
|
||||
if (elevation < 154) return moisture < -0.30F ? Kind.ROCKY_TERRACES : Kind.MOSSY_TERRACES;
|
||||
if (elevation < 214) {
|
||||
if (variation > 0.34F) return Kind.ROCKY_TERRACES;
|
||||
return moisture > 0.02F ? Kind.LOWER_GROVE : Kind.MOSSY_TERRACES;
|
||||
}
|
||||
if (variation > 0.22F) return Kind.ROCKY_TERRACES;
|
||||
return moisture > -0.12F ? Kind.FLOWER_MEADOW : Kind.MEADOW;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,633 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.PriorityQueue;
|
||||
import java.util.Set;
|
||||
|
||||
/** Alpha.6 surface basins, rocky shores and layered wall springs.
|
||||
* This version is separate from NaturalHydrology so previously created worlds keep their original terrain.
|
||||
* Results retain no chunks, sampler, random state or Minecraft objects. */
|
||||
public final class LayeredHydrology {
|
||||
public static final int RADIUS = 192;
|
||||
public static final int MAX_CARVE = 4;
|
||||
public static final int MAX_DEPTH = 3;
|
||||
private static final int GRID = 8;
|
||||
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
private static final int[][] NEIGHBORS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}};
|
||||
|
||||
private LayeredHydrology() {}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Sampler { float sample(int x, int y, int z); }
|
||||
|
||||
public enum Kind { POND, LAKE }
|
||||
public enum ShoreMaterial { SAND, GRAVEL, CLAY, STONE, GRASS }
|
||||
public record Point(int x, int z) {}
|
||||
public record Bounds(int minX, int minZ, int maxX, int maxZ) {}
|
||||
|
||||
public record Position(int x, int y, int z) {}
|
||||
public record FlowBounds(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {}
|
||||
public record Spring(long id, Position source, Position outlet, List<Position> flowPath, FlowBounds flowBounds) {
|
||||
public Spring { flowPath = List.copyOf(flowPath); }
|
||||
}
|
||||
|
||||
/** Replaces existing solids bedY-sedimentDepth+1..bedY, with two intact natural supports below.
|
||||
* Water occupies bedY+1..waterY; everything above it through carveTop is removed. For a dry
|
||||
* terrace waterY=-1, and bedY+1..carveTop is removed (at most two original blocks). */
|
||||
public record Cell(int x, int z, int waterY, int bedY, int carveTop,
|
||||
ShoreMaterial material, long featureId, int sedimentDepth) {
|
||||
public boolean hasWater() { return waterY >= 0; }
|
||||
}
|
||||
|
||||
public record Feature(long id, Kind kind, int waterY, List<Point> path, int waterCells, Bounds bounds) {
|
||||
public Feature { path = List.copyOf(path); }
|
||||
}
|
||||
|
||||
public static final class Plan {
|
||||
private final Map<Long, Cell> cells;
|
||||
private final Map<Long, List<Cell>> chunks;
|
||||
private final List<Feature> features;
|
||||
private final List<Spring> springs;
|
||||
private final Map<Long, List<Spring>> springChunks;
|
||||
private final Set<Position> springFlow;
|
||||
private final int sampledColumns;
|
||||
private final long densitySamples;
|
||||
|
||||
private Plan(Map<Long, Cell> source, List<Feature> features, List<Spring> springs, Set<Position> springFlow, int columns, long samples) {
|
||||
List<Cell> sorted = source.values().stream()
|
||||
.sorted(Comparator.comparingInt(Cell::x).thenComparingInt(Cell::z)).toList();
|
||||
Map<Long, Cell> ordered = new LinkedHashMap<>();
|
||||
Map<Long, List<Cell>> byChunk = new HashMap<>();
|
||||
for (Cell cell : sorted) {
|
||||
ordered.put(key(cell.x(), cell.z()), cell);
|
||||
byChunk.computeIfAbsent(key(cell.x() >> 4, cell.z() >> 4), ignored -> new ArrayList<>()).add(cell);
|
||||
}
|
||||
byChunk.replaceAll((ignored, values) -> List.copyOf(values));
|
||||
cells = Collections.unmodifiableMap(ordered);
|
||||
chunks = Map.copyOf(byChunk);
|
||||
this.features = List.copyOf(features);
|
||||
this.springs = List.copyOf(springs);
|
||||
this.springFlow = Set.copyOf(springFlow);
|
||||
Map<Long, List<Spring>> groupedSprings = new HashMap<>();
|
||||
for (Spring spring : springs) groupedSprings.computeIfAbsent(
|
||||
key(spring.source().x() >> 4, spring.source().z() >> 4), ignored -> new ArrayList<>()).add(spring);
|
||||
groupedSprings.replaceAll((ignored, values) -> List.copyOf(values));
|
||||
springChunks = Map.copyOf(groupedSprings);
|
||||
sampledColumns = columns;
|
||||
densitySamples = samples;
|
||||
}
|
||||
|
||||
public Cell cellAt(int x, int z) { return cells.get(key(x, z)); }
|
||||
public Collection<Cell> cells() { return cells.values(); }
|
||||
public List<Cell> cellsInChunk(int x, int z) { return chunks.getOrDefault(key(x, z), List.of()); }
|
||||
public List<Feature> features() { return features; }
|
||||
public List<Spring> springs() { return springs; }
|
||||
public List<Spring> springsInChunk(int x, int z) { return springChunks.getOrDefault(key(x, z), List.of()); }
|
||||
/** Prediction in undecorated density, not a restriction on Minecraft fluid simulation. */
|
||||
public boolean allowsSpringFlow(int x, int y, int z) { return springFlow.contains(new Position(x, y, z)); }
|
||||
public int sampledColumns() { return sampledColumns; }
|
||||
public long densitySamples() { return densitySamples; }
|
||||
}
|
||||
|
||||
public static Plan create(long seed, Sampler sampler) {
|
||||
return new Planner(seed, sampler).build();
|
||||
}
|
||||
|
||||
private record Column(int top, int solidBottom) {}
|
||||
private record Candidate(int x, int z, double score) {}
|
||||
private record Node(int x, int z, double cost) {}
|
||||
private static final Comparator<Node> NODE_ORDER = Comparator.comparingDouble(Node::cost)
|
||||
.thenComparingInt(Node::x).thenComparingInt(Node::z);
|
||||
|
||||
private static final class Planner {
|
||||
private final long seed;
|
||||
private final Sampler sampler;
|
||||
private final Map<Long, Column> columns = new HashMap<>();
|
||||
private final Map<Long, Integer> coarse = new HashMap<>();
|
||||
private final Map<Long, Cell> cells = new HashMap<>();
|
||||
private final List<Feature> features = new ArrayList<>();
|
||||
private final List<Spring> springs = new ArrayList<>();
|
||||
private final Set<Position> springFlow = new HashSet<>();
|
||||
private long samples;
|
||||
|
||||
Planner(long seed, Sampler sampler) { this.seed = seed; this.sampler = sampler; }
|
||||
|
||||
Plan build() {
|
||||
List<Candidate> candidates = candidates();
|
||||
int attempts = 0;
|
||||
for (Candidate candidate : candidates) {
|
||||
if (features.size() >= 5 || attempts++ >= 96) break;
|
||||
Point center = new Point(candidate.x(), candidate.z());
|
||||
if (nearWater(center.x(), center.z(), 38)) continue;
|
||||
Column column = column(center.x(), center.z());
|
||||
if (column.top() < 64 || column.top() - column.solidBottom() < 7) continue;
|
||||
Kind kind = features.size() < 2 ? Kind.LAKE : Kind.POND;
|
||||
int target = kind == Kind.LAKE ? 190 + (int) (random(seed, center.x(), center.z()) * 130)
|
||||
: 55 + (int) (random(seed + 1, center.x(), center.z()) * 55);
|
||||
// Prefer an existing low spot; a shallow cut is the fallback on level terrain.
|
||||
for (int level : new int[]{column.top() + 1, column.top(), column.top() - 1}) {
|
||||
Set<Long> footprint = growBasin(center, level, target);
|
||||
if (footprint == null) continue;
|
||||
long id = featureId(features.size());
|
||||
Map<Long, Cell> wet = waterCells(footprint, level, id);
|
||||
if (wet == null || !contained(wet)) continue;
|
||||
addFeature(id, kind, level, List.of(center), wet);
|
||||
break;
|
||||
}
|
||||
}
|
||||
addSedimentSheets();
|
||||
addLowerWallSprings();
|
||||
addSprings();
|
||||
return new Plan(cells, features, springs, springFlow, columns.size(), samples);
|
||||
}
|
||||
|
||||
private List<Candidate> candidates() {
|
||||
for (int x = -RADIUS; x <= RADIUS; x += GRID) {
|
||||
for (int z = -RADIUS; z <= RADIUS; z += GRID) {
|
||||
if (inside(x, z, RADIUS)) coarse.put(key(x, z), coarseTop(x, z));
|
||||
}
|
||||
}
|
||||
List<Candidate> result = new ArrayList<>();
|
||||
for (int x = -RADIUS + GRID; x < RADIUS; x += GRID) {
|
||||
for (int z = -RADIUS + GRID; z < RADIUS; z += GRID) {
|
||||
int height = coarse.getOrDefault(key(x, z), -1);
|
||||
if (height < 64 || !inside(x, z, RADIUS - 24)) continue;
|
||||
double average = 0;
|
||||
boolean safe = true;
|
||||
for (int[] d : CARDINALS) {
|
||||
int neighbor = coarse.getOrDefault(key(x + d[0] * GRID, z + d[1] * GRID), -1);
|
||||
if (neighbor < 64 || Math.abs(neighbor - height) > 16) safe = false;
|
||||
average += neighbor * 0.25;
|
||||
}
|
||||
if (safe) result.add(new Candidate(x, z,
|
||||
(average - height) * 2.0 + random(seed + 2, x, z) * 3.0));
|
||||
}
|
||||
}
|
||||
result.sort(Comparator.comparingDouble(Candidate::score).reversed()
|
||||
.thenComparingInt(Candidate::x).thenComparingInt(Candidate::z));
|
||||
return result;
|
||||
}
|
||||
|
||||
private int coarseTop(int x, int z) {
|
||||
for (int y = 383; y >= 32; y -= 4) if (solid(x, y, z)) return y;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private Column column(int x, int z) {
|
||||
long key = key(x, z);
|
||||
Column old = columns.get(key);
|
||||
if (old != null) return old;
|
||||
int top = -1;
|
||||
// Exact scan, including everything above the planned water: a cave roof cannot be
|
||||
// mistaken for a surface. Only candidates and their nearby shore need this refinement.
|
||||
for (int y = 383; y >= 24; y--) {
|
||||
if (solid(x, y, z)) { top = y; break; }
|
||||
}
|
||||
int bottom = top;
|
||||
if (top >= 0) {
|
||||
while (bottom > Math.max(16, top - 16) && solid(x, bottom - 1, z)) bottom--;
|
||||
}
|
||||
Column created = new Column(top, bottom);
|
||||
columns.put(key, created);
|
||||
return created;
|
||||
}
|
||||
|
||||
private boolean eligible(int x, int z, int level) {
|
||||
if (level < 32 || level >= 383 || !inside(x, z, RADIUS - 2)) return false;
|
||||
Cell old = cells.get(key(x, z));
|
||||
if (old != null && old.hasWater() && old.waterY() != level) return false;
|
||||
Column column = column(x, z);
|
||||
return column.top() >= level - 3 && column.top() < level + MAX_CARVE
|
||||
&& column.solidBottom() <= Math.min(column.top(), level - 1) - 4;
|
||||
}
|
||||
|
||||
private Set<Long> growBasin(Point center, int level, int target) {
|
||||
if (!eligible(center.x(), center.z(), level)) return null;
|
||||
Set<Long> wet = new HashSet<>();
|
||||
Map<Long, Double> costs = new HashMap<>();
|
||||
PriorityQueue<Node> queue = new PriorityQueue<>(NODE_ORDER);
|
||||
queue.add(new Node(center.x(), center.z(), 0));
|
||||
costs.put(key(center.x(), center.z()), 0.0);
|
||||
while (!queue.isEmpty() && wet.size() < target) {
|
||||
Node current = queue.remove();
|
||||
if (current.cost() > costs.getOrDefault(key(current.x(), current.z()), Double.POSITIVE_INFINITY)) continue;
|
||||
if (!eligible(current.x(), current.z(), level)) continue;
|
||||
wet.add(key(current.x(), current.z()));
|
||||
for (int[] d : NEIGHBORS) {
|
||||
int x = current.x() + d[0], z = current.z() + d[1];
|
||||
if (Math.hypot(x - center.x(), z - center.z()) > 28 || wet.contains(key(x, z))) continue;
|
||||
if (!eligible(x, z, level)) continue;
|
||||
double step = Math.hypot(d[0], d[1]);
|
||||
double cost = current.cost() + step * (1 + Math.max(0, column(x, z).top() - level) * 0.8
|
||||
+ terrainNoise(x, z) * 4.5 + field(x + 71, z - 93, 21, 107) * 3.5);
|
||||
if (cost >= costs.getOrDefault(key(x, z), Double.POSITIVE_INFINITY)) continue;
|
||||
costs.put(key(x, z), cost);
|
||||
queue.add(new Node(x, z, cost));
|
||||
}
|
||||
}
|
||||
wet = connectedTo(wet, key(center.x(), center.z()));
|
||||
if (wet.size() < Math.min(target, 30)) return null;
|
||||
// Filling a hollow requires every lower connected outlet to be included. Abort rather
|
||||
// than place a dam if this natural basin runs to a cliff, cave or an oversized catchment.
|
||||
List<Long> work = new ArrayList<>(wet);
|
||||
for (int i = 0; i < work.size(); i++) {
|
||||
Point point = point(work.get(i));
|
||||
for (int[] d : CARDINALS) {
|
||||
int x = point.x() + d[0], z = point.z() + d[1];
|
||||
long neighbor = key(x, z);
|
||||
if (wet.contains(neighbor) || column(x, z).top() >= level) continue;
|
||||
if (wet.size() >= 420 || Math.hypot(x - center.x(), z - center.z()) > 32
|
||||
|| !eligible(x, z, level)) return null;
|
||||
wet.add(neighbor);
|
||||
work.add(neighbor);
|
||||
}
|
||||
}
|
||||
return wet;
|
||||
}
|
||||
|
||||
private Map<Long, Cell> waterCells(Set<Long> footprint, int level, long id) {
|
||||
Map<Long, Cell> result = new HashMap<>();
|
||||
for (long key : footprint) {
|
||||
Point p = point(key);
|
||||
if (!eligible(p.x(), p.z(), level)) return null;
|
||||
boolean edge = false;
|
||||
for (int[] d : CARDINALS) edge |= !footprint.contains(key(p.x() + d[0], p.z() + d[1]));
|
||||
Column column = column(p.x(), p.z());
|
||||
int depth = Math.max(level - column.top() + 1, edge ? 1 : 2 + (terrainNoise(p.x(), p.z()) > 0.55 ? 1 : 0));
|
||||
depth = Math.min(depth, MAX_CARVE - Math.max(0, column.top() - level));
|
||||
if (depth < 1 || depth > MAX_DEPTH) return null;
|
||||
int bed = level - depth;
|
||||
if (column.top() < bed || column.solidBottom() > bed - 2) return null;
|
||||
ShoreMaterial material = wetMaterial(p.x(), p.z(), edge);
|
||||
int sedimentDepth = sedimentDepth(p.x(), p.z());
|
||||
if (column.solidBottom() > bed - sedimentDepth - 1) return null;
|
||||
result.put(key, new Cell(p.x(), p.z(), level, bed, Math.max(level, column.top()),
|
||||
material, id, sedimentDepth));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean contained(Map<Long, Cell> wet) {
|
||||
for (Cell cell : wet.values()) {
|
||||
for (int y = cell.bedY() - 2; y <= cell.bedY(); y++) {
|
||||
if (!solid(cell.x(), y, cell.z())) return false;
|
||||
}
|
||||
for (int[] d : CARDINALS) {
|
||||
int x = cell.x() + d[0], z = cell.z() + d[1];
|
||||
Cell neighbor = wet.get(key(x, z));
|
||||
if (neighbor == null) neighbor = cells.get(key(x, z));
|
||||
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) {
|
||||
if (neighbor != null && neighbor.hasWater()) {
|
||||
if (neighbor.waterY() != cell.waterY()) return false;
|
||||
if (y > neighbor.bedY()) continue;
|
||||
}
|
||||
if (!solid(x, y, z)) return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void addFeature(long id, Kind kind, int level, List<Point> path, Map<Long, Cell> wet) {
|
||||
int minX = Integer.MAX_VALUE, minZ = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE, maxZ = Integer.MIN_VALUE;
|
||||
int count = 0;
|
||||
for (Cell cell : wet.values()) {
|
||||
Cell old = cells.get(key(cell.x(), cell.z()));
|
||||
if (old == null || !old.hasWater()) { cells.put(key(cell.x(), cell.z()), cell); count++; }
|
||||
minX = Math.min(minX, cell.x()); minZ = Math.min(minZ, cell.z());
|
||||
maxX = Math.max(maxX, cell.x()); maxZ = Math.max(maxZ, cell.z());
|
||||
}
|
||||
features.add(new Feature(id, kind, level, path, count, new Bounds(minX, minZ, maxX, maxZ)));
|
||||
}
|
||||
|
||||
private static Set<Long> connectedTo(Set<Long> footprint, long start) {
|
||||
Set<Long> connected = new HashSet<>();
|
||||
List<Long> work = new ArrayList<>();
|
||||
connected.add(start); work.add(start);
|
||||
for (int i = 0; i < work.size(); i++) {
|
||||
Point point = point(work.get(i));
|
||||
for (int[] d : CARDINALS) {
|
||||
long next = key(point.x() + d[0], point.z() + d[1]);
|
||||
if (footprint.contains(next) && connected.add(next)) work.add(next);
|
||||
}
|
||||
}
|
||||
return connected;
|
||||
}
|
||||
|
||||
private record ShoreNode(int x, int z, int level, long feature, double cost) {}
|
||||
|
||||
/** Sediment follows shallow topography in broad, discontinuous sheets. A second nearby
|
||||
* pocket can share the same sheet; the result is not a one-block material ring. */
|
||||
private void addSedimentSheets() {
|
||||
Comparator<ShoreNode> order = Comparator.comparingDouble(ShoreNode::cost)
|
||||
.thenComparingInt(ShoreNode::x).thenComparingInt(ShoreNode::z).thenComparingLong(ShoreNode::feature);
|
||||
PriorityQueue<ShoreNode> queue = new PriorityQueue<>(order);
|
||||
Map<Long, Double> best = new HashMap<>();
|
||||
for (Cell cell : cells.values()) if (cell.hasWater()) {
|
||||
queue.add(new ShoreNode(cell.x(), cell.z(), cell.waterY(), cell.featureId(), 0));
|
||||
best.put(key(cell.x(), cell.z()), 0.0);
|
||||
}
|
||||
while (!queue.isEmpty()) {
|
||||
ShoreNode node = queue.remove();
|
||||
if (node.cost() > best.getOrDefault(key(node.x(), node.z()), Double.POSITIVE_INFINITY)) continue;
|
||||
Column column = column(node.x(), node.z());
|
||||
Cell existing = cells.get(key(node.x(), node.z()));
|
||||
if (existing == null) {
|
||||
double extent = 9 + field(node.x(), node.z(), 25, 131) * 10;
|
||||
if (node.cost() > extent) continue;
|
||||
int delta = column.top() - node.level();
|
||||
if (delta < 0 || delta > 5) continue;
|
||||
// Low shore: waterline, then one-block steps. Cut at most two existing blocks;
|
||||
// far deposits keep the original landform and feather back into grass/stone.
|
||||
int terrace = node.level() + (int) Math.floor(node.cost() / 4.5);
|
||||
int bed = Math.max(node.level(), Math.max(column.top() - 2, Math.min(column.top(), terrace)));
|
||||
int depth = sedimentDepth(node.x(), node.z());
|
||||
if (column.solidBottom() > bed - depth - 1) continue;
|
||||
boolean safe = true;
|
||||
for (int[] d : CARDINALS) {
|
||||
Cell next = cells.get(key(node.x() + d[0], node.z() + d[1]));
|
||||
if (next != null && next.hasWater() && bed < next.waterY()) safe = false;
|
||||
}
|
||||
if (!safe) continue;
|
||||
ShoreMaterial material = dryMaterial(node.x(), node.z(), node.cost() / extent);
|
||||
cells.put(key(node.x(), node.z()), new Cell(node.x(), node.z(), -1, bed,
|
||||
column.top(), material, node.feature(), depth));
|
||||
}
|
||||
for (int[] d : NEIGHBORS) {
|
||||
int x = node.x() + d[0], z = node.z() + d[1];
|
||||
if (!inside(x, z, RADIUS - 2)) continue;
|
||||
Cell water = cells.get(key(x, z));
|
||||
if (water != null && water.hasWater()) continue;
|
||||
Column next = column(x, z);
|
||||
if (next.top() < node.level() || next.top() > node.level() + 5) continue;
|
||||
double cost = node.cost() + Math.hypot(d[0], d[1])
|
||||
* (0.7 + Math.abs(next.top() - column.top()) * 0.32 + field(x, z, 12, 211) * 0.6);
|
||||
if (cost > 19 || cost >= best.getOrDefault(key(x, z), Double.POSITIVE_INFINITY)) continue;
|
||||
best.put(key(x, z), cost);
|
||||
queue.add(new ShoreNode(x, z, node.level(), node.feature(), cost));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int sedimentDepth(int x, int z) { return 3 + (int) (field(x, z, 15, 319) * 2.999); }
|
||||
|
||||
private ShoreMaterial wetMaterial(int x, int z, boolean edge) {
|
||||
double geology = field(x, z, 24, 503) * 0.75 + field(x, z, 7, 509) * 0.25;
|
||||
if (geology < 0.46) return ShoreMaterial.STONE;
|
||||
if (geology < 0.65) return ShoreMaterial.GRAVEL;
|
||||
if (!edge && geology > 0.78) return ShoreMaterial.CLAY;
|
||||
return ShoreMaterial.SAND;
|
||||
}
|
||||
|
||||
private ShoreMaterial dryMaterial(int x, int z, double distance) {
|
||||
double geology = field(x, z, 24, 503) * 0.75 + field(x, z, 7, 509) * 0.25;
|
||||
double grass = field(x + 103, z - 137, 19, 521);
|
||||
if (grass > 0.62 || distance > 0.60 + grass * 0.65) return ShoreMaterial.GRASS;
|
||||
if (geology < 0.46) return ShoreMaterial.STONE;
|
||||
if (geology < 0.65) return ShoreMaterial.GRAVEL;
|
||||
return ShoreMaterial.SAND;
|
||||
}
|
||||
|
||||
private record Cliff(int x, int z, int dx, int dz, double score) {}
|
||||
private record FlowNode(Position position, int reach) {}
|
||||
private record Flow(Set<Position> volume, List<Position> path, FlowBounds bounds) {}
|
||||
|
||||
/** Pick rare exposed rock niches at the edges of real upper surfaces. The source replaces
|
||||
* one rock block under an existing roof. Its outlet is already air over an eight-block drop;
|
||||
* no source, shelf, dam, rock column or ocean is ever placed in empty terrain. */
|
||||
private void addSprings() {
|
||||
List<Cliff> cliffs = new ArrayList<>();
|
||||
for (int x = -264; x <= 264; x += GRID) for (int z = -264; z <= 264; z += GRID) {
|
||||
if (!inside(x, z, 264)) continue;
|
||||
int top = coarse.computeIfAbsent(key(x, z), ignored -> coarseTop(point(ignored).x(), point(ignored).z()));
|
||||
if (top < 88) continue;
|
||||
for (int[] d : CARDINALS) {
|
||||
int nx = x + d[0] * GRID, nz = z + d[1] * GRID;
|
||||
int lower = coarse.computeIfAbsent(key(nx, nz), ignored -> coarseTop(point(ignored).x(), point(ignored).z()));
|
||||
if (top - lower < 16) continue;
|
||||
cliffs.add(new Cliff(x, z, d[0], d[1],
|
||||
Math.min(64, top - lower) * 0.06 + random(seed + 811, x + d[0], z + d[1]) * 3));
|
||||
}
|
||||
}
|
||||
cliffs.sort(Comparator.comparingDouble(Cliff::score).reversed()
|
||||
.thenComparingInt(Cliff::x).thenComparingInt(Cliff::z)
|
||||
.thenComparingInt(Cliff::dx).thenComparingInt(Cliff::dz));
|
||||
int attempts = 0;
|
||||
for (Cliff cliff : cliffs) {
|
||||
if (springs.size() >= 3 || attempts++ >= 160) break;
|
||||
boolean placed = false;
|
||||
for (int tangent : new int[]{0, -2, 2, -4, 4}) {
|
||||
for (int along = 0; along < GRID; along++) {
|
||||
int x = cliff.x() + cliff.dx() * along - cliff.dz() * tangent;
|
||||
int z = cliff.z() + cliff.dz() * along + cliff.dx() * tangent;
|
||||
if (!inside(x, z, 266)) continue;
|
||||
int ox = x + cliff.dx(), oz = z + cliff.dz();
|
||||
Column rock = column(x, z), outletColumn = column(ox, oz);
|
||||
if (rock.top() < 80 || rock.top() - outletColumn.top() < 14) continue;
|
||||
// Five blocks below the highest natural surface is below the soil cap.
|
||||
int floor = Math.max(64, Math.max(rock.top() - 20, outletColumn.top() + 8));
|
||||
for (int y = rock.top() - 5; y >= floor; y -= 3) {
|
||||
Position source = new Position(x, y, z), outlet = new Position(ox, y, oz);
|
||||
if (nearModifiedGround(source, 32) || !separatedSpring(source) || !springNiche(source, cliff.dx(), cliff.dz())) continue;
|
||||
Flow flow = traceSpring(source, outlet);
|
||||
if (flow == null || flow.bounds().minY() > y - 12) continue;
|
||||
long id = mix(seed ^ (0x5F81A6L + springs.size() * 0x9E3779B97F4A7C15L));
|
||||
springs.add(new Spring(id, source, outlet, flow.path(), flow.bounds()));
|
||||
springFlow.addAll(flow.volume());
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
if (placed) break;
|
||||
}
|
||||
if (placed) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean nearModifiedGround(Position source, int distance) {
|
||||
for (Cell cell : cells.values()) {
|
||||
int dy = Math.max(Math.max(cell.bedY() - cell.sedimentDepth() - source.y(), 0),
|
||||
source.y() - cell.carveTop());
|
||||
double horizontal = Math.hypot(source.x() - cell.x(), source.z() - cell.z());
|
||||
if (Math.hypot(horizontal, dy) < distance) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean separatedSpring(Position source) {
|
||||
for (Spring spring : springs) {
|
||||
double horizontal = Math.hypot(source.x() - spring.source().x(), source.z() - spring.source().z());
|
||||
int dy = Math.abs(source.y() - spring.source().y());
|
||||
if (Math.hypot(horizontal, dy) < 64 || dy < 12) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private record Wall(int x, int y, int z, int dx, int dz, double score) {}
|
||||
|
||||
/** Probe a few exposed faces below the highest surface as well. This finds overhangs and
|
||||
* lower strata that a highest-column heightmap cannot represent, without inventing a ledge. */
|
||||
private void addLowerWallSprings() {
|
||||
List<Wall> candidates = new ArrayList<>();
|
||||
for (int x = -240; x <= 240; x += 16) for (int z = -240; z <= 240; z += 16) {
|
||||
if (!inside(x, z, 240)) continue;
|
||||
int top = coarse.computeIfAbsent(key(x, z), ignored -> coarseTop(point(ignored).x(), point(ignored).z()));
|
||||
for (int y = top - 32; y >= 64; y -= 16) {
|
||||
if (!solid(x, y, z)) continue;
|
||||
for (int[] direction : CARDINALS) {
|
||||
if (solid(x + direction[0] * 4, y, z + direction[1] * 4)) continue;
|
||||
candidates.add(new Wall(x, y, z, direction[0], direction[1],
|
||||
random(seed + 1217 + y, x + direction[0], z + direction[1]) * 3
|
||||
+ Math.min(100, top - y) * 0.015));
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates.sort(Comparator.comparingDouble(Wall::score).reversed()
|
||||
.thenComparingInt(Wall::x).thenComparingInt(Wall::z).thenComparingInt(Wall::y)
|
||||
.thenComparingInt(Wall::dx).thenComparingInt(Wall::dz));
|
||||
int attempts = 0;
|
||||
for (Wall candidate : candidates) {
|
||||
if (springs.size() >= 2 || attempts++ >= 96) break;
|
||||
boolean placed = false;
|
||||
for (int tangent : new int[]{0, -2, 2}) {
|
||||
for (int along = 0; along < 4; along++) {
|
||||
int x = candidate.x() + candidate.dx() * along - candidate.dz() * tangent;
|
||||
int z = candidate.z() + candidate.dz() * along + candidate.dx() * tangent;
|
||||
for (int dy : new int[]{0, 3, -3, 6, -6}) {
|
||||
int y = candidate.y() + dy;
|
||||
if (y < 56 || column(x, z).top() - y < 24) continue;
|
||||
Position source = new Position(x, y, z);
|
||||
if (nearModifiedGround(source, 32) || !separatedSpring(source)
|
||||
|| !springNiche(source, candidate.dx(), candidate.dz())) continue;
|
||||
Position outlet = new Position(x + candidate.dx(), y, z + candidate.dz());
|
||||
Flow flow = traceSpring(source, outlet);
|
||||
if (flow == null || flow.bounds().minY() > y - 12) continue;
|
||||
long id = mix(seed ^ (0x5F81A6L + springs.size() * 0x9E3779B97F4A7C15L));
|
||||
springs.add(new Spring(id, source, outlet, flow.path(), flow.bounds()));
|
||||
springFlow.addAll(flow.volume());
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
if (placed) break;
|
||||
}
|
||||
if (placed) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean springNiche(Position source, int dx, int dz) {
|
||||
for (int y = source.y() - 3; y <= source.y() + 1; y++) {
|
||||
if (!solid(source.x(), y, source.z())) return false;
|
||||
}
|
||||
for (int[] d : CARDINALS) {
|
||||
if (d[0] == dx && d[1] == dz) continue;
|
||||
if (!solid(source.x() + d[0], source.y(), source.z() + d[1])) return false;
|
||||
}
|
||||
for (int y = source.y(); y >= source.y() - 8; y--) {
|
||||
if (solid(source.x() + dx, y, source.z() + dz)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Predict drainage through undecorated rock to select springs and inspection bounds.
|
||||
* Descend through air first, then explore seven horizontal steps on a shelf. Later trees
|
||||
* can divert the actual fluid: this prediction is not a bound on decorated-world physics.
|
||||
* Integration tests trace actual water back to its source within the inspected chunk halo. */
|
||||
private Flow traceSpring(Position source, Position outlet) {
|
||||
Map<Position, Integer> reach = new HashMap<>();
|
||||
Map<Position, Position> parent = new HashMap<>();
|
||||
ArrayDeque<FlowNode> queue = new ArrayDeque<>();
|
||||
reach.put(source, 7);
|
||||
queue.add(new FlowNode(source, 7));
|
||||
Position lowest = source;
|
||||
while (!queue.isEmpty()) {
|
||||
FlowNode node = queue.removeFirst();
|
||||
Position at = node.position();
|
||||
if (node.reach() < reach.getOrDefault(at, -1)) continue;
|
||||
if (at.y() < lowest.y()) lowest = at;
|
||||
if (reach.size() > 24000) return null;
|
||||
if (at.y() <= 0) continue;
|
||||
Position below = new Position(at.x(), at.y() - 1, at.z());
|
||||
if (!flowSolid(below, source)) {
|
||||
enqueueFlow(below, 7, at, queue, reach, parent);
|
||||
continue;
|
||||
}
|
||||
if (node.reach() <= 0) continue;
|
||||
for (int[] d : CARDINALS) {
|
||||
Position next = new Position(at.x() + d[0], at.y(), at.z() + d[1]);
|
||||
if (flowSolid(next, source)) continue;
|
||||
enqueueFlow(next, node.reach() - 1, at, queue, reach, parent);
|
||||
}
|
||||
}
|
||||
if (!reach.containsKey(outlet)) return null;
|
||||
int minX = source.x(), maxX = minX, minY = source.y(), maxY = minY;
|
||||
int minZ = source.z(), maxZ = minZ;
|
||||
for (Position position : reach.keySet()) {
|
||||
minX = Math.min(minX, position.x()); maxX = Math.max(maxX, position.x());
|
||||
minY = Math.min(minY, position.y()); maxY = Math.max(maxY, position.y());
|
||||
minZ = Math.min(minZ, position.z()); maxZ = Math.max(maxZ, position.z());
|
||||
}
|
||||
List<Position> path = new ArrayList<>();
|
||||
for (Position at = lowest; at != null; at = parent.get(at)) path.add(at);
|
||||
Collections.reverse(path);
|
||||
return new Flow(Set.copyOf(reach.keySet()), path, new FlowBounds(minX, minY, minZ, maxX, maxY, maxZ));
|
||||
}
|
||||
|
||||
private boolean flowSolid(Position at, Position source) {
|
||||
if (at.equals(source)) return false;
|
||||
Cell cell = cells.get(key(at.x(), at.z()));
|
||||
if (cell != null && at.y() > cell.bedY() && at.y() <= cell.carveTop()) return false;
|
||||
return solid(at.x(), at.y(), at.z());
|
||||
}
|
||||
|
||||
private static void enqueueFlow(Position next, int available, Position from, ArrayDeque<FlowNode> queue,
|
||||
Map<Position, Integer> reach, Map<Position, Position> parent) {
|
||||
if (available <= reach.getOrDefault(next, -1)) return;
|
||||
if (!reach.containsKey(next)) parent.put(next, from);
|
||||
reach.put(next, available);
|
||||
queue.addLast(new FlowNode(next, available));
|
||||
}
|
||||
|
||||
private boolean nearWater(int x, int z, int distance) {
|
||||
for (Feature feature : features) {
|
||||
Point point = feature.path().getFirst();
|
||||
if (Math.hypot(x - point.x(), z - point.z()) < distance) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean solid(int x, int y, int z) { samples++; return sampler.sample(x, y, z) > 0; }
|
||||
private long featureId(int index) { return mix(seed ^ (0x51A7E7L + index * 0x9E3779B97F4A7C15L)); }
|
||||
|
||||
private double terrainNoise(int x, int z) { return field(x, z, 8, 31); }
|
||||
|
||||
private double field(int x, int z, int scale, long salt) {
|
||||
int gx = Math.floorDiv(x, scale), gz = Math.floorDiv(z, scale);
|
||||
double tx = smooth(Math.floorMod(x, scale) / (double) scale);
|
||||
double tz = smooth(Math.floorMod(z, scale) / (double) scale);
|
||||
double a = random(seed + salt, gx, gz) * (1 - tx) + random(seed + salt, gx + 1, gz) * tx;
|
||||
double b = random(seed + salt, gx, gz + 1) * (1 - tx) + random(seed + salt, gx + 1, gz + 1) * tx;
|
||||
return a * (1 - tz) + b * tz;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean inside(int x, int z, int radius) { return Math.hypot((double) x, z) < radius; }
|
||||
private static long key(int x, int z) { return ((long) x << 32) | (z & 0xffffffffL); }
|
||||
private static Point point(long key) { return new Point((int) (key >> 32), (int) key); }
|
||||
private static double smooth(double t) { return t * t * (3 - 2 * t); }
|
||||
private static double random(long seed, int x, int z) { return (mix(seed ^ key(x, z)) >>> 11) * 0x1.0p-53; }
|
||||
private static long mix(long value) {
|
||||
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
|
||||
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
|
||||
return value ^ (value >>> 31);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
|
||||
/** Alpha.6 only: immutable plans cached by world state, applied to fresh terrain before decoration. */
|
||||
public final class LayeredHydrologyRuntime {
|
||||
private static final Map<RandomState, LayeredHydrology.Plan> PLANS =
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
private static final Map<RandomState, LayeredLavaDeposit.Plan> LAVA_PLANS =
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
|
||||
private LayeredHydrologyRuntime() {}
|
||||
|
||||
public static boolean enabled(NoiseBasedChunkGenerator generator) {
|
||||
return generator.generatorSettings().is(SanctuarySpawn.LAYERED_SETTINGS);
|
||||
}
|
||||
|
||||
public static LayeredHydrology.Plan layeredPlan(ServerLevel level) {
|
||||
return plan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(),
|
||||
level.getChunkSource().randomState());
|
||||
}
|
||||
|
||||
public static LayeredHydrology.Plan plan(NoiseBasedChunkGenerator generator, RandomState randomState) {
|
||||
if (!enabled(generator)) throw new IllegalArgumentException("Layered hydrology is not enabled for this generator");
|
||||
return PLANS.computeIfAbsent(randomState, state -> {
|
||||
long start = System.nanoTime();
|
||||
var density = generator.generatorSettings().value().noiseRouter().finalDensity();
|
||||
var plan = LayeredHydrology.create(state.seed(),
|
||||
(x, y, z) -> state.sampleBlockValueUncached(density, x, y, z));
|
||||
SanctuaryMod.LOGGER.info("Sanctuary layered hydrology: seed {}, {} features, {} columns, {} springs, {} ms",
|
||||
state.seed(), plan.features().size(), plan.cells().size(), plan.springs().size(),
|
||||
(System.nanoTime() - start) / 1_000_000);
|
||||
return plan;
|
||||
});
|
||||
}
|
||||
|
||||
public static LayeredLavaDeposit.Plan lavaPlan(ServerLevel level) {
|
||||
return lavaPlan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(),
|
||||
level.getChunkSource().randomState());
|
||||
}
|
||||
|
||||
public static LayeredLavaDeposit.Plan lavaPlan(NoiseBasedChunkGenerator generator, RandomState randomState) {
|
||||
// Acquire the water plan before the lava cache lock; the water planner never acquires it.
|
||||
var water = plan(generator, randomState);
|
||||
return LAVA_PLANS.computeIfAbsent(randomState, state -> {
|
||||
long start = System.nanoTime();
|
||||
var density = generator.generatorSettings().value().noiseRouter().finalDensity();
|
||||
var lava = LayeredLavaDeposit.create(state.seed(),
|
||||
(x, y, z) -> state.sampleBlockValueUncached(density, x, y, z), water);
|
||||
SanctuaryMod.LOGGER.info("Sanctuary starter lava: seed {}, {} sources, access {}, {} ms",
|
||||
state.seed(), lava.lavaCells().size(), lava.access(), (System.nanoTime() - start) / 1_000_000);
|
||||
return lava;
|
||||
});
|
||||
}
|
||||
|
||||
public static void apply(NoiseBasedChunkGenerator generator, RandomState randomState, ChunkAccess chunk) {
|
||||
if (!enabled(generator)) return;
|
||||
var chunkPos = chunk.getPos();
|
||||
if (Math.abs((long) chunkPos.x() * 16) > IslandShape.TERRAIN_LIMIT + 16L
|
||||
|| Math.abs((long) chunkPos.z() * 16) > IslandShape.TERRAIN_LIMIT + 16L) return;
|
||||
var plan = plan(generator, randomState);
|
||||
var cells = plan.cellsInChunk(chunkPos.x(), chunkPos.z());
|
||||
var springs = plan.springsInChunk(chunkPos.x(), chunkPos.z());
|
||||
var outlets = plan.springs().stream().map(LayeredHydrology.Spring::outlet)
|
||||
.filter(outlet -> (outlet.x() >> 4) == chunkPos.x() && (outlet.z() >> 4) == chunkPos.z()).toList();
|
||||
var lava = lavaPlan(generator, randomState);
|
||||
var lavaCells = lava.cellsInChunk(chunkPos.x(), chunkPos.z());
|
||||
var lavaSupports = lava.supportsInChunk(chunkPos.x(), chunkPos.z());
|
||||
if (cells.isEmpty() && springs.isEmpty() && outlets.isEmpty() && lavaCells.isEmpty() && lavaSupports.isEmpty()) return;
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
// Validate all replacements and two intact support layers before modifying this chunk.
|
||||
for (var cell : cells) {
|
||||
for (int y = cell.bedY() - cell.sedimentDepth() - 1; y <= cell.bedY(); y++) {
|
||||
requireSolid(chunk, pos.set(cell.x(), y, cell.z()), randomState.seed());
|
||||
}
|
||||
}
|
||||
for (var spring : springs) {
|
||||
var source = spring.source();
|
||||
requireSolid(chunk, pos.set(source.x(), source.y(), source.z()), randomState.seed());
|
||||
}
|
||||
for (var support : lavaSupports) {
|
||||
requireSolid(chunk, pos.set(support.x(), support.y(), support.z()), randomState.seed());
|
||||
if (chunk.getBlockState(pos).ignitedByLava()) {
|
||||
throw new IllegalStateException("Lava pocket requires nonflammable natural support at " + pos);
|
||||
}
|
||||
}
|
||||
for (var cell : lavaCells) {
|
||||
requireSolid(chunk, pos.set(cell.x(), cell.y(), cell.z()), randomState.seed());
|
||||
}
|
||||
for (var cell : cells) {
|
||||
for (int depth = 0; depth < cell.sedimentDepth(); depth++) {
|
||||
pos.set(cell.x(), cell.bedY() - depth, cell.z());
|
||||
BlockState material = switch (cell.material()) {
|
||||
case SAND -> Blocks.SAND.defaultBlockState();
|
||||
case GRAVEL -> Blocks.GRAVEL.defaultBlockState();
|
||||
case CLAY -> Blocks.CLAY.defaultBlockState();
|
||||
case STONE -> Blocks.STONE.defaultBlockState();
|
||||
case GRASS -> (depth == 0 ? Blocks.GRASS_BLOCK : Blocks.DIRT).defaultBlockState();
|
||||
};
|
||||
chunk.setBlockState(pos, material, 0);
|
||||
}
|
||||
for (int y = cell.bedY() + 1; y <= Math.max(cell.waterY(), cell.carveTop()); y++) {
|
||||
pos.set(cell.x(), y, cell.z());
|
||||
chunk.setBlockState(pos, cell.hasWater() && y <= cell.waterY()
|
||||
? Blocks.WATER.defaultBlockState() : Blocks.AIR.defaultBlockState(), 0);
|
||||
if (cell.hasWater() && y <= cell.waterY()) chunk.markPosForPostProcessing(pos);
|
||||
}
|
||||
}
|
||||
for (var spring : springs) {
|
||||
var source = spring.source();
|
||||
pos.set(source.x(), source.y(), source.z());
|
||||
chunk.setBlockState(pos, Blocks.WATER.defaultBlockState(), 0);
|
||||
// Only the source is generated. Vanilla fluid ticks create the descending waterfall.
|
||||
chunk.markPosForPostProcessing(pos);
|
||||
}
|
||||
for (var cell : lavaCells) {
|
||||
pos.set(cell.x(), cell.y(), cell.z());
|
||||
chunk.setBlockState(pos, (cell.lava() ? Blocks.LAVA : Blocks.AIR).defaultBlockState(), 0);
|
||||
if (cell.lava()) chunk.markPosForPostProcessing(pos);
|
||||
}
|
||||
// This existing vanilla queue is consumed after neighbouring chunks finish decoration.
|
||||
// Mark the outlet in its own chunk, including when its source belongs to a neighbour.
|
||||
for (var outlet : outlets) {
|
||||
pos.set(outlet.x(), outlet.y(), outlet.z());
|
||||
chunk.markPosForPostProcessing(pos);
|
||||
}
|
||||
Heightmap.primeHeightmaps(chunk, EnumSet.of(Heightmap.Types.WORLD_SURFACE_WG,
|
||||
Heightmap.Types.OCEAN_FLOOR_WG));
|
||||
}
|
||||
|
||||
private static void requireSolid(ChunkAccess chunk, BlockPos pos, long seed) {
|
||||
if (!chunk.getBlockState(pos).isCollisionShapeFullBlock(chunk, pos)) {
|
||||
throw new IllegalStateException("Layered hydrology differs from terrain at " + pos + " for seed " + seed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.util.stream.Stream;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeResolver;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Climate;
|
||||
|
||||
/** Alpha.6 palette: flowered upper meadows and distinct humid lower terraces. */
|
||||
public final class LayeredIslandBiomeSource extends BiomeSource {
|
||||
public static final ResourceKey<Biome> MEADOW = key("layered_meadow");
|
||||
public static final ResourceKey<Biome> FLOWER_MEADOW = key("layered_flower_meadow");
|
||||
public static final ResourceKey<Biome> LOWER_GROVE = key("layered_lower_grove");
|
||||
public static final ResourceKey<Biome> MOSSY_TERRACES = key("layered_mossy_terraces");
|
||||
public static final ResourceKey<Biome> ROCKY_TERRACES = key("layered_rocky_terraces");
|
||||
|
||||
public static final MapCodec<LayeredIslandBiomeSource> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||
Biome.CODEC.fieldOf("meadow").forGetter(source -> source.meadow),
|
||||
Biome.CODEC.fieldOf("flower_meadow").forGetter(source -> source.flowerMeadow),
|
||||
Biome.CODEC.fieldOf("lower_grove").forGetter(source -> source.lowerGrove),
|
||||
Biome.CODEC.fieldOf("mossy_terraces").forGetter(source -> source.mossyTerraces),
|
||||
Biome.CODEC.fieldOf("rocky_terraces").forGetter(source -> source.rockyTerraces)
|
||||
).apply(instance, LayeredIslandBiomeSource::new));
|
||||
|
||||
private final Holder<Biome> meadow;
|
||||
private final Holder<Biome> flowerMeadow;
|
||||
private final Holder<Biome> lowerGrove;
|
||||
private final Holder<Biome> mossyTerraces;
|
||||
private final Holder<Biome> rockyTerraces;
|
||||
|
||||
public LayeredIslandBiomeSource(Holder<Biome> meadow, Holder<Biome> flowerMeadow,
|
||||
Holder<Biome> lowerGrove, Holder<Biome> mossyTerraces, Holder<Biome> rockyTerraces) {
|
||||
this.meadow = meadow;
|
||||
this.flowerMeadow = flowerMeadow;
|
||||
this.lowerGrove = lowerGrove;
|
||||
this.mossyTerraces = mossyTerraces;
|
||||
this.rockyTerraces = rockyTerraces;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<LayeredIslandBiomeSource> codec() { return CODEC; }
|
||||
|
||||
@Override
|
||||
protected Stream<Holder<Biome>> collectPossibleBiomes() {
|
||||
return Stream.of(meadow, flowerMeadow, lowerGrove, mossyTerraces, rockyTerraces);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiomeResolver createResolver(Climate.Sampler sampler) {
|
||||
return (quartX, quartY, quartZ) -> {
|
||||
int x = quartX * 4, y = quartY * 4, z = quartZ * 4;
|
||||
float variation = sampler.temperature().sampleValue(x, 0, z);
|
||||
float moisture = sampler.humidity().sampleValue(x, 0, z);
|
||||
return switch (LayeredBiomePalette.select(y, variation, moisture)) {
|
||||
case MEADOW -> meadow;
|
||||
case FLOWER_MEADOW -> flowerMeadow;
|
||||
case LOWER_GROVE -> lowerGrove;
|
||||
case MOSSY_TERRACES -> mossyTerraces;
|
||||
case ROCKY_TERRACES -> rockyTerraces;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
private static ResourceKey<Biome> key(String path) {
|
||||
return ResourceKey.create(Registries.BIOME, SanctuaryMod.id(path));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/** One small, covered lava pocket in existing rock, reached from an existing outdoor ledge.
|
||||
* No solid blocks are added, and unsuitable seeds are allowed to produce no deposit. */
|
||||
public final class LayeredLavaDeposit {
|
||||
public static final int RADIUS = 192;
|
||||
public static final int WATER_CLEARANCE = 32;
|
||||
public static final int LAVA_VOLUME = 18;
|
||||
private static final int ENTRY_DISTANCE = 8;
|
||||
private static final int[][] DIRECTIONS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
private static final Comparator<Position> POSITION_ORDER = Comparator.comparingInt(Position::x)
|
||||
.thenComparingInt(Position::z).thenComparingInt(Position::y);
|
||||
private static final Comparator<Cell> CELL_ORDER = Comparator.comparingInt(Cell::x)
|
||||
.thenComparingInt(Cell::z).thenComparingInt(Cell::y);
|
||||
|
||||
private LayeredLavaDeposit() {}
|
||||
|
||||
public record Position(int x, int y, int z) {}
|
||||
/** lava=false removes existing rock to open the two-block-high chamber or approach. */
|
||||
public record Cell(int x, int y, int z, boolean lava) {}
|
||||
|
||||
public static final class Plan {
|
||||
private final List<Cell> cells;
|
||||
private final List<Cell> lavaCells;
|
||||
private final List<Position> supports;
|
||||
private final Optional<Position> access;
|
||||
private final Map<Long, List<Cell>> chunks;
|
||||
private final Map<Long, List<Position>> supportChunks;
|
||||
|
||||
private Plan(List<Cell> cells, Set<Position> supports, Position access) {
|
||||
this.cells = cells.stream().sorted(CELL_ORDER).toList();
|
||||
lavaCells = this.cells.stream().filter(Cell::lava).toList();
|
||||
this.supports = supports.stream().sorted(POSITION_ORDER).toList();
|
||||
this.access = Optional.ofNullable(access);
|
||||
Map<Long, List<Cell>> byChunk = new HashMap<>();
|
||||
for (Cell cell : this.cells) byChunk.computeIfAbsent(key(cell.x() >> 4, cell.z() >> 4),
|
||||
ignored -> new ArrayList<>()).add(cell);
|
||||
byChunk.replaceAll((ignored, values) -> List.copyOf(values));
|
||||
chunks = Map.copyOf(byChunk);
|
||||
Map<Long, List<Position>> bySupportChunk = new HashMap<>();
|
||||
for (Position support : this.supports) bySupportChunk.computeIfAbsent(key(support.x() >> 4, support.z() >> 4),
|
||||
ignored -> new ArrayList<>()).add(support);
|
||||
bySupportChunk.replaceAll((ignored, values) -> List.copyOf(values));
|
||||
supportChunks = Map.copyOf(bySupportChunk);
|
||||
}
|
||||
|
||||
public List<Cell> cells() { return cells; }
|
||||
public List<Cell> lavaCells() { return lavaCells; }
|
||||
public List<Position> supports() { return supports; }
|
||||
/** Player feet on the existing ledge; this column was naturally open to the sky. */
|
||||
public Optional<Position> access() { return access; }
|
||||
public List<Cell> cellsInChunk(int x, int z) { return chunks.getOrDefault(key(x, z), List.of()); }
|
||||
public List<Position> supportsInChunk(int x, int z) { return supportChunks.getOrDefault(key(x, z), List.of()); }
|
||||
}
|
||||
|
||||
public static Plan create(long seed, LayeredHydrology.Sampler sampler, LayeredHydrology.Plan hydrology) {
|
||||
return new Search(seed, sampler, hydrology).find();
|
||||
}
|
||||
|
||||
private record Candidate(int x, int z, int dx, int dz, double score) {}
|
||||
|
||||
private static final class Search {
|
||||
private final long seed;
|
||||
private final LayeredHydrology.Sampler sampler;
|
||||
private final LayeredHydrology.Plan hydrology;
|
||||
private final Map<Long, Integer> heights = new HashMap<>();
|
||||
|
||||
Search(long seed, LayeredHydrology.Sampler sampler, LayeredHydrology.Plan hydrology) {
|
||||
this.seed = seed;
|
||||
this.sampler = sampler;
|
||||
this.hydrology = hydrology;
|
||||
}
|
||||
|
||||
Plan find() {
|
||||
List<Candidate> candidates = new ArrayList<>();
|
||||
for (int x = -184; x <= 184; x += 8) for (int z = -184; z <= 184; z += 8) {
|
||||
if (!inside(x, z, RADIUS - 8)) continue;
|
||||
int ledge = top(x, z);
|
||||
if (ledge < 64 || ledge > 300) continue;
|
||||
for (int[] direction : DIRECTIONS) {
|
||||
int cx = x - direction[0] * ENTRY_DISTANCE, cz = z - direction[1] * ENTRY_DISTANCE;
|
||||
if (!inside(cx, cz, RADIUS - 12) || !clearOfWater(cx, cz)) continue;
|
||||
int rise = top(cx, cz) - ledge;
|
||||
if (rise < 12) continue;
|
||||
candidates.add(new Candidate(x, z, direction[0], direction[1],
|
||||
random(seed, cx, cz) + Math.min(rise, 40) * 0.015));
|
||||
}
|
||||
}
|
||||
candidates.sort(Comparator.comparingDouble(Candidate::score).reversed()
|
||||
.thenComparingInt(Candidate::x).thenComparingInt(Candidate::z)
|
||||
.thenComparingInt(Candidate::dx).thenComparingInt(Candidate::dz));
|
||||
int attempts = 0;
|
||||
for (Candidate candidate : candidates) {
|
||||
if (attempts++ >= 192) break;
|
||||
// Refine the coarse edge, including a ledge lying between two eight-block samples.
|
||||
for (int along = 0; along < 8; along++) for (int tangent : new int[]{0, -3, 3}) {
|
||||
int x = candidate.x() - candidate.dx() * along - candidate.dz() * tangent;
|
||||
int z = candidate.z() - candidate.dz() * along + candidate.dx() * tangent;
|
||||
Plan plan = tryNiche(x, z, candidate.dx(), candidate.dz());
|
||||
if (plan != null) return plan;
|
||||
}
|
||||
}
|
||||
return new Plan(List.of(), Set.of(), null);
|
||||
}
|
||||
|
||||
private Plan tryNiche(int entryX, int entryZ, int dx, int dz) {
|
||||
int cx = entryX - dx * ENTRY_DISTANCE, cz = entryZ - dz * ENTRY_DISTANCE;
|
||||
if (!inside(cx, cz, RADIUS - 12) || !clearOfWater(cx, cz)) return null;
|
||||
int level = top(entryX, entryZ);
|
||||
if (level < 64 || level > 300 || top(cx, cz) < level + 12) return null;
|
||||
// The player can discover and enter from an actual outdoor ledge, not a sealed cave.
|
||||
for (int y = level + 1; y < 384; y++) if (solid(entryX, y, entryZ)) return null;
|
||||
for (int y = level - 2; y <= level; y++) if (!solid(entryX, y, entryZ)) return null;
|
||||
|
||||
List<Cell> cells = new ArrayList<>();
|
||||
Set<Position> changed = new HashSet<>();
|
||||
Set<Position> supports = new HashSet<>();
|
||||
for (int x = cx - 1; x <= cx + 1; x++) for (int z = cz - 1; z <= cz + 1; z++) {
|
||||
for (int y = level - 1; y <= level + 2; y++) {
|
||||
if (!solid(x, y, z)) return null;
|
||||
add(cells, changed, x, y, z, y <= level);
|
||||
}
|
||||
}
|
||||
// A dry lip separates the ledge from the pool. Only the air above it is opened.
|
||||
for (int distance = 2; distance < ENTRY_DISTANCE; distance++) {
|
||||
int x = cx + dx * distance, z = cz + dz * distance;
|
||||
for (int y = level + 1; y <= level + 2; y++) {
|
||||
if (solid(x, y, z)) add(cells, changed, x, y, z, false);
|
||||
}
|
||||
}
|
||||
// Every carved column must lie under eight continuous natural rock blocks. This
|
||||
// excludes soil-cap excavation at the entrance as well as exposed surface lava.
|
||||
Map<Long, Integer> highest = new HashMap<>();
|
||||
for (Cell cell : cells) highest.merge(key(cell.x(), cell.z()), cell.y(), Math::max);
|
||||
for (var column : highest.entrySet()) {
|
||||
int x = (int) (column.getKey() >> 32), z = (int) (long) column.getKey();
|
||||
for (int y = column.getValue() + 1; y <= column.getValue() + 8; y++) {
|
||||
if (!requireRock(supports, x, y, z)) return null;
|
||||
}
|
||||
}
|
||||
// Three intact blocks under the pool, two-block-thick side walls, and a solid chamber
|
||||
// surround prevent escape through an adjacent cave. No part of this shell is created.
|
||||
for (int x = cx - 3; x <= cx + 3; x++) for (int z = cz - 3; z <= cz + 3; z++) {
|
||||
for (int y = level - 4; y <= level + 2; y++) {
|
||||
if (changed.contains(new Position(x, y, z))) continue;
|
||||
if (!requireRock(supports, x, y, z)) return null;
|
||||
}
|
||||
}
|
||||
// The natural roof also covers a conservative envelope around local lava fire spread.
|
||||
// Its thickness keeps later surface vegetation well above the lava chamber.
|
||||
for (int x = cx - 4; x <= cx + 4; x++) for (int z = cz - 4; z <= cz + 4; z++) {
|
||||
for (int y = level + 3; y <= level + 10; y++) {
|
||||
if (!requireRock(supports, x, y, z)) return null;
|
||||
}
|
||||
}
|
||||
for (int distance = 2; distance <= ENTRY_DISTANCE; distance++) {
|
||||
int x = cx + dx * distance, z = cz + dz * distance;
|
||||
for (int y = level - 2; y <= level; y++) {
|
||||
if (!requireRock(supports, x, y, z)) return null;
|
||||
}
|
||||
}
|
||||
supports.removeAll(changed);
|
||||
return new Plan(cells, supports, new Position(entryX, level + 1, entryZ));
|
||||
}
|
||||
|
||||
private boolean requireRock(Set<Position> supports, int x, int y, int z) {
|
||||
if (!solid(x, y, z)) return false;
|
||||
supports.add(new Position(x, y, z));
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean clearOfWater(int x, int z) {
|
||||
// The extra ten blocks include both the entrance and the complete protected shell.
|
||||
int clearance = WATER_CLEARANCE + 10;
|
||||
for (var cell : hydrology.cells()) {
|
||||
long dx = x - cell.x(), dz = z - cell.z();
|
||||
if (dx * dx + dz * dz < (long) clearance * clearance) return false;
|
||||
}
|
||||
for (var spring : hydrology.springs()) {
|
||||
var bounds = spring.flowBounds();
|
||||
long dx = Math.max(Math.max((long) bounds.minX() - x, 0), (long) x - bounds.maxX());
|
||||
long dz = Math.max(Math.max((long) bounds.minZ() - z, 0), (long) z - bounds.maxZ());
|
||||
if (dx * dx + dz * dz < (long) clearance * clearance) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private int top(int x, int z) {
|
||||
return heights.computeIfAbsent(key(x, z), ignored -> {
|
||||
for (int y = 383; y >= 32; y -= 4) {
|
||||
if (!solid(x, y, z)) continue;
|
||||
for (int exact = Math.min(383, y + 3); exact > y; exact--) if (solid(x, exact, z)) return exact;
|
||||
return y;
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
}
|
||||
|
||||
private boolean solid(int x, int y, int z) { return sampler.sample(x, y, z) > 0; }
|
||||
}
|
||||
|
||||
private static void add(List<Cell> cells, Set<Position> changed, int x, int y, int z, boolean lava) {
|
||||
if (changed.add(new Position(x, y, z))) cells.add(new Cell(x, y, z, lava));
|
||||
}
|
||||
|
||||
private static boolean inside(int x, int z, int radius) { return (long) x * x + (long) z * z < (long) radius * radius; }
|
||||
private static long key(int x, int z) { return ((long) x << 32) | (z & 0xffffffffL); }
|
||||
private static double random(long seed, int x, int z) {
|
||||
long value = seed ^ key(x, z) ^ 0x1A7A5EEDL;
|
||||
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
|
||||
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
|
||||
return ((value ^ (value >>> 31)) >>> 11) * 0x1.0p-53;
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.feature.Feature;
|
||||
|
||||
/** A few plants on existing suitable shores, after the forest has been decorated. */
|
||||
public final class LayeredShoreSugarCaneFeature implements Feature {
|
||||
public static final MapCodec<LayeredShoreSugarCaneFeature> CODEC = MapCodec.unit(LayeredShoreSugarCaneFeature::new);
|
||||
|
||||
@Override
|
||||
public MapCodec<LayeredShoreSugarCaneFeature> codec() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource random, BlockPos origin) {
|
||||
if (!(generator instanceof NoiseBasedChunkGenerator noise) || !LayeredHydrologyRuntime.enabled(noise)) return false;
|
||||
var plan = LayeredHydrologyRuntime.plan(noise, level.getLevel().getChunkSource().randomState());
|
||||
var cane = Blocks.SUGAR_CANE.defaultBlockState();
|
||||
boolean placed = false;
|
||||
for (var cell : plan.cellsInChunk(origin.getX() >> 4, origin.getZ() >> 4)) {
|
||||
if (cell.hasWater() || random.nextInt(7) != 0) continue;
|
||||
BlockPos base = new BlockPos(cell.x(), cell.bedY() + 1, cell.z());
|
||||
if (!level.getBlockState(base).isAir() || !cane.canSurvive(level, base)) continue;
|
||||
int height = 1 + random.nextInt(3);
|
||||
for (int y = 0; y < height; y++) {
|
||||
BlockPos pos = base.above(y);
|
||||
if (!level.getBlockState(pos).isAir() || !cane.canSurvive(level, pos)) break;
|
||||
setBlock(level, pos, cane);
|
||||
placed = true;
|
||||
}
|
||||
}
|
||||
return placed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.VegetationBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.material.Fluids;
|
||||
|
||||
/** One-time alpha.6 outlet finishing, using vanilla's persisted and consumed generation queue. */
|
||||
public final class LayeredSpringOutlets {
|
||||
public record Pending(List<LayeredHydrology.Position> sources, List<LayeredHydrology.Position> outlets) {
|
||||
public static final Pending EMPTY = new Pending(List.of(), List.of());
|
||||
public Pending { sources = List.copyOf(sources); outlets = List.copyOf(outlets); }
|
||||
public boolean isEmpty() { return sources.isEmpty() && outlets.isEmpty(); }
|
||||
}
|
||||
|
||||
private LayeredSpringOutlets() {}
|
||||
|
||||
/** Called before vanilla consumes the queue; a fully processed/reloaded chunk has no work. */
|
||||
public static Pending capturePending(ServerLevel level, LevelChunk chunk) {
|
||||
if (!(level.getChunkSource().getGenerator() instanceof NoiseBasedChunkGenerator noise)
|
||||
|| !LayeredHydrologyRuntime.enabled(noise)) return Pending.EMPTY;
|
||||
boolean queued = false;
|
||||
for (var section : chunk.getPostProcessing()) if (section != null && !section.isEmpty()) {
|
||||
queued = true;
|
||||
break;
|
||||
}
|
||||
if (!queued) return Pending.EMPTY;
|
||||
var plan = LayeredHydrologyRuntime.plan(noise, level.getChunkSource().randomState());
|
||||
List<LayeredHydrology.Position> sources = new ArrayList<>(), outlets = new ArrayList<>();
|
||||
for (var spring : plan.springs()) {
|
||||
if (markedHere(chunk, spring.source())) sources.add(spring.source());
|
||||
if (markedHere(chunk, spring.outlet())) outlets.add(spring.outlet());
|
||||
}
|
||||
return new Pending(sources, outlets);
|
||||
}
|
||||
|
||||
/** All neighbouring FEATURES have finished before a chunk reaches this stage. */
|
||||
public static void finish(ServerLevel level, LevelChunk chunk, Pending pending) {
|
||||
if (pending.isEmpty() || !(level.getChunkSource().getGenerator() instanceof NoiseBasedChunkGenerator noise)
|
||||
|| !LayeredHydrologyRuntime.enabled(noise)) return;
|
||||
var random = level.getChunkSource().randomState();
|
||||
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
|
||||
for (var outlet : pending.outlets()) {
|
||||
if (!owns(chunk, outlet)) throw new IllegalArgumentException("An outlet belongs to another chunk: " + outlet);
|
||||
if (random.sampleBlockValueUncached(density, outlet.x(), outlet.y(), outlet.z()) > 0) {
|
||||
throw new IllegalStateException("A spring outlet must already be natural air: " + outlet);
|
||||
}
|
||||
BlockPos pos = blockPos(outlet);
|
||||
BlockState current = chunk.getBlockState(pos);
|
||||
if (removableDecoration(current)) {
|
||||
// Only this declared air cell is reopened. Neighbour notification wakes the
|
||||
// adjacent source even if its own chunk was post-processed first.
|
||||
level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
|
||||
}
|
||||
}
|
||||
for (var source : pending.sources()) {
|
||||
if (!owns(chunk, source)) throw new IllegalArgumentException("A source belongs to another chunk: " + source);
|
||||
BlockPos pos = blockPos(source);
|
||||
BlockState current = chunk.getBlockState(pos);
|
||||
if (current.is(Blocks.WATER) && current.getFluidState().isSource()) {
|
||||
level.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(level));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean markedHere(LevelChunk chunk, LayeredHydrology.Position position) {
|
||||
if (!owns(chunk, position)) return false;
|
||||
var queue = chunk.getPostProcessing()[chunk.getSectionIndex(position.y())];
|
||||
return queue != null && queue.contains(ProtoChunk.packOffsetCoordinates(blockPos(position)));
|
||||
}
|
||||
|
||||
private static boolean owns(LevelChunk chunk, LayeredHydrology.Position position) {
|
||||
return (position.x() >> 4) == chunk.getPos().x() && (position.z() >> 4) == chunk.getPos().z();
|
||||
}
|
||||
|
||||
private static BlockPos blockPos(LayeredHydrology.Position position) {
|
||||
return new BlockPos(position.x(), position.y(), position.z());
|
||||
}
|
||||
|
||||
private static boolean removableDecoration(BlockState state) {
|
||||
if (state.hasBlockEntity()) return false;
|
||||
return state.is(Blocks.GLOW_LICHEN) || state.is(Blocks.VINE) || state.is(Blocks.MOSS_CARPET)
|
||||
|| state.getBlock() instanceof VegetationBlock || state.is(BlockTags.LEAVES) || state.is(BlockTags.LOGS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
/** Geometry checks shared by lower-ledge decoration and density diagnostics. */
|
||||
public final class LowerTerraceEcology {
|
||||
@FunctionalInterface
|
||||
public interface Sampler { float sample(int x, int y, int z); }
|
||||
|
||||
private LowerTerraceEcology() {}
|
||||
|
||||
public static int highestNaturalSurface(Sampler sampler, int x, int z) {
|
||||
for (int y = 383; y >= 24; y--) if (sampler.sample(x, y, z) > 0) return y;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** A lower natural floor: three solid blocks, headroom, and real terrain at least eight blocks above. */
|
||||
public static boolean isLowerTerrace(Sampler sampler, int x, int bedY, int z, int highestNaturalSurface) {
|
||||
if (bedY < 32 || bedY > 312 || highestNaturalSurface < bedY + 8) return false;
|
||||
for (int y = bedY - 2; y <= bedY; y++) if (sampler.sample(x, y, z) <= 0) return false;
|
||||
return sampler.sample(x, bedY + 1, z) <= 0 && sampler.sample(x, bedY + 2, z) <= 0;
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
import net.minecraft.world.level.levelgen.feature.Feature;
|
||||
|
||||
/** Decorates actual lower natural floors after the ordinary highest-surface vegetation.
|
||||
* Each chunk owns its columns; planned water, sediments, lava and their supports are excluded. */
|
||||
public final class LowerTerraceVegetationFeature implements Feature {
|
||||
public static final MapCodec<LowerTerraceVegetationFeature> CODEC = MapCodec.unit(LowerTerraceVegetationFeature::new);
|
||||
|
||||
@Override
|
||||
public MapCodec<LowerTerraceVegetationFeature> codec() { return CODEC; }
|
||||
|
||||
@Override
|
||||
public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource random, BlockPos origin) {
|
||||
if (!(generator instanceof NoiseBasedChunkGenerator noise) || !LayeredHydrologyRuntime.enabled(noise)) return false;
|
||||
int minX = (origin.getX() >> 4) * 16, minZ = (origin.getZ() >> 4) * 16;
|
||||
if (Math.abs((long) minX) > 288 || Math.abs((long) minZ) > 288) return false;
|
||||
var state = level.getLevel().getChunkSource().randomState();
|
||||
var water = LayeredHydrologyRuntime.plan(noise, state);
|
||||
var lava = LayeredHydrologyRuntime.lavaPlan(noise, state);
|
||||
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
|
||||
LowerTerraceEcology.Sampler sampler = (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z);
|
||||
var climate = state.createClimateSampler(SamplerContext.EMPTY_UNCACHED);
|
||||
Set<Long> lavaColumns = new HashSet<>();
|
||||
for (var cell : lava.cells()) protectColumn(lavaColumns, cell.x(), cell.z());
|
||||
for (var support : lava.supports()) protectColumn(lavaColumns, support.x(), support.z());
|
||||
boolean changed = false;
|
||||
BlockPos.MutableBlockPos floor = new BlockPos.MutableBlockPos();
|
||||
for (int x = minX; x < minX + 16; x++) for (int z = minZ; z < minZ + 16; z++) {
|
||||
if (blockedColumn(water, lavaColumns, x, z)) continue;
|
||||
// A coherent moisture patch controls coverage, rather than an alternating block grid.
|
||||
float growth = climate.humidity().sampleValue(x + 317, 0, z - 191);
|
||||
if (growth < -0.30F) continue;
|
||||
int highest = -1, terraces = 0;
|
||||
for (int y = 319; y >= 32; y--) {
|
||||
floor.set(x, y, z);
|
||||
BlockState ground = level.getBlockState(floor);
|
||||
if (!replaceableGround(ground)) continue;
|
||||
if (highest < 0) {
|
||||
if (sampler.sample(x, y, z) > 0) highest = y;
|
||||
continue;
|
||||
}
|
||||
if (y > highest - 8) continue;
|
||||
BlockPos plantPos = floor.above();
|
||||
if (!level.getBlockState(plantPos).isAir() || !level.getBlockState(plantPos.above()).isAir()) continue;
|
||||
if (!LowerTerraceEcology.isLowerTerrace(sampler, x, y, z, highest)) continue;
|
||||
if (!ground.isCollisionShapeFullBlock(level, floor)
|
||||
|| !level.getBlockState(floor.below()).isCollisionShapeFullBlock(level, floor.below())
|
||||
|| !level.getBlockState(floor.below(2)).isCollisionShapeFullBlock(level, floor.below(2))) continue;
|
||||
var biome = level.getBiome(plantPos);
|
||||
if (biome.is(LayeredIslandBiomeSource.ROCKY_TERRACES)) continue;
|
||||
boolean mossy = biome.is(LayeredIslandBiomeSource.MOSSY_TERRACES);
|
||||
// Moss survives shaded ledges without needing artificial lighting. Existing grass
|
||||
// is retained outside the moss biome; no supporting terrain is added to air.
|
||||
if (mossy || !ground.is(Blocks.GRASS_BLOCK)) {
|
||||
setBlock(level, floor, Blocks.MOSS_BLOCK.defaultBlockState());
|
||||
changed = true;
|
||||
}
|
||||
if (random.nextInt(5) < 2) {
|
||||
BlockState plant = plant(mossy, random.nextInt(10));
|
||||
if (plant.canSurvive(level, plantPos)) {
|
||||
setBlock(level, plantPos, plant);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (++terraces >= 4) break;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private static BlockState plant(boolean mossy, int choice) {
|
||||
if (mossy) return switch (choice) {
|
||||
case 0 -> Blocks.FLOWERING_AZALEA.defaultBlockState();
|
||||
case 1 -> Blocks.AZALEA.defaultBlockState();
|
||||
case 2, 3 -> Blocks.FERN.defaultBlockState();
|
||||
default -> Blocks.MOSS_CARPET.defaultBlockState();
|
||||
};
|
||||
return switch (choice) {
|
||||
case 0 -> Blocks.OXEYE_DAISY.defaultBlockState();
|
||||
case 1 -> Blocks.CORNFLOWER.defaultBlockState();
|
||||
case 2 -> Blocks.AZURE_BLUET.defaultBlockState();
|
||||
case 3 -> Blocks.DANDELION.defaultBlockState();
|
||||
case 4 -> Blocks.BUSH.defaultBlockState();
|
||||
default -> Blocks.SHORT_GRASS.defaultBlockState();
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean replaceableGround(BlockState state) {
|
||||
return state.is(Blocks.STONE) || state.is(Blocks.GRANITE) || state.is(Blocks.DIORITE)
|
||||
|| state.is(Blocks.ANDESITE) || state.is(Blocks.TUFF) || state.is(Blocks.DIRT)
|
||||
|| state.is(Blocks.GRASS_BLOCK) || state.is(Blocks.ROOTED_DIRT) || state.is(Blocks.COARSE_DIRT)
|
||||
|| state.is(Blocks.MOSS_BLOCK);
|
||||
}
|
||||
|
||||
private static boolean blockedColumn(LayeredHydrology.Plan water, Set<Long> lavaColumns, int x, int z) {
|
||||
if (lavaColumns.contains(key(x, z))) return true;
|
||||
for (int dx = -1; dx <= 1; dx++) for (int dz = -1; dz <= 1; dz++) {
|
||||
if (water.cellAt(x + dx, z + dz) != null) return true;
|
||||
}
|
||||
for (var spring : water.springs()) if (inside(spring.flowBounds(), x, z)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean inside(LayeredHydrology.FlowBounds bounds, int x, int z) {
|
||||
return x >= bounds.minX() - 2 && x <= bounds.maxX() + 2
|
||||
&& z >= bounds.minZ() - 2 && z <= bounds.maxZ() + 2;
|
||||
}
|
||||
|
||||
private static void protectColumn(Set<Long> columns, int x, int z) {
|
||||
for (int dx = -2; dx <= 2; dx++) for (int dz = -2; dz <= 2; dz++) columns.add(key(x + dx, z + dz));
|
||||
}
|
||||
|
||||
private static long key(int x, int z) { return ((long) x << 32) | (z & 0xffffffffL); }
|
||||
}
|
||||
@@ -0,0 +1,698 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.PriorityQueue;
|
||||
import java.util.Set;
|
||||
|
||||
/** Alpha.5 surface water, broad natural sediment sheets and occasional exposed rock springs.
|
||||
* This version is separate from SurfaceHydrology so previously created worlds keep alpha.4 terrain.
|
||||
* Results retain no chunks, sampler, random state or Minecraft objects. */
|
||||
public final class NaturalHydrology {
|
||||
public static final int RADIUS = 192;
|
||||
public static final int MAX_CARVE = 4;
|
||||
public static final int MAX_DEPTH = 3;
|
||||
private static final int GRID = 8;
|
||||
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
private static final int[][] NEIGHBORS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}};
|
||||
|
||||
private NaturalHydrology() {}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Sampler { float sample(int x, int y, int z); }
|
||||
|
||||
public enum Kind { POND, LAKE, STREAM }
|
||||
public enum ShoreMaterial { SAND, GRAVEL, CLAY, STONE, GRASS }
|
||||
public record Point(int x, int z) {}
|
||||
public record Bounds(int minX, int minZ, int maxX, int maxZ) {}
|
||||
|
||||
public record Position(int x, int y, int z) {}
|
||||
public record FlowBounds(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {}
|
||||
public record Spring(long id, Position source, Position outlet, List<Position> flowPath, FlowBounds flowBounds) {
|
||||
public Spring { flowPath = List.copyOf(flowPath); }
|
||||
}
|
||||
|
||||
/** Replaces existing solids bedY-sedimentDepth+1..bedY, with two intact natural supports below.
|
||||
* Water occupies bedY+1..waterY; everything above it through carveTop is removed. For a dry
|
||||
* terrace waterY=-1, and bedY+1..carveTop is removed (at most two original blocks). */
|
||||
public record Cell(int x, int z, int waterY, int bedY, int carveTop,
|
||||
ShoreMaterial material, long featureId, int sedimentDepth) {
|
||||
public boolean hasWater() { return waterY >= 0; }
|
||||
}
|
||||
|
||||
public record Feature(long id, Kind kind, int waterY, List<Point> path, int waterCells, Bounds bounds) {
|
||||
public Feature { path = List.copyOf(path); }
|
||||
}
|
||||
|
||||
public static final class Plan {
|
||||
private final Map<Long, Cell> cells;
|
||||
private final Map<Long, List<Cell>> chunks;
|
||||
private final List<Feature> features;
|
||||
private final List<Spring> springs;
|
||||
private final Map<Long, List<Spring>> springChunks;
|
||||
private final Set<Position> springFlow;
|
||||
private final int sampledColumns;
|
||||
private final long densitySamples;
|
||||
|
||||
private Plan(Map<Long, Cell> source, List<Feature> features, List<Spring> springs, Set<Position> springFlow, int columns, long samples) {
|
||||
List<Cell> sorted = source.values().stream()
|
||||
.sorted(Comparator.comparingInt(Cell::x).thenComparingInt(Cell::z)).toList();
|
||||
Map<Long, Cell> ordered = new LinkedHashMap<>();
|
||||
Map<Long, List<Cell>> byChunk = new HashMap<>();
|
||||
for (Cell cell : sorted) {
|
||||
ordered.put(key(cell.x(), cell.z()), cell);
|
||||
byChunk.computeIfAbsent(key(cell.x() >> 4, cell.z() >> 4), ignored -> new ArrayList<>()).add(cell);
|
||||
}
|
||||
byChunk.replaceAll((ignored, values) -> List.copyOf(values));
|
||||
cells = Collections.unmodifiableMap(ordered);
|
||||
chunks = Map.copyOf(byChunk);
|
||||
this.features = List.copyOf(features);
|
||||
this.springs = List.copyOf(springs);
|
||||
this.springFlow = Set.copyOf(springFlow);
|
||||
Map<Long, List<Spring>> groupedSprings = new HashMap<>();
|
||||
for (Spring spring : springs) groupedSprings.computeIfAbsent(
|
||||
key(spring.source().x() >> 4, spring.source().z() >> 4), ignored -> new ArrayList<>()).add(spring);
|
||||
groupedSprings.replaceAll((ignored, values) -> List.copyOf(values));
|
||||
springChunks = Map.copyOf(groupedSprings);
|
||||
sampledColumns = columns;
|
||||
densitySamples = samples;
|
||||
}
|
||||
|
||||
public Cell cellAt(int x, int z) { return cells.get(key(x, z)); }
|
||||
public Collection<Cell> cells() { return cells.values(); }
|
||||
public List<Cell> cellsInChunk(int x, int z) { return chunks.getOrDefault(key(x, z), List.of()); }
|
||||
public List<Feature> features() { return features; }
|
||||
public List<Spring> springs() { return springs; }
|
||||
public List<Spring> springsInChunk(int x, int z) { return springChunks.getOrDefault(key(x, z), List.of()); }
|
||||
/** Prediction in undecorated density, not a restriction on Minecraft fluid simulation. */
|
||||
public boolean allowsSpringFlow(int x, int y, int z) { return springFlow.contains(new Position(x, y, z)); }
|
||||
public int sampledColumns() { return sampledColumns; }
|
||||
public long densitySamples() { return densitySamples; }
|
||||
}
|
||||
|
||||
public static Plan create(long seed, Sampler sampler) {
|
||||
return new Planner(seed, sampler).build();
|
||||
}
|
||||
|
||||
private record Column(int top, int solidBottom) {}
|
||||
private record Candidate(int x, int z, double score) {}
|
||||
private record Node(int x, int z, double cost) {}
|
||||
private static final Comparator<Node> NODE_ORDER = Comparator.comparingDouble(Node::cost)
|
||||
.thenComparingInt(Node::x).thenComparingInt(Node::z);
|
||||
|
||||
private static final class Planner {
|
||||
private final long seed;
|
||||
private final Sampler sampler;
|
||||
private final Map<Long, Column> columns = new HashMap<>();
|
||||
private final Map<Long, Integer> coarse = new HashMap<>();
|
||||
private final Map<Long, Cell> cells = new HashMap<>();
|
||||
private final List<Feature> features = new ArrayList<>();
|
||||
private final List<Spring> springs = new ArrayList<>();
|
||||
private final Set<Position> springFlow = new HashSet<>();
|
||||
private long samples;
|
||||
|
||||
Planner(long seed, Sampler sampler) { this.seed = seed; this.sampler = sampler; }
|
||||
|
||||
Plan build() {
|
||||
List<Candidate> candidates = candidates();
|
||||
int attempts = 0;
|
||||
for (Candidate candidate : candidates) {
|
||||
if (features.size() >= 5 || attempts++ >= 96) break;
|
||||
Point center = new Point(candidate.x(), candidate.z());
|
||||
if (nearWater(center.x(), center.z(), 38)) continue;
|
||||
Column column = column(center.x(), center.z());
|
||||
if (column.top() < 64 || column.top() - column.solidBottom() < 7) continue;
|
||||
Kind kind = features.size() < 2 ? Kind.LAKE : Kind.POND;
|
||||
int target = kind == Kind.LAKE ? 190 + (int) (random(seed, center.x(), center.z()) * 130)
|
||||
: 55 + (int) (random(seed + 1, center.x(), center.z()) * 55);
|
||||
// Prefer an existing low spot; a shallow cut is the fallback on level terrain.
|
||||
for (int level : new int[]{column.top() + 1, column.top(), column.top() - 1}) {
|
||||
Set<Long> footprint = growBasin(center, level, target);
|
||||
if (footprint == null) continue;
|
||||
long id = featureId(features.size());
|
||||
Map<Long, Cell> wet = waterCells(footprint, level, id);
|
||||
if (wet == null || !contained(wet)) continue;
|
||||
addFeature(id, kind, level, List.of(center), wet);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// One quiet stream connects a basin to a nearby surface refuge at the same water level.
|
||||
// Its route follows low excavation cost through the relief; it has no falling outlets.
|
||||
boolean stream = false;
|
||||
for (Feature source : List.copyOf(features)) if (addStream(source)) { stream = true; break; }
|
||||
if (!stream) addPlateauStream(candidates);
|
||||
addSedimentSheets();
|
||||
addSprings();
|
||||
return new Plan(cells, features, springs, springFlow, columns.size(), samples);
|
||||
}
|
||||
|
||||
private List<Candidate> candidates() {
|
||||
for (int x = -RADIUS; x <= RADIUS; x += GRID) {
|
||||
for (int z = -RADIUS; z <= RADIUS; z += GRID) {
|
||||
if (inside(x, z, RADIUS)) coarse.put(key(x, z), coarseTop(x, z));
|
||||
}
|
||||
}
|
||||
List<Candidate> result = new ArrayList<>();
|
||||
for (int x = -RADIUS + GRID; x < RADIUS; x += GRID) {
|
||||
for (int z = -RADIUS + GRID; z < RADIUS; z += GRID) {
|
||||
int height = coarse.getOrDefault(key(x, z), -1);
|
||||
if (height < 64 || !inside(x, z, RADIUS - 24)) continue;
|
||||
double average = 0;
|
||||
boolean safe = true;
|
||||
for (int[] d : CARDINALS) {
|
||||
int neighbor = coarse.getOrDefault(key(x + d[0] * GRID, z + d[1] * GRID), -1);
|
||||
if (neighbor < 64 || Math.abs(neighbor - height) > 16) safe = false;
|
||||
average += neighbor * 0.25;
|
||||
}
|
||||
if (safe) result.add(new Candidate(x, z,
|
||||
(average - height) * 2.0 + random(seed + 2, x, z) * 3.0));
|
||||
}
|
||||
}
|
||||
result.sort(Comparator.comparingDouble(Candidate::score).reversed()
|
||||
.thenComparingInt(Candidate::x).thenComparingInt(Candidate::z));
|
||||
return result;
|
||||
}
|
||||
|
||||
private int coarseTop(int x, int z) {
|
||||
for (int y = 383; y >= 32; y -= 4) if (solid(x, y, z)) return y;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private Column column(int x, int z) {
|
||||
long key = key(x, z);
|
||||
Column old = columns.get(key);
|
||||
if (old != null) return old;
|
||||
int top = -1;
|
||||
// Exact scan, including everything above the planned water: a cave roof cannot be
|
||||
// mistaken for a surface. Only candidates and their nearby shore need this refinement.
|
||||
for (int y = 383; y >= 24; y--) {
|
||||
if (solid(x, y, z)) { top = y; break; }
|
||||
}
|
||||
int bottom = top;
|
||||
if (top >= 0) {
|
||||
while (bottom > Math.max(16, top - 16) && solid(x, bottom - 1, z)) bottom--;
|
||||
}
|
||||
Column created = new Column(top, bottom);
|
||||
columns.put(key, created);
|
||||
return created;
|
||||
}
|
||||
|
||||
private boolean eligible(int x, int z, int level) {
|
||||
if (level < 32 || level >= 383 || !inside(x, z, RADIUS - 2)) return false;
|
||||
Cell old = cells.get(key(x, z));
|
||||
if (old != null && old.hasWater() && old.waterY() != level) return false;
|
||||
Column column = column(x, z);
|
||||
return column.top() >= level - 3 && column.top() < level + MAX_CARVE
|
||||
&& column.solidBottom() <= Math.min(column.top(), level - 1) - 4;
|
||||
}
|
||||
|
||||
private Set<Long> growBasin(Point center, int level, int target) {
|
||||
if (!eligible(center.x(), center.z(), level)) return null;
|
||||
Set<Long> wet = new HashSet<>();
|
||||
Map<Long, Double> costs = new HashMap<>();
|
||||
PriorityQueue<Node> queue = new PriorityQueue<>(NODE_ORDER);
|
||||
queue.add(new Node(center.x(), center.z(), 0));
|
||||
costs.put(key(center.x(), center.z()), 0.0);
|
||||
while (!queue.isEmpty() && wet.size() < target) {
|
||||
Node current = queue.remove();
|
||||
if (current.cost() > costs.getOrDefault(key(current.x(), current.z()), Double.POSITIVE_INFINITY)) continue;
|
||||
if (!eligible(current.x(), current.z(), level)) continue;
|
||||
wet.add(key(current.x(), current.z()));
|
||||
for (int[] d : NEIGHBORS) {
|
||||
int x = current.x() + d[0], z = current.z() + d[1];
|
||||
if (Math.hypot(x - center.x(), z - center.z()) > 28 || wet.contains(key(x, z))) continue;
|
||||
if (!eligible(x, z, level)) continue;
|
||||
double step = Math.hypot(d[0], d[1]);
|
||||
double cost = current.cost() + step * (1 + Math.max(0, column(x, z).top() - level) * 0.8
|
||||
+ terrainNoise(x, z) * 4.5 + field(x + 71, z - 93, 21, 107) * 3.5);
|
||||
if (cost >= costs.getOrDefault(key(x, z), Double.POSITIVE_INFINITY)) continue;
|
||||
costs.put(key(x, z), cost);
|
||||
queue.add(new Node(x, z, cost));
|
||||
}
|
||||
}
|
||||
wet = connectedTo(wet, key(center.x(), center.z()));
|
||||
if (wet.size() < Math.min(target, 30)) return null;
|
||||
// Filling a hollow requires every lower connected outlet to be included. Abort rather
|
||||
// than place a dam if this natural basin runs to a cliff, cave or an oversized catchment.
|
||||
List<Long> work = new ArrayList<>(wet);
|
||||
for (int i = 0; i < work.size(); i++) {
|
||||
Point point = point(work.get(i));
|
||||
for (int[] d : CARDINALS) {
|
||||
int x = point.x() + d[0], z = point.z() + d[1];
|
||||
long neighbor = key(x, z);
|
||||
if (wet.contains(neighbor) || column(x, z).top() >= level) continue;
|
||||
if (wet.size() >= 420 || Math.hypot(x - center.x(), z - center.z()) > 32
|
||||
|| !eligible(x, z, level)) return null;
|
||||
wet.add(neighbor);
|
||||
work.add(neighbor);
|
||||
}
|
||||
}
|
||||
return wet;
|
||||
}
|
||||
|
||||
private Map<Long, Cell> waterCells(Set<Long> footprint, int level, long id) {
|
||||
Map<Long, Cell> result = new HashMap<>();
|
||||
for (long key : footprint) {
|
||||
Point p = point(key);
|
||||
if (!eligible(p.x(), p.z(), level)) return null;
|
||||
boolean edge = false;
|
||||
for (int[] d : CARDINALS) edge |= !footprint.contains(key(p.x() + d[0], p.z() + d[1]));
|
||||
Column column = column(p.x(), p.z());
|
||||
int depth = Math.max(level - column.top() + 1, edge ? 1 : 2 + (terrainNoise(p.x(), p.z()) > 0.55 ? 1 : 0));
|
||||
depth = Math.min(depth, MAX_CARVE - Math.max(0, column.top() - level));
|
||||
if (depth < 1 || depth > MAX_DEPTH) return null;
|
||||
int bed = level - depth;
|
||||
if (column.top() < bed || column.solidBottom() > bed - 2) return null;
|
||||
ShoreMaterial material = wetMaterial(p.x(), p.z(), edge);
|
||||
int sedimentDepth = sedimentDepth(p.x(), p.z());
|
||||
if (column.solidBottom() > bed - sedimentDepth - 1) return null;
|
||||
result.put(key, new Cell(p.x(), p.z(), level, bed, Math.max(level, column.top()),
|
||||
material, id, sedimentDepth));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean contained(Map<Long, Cell> wet) {
|
||||
for (Cell cell : wet.values()) {
|
||||
for (int y = cell.bedY() - 2; y <= cell.bedY(); y++) {
|
||||
if (!solid(cell.x(), y, cell.z())) return false;
|
||||
}
|
||||
for (int[] d : CARDINALS) {
|
||||
int x = cell.x() + d[0], z = cell.z() + d[1];
|
||||
Cell neighbor = wet.get(key(x, z));
|
||||
if (neighbor == null) neighbor = cells.get(key(x, z));
|
||||
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) {
|
||||
if (neighbor != null && neighbor.hasWater()) {
|
||||
if (neighbor.waterY() != cell.waterY()) return false;
|
||||
if (y > neighbor.bedY()) continue;
|
||||
}
|
||||
if (!solid(x, y, z)) return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void addFeature(long id, Kind kind, int level, List<Point> path, Map<Long, Cell> wet) {
|
||||
int minX = Integer.MAX_VALUE, minZ = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE, maxZ = Integer.MIN_VALUE;
|
||||
int count = 0;
|
||||
for (Cell cell : wet.values()) {
|
||||
Cell old = cells.get(key(cell.x(), cell.z()));
|
||||
if (old == null || !old.hasWater()) { cells.put(key(cell.x(), cell.z()), cell); count++; }
|
||||
minX = Math.min(minX, cell.x()); minZ = Math.min(minZ, cell.z());
|
||||
maxX = Math.max(maxX, cell.x()); maxZ = Math.max(maxZ, cell.z());
|
||||
}
|
||||
features.add(new Feature(id, kind, level, path, count, new Bounds(minX, minZ, maxX, maxZ)));
|
||||
}
|
||||
|
||||
private boolean addStream(Feature source) {
|
||||
Point start = source.path().getFirst();
|
||||
int level = source.waterY();
|
||||
Map<Long, Double> costs = new HashMap<>();
|
||||
Map<Long, Long> parent = new HashMap<>();
|
||||
PriorityQueue<Node> queue = new PriorityQueue<>(NODE_ORDER);
|
||||
List<Node> refuges = new ArrayList<>();
|
||||
long startKey = key(start.x(), start.z());
|
||||
costs.put(startKey, 0.0);
|
||||
queue.add(new Node(start.x(), start.z(), 0));
|
||||
int visited = 0;
|
||||
while (!queue.isEmpty() && visited++ < 1200) {
|
||||
Node node = queue.remove();
|
||||
long at = key(node.x(), node.z());
|
||||
if (node.cost() > costs.getOrDefault(at, Double.POSITIVE_INFINITY)) continue;
|
||||
double distance = Math.hypot(node.x() - start.x(), node.z() - start.z());
|
||||
if (distance >= 44 && distance <= 76 && column(node.x(), node.z()).top() <= level + 3) refuges.add(node);
|
||||
for (int[] d : NEIGHBORS) {
|
||||
int x = node.x() + d[0] * 4, z = node.z() + d[1] * 4;
|
||||
if (Math.hypot(x - start.x(), z - start.z()) > 88 || !eligible(x, z, level)) continue;
|
||||
long next = key(x, z);
|
||||
double cost = node.cost() + Math.hypot(d[0], d[1])
|
||||
* (4 + Math.abs(column(x, z).top() - level) * 1.4 + terrainNoise(x, z) * 4);
|
||||
if (cost >= costs.getOrDefault(next, Double.POSITIVE_INFINITY)) continue;
|
||||
costs.put(next, cost); parent.put(next, at); queue.add(new Node(x, z, cost));
|
||||
}
|
||||
}
|
||||
refuges.sort(Comparator.comparingDouble((Node n) -> n.cost() / Math.hypot(n.x() - start.x(), n.z() - start.z()))
|
||||
.thenComparingInt(Node::x).thenComparingInt(Node::z));
|
||||
int attempts = 0;
|
||||
for (Node refuge : refuges) {
|
||||
if (attempts++ >= 24) break;
|
||||
List<Point> path = new ArrayList<>();
|
||||
long cursor = key(refuge.x(), refuge.z());
|
||||
while (cursor != startKey) { path.add(point(cursor)); cursor = parent.get(cursor); }
|
||||
path.add(start); Collections.reverse(path);
|
||||
Set<Long> corridor = new HashSet<>();
|
||||
for (int i = 1; i < path.size(); i++) {
|
||||
Point a = path.get(i - 1), b = path.get(i);
|
||||
int length = Math.max(Math.abs(b.x() - a.x()), Math.abs(b.z() - a.z()));
|
||||
for (int step = 0; step <= length; step++) {
|
||||
int x = a.x() + (b.x() - a.x()) * step / length;
|
||||
int z = a.z() + (b.z() - a.z()) * step / length;
|
||||
int width = terrainNoise(x + 17, z + 29) > 0.65 ? 2 : 1;
|
||||
for (int dx = -width; dx <= width; dx++) for (int dz = -width; dz <= width; dz++) {
|
||||
if (dx * dx + dz * dz <= width * width + 1) corridor.add(key(x + dx, z + dz));
|
||||
}
|
||||
}
|
||||
}
|
||||
Set<Long> refugePool = growBasin(new Point(refuge.x(), refuge.z()), level, 36);
|
||||
if (refugePool == null) continue;
|
||||
corridor.addAll(refugePool);
|
||||
if (!closeStreamOutlets(corridor, level)) continue;
|
||||
long id = featureId(features.size());
|
||||
Map<Long, Cell> wet = waterCells(corridor, level, id);
|
||||
if (wet == null || !contained(wet)) continue;
|
||||
addFeature(id, Kind.STREAM, level, path, wet);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Low lake floors may be trapped below every nearby plateau. Try a small natural surface
|
||||
* refuge as a source too, retaining it only if a fully contained stream can actually leave it. */
|
||||
private boolean addPlateauStream(List<Candidate> candidates) {
|
||||
List<Candidate> plateaus = candidates.stream().filter(candidate -> flatness(candidate.x(), candidate.z()) <= 6)
|
||||
.sorted(Comparator.comparingDouble((Candidate c) -> flatness(c.x(), c.z())
|
||||
- random(seed + 91, c.x(), c.z()) * 2).thenComparingInt(Candidate::x).thenComparingInt(Candidate::z)).toList();
|
||||
int attempts = 0;
|
||||
for (Candidate candidate : plateaus) {
|
||||
if (attempts++ >= 20) break;
|
||||
Point center = new Point(candidate.x(), candidate.z());
|
||||
if (nearWater(center.x(), center.z(), 24)) continue;
|
||||
Column column = column(center.x(), center.z());
|
||||
if (column.top() < 64 || column.top() - column.solidBottom() < 7) continue;
|
||||
for (int level : new int[]{column.top(), column.top() - 1}) {
|
||||
Set<Long> footprint = growBasin(center, level, 40);
|
||||
if (footprint == null) continue;
|
||||
long id = featureId(features.size());
|
||||
Map<Long, Cell> wet = waterCells(footprint, level, id);
|
||||
if (wet == null || !contained(wet)) continue;
|
||||
addFeature(id, Kind.POND, level, List.of(center), wet);
|
||||
if (addStream(features.getLast())) return true;
|
||||
features.removeLast();
|
||||
cells.values().removeIf(cell -> cell.featureId() == id);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private int flatness(int x, int z) {
|
||||
int low = coarse.getOrDefault(key(x, z), -1000), high = low;
|
||||
for (int[] d : CARDINALS) {
|
||||
int value = coarse.getOrDefault(key(x + d[0] * GRID, z + d[1] * GRID), -1000);
|
||||
low = Math.min(low, value); high = Math.max(high, value);
|
||||
}
|
||||
return high - low;
|
||||
}
|
||||
|
||||
/** Include small connected low spots beside a channel; reject large/deep/open outlets.
|
||||
* This adds water to a verified natural hollow, never rock to close a leak. */
|
||||
private boolean closeStreamOutlets(Set<Long> corridor, int level) {
|
||||
int maximum = Math.min(900, corridor.size() + 180);
|
||||
List<Long> work = new ArrayList<>(corridor);
|
||||
for (int i = 0; i < work.size(); i++) {
|
||||
Point point = point(work.get(i));
|
||||
for (int[] d : CARDINALS) {
|
||||
int x = point.x() + d[0], z = point.z() + d[1];
|
||||
long next = key(x, z);
|
||||
Cell old = cells.get(next);
|
||||
if (corridor.contains(next) || old != null && old.hasWater() && old.waterY() == level
|
||||
|| column(x, z).top() >= level) continue;
|
||||
if (corridor.size() >= maximum || !eligible(x, z, level)) return false;
|
||||
corridor.add(next); work.add(next);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Set<Long> connectedTo(Set<Long> footprint, long start) {
|
||||
Set<Long> connected = new HashSet<>();
|
||||
List<Long> work = new ArrayList<>();
|
||||
connected.add(start); work.add(start);
|
||||
for (int i = 0; i < work.size(); i++) {
|
||||
Point point = point(work.get(i));
|
||||
for (int[] d : CARDINALS) {
|
||||
long next = key(point.x() + d[0], point.z() + d[1]);
|
||||
if (footprint.contains(next) && connected.add(next)) work.add(next);
|
||||
}
|
||||
}
|
||||
return connected;
|
||||
}
|
||||
|
||||
private record ShoreNode(int x, int z, int level, long feature, double cost) {}
|
||||
|
||||
/** Sediment follows shallow topography in broad, discontinuous sheets. A second nearby
|
||||
* pocket can share the same sheet; the result is not a one-block material ring. */
|
||||
private void addSedimentSheets() {
|
||||
Comparator<ShoreNode> order = Comparator.comparingDouble(ShoreNode::cost)
|
||||
.thenComparingInt(ShoreNode::x).thenComparingInt(ShoreNode::z).thenComparingLong(ShoreNode::feature);
|
||||
PriorityQueue<ShoreNode> queue = new PriorityQueue<>(order);
|
||||
Map<Long, Double> best = new HashMap<>();
|
||||
for (Cell cell : cells.values()) if (cell.hasWater()) {
|
||||
queue.add(new ShoreNode(cell.x(), cell.z(), cell.waterY(), cell.featureId(), 0));
|
||||
best.put(key(cell.x(), cell.z()), 0.0);
|
||||
}
|
||||
while (!queue.isEmpty()) {
|
||||
ShoreNode node = queue.remove();
|
||||
if (node.cost() > best.getOrDefault(key(node.x(), node.z()), Double.POSITIVE_INFINITY)) continue;
|
||||
Column column = column(node.x(), node.z());
|
||||
Cell existing = cells.get(key(node.x(), node.z()));
|
||||
if (existing == null) {
|
||||
double extent = 9 + field(node.x(), node.z(), 25, 131) * 10;
|
||||
if (node.cost() > extent) continue;
|
||||
int delta = column.top() - node.level();
|
||||
if (delta < 0 || delta > 5) continue;
|
||||
// Low shore: waterline, then one-block steps. Cut at most two existing blocks;
|
||||
// far deposits keep the original landform and feather back into grass/stone.
|
||||
int terrace = node.level() + (int) Math.floor(node.cost() / 4.5);
|
||||
int bed = Math.max(node.level(), Math.max(column.top() - 2, Math.min(column.top(), terrace)));
|
||||
int depth = sedimentDepth(node.x(), node.z());
|
||||
if (column.solidBottom() > bed - depth - 1) continue;
|
||||
boolean safe = true;
|
||||
for (int[] d : CARDINALS) {
|
||||
Cell next = cells.get(key(node.x() + d[0], node.z() + d[1]));
|
||||
if (next != null && next.hasWater() && bed < next.waterY()) safe = false;
|
||||
}
|
||||
if (!safe) continue;
|
||||
ShoreMaterial material = dryMaterial(node.x(), node.z(), node.cost() / extent);
|
||||
cells.put(key(node.x(), node.z()), new Cell(node.x(), node.z(), -1, bed,
|
||||
column.top(), material, node.feature(), depth));
|
||||
}
|
||||
for (int[] d : NEIGHBORS) {
|
||||
int x = node.x() + d[0], z = node.z() + d[1];
|
||||
if (!inside(x, z, RADIUS - 2)) continue;
|
||||
Cell water = cells.get(key(x, z));
|
||||
if (water != null && water.hasWater()) continue;
|
||||
Column next = column(x, z);
|
||||
if (next.top() < node.level() || next.top() > node.level() + 5) continue;
|
||||
double cost = node.cost() + Math.hypot(d[0], d[1])
|
||||
* (0.7 + Math.abs(next.top() - column.top()) * 0.32 + field(x, z, 12, 211) * 0.6);
|
||||
if (cost > 19 || cost >= best.getOrDefault(key(x, z), Double.POSITIVE_INFINITY)) continue;
|
||||
best.put(key(x, z), cost);
|
||||
queue.add(new ShoreNode(x, z, node.level(), node.feature(), cost));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int sedimentDepth(int x, int z) { return 3 + (int) (field(x, z, 15, 319) * 2.999); }
|
||||
|
||||
private ShoreMaterial wetMaterial(int x, int z, boolean edge) {
|
||||
double geology = field(x, z, 24, 503) * 0.75 + field(x, z, 7, 509) * 0.25;
|
||||
if (geology < 0.29) return ShoreMaterial.STONE;
|
||||
if (geology < 0.50) return ShoreMaterial.GRAVEL;
|
||||
if (!edge && geology > 0.78) return ShoreMaterial.CLAY;
|
||||
return ShoreMaterial.SAND;
|
||||
}
|
||||
|
||||
private ShoreMaterial dryMaterial(int x, int z, double distance) {
|
||||
double geology = field(x, z, 24, 503) * 0.75 + field(x, z, 7, 509) * 0.25;
|
||||
double grass = field(x + 103, z - 137, 19, 521);
|
||||
if (grass > 0.68 || distance > 0.60 + grass * 0.65) return ShoreMaterial.GRASS;
|
||||
if (geology < 0.29) return ShoreMaterial.STONE;
|
||||
if (geology < 0.50) return ShoreMaterial.GRAVEL;
|
||||
return ShoreMaterial.SAND;
|
||||
}
|
||||
|
||||
private record Cliff(int x, int z, int dx, int dz, double score) {}
|
||||
private record FlowNode(Position position, int reach) {}
|
||||
private record Flow(Set<Position> volume, List<Position> path, FlowBounds bounds) {}
|
||||
|
||||
/** Pick rare exposed rock niches at the edges of real upper surfaces. The source replaces
|
||||
* one rock block under an existing roof. Its outlet is already air over an eight-block drop;
|
||||
* no source, shelf, dam, rock column or ocean is ever placed in empty terrain. */
|
||||
private void addSprings() {
|
||||
List<Cliff> cliffs = new ArrayList<>();
|
||||
for (int x = -264; x <= 264; x += GRID) for (int z = -264; z <= 264; z += GRID) {
|
||||
if (!inside(x, z, 264)) continue;
|
||||
int top = coarse.computeIfAbsent(key(x, z), ignored -> coarseTop(point(ignored).x(), point(ignored).z()));
|
||||
if (top < 88) continue;
|
||||
for (int[] d : CARDINALS) {
|
||||
int nx = x + d[0] * GRID, nz = z + d[1] * GRID;
|
||||
int lower = coarse.computeIfAbsent(key(nx, nz), ignored -> coarseTop(point(ignored).x(), point(ignored).z()));
|
||||
if (top - lower < 16) continue;
|
||||
cliffs.add(new Cliff(x, z, d[0], d[1],
|
||||
Math.min(64, top - lower) * 0.06 + random(seed + 811, x + d[0], z + d[1]) * 3));
|
||||
}
|
||||
}
|
||||
cliffs.sort(Comparator.comparingDouble(Cliff::score).reversed()
|
||||
.thenComparingInt(Cliff::x).thenComparingInt(Cliff::z)
|
||||
.thenComparingInt(Cliff::dx).thenComparingInt(Cliff::dz));
|
||||
int attempts = 0;
|
||||
for (Cliff cliff : cliffs) {
|
||||
if (springs.size() >= 3 || attempts++ >= 160) break;
|
||||
boolean placed = false;
|
||||
for (int tangent : new int[]{0, -2, 2, -4, 4}) {
|
||||
for (int along = 0; along < GRID; along++) {
|
||||
int x = cliff.x() + cliff.dx() * along - cliff.dz() * tangent;
|
||||
int z = cliff.z() + cliff.dz() * along + cliff.dx() * tangent;
|
||||
if (!inside(x, z, 266) || nearModifiedGround(x, z, 32)) continue;
|
||||
int ox = x + cliff.dx(), oz = z + cliff.dz();
|
||||
Column rock = column(x, z), outletColumn = column(ox, oz);
|
||||
if (rock.top() < 80 || rock.top() - outletColumn.top() < 14) continue;
|
||||
// Five blocks below the highest natural surface is below the soil cap.
|
||||
int floor = Math.max(64, Math.max(rock.top() - 20, outletColumn.top() + 8));
|
||||
for (int y = rock.top() - 5; y >= floor; y -= 3) {
|
||||
Position source = new Position(x, y, z), outlet = new Position(ox, y, oz);
|
||||
if (!separatedSpring(source) || !springNiche(source, cliff.dx(), cliff.dz())) continue;
|
||||
Flow flow = traceSpring(source, outlet);
|
||||
if (flow == null || flow.bounds().minY() > y - 12) continue;
|
||||
long id = mix(seed ^ (0x5F81A6L + springs.size() * 0x9E3779B97F4A7C15L));
|
||||
springs.add(new Spring(id, source, outlet, flow.path(), flow.bounds()));
|
||||
springFlow.addAll(flow.volume());
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
if (placed) break;
|
||||
}
|
||||
if (placed) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean nearModifiedGround(int x, int z, int distance) {
|
||||
for (Cell cell : cells.values()) if (Math.hypot(x - cell.x(), z - cell.z()) < distance) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean separatedSpring(Position source) {
|
||||
for (Spring spring : springs) {
|
||||
if (Math.hypot(source.x() - spring.source().x(), source.z() - spring.source().z()) < 64
|
||||
|| Math.abs(source.y() - spring.source().y()) < 12) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean springNiche(Position source, int dx, int dz) {
|
||||
for (int y = source.y() - 3; y <= source.y() + 1; y++) {
|
||||
if (!solid(source.x(), y, source.z())) return false;
|
||||
}
|
||||
for (int[] d : CARDINALS) {
|
||||
if (d[0] == dx && d[1] == dz) continue;
|
||||
if (!solid(source.x() + d[0], source.y(), source.z() + d[1])) return false;
|
||||
}
|
||||
for (int y = source.y(); y >= source.y() - 8; y--) {
|
||||
if (solid(source.x() + dx, y, source.z() + dz)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Predict drainage through undecorated rock to select springs and inspection bounds.
|
||||
* Descend through air first, then explore seven horizontal steps on a shelf. Later trees
|
||||
* can divert the actual fluid: this prediction is not a bound on decorated-world physics.
|
||||
* Integration tests trace actual water back to its source within the inspected chunk halo. */
|
||||
private Flow traceSpring(Position source, Position outlet) {
|
||||
Map<Position, Integer> reach = new HashMap<>();
|
||||
Map<Position, Position> parent = new HashMap<>();
|
||||
ArrayDeque<FlowNode> queue = new ArrayDeque<>();
|
||||
reach.put(source, 7);
|
||||
queue.add(new FlowNode(source, 7));
|
||||
Position lowest = source;
|
||||
while (!queue.isEmpty()) {
|
||||
FlowNode node = queue.removeFirst();
|
||||
Position at = node.position();
|
||||
if (node.reach() < reach.getOrDefault(at, -1)) continue;
|
||||
if (at.y() < lowest.y()) lowest = at;
|
||||
if (reach.size() > 24000) return null;
|
||||
if (at.y() <= 0) continue;
|
||||
Position below = new Position(at.x(), at.y() - 1, at.z());
|
||||
if (!flowSolid(below, source)) {
|
||||
enqueueFlow(below, 7, at, queue, reach, parent);
|
||||
continue;
|
||||
}
|
||||
if (node.reach() <= 0) continue;
|
||||
for (int[] d : CARDINALS) {
|
||||
Position next = new Position(at.x() + d[0], at.y(), at.z() + d[1]);
|
||||
if (flowSolid(next, source)) continue;
|
||||
enqueueFlow(next, node.reach() - 1, at, queue, reach, parent);
|
||||
}
|
||||
}
|
||||
if (!reach.containsKey(outlet)) return null;
|
||||
int minX = source.x(), maxX = minX, minY = source.y(), maxY = minY;
|
||||
int minZ = source.z(), maxZ = minZ;
|
||||
for (Position position : reach.keySet()) {
|
||||
minX = Math.min(minX, position.x()); maxX = Math.max(maxX, position.x());
|
||||
minY = Math.min(minY, position.y()); maxY = Math.max(maxY, position.y());
|
||||
minZ = Math.min(minZ, position.z()); maxZ = Math.max(maxZ, position.z());
|
||||
}
|
||||
List<Position> path = new ArrayList<>();
|
||||
for (Position at = lowest; at != null; at = parent.get(at)) path.add(at);
|
||||
Collections.reverse(path);
|
||||
return new Flow(Set.copyOf(reach.keySet()), path, new FlowBounds(minX, minY, minZ, maxX, maxY, maxZ));
|
||||
}
|
||||
|
||||
private boolean flowSolid(Position at, Position source) {
|
||||
if (at.equals(source)) return false;
|
||||
Cell cell = cells.get(key(at.x(), at.z()));
|
||||
if (cell != null && at.y() > cell.bedY() && at.y() <= cell.carveTop()) return false;
|
||||
return solid(at.x(), at.y(), at.z());
|
||||
}
|
||||
|
||||
private static void enqueueFlow(Position next, int available, Position from, ArrayDeque<FlowNode> queue,
|
||||
Map<Position, Integer> reach, Map<Position, Position> parent) {
|
||||
if (available <= reach.getOrDefault(next, -1)) return;
|
||||
if (!reach.containsKey(next)) parent.put(next, from);
|
||||
reach.put(next, available);
|
||||
queue.addLast(new FlowNode(next, available));
|
||||
}
|
||||
|
||||
private boolean nearWater(int x, int z, int distance) {
|
||||
for (Feature feature : features) {
|
||||
Point point = feature.path().getFirst();
|
||||
if (Math.hypot(x - point.x(), z - point.z()) < distance) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean solid(int x, int y, int z) { samples++; return sampler.sample(x, y, z) > 0; }
|
||||
private long featureId(int index) { return mix(seed ^ (0x51A7E7L + index * 0x9E3779B97F4A7C15L)); }
|
||||
|
||||
private double terrainNoise(int x, int z) { return field(x, z, 8, 31); }
|
||||
|
||||
private double field(int x, int z, int scale, long salt) {
|
||||
int gx = Math.floorDiv(x, scale), gz = Math.floorDiv(z, scale);
|
||||
double tx = smooth(Math.floorMod(x, scale) / (double) scale);
|
||||
double tz = smooth(Math.floorMod(z, scale) / (double) scale);
|
||||
double a = random(seed + salt, gx, gz) * (1 - tx) + random(seed + salt, gx + 1, gz) * tx;
|
||||
double b = random(seed + salt, gx, gz + 1) * (1 - tx) + random(seed + salt, gx + 1, gz + 1) * tx;
|
||||
return a * (1 - tz) + b * tz;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean inside(int x, int z, int radius) { return Math.hypot((double) x, z) < radius; }
|
||||
private static long key(int x, int z) { return ((long) x << 32) | (z & 0xffffffffL); }
|
||||
private static Point point(long key) { return new Point((int) (key >> 32), (int) key); }
|
||||
private static double smooth(double t) { return t * t * (3 - 2 * t); }
|
||||
private static double random(long seed, int x, int z) { return (mix(seed ^ key(x, z)) >>> 11) * 0x1.0p-53; }
|
||||
private static long mix(long value) {
|
||||
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
|
||||
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
|
||||
return value ^ (value >>> 31);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
|
||||
/** Alpha.5 only: immutable plans cached by world state, applied to fresh terrain before decoration. */
|
||||
public final class NaturalHydrologyRuntime {
|
||||
private static final Map<RandomState, NaturalHydrology.Plan> PLANS =
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
private static final Map<RandomState, StarterLavaDeposit.Plan> LAVA_PLANS =
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
|
||||
private NaturalHydrologyRuntime() {}
|
||||
|
||||
public static boolean enabled(NoiseBasedChunkGenerator generator) {
|
||||
return generator.generatorSettings().is(SanctuarySpawn.NATURAL_SETTINGS);
|
||||
}
|
||||
|
||||
public static NaturalHydrology.Plan naturalPlan(ServerLevel level) {
|
||||
return plan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(),
|
||||
level.getChunkSource().randomState());
|
||||
}
|
||||
|
||||
public static NaturalHydrology.Plan plan(NoiseBasedChunkGenerator generator, RandomState randomState) {
|
||||
if (!enabled(generator)) throw new IllegalArgumentException("Natural hydrology is not enabled for this generator");
|
||||
return PLANS.computeIfAbsent(randomState, state -> {
|
||||
long start = System.nanoTime();
|
||||
var density = generator.generatorSettings().value().noiseRouter().finalDensity();
|
||||
var plan = NaturalHydrology.create(state.seed(),
|
||||
(x, y, z) -> state.sampleBlockValueUncached(density, x, y, z));
|
||||
SanctuaryMod.LOGGER.info("Sanctuary natural hydrology: seed {}, {} features, {} columns, {} springs, {} ms",
|
||||
state.seed(), plan.features().size(), plan.cells().size(), plan.springs().size(),
|
||||
(System.nanoTime() - start) / 1_000_000);
|
||||
return plan;
|
||||
});
|
||||
}
|
||||
|
||||
public static StarterLavaDeposit.Plan lavaPlan(ServerLevel level) {
|
||||
return lavaPlan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(),
|
||||
level.getChunkSource().randomState());
|
||||
}
|
||||
|
||||
public static StarterLavaDeposit.Plan lavaPlan(NoiseBasedChunkGenerator generator, RandomState randomState) {
|
||||
// Acquire the water plan before the lava cache lock; the water planner never acquires it.
|
||||
var water = plan(generator, randomState);
|
||||
return LAVA_PLANS.computeIfAbsent(randomState, state -> {
|
||||
long start = System.nanoTime();
|
||||
var density = generator.generatorSettings().value().noiseRouter().finalDensity();
|
||||
var lava = StarterLavaDeposit.create(state.seed(),
|
||||
(x, y, z) -> state.sampleBlockValueUncached(density, x, y, z), water);
|
||||
SanctuaryMod.LOGGER.info("Sanctuary starter lava: seed {}, {} sources, access {}, {} ms",
|
||||
state.seed(), lava.lavaCells().size(), lava.access(), (System.nanoTime() - start) / 1_000_000);
|
||||
return lava;
|
||||
});
|
||||
}
|
||||
|
||||
public static void apply(NoiseBasedChunkGenerator generator, RandomState randomState, ChunkAccess chunk) {
|
||||
if (!enabled(generator)) return;
|
||||
var chunkPos = chunk.getPos();
|
||||
if (Math.abs((long) chunkPos.x() * 16) > IslandShape.TERRAIN_LIMIT + 16L
|
||||
|| Math.abs((long) chunkPos.z() * 16) > IslandShape.TERRAIN_LIMIT + 16L) return;
|
||||
var plan = plan(generator, randomState);
|
||||
var cells = plan.cellsInChunk(chunkPos.x(), chunkPos.z());
|
||||
var springs = plan.springsInChunk(chunkPos.x(), chunkPos.z());
|
||||
var lava = lavaPlan(generator, randomState);
|
||||
var lavaCells = lava.cellsInChunk(chunkPos.x(), chunkPos.z());
|
||||
var lavaSupports = lava.supportsInChunk(chunkPos.x(), chunkPos.z());
|
||||
if (cells.isEmpty() && springs.isEmpty() && lavaCells.isEmpty() && lavaSupports.isEmpty()) return;
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
// Validate all replacements and two intact support layers before modifying this chunk.
|
||||
for (var cell : cells) {
|
||||
for (int y = cell.bedY() - cell.sedimentDepth() - 1; y <= cell.bedY(); y++) {
|
||||
requireSolid(chunk, pos.set(cell.x(), y, cell.z()), randomState.seed());
|
||||
}
|
||||
}
|
||||
for (var spring : springs) {
|
||||
var source = spring.source();
|
||||
requireSolid(chunk, pos.set(source.x(), source.y(), source.z()), randomState.seed());
|
||||
}
|
||||
for (var support : lavaSupports) {
|
||||
requireSolid(chunk, pos.set(support.x(), support.y(), support.z()), randomState.seed());
|
||||
if (chunk.getBlockState(pos).ignitedByLava()) {
|
||||
throw new IllegalStateException("Lava pocket requires nonflammable natural support at " + pos);
|
||||
}
|
||||
}
|
||||
for (var cell : lavaCells) {
|
||||
requireSolid(chunk, pos.set(cell.x(), cell.y(), cell.z()), randomState.seed());
|
||||
}
|
||||
for (var cell : cells) {
|
||||
for (int depth = 0; depth < cell.sedimentDepth(); depth++) {
|
||||
pos.set(cell.x(), cell.bedY() - depth, cell.z());
|
||||
BlockState material = switch (cell.material()) {
|
||||
case SAND -> Blocks.SAND.defaultBlockState();
|
||||
case GRAVEL -> Blocks.GRAVEL.defaultBlockState();
|
||||
case CLAY -> Blocks.CLAY.defaultBlockState();
|
||||
case STONE -> Blocks.STONE.defaultBlockState();
|
||||
case GRASS -> (depth == 0 ? Blocks.GRASS_BLOCK : Blocks.DIRT).defaultBlockState();
|
||||
};
|
||||
chunk.setBlockState(pos, material, 0);
|
||||
}
|
||||
for (int y = cell.bedY() + 1; y <= Math.max(cell.waterY(), cell.carveTop()); y++) {
|
||||
pos.set(cell.x(), y, cell.z());
|
||||
chunk.setBlockState(pos, cell.hasWater() && y <= cell.waterY()
|
||||
? Blocks.WATER.defaultBlockState() : Blocks.AIR.defaultBlockState(), 0);
|
||||
if (cell.hasWater() && y <= cell.waterY()) chunk.markPosForPostProcessing(pos);
|
||||
}
|
||||
}
|
||||
for (var spring : springs) {
|
||||
var source = spring.source();
|
||||
pos.set(source.x(), source.y(), source.z());
|
||||
chunk.setBlockState(pos, Blocks.WATER.defaultBlockState(), 0);
|
||||
// Only the source is generated. Vanilla fluid ticks create the descending waterfall.
|
||||
chunk.markPosForPostProcessing(pos);
|
||||
}
|
||||
for (var cell : lavaCells) {
|
||||
pos.set(cell.x(), cell.y(), cell.z());
|
||||
chunk.setBlockState(pos, (cell.lava() ? Blocks.LAVA : Blocks.AIR).defaultBlockState(), 0);
|
||||
if (cell.lava()) chunk.markPosForPostProcessing(pos);
|
||||
}
|
||||
Heightmap.primeHeightmaps(chunk, EnumSet.of(Heightmap.Types.WORLD_SURFACE_WG,
|
||||
Heightmap.Types.OCEAN_FLOOR_WG));
|
||||
}
|
||||
|
||||
private static void requireSolid(ChunkAccess chunk, BlockPos pos, long seed) {
|
||||
if (!chunk.getBlockState(pos).isCollisionShapeFullBlock(chunk, pos)) {
|
||||
throw new IllegalStateException("Natural hydrology differs from terrain at " + pos + " for seed " + seed);
|
||||
}
|
||||
}
|
||||
}
|
||||
+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 NaturalShoreSugarCaneFeature implements Feature {
|
||||
public static final MapCodec<NaturalShoreSugarCaneFeature> CODEC = MapCodec.unit(NaturalShoreSugarCaneFeature::new);
|
||||
|
||||
@Override
|
||||
public MapCodec<NaturalShoreSugarCaneFeature> codec() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource random, BlockPos origin) {
|
||||
if (!(generator instanceof NoiseBasedChunkGenerator noise) || !NaturalHydrologyRuntime.enabled(noise)) return false;
|
||||
var plan = NaturalHydrologyRuntime.plan(noise, level.getLevel().getChunkSource().randomState());
|
||||
var cane = Blocks.SUGAR_CANE.defaultBlockState();
|
||||
boolean placed = false;
|
||||
for (var cell : plan.cellsInChunk(origin.getX() >> 4, origin.getZ() >> 4)) {
|
||||
if (cell.hasWater() || random.nextInt(7) != 0) continue;
|
||||
BlockPos base = new BlockPos(cell.x(), cell.bedY() + 1, cell.z());
|
||||
if (!level.getBlockState(base).isAir() || !cane.canSurvive(level, base)) continue;
|
||||
int height = 1 + random.nextInt(3);
|
||||
for (int y = 0; y < height; y++) {
|
||||
BlockPos pos = base.above(y);
|
||||
if (!level.getBlockState(pos).isAir() || !cane.canSurvive(level, pos)) break;
|
||||
setBlock(level, pos, cane);
|
||||
placed = true;
|
||||
}
|
||||
}
|
||||
return placed;
|
||||
}
|
||||
}
|
||||
@@ -19,13 +19,24 @@ public final class SanctuarySpawn {
|
||||
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("sanctuary"));
|
||||
public static final ResourceKey<NoiseGeneratorSettings> RAISED_SETTINGS =
|
||||
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("sanctuary_384"));
|
||||
public static final ResourceKey<NoiseGeneratorSettings> HYDROLOGY_SETTINGS =
|
||||
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("sanctuary_hydrology"));
|
||||
|
||||
public static final ResourceKey<NoiseGeneratorSettings> NATURAL_SETTINGS =
|
||||
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("sanctuary_natural"));
|
||||
|
||||
public static final ResourceKey<NoiseGeneratorSettings> LAYERED_SETTINGS =
|
||||
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("sanctuary_layered"));
|
||||
|
||||
private SanctuarySpawn() {}
|
||||
|
||||
public static boolean usesSanctuaryGenerator(ServerLevel level) {
|
||||
return level.dimension() == Level.OVERWORLD
|
||||
&& level.getChunkSource().getGenerator() instanceof NoiseBasedChunkGenerator generator
|
||||
&& (generator.generatorSettings().is(SETTINGS) || generator.generatorSettings().is(RAISED_SETTINGS));
|
||||
&& (generator.generatorSettings().is(SETTINGS) || generator.generatorSettings().is(RAISED_SETTINGS)
|
||||
|| generator.generatorSettings().is(HYDROLOGY_SETTINGS)
|
||||
|| generator.generatorSettings().is(NATURAL_SETTINGS)
|
||||
|| generator.generatorSettings().is(LAYERED_SETTINGS));
|
||||
}
|
||||
|
||||
/** Called only during first creation; saved spawns and /setworldspawn survive subsequent loads. */
|
||||
|
||||
@@ -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 ShoreSugarCaneFeature implements Feature {
|
||||
public static final MapCodec<ShoreSugarCaneFeature> CODEC = MapCodec.unit(ShoreSugarCaneFeature::new);
|
||||
|
||||
@Override
|
||||
public MapCodec<ShoreSugarCaneFeature> codec() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource random, BlockPos origin) {
|
||||
if (!(generator instanceof NoiseBasedChunkGenerator noise) || !HydrologyRuntime.enabled(noise)) return false;
|
||||
var plan = HydrologyRuntime.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,223 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/** One small, covered lava pocket in existing rock, reached from an existing outdoor ledge.
|
||||
* No solid blocks are added, and unsuitable seeds are allowed to produce no deposit. */
|
||||
public final class StarterLavaDeposit {
|
||||
public static final int RADIUS = 192;
|
||||
public static final int WATER_CLEARANCE = 32;
|
||||
public static final int LAVA_VOLUME = 18;
|
||||
private static final int ENTRY_DISTANCE = 8;
|
||||
private static final int[][] DIRECTIONS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
private static final Comparator<Position> POSITION_ORDER = Comparator.comparingInt(Position::x)
|
||||
.thenComparingInt(Position::z).thenComparingInt(Position::y);
|
||||
private static final Comparator<Cell> CELL_ORDER = Comparator.comparingInt(Cell::x)
|
||||
.thenComparingInt(Cell::z).thenComparingInt(Cell::y);
|
||||
|
||||
private StarterLavaDeposit() {}
|
||||
|
||||
public record Position(int x, int y, int z) {}
|
||||
/** lava=false removes existing rock to open the two-block-high chamber or approach. */
|
||||
public record Cell(int x, int y, int z, boolean lava) {}
|
||||
|
||||
public static final class Plan {
|
||||
private final List<Cell> cells;
|
||||
private final List<Cell> lavaCells;
|
||||
private final List<Position> supports;
|
||||
private final Optional<Position> access;
|
||||
private final Map<Long, List<Cell>> chunks;
|
||||
private final Map<Long, List<Position>> supportChunks;
|
||||
|
||||
private Plan(List<Cell> cells, Set<Position> supports, Position access) {
|
||||
this.cells = cells.stream().sorted(CELL_ORDER).toList();
|
||||
lavaCells = this.cells.stream().filter(Cell::lava).toList();
|
||||
this.supports = supports.stream().sorted(POSITION_ORDER).toList();
|
||||
this.access = Optional.ofNullable(access);
|
||||
Map<Long, List<Cell>> byChunk = new HashMap<>();
|
||||
for (Cell cell : this.cells) byChunk.computeIfAbsent(key(cell.x() >> 4, cell.z() >> 4),
|
||||
ignored -> new ArrayList<>()).add(cell);
|
||||
byChunk.replaceAll((ignored, values) -> List.copyOf(values));
|
||||
chunks = Map.copyOf(byChunk);
|
||||
Map<Long, List<Position>> bySupportChunk = new HashMap<>();
|
||||
for (Position support : this.supports) bySupportChunk.computeIfAbsent(key(support.x() >> 4, support.z() >> 4),
|
||||
ignored -> new ArrayList<>()).add(support);
|
||||
bySupportChunk.replaceAll((ignored, values) -> List.copyOf(values));
|
||||
supportChunks = Map.copyOf(bySupportChunk);
|
||||
}
|
||||
|
||||
public List<Cell> cells() { return cells; }
|
||||
public List<Cell> lavaCells() { return lavaCells; }
|
||||
public List<Position> supports() { return supports; }
|
||||
/** Player feet on the existing ledge; this column was naturally open to the sky. */
|
||||
public Optional<Position> access() { return access; }
|
||||
public List<Cell> cellsInChunk(int x, int z) { return chunks.getOrDefault(key(x, z), List.of()); }
|
||||
public List<Position> supportsInChunk(int x, int z) { return supportChunks.getOrDefault(key(x, z), List.of()); }
|
||||
}
|
||||
|
||||
public static Plan create(long seed, NaturalHydrology.Sampler sampler, NaturalHydrology.Plan hydrology) {
|
||||
return new Search(seed, sampler, hydrology).find();
|
||||
}
|
||||
|
||||
private record Candidate(int x, int z, int dx, int dz, double score) {}
|
||||
|
||||
private static final class Search {
|
||||
private final long seed;
|
||||
private final NaturalHydrology.Sampler sampler;
|
||||
private final NaturalHydrology.Plan hydrology;
|
||||
private final Map<Long, Integer> heights = new HashMap<>();
|
||||
|
||||
Search(long seed, NaturalHydrology.Sampler sampler, NaturalHydrology.Plan hydrology) {
|
||||
this.seed = seed;
|
||||
this.sampler = sampler;
|
||||
this.hydrology = hydrology;
|
||||
}
|
||||
|
||||
Plan find() {
|
||||
List<Candidate> candidates = new ArrayList<>();
|
||||
for (int x = -184; x <= 184; x += 8) for (int z = -184; z <= 184; z += 8) {
|
||||
if (!inside(x, z, RADIUS - 8)) continue;
|
||||
int ledge = top(x, z);
|
||||
if (ledge < 64 || ledge > 300) continue;
|
||||
for (int[] direction : DIRECTIONS) {
|
||||
int cx = x - direction[0] * ENTRY_DISTANCE, cz = z - direction[1] * ENTRY_DISTANCE;
|
||||
if (!inside(cx, cz, RADIUS - 12) || !clearOfWater(cx, cz)) continue;
|
||||
int rise = top(cx, cz) - ledge;
|
||||
if (rise < 12) continue;
|
||||
candidates.add(new Candidate(x, z, direction[0], direction[1],
|
||||
random(seed, cx, cz) + Math.min(rise, 40) * 0.015));
|
||||
}
|
||||
}
|
||||
candidates.sort(Comparator.comparingDouble(Candidate::score).reversed()
|
||||
.thenComparingInt(Candidate::x).thenComparingInt(Candidate::z)
|
||||
.thenComparingInt(Candidate::dx).thenComparingInt(Candidate::dz));
|
||||
int attempts = 0;
|
||||
for (Candidate candidate : candidates) {
|
||||
if (attempts++ >= 192) break;
|
||||
// Refine the coarse edge, including a ledge lying between two eight-block samples.
|
||||
for (int along = 0; along < 8; along++) for (int tangent : new int[]{0, -3, 3}) {
|
||||
int x = candidate.x() - candidate.dx() * along - candidate.dz() * tangent;
|
||||
int z = candidate.z() - candidate.dz() * along + candidate.dx() * tangent;
|
||||
Plan plan = tryNiche(x, z, candidate.dx(), candidate.dz());
|
||||
if (plan != null) return plan;
|
||||
}
|
||||
}
|
||||
return new Plan(List.of(), Set.of(), null);
|
||||
}
|
||||
|
||||
private Plan tryNiche(int entryX, int entryZ, int dx, int dz) {
|
||||
int cx = entryX - dx * ENTRY_DISTANCE, cz = entryZ - dz * ENTRY_DISTANCE;
|
||||
if (!inside(cx, cz, RADIUS - 12) || !clearOfWater(cx, cz)) return null;
|
||||
int level = top(entryX, entryZ);
|
||||
if (level < 64 || level > 300 || top(cx, cz) < level + 12) return null;
|
||||
// The player can discover and enter from an actual outdoor ledge, not a sealed cave.
|
||||
for (int y = level + 1; y < 384; y++) if (solid(entryX, y, entryZ)) return null;
|
||||
for (int y = level - 2; y <= level; y++) if (!solid(entryX, y, entryZ)) return null;
|
||||
|
||||
List<Cell> cells = new ArrayList<>();
|
||||
Set<Position> changed = new HashSet<>();
|
||||
Set<Position> supports = new HashSet<>();
|
||||
for (int x = cx - 1; x <= cx + 1; x++) for (int z = cz - 1; z <= cz + 1; z++) {
|
||||
for (int y = level - 1; y <= level + 2; y++) {
|
||||
if (!solid(x, y, z)) return null;
|
||||
add(cells, changed, x, y, z, y <= level);
|
||||
}
|
||||
}
|
||||
// A dry lip separates the ledge from the pool. Only the air above it is opened.
|
||||
for (int distance = 2; distance < ENTRY_DISTANCE; distance++) {
|
||||
int x = cx + dx * distance, z = cz + dz * distance;
|
||||
for (int y = level + 1; y <= level + 2; y++) {
|
||||
if (solid(x, y, z)) add(cells, changed, x, y, z, false);
|
||||
}
|
||||
}
|
||||
// Every carved column must lie under eight continuous natural rock blocks. This
|
||||
// excludes soil-cap excavation at the entrance as well as exposed surface lava.
|
||||
Map<Long, Integer> highest = new HashMap<>();
|
||||
for (Cell cell : cells) highest.merge(key(cell.x(), cell.z()), cell.y(), Math::max);
|
||||
for (var column : highest.entrySet()) {
|
||||
int x = (int) (column.getKey() >> 32), z = (int) (long) column.getKey();
|
||||
for (int y = column.getValue() + 1; y <= column.getValue() + 8; y++) {
|
||||
if (!requireRock(supports, x, y, z)) return null;
|
||||
}
|
||||
}
|
||||
// Three intact blocks under the pool, two-block-thick side walls, and a solid chamber
|
||||
// surround prevent escape through an adjacent cave. No part of this shell is created.
|
||||
for (int x = cx - 3; x <= cx + 3; x++) for (int z = cz - 3; z <= cz + 3; z++) {
|
||||
for (int y = level - 4; y <= level + 2; y++) {
|
||||
if (changed.contains(new Position(x, y, z))) continue;
|
||||
if (!requireRock(supports, x, y, z)) return null;
|
||||
}
|
||||
}
|
||||
// The natural roof also covers a conservative envelope around local lava fire spread.
|
||||
// Its thickness keeps later surface vegetation well above the lava chamber.
|
||||
for (int x = cx - 4; x <= cx + 4; x++) for (int z = cz - 4; z <= cz + 4; z++) {
|
||||
for (int y = level + 3; y <= level + 10; y++) {
|
||||
if (!requireRock(supports, x, y, z)) return null;
|
||||
}
|
||||
}
|
||||
for (int distance = 2; distance <= ENTRY_DISTANCE; distance++) {
|
||||
int x = cx + dx * distance, z = cz + dz * distance;
|
||||
for (int y = level - 2; y <= level; y++) {
|
||||
if (!requireRock(supports, x, y, z)) return null;
|
||||
}
|
||||
}
|
||||
supports.removeAll(changed);
|
||||
return new Plan(cells, supports, new Position(entryX, level + 1, entryZ));
|
||||
}
|
||||
|
||||
private boolean requireRock(Set<Position> supports, int x, int y, int z) {
|
||||
if (!solid(x, y, z)) return false;
|
||||
supports.add(new Position(x, y, z));
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean clearOfWater(int x, int z) {
|
||||
// The extra ten blocks include both the entrance and the complete protected shell.
|
||||
int clearance = WATER_CLEARANCE + 10;
|
||||
for (var cell : hydrology.cells()) {
|
||||
long dx = x - cell.x(), dz = z - cell.z();
|
||||
if (dx * dx + dz * dz < (long) clearance * clearance) return false;
|
||||
}
|
||||
for (var spring : hydrology.springs()) {
|
||||
var bounds = spring.flowBounds();
|
||||
long dx = Math.max(Math.max((long) bounds.minX() - x, 0), (long) x - bounds.maxX());
|
||||
long dz = Math.max(Math.max((long) bounds.minZ() - z, 0), (long) z - bounds.maxZ());
|
||||
if (dx * dx + dz * dz < (long) clearance * clearance) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private int top(int x, int z) {
|
||||
return heights.computeIfAbsent(key(x, z), ignored -> {
|
||||
for (int y = 383; y >= 32; y -= 4) {
|
||||
if (!solid(x, y, z)) continue;
|
||||
for (int exact = Math.min(383, y + 3); exact > y; exact--) if (solid(x, exact, z)) return exact;
|
||||
return y;
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
}
|
||||
|
||||
private boolean solid(int x, int y, int z) { return sampler.sample(x, y, z) > 0; }
|
||||
}
|
||||
|
||||
private static void add(List<Cell> cells, Set<Position> changed, int x, int y, int z, boolean lava) {
|
||||
if (changed.add(new Position(x, y, z))) cells.add(new Cell(x, y, z, lava));
|
||||
}
|
||||
|
||||
private static boolean inside(int x, int z, int radius) { return (long) x * x + (long) z * z < (long) radius * radius; }
|
||||
private static long key(int x, int z) { return ((long) x << 32) | (z & 0xffffffffL); }
|
||||
private static double random(long seed, int x, int z) {
|
||||
long value = seed ^ key(x, z) ^ 0x1A7A5EEDL;
|
||||
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
|
||||
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
|
||||
return ((value ^ (value >>> 31)) >>> 11) * 0x1.0p-53;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
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;
|
||||
|
||||
/** A seed-only surface-water plan. No chunks, random generation order or Minecraft objects are retained. */
|
||||
public final class SurfaceHydrology {
|
||||
public static final int RADIUS = 192;
|
||||
public static final int MAX_CARVE = 6;
|
||||
public static final int MAX_DEPTH = 4;
|
||||
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 SurfaceHydrology() {}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Sampler { float sample(int x, int y, int z); }
|
||||
|
||||
public enum Kind { POND, LAKE, STREAM }
|
||||
public enum ShoreMaterial { SAND, GRAVEL, CLAY }
|
||||
public record Point(int x, int z) {}
|
||||
public record Bounds(int minX, int minZ, int maxX, int maxZ) {}
|
||||
|
||||
/** bedY is an existing solid block. carveTop includes waterY when a natural hollow is filled. */
|
||||
public record Cell(int x, int z, int waterY, int bedY, int carveTop,
|
||||
ShoreMaterial material, long featureId) {
|
||||
public boolean hasWater() { return waterY >= 0; }
|
||||
}
|
||||
|
||||
public record Feature(long id, Kind kind, int waterY, List<Point> path, int waterCells, Bounds bounds) {
|
||||
public Feature { path = List.copyOf(path); }
|
||||
}
|
||||
|
||||
public static final class Plan {
|
||||
private final Map<Long, Cell> cells;
|
||||
private final Map<Long, List<Cell>> chunks;
|
||||
private final List<Feature> features;
|
||||
private final int sampledColumns;
|
||||
private final long densitySamples;
|
||||
|
||||
private Plan(Map<Long, Cell> source, List<Feature> features, int columns, long samples) {
|
||||
List<Cell> sorted = source.values().stream()
|
||||
.sorted(Comparator.comparingInt(Cell::x).thenComparingInt(Cell::z)).toList();
|
||||
Map<Long, Cell> ordered = new LinkedHashMap<>();
|
||||
Map<Long, List<Cell>> byChunk = new HashMap<>();
|
||||
for (Cell cell : sorted) {
|
||||
ordered.put(key(cell.x(), cell.z()), cell);
|
||||
byChunk.computeIfAbsent(key(cell.x() >> 4, cell.z() >> 4), ignored -> new ArrayList<>()).add(cell);
|
||||
}
|
||||
byChunk.replaceAll((ignored, values) -> List.copyOf(values));
|
||||
cells = Collections.unmodifiableMap(ordered);
|
||||
chunks = Map.copyOf(byChunk);
|
||||
this.features = List.copyOf(features);
|
||||
sampledColumns = columns;
|
||||
densitySamples = samples;
|
||||
}
|
||||
|
||||
public Cell cellAt(int x, int z) { return cells.get(key(x, z)); }
|
||||
public Collection<Cell> cells() { return cells.values(); }
|
||||
public List<Cell> cellsInChunk(int x, int z) { return chunks.getOrDefault(key(x, z), List.of()); }
|
||||
public List<Feature> features() { return features; }
|
||||
public 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 long samples;
|
||||
|
||||
Planner(long seed, Sampler sampler) { this.seed = seed; this.sampler = sampler; }
|
||||
|
||||
Plan build() {
|
||||
List<Candidate> candidates = candidates();
|
||||
int attempts = 0;
|
||||
for (Candidate candidate : candidates) {
|
||||
if (features.size() >= 5 || attempts++ >= 96) break;
|
||||
Point center = new Point(candidate.x(), candidate.z());
|
||||
if (nearWater(center.x(), center.z(), 38)) continue;
|
||||
Column column = column(center.x(), center.z());
|
||||
if (column.top() < 64 || column.top() - column.solidBottom() < 7) continue;
|
||||
Kind kind = features.size() < 2 ? Kind.LAKE : Kind.POND;
|
||||
int target = kind == Kind.LAKE ? 150 + (int) (random(seed, center.x(), center.z()) * 100)
|
||||
: 40 + (int) (random(seed + 1, center.x(), center.z()) * 45);
|
||||
// Prefer an existing low spot; a shallow cut is the fallback on level terrain.
|
||||
for (int level : new int[]{column.top() + 1, column.top() - 1, column.top() - 2}) {
|
||||
Set<Long> footprint = growBasin(center, level, target);
|
||||
if (footprint == null) continue;
|
||||
long id = featureId(features.size());
|
||||
Map<Long, Cell> wet = waterCells(footprint, level, id);
|
||||
if (wet == null || !contained(wet)) continue;
|
||||
addFeature(id, kind, level, List.of(center), wet);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// One quiet stream connects a basin to a nearby surface refuge at the same water level.
|
||||
// Its route follows low excavation cost through the relief; it has no falling outlets.
|
||||
boolean stream = false;
|
||||
for (Feature source : List.copyOf(features)) if (addStream(source)) { stream = true; break; }
|
||||
if (!stream) addPlateauStream(candidates);
|
||||
addBanks();
|
||||
return new Plan(cells, features, columns.size(), samples);
|
||||
}
|
||||
|
||||
private List<Candidate> candidates() {
|
||||
for (int x = -RADIUS; x <= RADIUS; x += GRID) {
|
||||
for (int z = -RADIUS; z <= RADIUS; z += GRID) {
|
||||
if (inside(x, z, RADIUS)) coarse.put(key(x, z), coarseTop(x, z));
|
||||
}
|
||||
}
|
||||
List<Candidate> result = new ArrayList<>();
|
||||
for (int x = -RADIUS + GRID; x < RADIUS; x += GRID) {
|
||||
for (int z = -RADIUS + GRID; z < RADIUS; z += GRID) {
|
||||
int height = coarse.getOrDefault(key(x, z), -1);
|
||||
if (height < 64 || !inside(x, z, RADIUS - 24)) continue;
|
||||
double average = 0;
|
||||
boolean safe = true;
|
||||
for (int[] d : CARDINALS) {
|
||||
int neighbor = coarse.getOrDefault(key(x + d[0] * GRID, z + d[1] * GRID), -1);
|
||||
if (neighbor < 64 || Math.abs(neighbor - height) > 16) safe = false;
|
||||
average += neighbor * 0.25;
|
||||
}
|
||||
if (safe) result.add(new Candidate(x, z,
|
||||
(average - height) * 2.0 + random(seed + 2, x, z) * 3.0));
|
||||
}
|
||||
}
|
||||
result.sort(Comparator.comparingDouble(Candidate::score).reversed()
|
||||
.thenComparingInt(Candidate::x).thenComparingInt(Candidate::z));
|
||||
return result;
|
||||
}
|
||||
|
||||
private int coarseTop(int x, int z) {
|
||||
for (int y = 383; y >= 32; y -= 4) if (solid(x, y, z)) return y;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private Column column(int x, int z) {
|
||||
long key = key(x, z);
|
||||
Column old = columns.get(key);
|
||||
if (old != null) return old;
|
||||
int top = -1;
|
||||
// Exact scan, including everything above the planned water: a cave roof cannot be
|
||||
// mistaken for a surface. Only candidates and their nearby shore need this refinement.
|
||||
for (int y = 383; y >= 24; y--) {
|
||||
if (solid(x, y, z)) { top = y; break; }
|
||||
}
|
||||
int bottom = top;
|
||||
if (top >= 0) {
|
||||
while (bottom > Math.max(16, top - 16) && solid(x, bottom - 1, z)) bottom--;
|
||||
}
|
||||
Column created = new Column(top, bottom);
|
||||
columns.put(key, created);
|
||||
return created;
|
||||
}
|
||||
|
||||
private boolean eligible(int x, int z, int level) {
|
||||
if (level < 32 || level >= 383 || !inside(x, z, RADIUS - 2)) return false;
|
||||
Cell old = cells.get(key(x, z));
|
||||
if (old != null && old.hasWater() && old.waterY() != level) return false;
|
||||
Column column = column(x, z);
|
||||
return column.top() >= level - 3 && column.top() < level + MAX_CARVE
|
||||
&& column.solidBottom() <= Math.min(column.top(), level - 1) - 2;
|
||||
}
|
||||
|
||||
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()) > 24 || 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) * 2.0);
|
||||
if (cost >= costs.getOrDefault(key(x, z), Double.POSITIVE_INFINITY)) continue;
|
||||
costs.put(key(x, z), cost);
|
||||
queue.add(new Node(x, z, cost));
|
||||
}
|
||||
}
|
||||
wet = connectedTo(wet, key(center.x(), center.z()));
|
||||
if (wet.size() < Math.min(target, 30)) return null;
|
||||
// Filling a hollow requires every lower connected outlet to be included. Abort rather
|
||||
// than place a dam if this natural basin runs to a cliff, cave or an oversized catchment.
|
||||
List<Long> work = new ArrayList<>(wet);
|
||||
for (int i = 0; i < work.size(); i++) {
|
||||
Point point = point(work.get(i));
|
||||
for (int[] d : CARDINALS) {
|
||||
int x = point.x() + d[0], z = point.z() + d[1];
|
||||
long neighbor = key(x, z);
|
||||
if (wet.contains(neighbor) || column(x, z).top() >= level) continue;
|
||||
if (wet.size() >= 420 || Math.hypot(x - center.x(), z - center.z()) > 28
|
||||
|| !eligible(x, z, level)) return null;
|
||||
wet.add(neighbor);
|
||||
work.add(neighbor);
|
||||
}
|
||||
}
|
||||
return wet;
|
||||
}
|
||||
|
||||
private Map<Long, Cell> waterCells(Set<Long> footprint, int level, long id) {
|
||||
Map<Long, Cell> result = new HashMap<>();
|
||||
for (long key : footprint) {
|
||||
Point p = point(key);
|
||||
if (!eligible(p.x(), p.z(), level)) return null;
|
||||
boolean edge = false;
|
||||
for (int[] d : CARDINALS) edge |= !footprint.contains(key(p.x() + d[0], p.z() + d[1]));
|
||||
Column column = column(p.x(), p.z());
|
||||
int depth = Math.max(level - column.top() + 1, edge ? 1 : 2 + (terrainNoise(p.x(), p.z()) > 0.55 ? 1 : 0));
|
||||
depth = Math.min(depth, MAX_CARVE - Math.max(0, column.top() - level));
|
||||
if (depth < 1 || depth > MAX_DEPTH) return null;
|
||||
int bed = level - depth;
|
||||
if (column.top() < bed || column.solidBottom() > bed - 2) return null;
|
||||
ShoreMaterial material = edge ? ShoreMaterial.SAND
|
||||
: terrainNoise(p.x() + 73, p.z() - 19) > 0.65 ? ShoreMaterial.CLAY : ShoreMaterial.GRAVEL;
|
||||
result.put(key, new Cell(p.x(), p.z(), level, bed, Math.max(level, column.top()), material, id));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean contained(Map<Long, Cell> wet) {
|
||||
for (Cell cell : wet.values()) {
|
||||
for (int y = cell.bedY() - 2; y <= cell.bedY(); y++) {
|
||||
if (!solid(cell.x(), y, cell.z())) return false;
|
||||
}
|
||||
for (int[] d : CARDINALS) {
|
||||
int x = cell.x() + d[0], z = cell.z() + d[1];
|
||||
Cell neighbor = wet.get(key(x, z));
|
||||
if (neighbor == null) neighbor = cells.get(key(x, z));
|
||||
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) {
|
||||
if (neighbor != null && neighbor.hasWater()) {
|
||||
if (neighbor.waterY() != cell.waterY()) return false;
|
||||
if (y > neighbor.bedY()) continue;
|
||||
}
|
||||
if (!solid(x, y, z)) return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void addFeature(long id, Kind kind, int level, List<Point> path, Map<Long, Cell> wet) {
|
||||
int minX = Integer.MAX_VALUE, minZ = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE, maxZ = Integer.MIN_VALUE;
|
||||
int count = 0;
|
||||
for (Cell cell : wet.values()) {
|
||||
Cell old = cells.get(key(cell.x(), cell.z()));
|
||||
if (old == null || !old.hasWater()) { cells.put(key(cell.x(), cell.z()), cell); count++; }
|
||||
minX = Math.min(minX, cell.x()); minZ = Math.min(minZ, cell.z());
|
||||
maxX = Math.max(maxX, cell.x()); maxZ = Math.max(maxZ, cell.z());
|
||||
}
|
||||
features.add(new Feature(id, kind, level, path, count, new Bounds(minX, minZ, maxX, maxZ)));
|
||||
}
|
||||
|
||||
private boolean addStream(Feature source) {
|
||||
Point start = source.path().getFirst();
|
||||
int level = source.waterY();
|
||||
Map<Long, Double> costs = new HashMap<>();
|
||||
Map<Long, Long> parent = new HashMap<>();
|
||||
PriorityQueue<Node> queue = new PriorityQueue<>(NODE_ORDER);
|
||||
List<Node> refuges = new ArrayList<>();
|
||||
long startKey = key(start.x(), start.z());
|
||||
costs.put(startKey, 0.0);
|
||||
queue.add(new Node(start.x(), start.z(), 0));
|
||||
int visited = 0;
|
||||
while (!queue.isEmpty() && visited++ < 1200) {
|
||||
Node node = queue.remove();
|
||||
long at = key(node.x(), node.z());
|
||||
if (node.cost() > costs.getOrDefault(at, Double.POSITIVE_INFINITY)) continue;
|
||||
double distance = Math.hypot(node.x() - start.x(), node.z() - start.z());
|
||||
if (distance >= 44 && distance <= 76 && column(node.x(), node.z()).top() <= level + 3) refuges.add(node);
|
||||
for (int[] d : NEIGHBORS) {
|
||||
int x = node.x() + d[0] * 4, z = node.z() + d[1] * 4;
|
||||
if (Math.hypot(x - start.x(), z - start.z()) > 88 || !eligible(x, z, level)) continue;
|
||||
long next = key(x, z);
|
||||
double cost = node.cost() + Math.hypot(d[0], d[1])
|
||||
* (4 + Math.abs(column(x, z).top() - level) * 1.4 + terrainNoise(x, z) * 4);
|
||||
if (cost >= costs.getOrDefault(next, Double.POSITIVE_INFINITY)) continue;
|
||||
costs.put(next, cost); parent.put(next, at); queue.add(new Node(x, z, cost));
|
||||
}
|
||||
}
|
||||
refuges.sort(Comparator.comparingDouble((Node n) -> n.cost() / Math.hypot(n.x() - start.x(), n.z() - start.z()))
|
||||
.thenComparingInt(Node::x).thenComparingInt(Node::z));
|
||||
int attempts = 0;
|
||||
for (Node refuge : refuges) {
|
||||
if (attempts++ >= 24) break;
|
||||
List<Point> path = new ArrayList<>();
|
||||
long cursor = key(refuge.x(), refuge.z());
|
||||
while (cursor != startKey) { path.add(point(cursor)); cursor = parent.get(cursor); }
|
||||
path.add(start); Collections.reverse(path);
|
||||
Set<Long> corridor = new HashSet<>();
|
||||
for (int i = 1; i < path.size(); i++) {
|
||||
Point a = path.get(i - 1), b = path.get(i);
|
||||
int length = Math.max(Math.abs(b.x() - a.x()), Math.abs(b.z() - a.z()));
|
||||
for (int step = 0; step <= length; step++) {
|
||||
int x = a.x() + (b.x() - a.x()) * step / length;
|
||||
int z = a.z() + (b.z() - a.z()) * step / length;
|
||||
int width = terrainNoise(x + 17, z + 29) > 0.65 ? 2 : 1;
|
||||
for (int dx = -width; dx <= width; dx++) for (int dz = -width; dz <= width; dz++) {
|
||||
if (dx * dx + dz * dz <= width * width + 1) corridor.add(key(x + dx, z + dz));
|
||||
}
|
||||
}
|
||||
}
|
||||
Set<Long> refugePool = growBasin(new Point(refuge.x(), refuge.z()), level, 36);
|
||||
if (refugePool == null) continue;
|
||||
corridor.addAll(refugePool);
|
||||
if (!closeStreamOutlets(corridor, level)) continue;
|
||||
long id = featureId(features.size());
|
||||
Map<Long, Cell> wet = waterCells(corridor, level, id);
|
||||
if (wet == null || !contained(wet)) continue;
|
||||
addFeature(id, Kind.STREAM, level, path, wet);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Low lake floors may be trapped below every nearby plateau. Try a small natural surface
|
||||
* refuge as a source too, retaining it only if a fully contained stream can actually leave it. */
|
||||
private boolean addPlateauStream(List<Candidate> candidates) {
|
||||
List<Candidate> plateaus = candidates.stream().filter(candidate -> flatness(candidate.x(), candidate.z()) <= 6)
|
||||
.sorted(Comparator.comparingDouble((Candidate c) -> flatness(c.x(), c.z())
|
||||
- random(seed + 91, c.x(), c.z()) * 2).thenComparingInt(Candidate::x).thenComparingInt(Candidate::z)).toList();
|
||||
int attempts = 0;
|
||||
for (Candidate candidate : plateaus) {
|
||||
if (attempts++ >= 20) break;
|
||||
Point center = new Point(candidate.x(), candidate.z());
|
||||
if (nearWater(center.x(), center.z(), 24)) continue;
|
||||
Column column = column(center.x(), center.z());
|
||||
if (column.top() < 64 || column.top() - column.solidBottom() < 7) continue;
|
||||
for (int level : new int[]{column.top() - 1, column.top() - 2}) {
|
||||
Set<Long> footprint = growBasin(center, level, 40);
|
||||
if (footprint == null) continue;
|
||||
long id = featureId(features.size());
|
||||
Map<Long, Cell> wet = waterCells(footprint, level, id);
|
||||
if (wet == null || !contained(wet)) continue;
|
||||
addFeature(id, Kind.POND, level, List.of(center), wet);
|
||||
if (addStream(features.getLast())) return true;
|
||||
features.removeLast();
|
||||
cells.values().removeIf(cell -> cell.featureId() == id);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private int flatness(int x, int z) {
|
||||
int low = coarse.getOrDefault(key(x, z), -1000), high = low;
|
||||
for (int[] d : CARDINALS) {
|
||||
int value = coarse.getOrDefault(key(x + d[0] * GRID, z + d[1] * GRID), -1000);
|
||||
low = Math.min(low, value); high = Math.max(high, value);
|
||||
}
|
||||
return high - low;
|
||||
}
|
||||
|
||||
/** Include small connected low spots beside a channel; reject large/deep/open outlets.
|
||||
* This adds water to a verified natural hollow, never rock to close a leak. */
|
||||
private boolean closeStreamOutlets(Set<Long> corridor, int level) {
|
||||
int maximum = Math.min(900, corridor.size() + 180);
|
||||
List<Long> work = new ArrayList<>(corridor);
|
||||
for (int i = 0; i < work.size(); i++) {
|
||||
Point point = point(work.get(i));
|
||||
for (int[] d : CARDINALS) {
|
||||
int x = point.x() + d[0], z = point.z() + d[1];
|
||||
long next = key(x, z);
|
||||
Cell old = cells.get(next);
|
||||
if (corridor.contains(next) || old != null && old.hasWater() && old.waterY() == level
|
||||
|| column(x, z).top() >= level) continue;
|
||||
if (corridor.size() >= maximum || !eligible(x, z, level)) return false;
|
||||
corridor.add(next); work.add(next);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Set<Long> connectedTo(Set<Long> footprint, long start) {
|
||||
Set<Long> connected = new HashSet<>();
|
||||
List<Long> work = new ArrayList<>();
|
||||
connected.add(start); work.add(start);
|
||||
for (int i = 0; i < work.size(); i++) {
|
||||
Point point = point(work.get(i));
|
||||
for (int[] d : CARDINALS) {
|
||||
long next = key(point.x() + d[0], point.z() + d[1]);
|
||||
if (footprint.contains(next) && connected.add(next)) work.add(next);
|
||||
}
|
||||
}
|
||||
return connected;
|
||||
}
|
||||
|
||||
private void addBanks() {
|
||||
List<Cell> water = cells.values().stream().filter(Cell::hasWater)
|
||||
.sorted(Comparator.comparingInt(Cell::x).thenComparingInt(Cell::z)).toList();
|
||||
for (Cell cell : water) {
|
||||
for (int[] d : CARDINALS) {
|
||||
int x = cell.x() + d[0], z = cell.z() + d[1];
|
||||
if (cells.containsKey(key(x, z))) continue;
|
||||
Column column = column(x, z);
|
||||
if (column.top() < cell.waterY() || column.top() > cell.waterY() + 2
|
||||
|| column.solidBottom() > column.top() - 2) continue;
|
||||
cells.put(key(x, z), new Cell(x, z, -1, column.top(), column.top(),
|
||||
ShoreMaterial.SAND, cell.featureId()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
int gx = Math.floorDiv(x, 8), gz = Math.floorDiv(z, 8);
|
||||
double tx = smooth(Math.floorMod(x, 8) / 8.0), tz = smooth(Math.floorMod(z, 8) / 8.0);
|
||||
double a = random(seed + 31, gx, gz) * (1 - tx) + random(seed + 31, gx + 1, gz) * tx;
|
||||
double b = random(seed + 31, gx, gz + 1) * (1 - tx) + random(seed + 31, 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);
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.stream.Stream;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeResolver;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Climate;
|
||||
|
||||
/** Horizontal temperate patches sampled from this world's seeded climate router. */
|
||||
public final class TemperateIslandBiomeSource extends BiomeSource {
|
||||
public static final MapCodec<TemperateIslandBiomeSource> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||
Biome.CODEC.fieldOf("forest").forGetter(source -> source.forest),
|
||||
Biome.CODEC.fieldOf("plains").forGetter(source -> source.plains),
|
||||
Biome.CODEC.fieldOf("birch_forest").forGetter(source -> source.birchForest),
|
||||
Biome.CODEC.fieldOf("flower_forest").forGetter(source -> source.flowerForest),
|
||||
Biome.CODEC.fieldOf("dappled_forest").forGetter(source -> source.dappledForest)
|
||||
).apply(instance, TemperateIslandBiomeSource::new));
|
||||
|
||||
private final Holder<Biome> forest;
|
||||
private final Holder<Biome> plains;
|
||||
private final Holder<Biome> birchForest;
|
||||
private final Holder<Biome> flowerForest;
|
||||
private final Holder<Biome> dappledForest;
|
||||
|
||||
public TemperateIslandBiomeSource(Holder<Biome> forest, Holder<Biome> plains,
|
||||
Holder<Biome> birchForest, Holder<Biome> flowerForest, Holder<Biome> dappledForest) {
|
||||
this.forest = forest;
|
||||
this.plains = plains;
|
||||
this.birchForest = birchForest;
|
||||
this.flowerForest = flowerForest;
|
||||
this.dappledForest = dappledForest;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<TemperateIslandBiomeSource> codec() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Stream<Holder<Biome>> collectPossibleBiomes() {
|
||||
return Stream.of(forest, plains, birchForest, flowerForest, dappledForest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiomeResolver createResolver(Climate.Sampler sampler) {
|
||||
// The resolver receives quart coordinates. The two bound density samplers use block
|
||||
// coordinates, and already carry the world's seed through RandomState. Sampling Y=0
|
||||
// keeps a shore and the rock beneath it in the same biome, including within overhangs.
|
||||
return (quartX, quartY, quartZ) -> {
|
||||
int x = quartX * 4;
|
||||
int z = quartZ * 4;
|
||||
float variation = sampler.temperature().sampleValue(x, 0, z);
|
||||
float moisture = sampler.humidity().sampleValue(x, 0, z);
|
||||
if (variation > 0.18F) return dappledForest;
|
||||
if (variation < -0.28F) return plains;
|
||||
if (moisture > 0.25F) return birchForest;
|
||||
if (moisture < -0.30F) return flowerForest;
|
||||
return forest;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,15 @@
|
||||
{
|
||||
"generator.sanctuary.sanctuary": "Sanctuary",
|
||||
"biome.sanctuary.starter_forest": "Sanctuary Forest"
|
||||
"biome.sanctuary.starter_forest": "Sanctuary Forest",
|
||||
"biome.sanctuary.starter_forest_hydrology": "Sanctuary Forest and Shores",
|
||||
"biome.sanctuary.temperate_forest": "Sanctuary Temperate Forest",
|
||||
"biome.sanctuary.temperate_plains": "Sanctuary Plains",
|
||||
"biome.sanctuary.temperate_birch_forest": "Sanctuary Birch Forest",
|
||||
"biome.sanctuary.temperate_flower_forest": "Sanctuary Flower Forest",
|
||||
"biome.sanctuary.temperate_dappled_forest": "Sanctuary Dappled Forest",
|
||||
"biome.sanctuary.layered_meadow": "Highland Meadow",
|
||||
"biome.sanctuary.layered_flower_meadow": "Flowered Highland Meadow",
|
||||
"biome.sanctuary.layered_lower_grove": "Ledge Woodland",
|
||||
"biome.sanctuary.layered_mossy_terraces": "Mossy Ledges",
|
||||
"biome.sanctuary.layered_rocky_terraces": "Rocky Terraces"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
{
|
||||
"generator.sanctuary.sanctuary": "Sanctuary",
|
||||
"biome.sanctuary.starter_forest": "For\u00eat de Sanctuary"
|
||||
"biome.sanctuary.starter_forest": "Forêt de Sanctuary",
|
||||
"biome.sanctuary.starter_forest_hydrology": "Forêt et rives de Sanctuary",
|
||||
"biome.sanctuary.temperate_forest": "Forêt tempérée de Sanctuary",
|
||||
"biome.sanctuary.temperate_plains": "Prairies de Sanctuary",
|
||||
"biome.sanctuary.temperate_birch_forest": "Boulaie de Sanctuary",
|
||||
"biome.sanctuary.temperate_flower_forest": "Forêt fleurie de Sanctuary",
|
||||
"biome.sanctuary.temperate_dappled_forest": "Forêt diaprée de Sanctuary",
|
||||
"biome.sanctuary.layered_meadow": "Prairie des hauteurs",
|
||||
"biome.sanctuary.layered_flower_meadow": "Prairie fleurie des hauteurs",
|
||||
"biome.sanctuary.layered_lower_grove": "Sous-bois des corniches",
|
||||
"biome.sanctuary.layered_mossy_terraces": "Corniches moussues",
|
||||
"biome.sanctuary.layered_rocky_terraces": "Terrasses rocheuses"
|
||||
}
|
||||
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.meadow"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:donkey",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 2,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 1
|
||||
},
|
||||
{
|
||||
"type": "minecraft:rabbit",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 2
|
||||
},
|
||||
"weight": 2
|
||||
},
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 2
|
||||
},
|
||||
"weight": 2
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.8,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:ore_dirt",
|
||||
"minecraft:ore_granite_upper",
|
||||
"minecraft:ore_granite_lower",
|
||||
"minecraft:ore_diorite_upper",
|
||||
"minecraft:ore_diorite_lower",
|
||||
"minecraft:ore_andesite_upper",
|
||||
"minecraft:ore_andesite_lower",
|
||||
"minecraft:ore_tuff",
|
||||
"sanctuary:island_ore_coal",
|
||||
"sanctuary:island_ore_iron",
|
||||
"sanctuary:island_ore_copper",
|
||||
"sanctuary:island_ore_gold",
|
||||
"sanctuary:island_ore_redstone",
|
||||
"sanctuary:island_ore_lapis",
|
||||
"sanctuary:island_ore_diamond"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:patch_tall_grass_2",
|
||||
"minecraft:patch_grass_meadow",
|
||||
"minecraft:flower_meadow",
|
||||
"minecraft:trees_meadow",
|
||||
"minecraft:wildflowers_meadow",
|
||||
"minecraft:flower_flower_forest",
|
||||
"sanctuary:layered_shore_sugar_cane",
|
||||
"sanctuary:lower_terrace_vegetation"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.7
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.forest"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.8,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:ore_dirt",
|
||||
"minecraft:ore_granite_upper",
|
||||
"minecraft:ore_granite_lower",
|
||||
"minecraft:ore_diorite_upper",
|
||||
"minecraft:ore_diorite_lower",
|
||||
"minecraft:ore_andesite_upper",
|
||||
"minecraft:ore_andesite_lower",
|
||||
"minecraft:ore_tuff",
|
||||
"sanctuary:island_ore_coal",
|
||||
"sanctuary:island_ore_iron",
|
||||
"sanctuary:island_ore_copper",
|
||||
"sanctuary:island_ore_gold",
|
||||
"sanctuary:island_ore_redstone",
|
||||
"sanctuary:island_ore_lapis",
|
||||
"sanctuary:island_ore_diamond"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:forest_flowers",
|
||||
"minecraft:wildflowers_birch_forest",
|
||||
"minecraft:trees_birch",
|
||||
"minecraft:patch_bush",
|
||||
"minecraft:flower_default",
|
||||
"minecraft:patch_grass_forest",
|
||||
"minecraft:brown_mushroom_normal",
|
||||
"minecraft:red_mushroom_normal",
|
||||
"minecraft:patch_pumpkin",
|
||||
"minecraft:patch_sugar_cane",
|
||||
"minecraft:patch_firefly_bush_near_water",
|
||||
"sanctuary:layered_shore_sugar_cane",
|
||||
"sanctuary:lower_terrace_vegetation"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.7
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
},
|
||||
{
|
||||
"type": "minecraft:horse",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 2
|
||||
},
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:donkey",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 3,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 1
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 90
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_horse",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.8,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:ore_dirt",
|
||||
"minecraft:ore_granite_upper",
|
||||
"minecraft:ore_granite_lower",
|
||||
"minecraft:ore_diorite_upper",
|
||||
"minecraft:ore_diorite_lower",
|
||||
"minecraft:ore_andesite_upper",
|
||||
"minecraft:ore_andesite_lower",
|
||||
"minecraft:ore_tuff",
|
||||
"sanctuary:island_ore_coal",
|
||||
"sanctuary:island_ore_iron",
|
||||
"sanctuary:island_ore_copper",
|
||||
"sanctuary:island_ore_gold",
|
||||
"sanctuary:island_ore_redstone",
|
||||
"sanctuary:island_ore_lapis",
|
||||
"sanctuary:island_ore_diamond"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:patch_tall_grass_2",
|
||||
"minecraft:patch_grass_meadow",
|
||||
"minecraft:flower_meadow",
|
||||
"minecraft:trees_meadow",
|
||||
"minecraft:wildflowers_meadow",
|
||||
"minecraft:patch_pumpkin",
|
||||
"minecraft:patch_sugar_cane",
|
||||
"minecraft:patch_firefly_bush_near_water",
|
||||
"sanctuary:layered_shore_sugar_cane",
|
||||
"sanctuary:lower_terrace_vegetation"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.7
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.lush_caves"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"axolotls": [
|
||||
{
|
||||
"type": "minecraft:axolotl",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"water_ambient": [
|
||||
{
|
||||
"type": "minecraft:tropical_fish",
|
||||
"count": 8,
|
||||
"weight": 25
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.8,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:ore_dirt",
|
||||
"minecraft:ore_granite_upper",
|
||||
"minecraft:ore_granite_lower",
|
||||
"minecraft:ore_diorite_upper",
|
||||
"minecraft:ore_diorite_lower",
|
||||
"minecraft:ore_andesite_upper",
|
||||
"minecraft:ore_andesite_lower",
|
||||
"minecraft:ore_tuff",
|
||||
"sanctuary:island_ore_coal",
|
||||
"sanctuary:island_ore_iron",
|
||||
"sanctuary:island_ore_copper",
|
||||
"sanctuary:island_ore_gold",
|
||||
"sanctuary:island_ore_redstone",
|
||||
"sanctuary:island_ore_lapis",
|
||||
"sanctuary:island_ore_diamond"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"sanctuary:layered_shore_sugar_cane",
|
||||
"sanctuary:lower_terrace_vegetation"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.7
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.meadow"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:donkey",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 2,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 1
|
||||
},
|
||||
{
|
||||
"type": "minecraft:rabbit",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 2
|
||||
},
|
||||
"weight": 2
|
||||
},
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 2
|
||||
},
|
||||
"weight": 2
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.8,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:ore_dirt",
|
||||
"minecraft:ore_granite_upper",
|
||||
"minecraft:ore_granite_lower",
|
||||
"minecraft:ore_diorite_upper",
|
||||
"minecraft:ore_diorite_lower",
|
||||
"minecraft:ore_andesite_upper",
|
||||
"minecraft:ore_andesite_lower",
|
||||
"minecraft:ore_tuff",
|
||||
"sanctuary:island_ore_coal",
|
||||
"sanctuary:island_ore_iron",
|
||||
"sanctuary:island_ore_copper",
|
||||
"sanctuary:island_ore_gold",
|
||||
"sanctuary:island_ore_redstone",
|
||||
"sanctuary:island_ore_lapis",
|
||||
"sanctuary:island_ore_diamond"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"sanctuary:layered_shore_sugar_cane",
|
||||
"sanctuary:lower_terrace_vegetation"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.7
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.forest"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
},
|
||||
{
|
||||
"type": "minecraft:wolf",
|
||||
"count": 4,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.8,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:ore_dirt",
|
||||
"minecraft:ore_granite_upper",
|
||||
"minecraft:ore_granite_lower",
|
||||
"minecraft:ore_diorite_upper",
|
||||
"minecraft:ore_diorite_lower",
|
||||
"minecraft:ore_andesite_upper",
|
||||
"minecraft:ore_andesite_lower",
|
||||
"minecraft:ore_tuff",
|
||||
"minecraft:ore_coal_upper",
|
||||
"minecraft:ore_coal_lower",
|
||||
"minecraft:ore_iron_upper",
|
||||
"minecraft:ore_iron_middle",
|
||||
"minecraft:ore_iron_small",
|
||||
"minecraft:ore_gold",
|
||||
"minecraft:ore_gold_lower",
|
||||
"minecraft:ore_redstone",
|
||||
"minecraft:ore_redstone_lower",
|
||||
"minecraft:ore_diamond",
|
||||
"minecraft:ore_diamond_medium",
|
||||
"minecraft:ore_diamond_large",
|
||||
"minecraft:ore_diamond_buried",
|
||||
"minecraft:ore_lapis",
|
||||
"minecraft:ore_lapis_buried",
|
||||
"minecraft:ore_copper"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"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: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.forest"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#7aa5ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.65,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:ore_dirt",
|
||||
"minecraft:ore_granite_upper",
|
||||
"minecraft:ore_granite_lower",
|
||||
"minecraft:ore_diorite_upper",
|
||||
"minecraft:ore_diorite_lower",
|
||||
"minecraft:ore_andesite_upper",
|
||||
"minecraft:ore_andesite_lower",
|
||||
"minecraft:ore_tuff",
|
||||
"sanctuary:island_ore_coal",
|
||||
"sanctuary:island_ore_iron",
|
||||
"sanctuary:island_ore_copper",
|
||||
"sanctuary:island_ore_gold",
|
||||
"sanctuary:island_ore_redstone",
|
||||
"sanctuary:island_ore_lapis",
|
||||
"sanctuary:island_ore_diamond"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:forest_flowers",
|
||||
"minecraft:wildflowers_birch_forest",
|
||||
"minecraft:trees_birch",
|
||||
"minecraft:patch_bush",
|
||||
"minecraft:flower_default",
|
||||
"minecraft:patch_grass_forest",
|
||||
"minecraft:brown_mushroom_normal",
|
||||
"minecraft:red_mushroom_normal",
|
||||
"minecraft:patch_pumpkin",
|
||||
"minecraft:patch_sugar_cane",
|
||||
"minecraft:patch_firefly_bush_near_water",
|
||||
"sanctuary:natural_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.6
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.forest"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
},
|
||||
{
|
||||
"type": "minecraft:rabbit",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 2
|
||||
},
|
||||
"weight": 4
|
||||
},
|
||||
{
|
||||
"type": "minecraft:fox",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 2
|
||||
},
|
||||
"weight": 4
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/fog_color": "#ccd8e2",
|
||||
"minecraft:visual/sky_color": "#7ca3ff",
|
||||
"minecraft:visual/water_fog_color": "#375154"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.65,
|
||||
"effects": {
|
||||
"dry_foliage_color": "#8c3a04",
|
||||
"foliage_color": "#e68e30",
|
||||
"grass_color": "#df6827",
|
||||
"water_color": "#375154"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:ore_dirt",
|
||||
"minecraft:ore_granite_upper",
|
||||
"minecraft:ore_granite_lower",
|
||||
"minecraft:ore_diorite_upper",
|
||||
"minecraft:ore_diorite_lower",
|
||||
"minecraft:ore_andesite_upper",
|
||||
"minecraft:ore_andesite_lower",
|
||||
"minecraft:ore_tuff",
|
||||
"sanctuary:island_ore_coal",
|
||||
"sanctuary:island_ore_iron",
|
||||
"sanctuary:island_ore_copper",
|
||||
"sanctuary:island_ore_gold",
|
||||
"sanctuary:island_ore_redstone",
|
||||
"sanctuary:island_ore_lapis",
|
||||
"sanctuary:island_ore_diamond"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:trees_dappled_forest",
|
||||
"minecraft:brown_mushroom_dappled_forest",
|
||||
"minecraft:patch_red_shrub",
|
||||
"minecraft:patch_grass_forest",
|
||||
"sanctuary:natural_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.6
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.flower_forest"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
},
|
||||
{
|
||||
"type": "minecraft:rabbit",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 3,
|
||||
"min_inclusive": 2
|
||||
},
|
||||
"weight": 4
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.8,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:ore_dirt",
|
||||
"minecraft:ore_granite_upper",
|
||||
"minecraft:ore_granite_lower",
|
||||
"minecraft:ore_diorite_upper",
|
||||
"minecraft:ore_diorite_lower",
|
||||
"minecraft:ore_andesite_upper",
|
||||
"minecraft:ore_andesite_lower",
|
||||
"minecraft:ore_tuff",
|
||||
"sanctuary:island_ore_coal",
|
||||
"sanctuary:island_ore_iron",
|
||||
"sanctuary:island_ore_copper",
|
||||
"sanctuary:island_ore_gold",
|
||||
"sanctuary:island_ore_redstone",
|
||||
"sanctuary:island_ore_lapis",
|
||||
"sanctuary:island_ore_diamond"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:flower_forest_flowers",
|
||||
"minecraft:trees_flower_forest",
|
||||
"minecraft:flower_flower_forest",
|
||||
"minecraft:patch_grass_badlands",
|
||||
"minecraft:brown_mushroom_normal",
|
||||
"minecraft:red_mushroom_normal",
|
||||
"minecraft:patch_pumpkin",
|
||||
"minecraft:patch_sugar_cane",
|
||||
"minecraft:patch_firefly_bush_near_water",
|
||||
"sanctuary:natural_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.7
|
||||
}
|
||||
+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
|
||||
},
|
||||
{
|
||||
"type": "minecraft:wolf",
|
||||
"count": 4,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.8,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:ore_dirt",
|
||||
"minecraft:ore_granite_upper",
|
||||
"minecraft:ore_granite_lower",
|
||||
"minecraft:ore_diorite_upper",
|
||||
"minecraft:ore_diorite_lower",
|
||||
"minecraft:ore_andesite_upper",
|
||||
"minecraft:ore_andesite_lower",
|
||||
"minecraft:ore_tuff",
|
||||
"sanctuary:island_ore_coal",
|
||||
"sanctuary:island_ore_iron",
|
||||
"sanctuary:island_ore_copper",
|
||||
"sanctuary:island_ore_gold",
|
||||
"sanctuary:island_ore_redstone",
|
||||
"sanctuary:island_ore_lapis",
|
||||
"sanctuary:island_ore_diamond"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:forest_flowers",
|
||||
"minecraft:trees_birch_and_oak_leaf_litter",
|
||||
"minecraft:patch_bush",
|
||||
"minecraft:flower_default",
|
||||
"minecraft:patch_grass_forest",
|
||||
"minecraft:brown_mushroom_normal",
|
||||
"minecraft:red_mushroom_normal",
|
||||
"minecraft:patch_pumpkin",
|
||||
"minecraft:patch_sugar_cane",
|
||||
"minecraft:patch_firefly_bush_near_water",
|
||||
"sanctuary:natural_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.7
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
},
|
||||
{
|
||||
"type": "minecraft:horse",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 2
|
||||
},
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:donkey",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 3,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 1
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 90
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_horse",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#78a7ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.65,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:ore_dirt",
|
||||
"minecraft:ore_granite_upper",
|
||||
"minecraft:ore_granite_lower",
|
||||
"minecraft:ore_diorite_upper",
|
||||
"minecraft:ore_diorite_lower",
|
||||
"minecraft:ore_andesite_upper",
|
||||
"minecraft:ore_andesite_lower",
|
||||
"minecraft:ore_tuff",
|
||||
"sanctuary:island_ore_coal",
|
||||
"sanctuary:island_ore_iron",
|
||||
"sanctuary:island_ore_copper",
|
||||
"sanctuary:island_ore_gold",
|
||||
"sanctuary:island_ore_redstone",
|
||||
"sanctuary:island_ore_lapis",
|
||||
"sanctuary:island_ore_diamond"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:patch_tall_grass_2",
|
||||
"minecraft:patch_bush",
|
||||
"minecraft:trees_plains",
|
||||
"minecraft:flower_plains",
|
||||
"minecraft:patch_grass_plain",
|
||||
"minecraft:brown_mushroom_normal",
|
||||
"minecraft:red_mushroom_normal",
|
||||
"minecraft:patch_pumpkin",
|
||||
"minecraft:patch_sugar_cane",
|
||||
"minecraft:patch_firefly_bush_near_water",
|
||||
"sanctuary:natural_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.8
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:temperate_moisture",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 0.0
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:temperate_variation",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 0.0
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"type": "minecraft:ore",
|
||||
"discard_chance_on_air_exposure": 0.25,
|
||||
"size": 6,
|
||||
"targets": [
|
||||
{
|
||||
"state": "minecraft:coal_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:any_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
},
|
||||
{
|
||||
"max_inclusive": 2031,
|
||||
"min_inclusive": 0,
|
||||
"predicate_type": "minecraft:height_match"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:not",
|
||||
"rule": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
}
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:stone_ore_replaceables"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"state": "minecraft:deepslate_coal_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:any_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
},
|
||||
{
|
||||
"max_inclusive": 8,
|
||||
"min_inclusive": -2032,
|
||||
"predicate_type": "minecraft:height_match"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:not",
|
||||
"rule": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
}
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:deepslate_ore_replaceables"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"type": "minecraft:ore",
|
||||
"discard_chance_on_air_exposure": 0.25,
|
||||
"size": 4,
|
||||
"targets": [
|
||||
{
|
||||
"state": "minecraft:copper_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:any_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
},
|
||||
{
|
||||
"max_inclusive": 2031,
|
||||
"min_inclusive": 0,
|
||||
"predicate_type": "minecraft:height_match"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:not",
|
||||
"rule": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
}
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:stone_ore_replaceables"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"state": "minecraft:deepslate_copper_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:any_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
},
|
||||
{
|
||||
"max_inclusive": 8,
|
||||
"min_inclusive": -2032,
|
||||
"predicate_type": "minecraft:height_match"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:not",
|
||||
"rule": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
}
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:deepslate_ore_replaceables"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"type": "minecraft:ore",
|
||||
"discard_chance_on_air_exposure": 0.7,
|
||||
"size": 3,
|
||||
"targets": [
|
||||
{
|
||||
"state": "minecraft:diamond_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:any_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
},
|
||||
{
|
||||
"max_inclusive": 2031,
|
||||
"min_inclusive": 0,
|
||||
"predicate_type": "minecraft:height_match"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:not",
|
||||
"rule": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
}
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:stone_ore_replaceables"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"state": "minecraft:deepslate_diamond_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:any_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
},
|
||||
{
|
||||
"max_inclusive": 8,
|
||||
"min_inclusive": -2032,
|
||||
"predicate_type": "minecraft:height_match"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:not",
|
||||
"rule": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
}
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:deepslate_ore_replaceables"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"type": "minecraft:ore",
|
||||
"discard_chance_on_air_exposure": 0.4,
|
||||
"size": 4,
|
||||
"targets": [
|
||||
{
|
||||
"state": "minecraft:gold_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:any_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
},
|
||||
{
|
||||
"max_inclusive": 2031,
|
||||
"min_inclusive": 0,
|
||||
"predicate_type": "minecraft:height_match"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:not",
|
||||
"rule": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
}
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:stone_ore_replaceables"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"state": "minecraft:deepslate_gold_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:any_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
},
|
||||
{
|
||||
"max_inclusive": 8,
|
||||
"min_inclusive": -2032,
|
||||
"predicate_type": "minecraft:height_match"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:not",
|
||||
"rule": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
}
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:deepslate_ore_replaceables"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"type": "minecraft:ore",
|
||||
"discard_chance_on_air_exposure": 0.25,
|
||||
"size": 4,
|
||||
"targets": [
|
||||
{
|
||||
"state": "minecraft:iron_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:any_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
},
|
||||
{
|
||||
"max_inclusive": 2031,
|
||||
"min_inclusive": 0,
|
||||
"predicate_type": "minecraft:height_match"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:not",
|
||||
"rule": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
}
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:stone_ore_replaceables"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"state": "minecraft:deepslate_iron_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:any_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
},
|
||||
{
|
||||
"max_inclusive": 8,
|
||||
"min_inclusive": -2032,
|
||||
"predicate_type": "minecraft:height_match"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:not",
|
||||
"rule": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
}
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:deepslate_ore_replaceables"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"type": "minecraft:ore",
|
||||
"discard_chance_on_air_exposure": 0.4,
|
||||
"size": 3,
|
||||
"targets": [
|
||||
{
|
||||
"state": "minecraft:lapis_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:any_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
},
|
||||
{
|
||||
"max_inclusive": 2031,
|
||||
"min_inclusive": 0,
|
||||
"predicate_type": "minecraft:height_match"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:not",
|
||||
"rule": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
}
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:stone_ore_replaceables"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"state": "minecraft:deepslate_lapis_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:any_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
},
|
||||
{
|
||||
"max_inclusive": 8,
|
||||
"min_inclusive": -2032,
|
||||
"predicate_type": "minecraft:height_match"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:not",
|
||||
"rule": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
}
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:deepslate_ore_replaceables"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"type": "minecraft:ore",
|
||||
"discard_chance_on_air_exposure": 0.4,
|
||||
"size": 4,
|
||||
"targets": [
|
||||
{
|
||||
"state": "minecraft:redstone_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:any_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
},
|
||||
{
|
||||
"max_inclusive": 2031,
|
||||
"min_inclusive": 0,
|
||||
"predicate_type": "minecraft:height_match"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:not",
|
||||
"rule": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
}
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:stone_ore_replaceables"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"state": "minecraft:deepslate_redstone_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:any_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
},
|
||||
{
|
||||
"max_inclusive": 8,
|
||||
"min_inclusive": -2032,
|
||||
"predicate_type": "minecraft:height_match"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:all_of",
|
||||
"rules": [
|
||||
{
|
||||
"predicate_type": "minecraft:not",
|
||||
"rule": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:height_specific_ore_replaceables"
|
||||
}
|
||||
},
|
||||
{
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "minecraft:deepslate_ore_replaceables"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"type": "minecraft:sequence",
|
||||
"sequence": [
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:biome",
|
||||
"biome_is": "sanctuary:layered_rocky_terraces"
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:stone"
|
||||
}
|
||||
},
|
||||
"sanctuary:starter_island"
|
||||
]
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"base_octave": -5,
|
||||
"octave_count": 3,
|
||||
"amplitude_modifiers": [
|
||||
1.0,
|
||||
0.5,
|
||||
0.25
|
||||
]
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"base_octave": -6,
|
||||
"octave_count": 3,
|
||||
"amplitude_modifiers": [
|
||||
1.0,
|
||||
0.5,
|
||||
0.25
|
||||
]
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"default_block": "minecraft:stone",
|
||||
"default_fluid": "minecraft:air",
|
||||
"disable_mob_generation": false,
|
||||
"legacy_random_source": true,
|
||||
"material_rule": "sanctuary:starter_island",
|
||||
"noise": {
|
||||
"height": 384,
|
||||
"min_y": 0
|
||||
},
|
||||
"noise_router": {
|
||||
"chunk_surface_level": 0.0,
|
||||
"continents": 0.0,
|
||||
"depth": 0.0,
|
||||
"erosion": 0.0,
|
||||
"final_density": "sanctuary:final_density_384",
|
||||
"ridges": 0.0,
|
||||
"temperature": 0.0,
|
||||
"vegetation": 0.0
|
||||
},
|
||||
"sea_level": -64,
|
||||
"spawn_target": []
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"default_block": "minecraft:stone",
|
||||
"default_fluid": "minecraft:air",
|
||||
"disable_mob_generation": false,
|
||||
"legacy_random_source": true,
|
||||
"material_rule": "sanctuary:layered_island",
|
||||
"noise": {
|
||||
"height": 384,
|
||||
"min_y": 0
|
||||
},
|
||||
"noise_router": {
|
||||
"chunk_surface_level": 0.0,
|
||||
"continents": 0.0,
|
||||
"depth": 0.0,
|
||||
"erosion": 0.0,
|
||||
"final_density": "sanctuary:final_density_384",
|
||||
"ridges": 0.0,
|
||||
"temperature": "sanctuary:temperate_variation",
|
||||
"vegetation": "sanctuary:temperate_moisture"
|
||||
},
|
||||
"sea_level": -64,
|
||||
"spawn_target": []
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"default_block": "minecraft:stone",
|
||||
"default_fluid": "minecraft:air",
|
||||
"disable_mob_generation": false,
|
||||
"legacy_random_source": true,
|
||||
"material_rule": "sanctuary:starter_island",
|
||||
"noise": {
|
||||
"height": 384,
|
||||
"min_y": 0
|
||||
},
|
||||
"noise_router": {
|
||||
"chunk_surface_level": 0.0,
|
||||
"continents": 0.0,
|
||||
"depth": 0.0,
|
||||
"erosion": 0.0,
|
||||
"final_density": "sanctuary:final_density_384",
|
||||
"ridges": 0.0,
|
||||
"temperature": "sanctuary:temperate_variation",
|
||||
"vegetation": "sanctuary:temperate_moisture"
|
||||
},
|
||||
"sea_level": -64,
|
||||
"spawn_target": []
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"feature": "sanctuary:island_ore_coal",
|
||||
"placement": [
|
||||
{
|
||||
"type": "minecraft:count",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"type": "minecraft:in_square"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:height_range",
|
||||
"height": {
|
||||
"type": "minecraft:trapezoid",
|
||||
"min_inclusive": {
|
||||
"absolute": 96
|
||||
},
|
||||
"max_inclusive": {
|
||||
"absolute": 288
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:biome"
|
||||
}
|
||||
]
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"feature": "sanctuary:island_ore_copper",
|
||||
"placement": [
|
||||
{
|
||||
"type": "minecraft:count",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"type": "minecraft:in_square"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:height_range",
|
||||
"height": {
|
||||
"type": "minecraft:trapezoid",
|
||||
"min_inclusive": {
|
||||
"absolute": 96
|
||||
},
|
||||
"max_inclusive": {
|
||||
"absolute": 248
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:biome"
|
||||
}
|
||||
]
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"feature": "sanctuary:island_ore_diamond",
|
||||
"placement": [
|
||||
{
|
||||
"type": "minecraft:rarity_filter",
|
||||
"chance": 4
|
||||
},
|
||||
{
|
||||
"type": "minecraft:in_square"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:height_range",
|
||||
"height": {
|
||||
"type": "minecraft:trapezoid",
|
||||
"min_inclusive": {
|
||||
"absolute": 104
|
||||
},
|
||||
"max_inclusive": {
|
||||
"absolute": 208
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:biome"
|
||||
}
|
||||
]
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"feature": "sanctuary:island_ore_gold",
|
||||
"placement": [
|
||||
{
|
||||
"type": "minecraft:count",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"type": "minecraft:in_square"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:height_range",
|
||||
"height": {
|
||||
"type": "minecraft:trapezoid",
|
||||
"min_inclusive": {
|
||||
"absolute": 104
|
||||
},
|
||||
"max_inclusive": {
|
||||
"absolute": 224
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:biome"
|
||||
}
|
||||
]
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"feature": "sanctuary:island_ore_iron",
|
||||
"placement": [
|
||||
{
|
||||
"type": "minecraft:count",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"type": "minecraft:in_square"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:height_range",
|
||||
"height": {
|
||||
"type": "minecraft:trapezoid",
|
||||
"min_inclusive": {
|
||||
"absolute": 88
|
||||
},
|
||||
"max_inclusive": {
|
||||
"absolute": 248
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:biome"
|
||||
}
|
||||
]
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"feature": "sanctuary:island_ore_lapis",
|
||||
"placement": [
|
||||
{
|
||||
"type": "minecraft:rarity_filter",
|
||||
"chance": 2
|
||||
},
|
||||
{
|
||||
"type": "minecraft:in_square"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:height_range",
|
||||
"height": {
|
||||
"type": "minecraft:trapezoid",
|
||||
"min_inclusive": {
|
||||
"absolute": 112
|
||||
},
|
||||
"max_inclusive": {
|
||||
"absolute": 232
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:biome"
|
||||
}
|
||||
]
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"feature": "sanctuary:island_ore_redstone",
|
||||
"placement": [
|
||||
{
|
||||
"type": "minecraft:count",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"type": "minecraft:in_square"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:height_range",
|
||||
"height": {
|
||||
"type": "minecraft:trapezoid",
|
||||
"min_inclusive": {
|
||||
"absolute": 96
|
||||
},
|
||||
"max_inclusive": {
|
||||
"absolute": 224
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:biome"
|
||||
}
|
||||
]
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"feature": { "type": "sanctuary:layered_shore_sugar_cane" },
|
||||
"placement": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"feature": {
|
||||
"type": "sanctuary:lower_terrace_vegetation"
|
||||
},
|
||||
"placement": []
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"feature": { "type": "sanctuary:natural_shore_sugar_cane" },
|
||||
"placement": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"feature": {
|
||||
"type": "sanctuary:shore_sugar_cane"
|
||||
},
|
||||
"placement": []
|
||||
}
|
||||
+7
-3
@@ -5,10 +5,14 @@
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": {
|
||||
"type": "minecraft:fixed",
|
||||
"biome": "sanctuary:starter_forest"
|
||||
"type": "sanctuary:layered_island",
|
||||
"meadow": "sanctuary:layered_meadow",
|
||||
"flower_meadow": "sanctuary:layered_flower_meadow",
|
||||
"lower_grove": "sanctuary:layered_lower_grove",
|
||||
"mossy_terraces": "sanctuary:layered_mossy_terraces",
|
||||
"rocky_terraces": "sanctuary:layered_rocky_terraces"
|
||||
},
|
||||
"settings": "sanctuary:sanctuary_384"
|
||||
"settings": "sanctuary:sanctuary_layered"
|
||||
}
|
||||
},
|
||||
"minecraft:the_end": {
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
"package": "fr.koka.sanctuary.mixin",
|
||||
"compatibilityLevel": "JAVA_25",
|
||||
"mixins": [
|
||||
"InitialSpawnMixin"
|
||||
"InitialSpawnMixin",
|
||||
"SurfaceHydrologyMixin",
|
||||
"LayeredSpringOutletMixin"
|
||||
],
|
||||
"client": [
|
||||
"client.SanctuarySkyMixin",
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
/** Lower-layer decoration must find an existing substantial ledge, never manufacture its floor. */
|
||||
public final class LayeredEcologySmoke {
|
||||
public static void main(String[] args) {
|
||||
LowerTerraceEcology.Sampler layered = (x, y, z) ->
|
||||
(y >= 240 && y <= 248) || (y >= 170 && y <= 190) ? 1 : -1;
|
||||
int highest = LowerTerraceEcology.highestNaturalSurface(layered, 0, 0);
|
||||
require(highest == 248, "The highest natural layer must be identified independently of plants");
|
||||
require(LowerTerraceEcology.isLowerTerrace(layered, 0, 190, 0, highest),
|
||||
"A thick lower floor with headroom and an upper layer must qualify");
|
||||
require(!LowerTerraceEcology.isLowerTerrace(layered, 0, highest, 0, highest),
|
||||
"Ordinary highest-surface vegetation must not be decorated again as a lower terrace");
|
||||
require(!LowerTerraceEcology.isLowerTerrace(layered, 0, 189, 0, highest),
|
||||
"A buried block is not an exposed floor");
|
||||
LowerTerraceEcology.Sampler thin = (x, y, z) -> y < 189 ? -1 : layered.sample(x, y, z);
|
||||
require(!LowerTerraceEcology.isLowerTerrace(thin, 0, 190, 0, highest),
|
||||
"A floor thinner than three natural blocks must remain untouched");
|
||||
LowerTerraceEcology.Sampler perforated = (x, y, z) -> y == 189 ? 0 : layered.sample(x, y, z);
|
||||
require(!LowerTerraceEcology.isLowerTerrace(perforated, 0, 190, 0, highest),
|
||||
"Zero-density holes in a floor must not become supports");
|
||||
LowerTerraceEcology.Sampler lowRoof = (x, y, z) -> y == 192 ? 1 : layered.sample(x, y, z);
|
||||
require(!LowerTerraceEcology.isLowerTerrace(lowRoof, 0, 190, 0, highest),
|
||||
"A terrace without two blocks of natural headroom must remain untouched");
|
||||
LowerTerraceEcology.Sampler single = (x, y, z) -> y >= 170 && y <= 190 ? 1 : -1;
|
||||
require(!LowerTerraceEcology.isLowerTerrace(single, 0, 190, 0,
|
||||
LowerTerraceEcology.highestNaturalSurface(single, 0, 0)), "An isolated highest surface is not a lower layer");
|
||||
require(LowerTerraceEcology.highestNaturalSurface((x, y, z) -> -1, 0, 0) == -1,
|
||||
"The void must contain no invented surface");
|
||||
require(!LowerTerraceEcology.isLowerTerrace((x, y, z) -> -1, 0, 100, 0, -1),
|
||||
"The void must receive no lower-terrace decoration");
|
||||
|
||||
var lower = LayeredBiomePalette.select(96, 0, 0.2F);
|
||||
var middle = LayeredBiomePalette.select(184, 0, 0.2F);
|
||||
var upper = LayeredBiomePalette.select(260, 0, 0.2F);
|
||||
require(lower != middle && lower != upper && middle != upper,
|
||||
"The same horizontal climate must expose distinct biomes on separate layers");
|
||||
require(upper == LayeredBiomePalette.Kind.FLOWER_MEADOW,
|
||||
"A moist temperate upper layer must retain a flowered meadow");
|
||||
require(LayeredBiomePalette.select(220, -0.2F, 0.2F)
|
||||
!= LayeredBiomePalette.select(220, 0.2F, 0.2F), "Layer boundaries must follow horizontal climate variation");
|
||||
EnumSet<LayeredBiomePalette.Kind> palette = EnumSet.noneOf(LayeredBiomePalette.Kind.class);
|
||||
for (int y = 32; y <= 312; y += 4) for (int v = -10; v <= 10; v++) for (int m = -10; m <= 10; m++) {
|
||||
palette.add(LayeredBiomePalette.select(y, v / 10.0F, m / 10.0F));
|
||||
}
|
||||
require(palette.equals(EnumSet.allOf(LayeredBiomePalette.Kind.class)),
|
||||
"Every declared starter biome must be reachable within the island's vertical bounds");
|
||||
System.out.println("LayeredEcologySmoke: supported lower floors, headroom, void exclusion, distinct vertical biomes and warped boundaries passed.");
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Determinism and containment checks independent of Minecraft, using controlled floating terrain. */
|
||||
public final class LayeredHydrologySmoke {
|
||||
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
|
||||
public static void main(String[] args) {
|
||||
LayeredHydrology.Sampler terrain = (x, y, z) -> {
|
||||
int top = 200 + (int) Math.round(2 * Math.sin(x / 24.0) + 2 * Math.cos(z / 31.0));
|
||||
return Math.hypot(x, z) < 220 && y <= top && y >= 145 ? 0.45F : -0.45F;
|
||||
};
|
||||
var first = LayeredHydrology.create(0, terrain);
|
||||
var replay = LayeredHydrology.create(0, terrain);
|
||||
var changed = LayeredHydrology.create(42, terrain);
|
||||
require(!first.features().isEmpty(), "The safe rolling surface should contain surface water");
|
||||
require(List.copyOf(first.cells()).equals(List.copyOf(replay.cells())) && first.features().equals(replay.features())
|
||||
&& first.springs().equals(replay.springs()),
|
||||
"The same seed and density must replay exactly");
|
||||
require(!List.copyOf(first.cells()).equals(List.copyOf(changed.cells())), "The world seed must influence water placement");
|
||||
verify(first, terrain);
|
||||
verify(changed, terrain);
|
||||
LayeredHydrology.Sampler pierced = (x, y, z) ->
|
||||
Math.floorMod(x, 37) < 2 && Math.floorMod(z, 41) < 2 ? -0.45F : terrain.sample(x, y, z);
|
||||
var aroundShafts = LayeredHydrology.create(0, pierced);
|
||||
require(!aroundShafts.features().isEmpty(), "Safe areas around natural shafts should remain usable");
|
||||
verify(aroundShafts, pierced);
|
||||
require(LayeredHydrology.create(0, (x, y, z) -> -1).cells().isEmpty(), "An empty world must not acquire a guaranteed basin");
|
||||
var roof = LayeredHydrology.create(0, (x, y, z) ->
|
||||
Math.hypot(x, z) < 220 && (y == 240 || y >= 145 && y <= 200) ? 0.45F : -0.45F);
|
||||
require(roof.cells().isEmpty(), "Do not create underground lakes below a thin higher roof");
|
||||
var slab = LayeredHydrology.create(0, (x, y, z) ->
|
||||
Math.hypot(x, z) < 220 && y >= 199 && y <= 200 ? 0.45F : -0.45F);
|
||||
require(slab.cells().isEmpty(), "A thin floating slab cannot safely support water");
|
||||
var flat = LayeredHydrology.create(0, (x, y, z) ->
|
||||
Math.hypot(x, z) < 220 && y >= 145 && y <= 200 ? 0.45F : -0.45F);
|
||||
verify(flat, (x, y, z) -> Math.hypot(x, z) < 220 && y >= 145 && y <= 200 ? 0.45F : -0.45F);
|
||||
require(!flat.springs().isEmpty(), "A natural cliff over the void must exercise rock spring placement");
|
||||
long wet = flat.cells().stream().filter(LayeredHydrology.Cell::hasWater).count();
|
||||
require(flat.cells().size() > wet * 2, "Sediment sheets must extend beyond a narrow one-block ring");
|
||||
require(flat.cells().stream().anyMatch(cell -> cell.material() == LayeredHydrology.ShoreMaterial.STONE),
|
||||
"Some natural shores should be rocky");
|
||||
require(flat.cells().stream().anyMatch(cell -> cell.material() == LayeredHydrology.ShoreMaterial.GRASS),
|
||||
"Some natural shores should retain grassy terrain");
|
||||
require(flat.cells().stream().anyMatch(cell -> cell.sedimentDepth() == 5), "Some deposits should be five blocks thick");
|
||||
require(flat.features().stream().allMatch(feature -> feature.path().size() == 1),
|
||||
"Surface features should be basins rather than forced short channels");
|
||||
LayeredHydrology.Sampler longSlope = (x, y, z) -> {
|
||||
int top = 230 - Math.floorDiv(x + 192, 16);
|
||||
return Math.hypot(x, z) < 235 && y >= 80 && y <= top ? 0.45F : -0.45F;
|
||||
};
|
||||
var layered = LayeredHydrology.create(0, longSlope);
|
||||
verify(layered, longSlope);
|
||||
require(layered.springs().stream().anyMatch(spring -> spring.source().y()
|
||||
<= 230 - Math.floorDiv(spring.source().x() + 192, 16) - 24),
|
||||
"The layered terrain must exercise a spring well below the highest surface");
|
||||
System.out.println("LayeredHydrologySmoke: closed surface basins, layered rock springs and safe deposits passed.");
|
||||
}
|
||||
|
||||
private static void verify(LayeredHydrology.Plan plan, LayeredHydrology.Sampler terrain) {
|
||||
Map<String, LayeredHydrology.Cell> byChunks = new HashMap<>();
|
||||
// Reverse chunk traversal must reconstruct the same cells without generating neighbours.
|
||||
for (int cx = 18; cx >= -18; cx--) for (int cz = 18; cz >= -18; cz--) {
|
||||
var chunk = plan.cellsInChunk(cx, cz);
|
||||
LayeredHydrology.Cell previous = null;
|
||||
for (var cell : chunk) {
|
||||
require(cell.x() >> 4 == cx && cell.z() >> 4 == cz, "A chunk slice writes outside its chunk");
|
||||
require(previous == null || previous.x() < cell.x() || previous.x() == cell.x() && previous.z() < cell.z(),
|
||||
"Cell order must be stable for seeded shore vegetation");
|
||||
previous = cell;
|
||||
require(byChunks.put(cell.x() + "," + cell.z(), cell) == null, "A cell belongs to multiple chunk slices");
|
||||
}
|
||||
}
|
||||
require(byChunks.size() == plan.cells().size(), "Chunk slices omitted planned cells");
|
||||
for (var cell : plan.cells()) {
|
||||
require(cell.equals(byChunks.get(cell.x() + "," + cell.z())), "Chunk traversal changed the plan");
|
||||
for (int y = cell.bedY() - cell.sedimentDepth() - 1; y <= cell.bedY(); y++) {
|
||||
require(terrain.sample(cell.x(), y, cell.z()) > 0, "The entire sediment layer and two supporting blocks must already exist");
|
||||
}
|
||||
require(cell.sedimentDepth() >= 3 && cell.sedimentDepth() <= 5, "Sediment thickness must vary between 3 and 5");
|
||||
if (!cell.hasWater()) {
|
||||
require(cell.carveTop() - cell.bedY() >= 0 && cell.carveTop() - cell.bedY() <= 2,
|
||||
"Dry terraces must remove at most two blocks");
|
||||
continue;
|
||||
}
|
||||
require(cell.waterY() - cell.bedY() >= 1 && cell.waterY() - cell.bedY() <= LayeredHydrology.MAX_DEPTH, "Invalid shallow-water depth");
|
||||
require(cell.carveTop() - cell.bedY() <= LayeredHydrology.MAX_CARVE, "Excavation exceeds four blocks");
|
||||
for (int y = cell.carveTop() + 1; y < 384; y++) {
|
||||
require(terrain.sample(cell.x(), y, cell.z()) <= 0, "Water placed below a natural roof");
|
||||
}
|
||||
for (int[] d : CARDINALS) {
|
||||
int x = cell.x() + d[0], z = cell.z() + d[1];
|
||||
var neighbor = plan.cellAt(x, z);
|
||||
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) {
|
||||
boolean plannedWater = neighbor != null && neighbor.hasWater()
|
||||
&& y > neighbor.bedY() && y <= neighbor.waterY();
|
||||
boolean carvedAir = neighbor != null && y > neighbor.bedY() && y <= neighbor.carveTop()
|
||||
&& !plannedWater;
|
||||
require(plannedWater || !carvedAir && terrain.sample(x, y, z) > 0,
|
||||
"A water cell has an open horizontal outlet, including excavated banks: " + cell + " toward " + x + "," + y + "," + z + " neighbor=" + neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (var spring : plan.springs()) {
|
||||
var source = spring.source();
|
||||
var outlet = spring.outlet();
|
||||
require(terrain.sample(source.x(), source.y(), source.z()) > 0, "Spring must replace existing rock");
|
||||
require(terrain.sample(source.x(), source.y() + 1, source.z()) > 0, "Rock spring must have a natural roof");
|
||||
require(Math.abs(source.x() - outlet.x()) + Math.abs(source.z() - outlet.z()) == 1
|
||||
&& source.y() == outlet.y(), "Spring outlet must be adjacent to its source");
|
||||
for (int[] direction : CARDINALS) {
|
||||
int x = source.x() + direction[0], z = source.z() + direction[1];
|
||||
if (x == outlet.x() && z == outlet.z()) continue;
|
||||
require(terrain.sample(x, source.y(), z) > 0, "A spring niche must have three natural lateral walls");
|
||||
}
|
||||
for (int y = source.y() - 3; y < source.y(); y++) {
|
||||
require(terrain.sample(source.x(), y, source.z()) > 0, "Source needs an intact natural rock floor");
|
||||
}
|
||||
for (int y = source.y(); y >= source.y() - 8; y--) {
|
||||
require(terrain.sample(outlet.x(), y, outlet.z()) <= 0, "Waterfall needs an exposed natural drop");
|
||||
}
|
||||
for (var cell : plan.cells()) {
|
||||
int dy = Math.max(Math.max(cell.bedY() - cell.sedimentDepth() - source.y(), 0), source.y() - cell.carveTop());
|
||||
require(Math.hypot(Math.hypot(source.x() - cell.x(), source.z() - cell.z()), dy) >= 32,
|
||||
"Rock springs must stay away from modified terrain in three dimensions");
|
||||
}
|
||||
require(spring.flowBounds().minY() <= source.y() - 12, "Spring requires a real descending route");
|
||||
require(spring.flowPath().getFirst().equals(source), "Flow path starts at the single carved source");
|
||||
LayeredHydrology.Position previous = null;
|
||||
for (var position : spring.flowPath()) {
|
||||
require(plan.allowsSpringFlow(position.x(), position.y(), position.z()), "Path must lie inside the flow envelope");
|
||||
require(position.equals(source) || terrain.sample(position.x(), position.y(), position.z()) <= 0,
|
||||
"Waterfall path must use existing air; no cliff excavation or rock filling");
|
||||
if (previous != null) require(position.y() <= previous.y()
|
||||
&& Math.abs(position.x() - previous.x()) + Math.abs(position.y() - previous.y())
|
||||
+ Math.abs(position.z() - previous.z()) == 1, "Flow path must be contiguous and never ascend");
|
||||
previous = position;
|
||||
}
|
||||
require(plan.springsInChunk(source.x() >> 4, source.z() >> 4).contains(spring), "Spring belongs to its source chunk");
|
||||
require(!plan.allowsSpringFlow(10000, source.y(), 10000), "Bare-terrain prediction must exclude unrelated void water");
|
||||
}
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Determinism and containment checks independent of Minecraft, using controlled floating terrain. */
|
||||
public final class NaturalHydrologySmoke {
|
||||
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
|
||||
public static void main(String[] args) {
|
||||
NaturalHydrology.Sampler terrain = (x, y, z) -> {
|
||||
int top = 200 + (int) Math.round(2 * Math.sin(x / 24.0) + 2 * Math.cos(z / 31.0));
|
||||
return Math.hypot(x, z) < 220 && y <= top && y >= 145 ? 0.45F : -0.45F;
|
||||
};
|
||||
var first = NaturalHydrology.create(0, terrain);
|
||||
var replay = NaturalHydrology.create(0, terrain);
|
||||
var changed = NaturalHydrology.create(42, terrain);
|
||||
require(!first.features().isEmpty(), "The safe rolling surface should contain surface water");
|
||||
require(List.copyOf(first.cells()).equals(List.copyOf(replay.cells())) && first.features().equals(replay.features())
|
||||
&& first.springs().equals(replay.springs()),
|
||||
"The same seed and density must replay exactly");
|
||||
require(!List.copyOf(first.cells()).equals(List.copyOf(changed.cells())), "The world seed must influence water placement");
|
||||
verify(first, terrain);
|
||||
verify(changed, terrain);
|
||||
NaturalHydrology.Sampler pierced = (x, y, z) ->
|
||||
Math.floorMod(x, 37) < 2 && Math.floorMod(z, 41) < 2 ? -0.45F : terrain.sample(x, y, z);
|
||||
var aroundShafts = NaturalHydrology.create(0, pierced);
|
||||
require(!aroundShafts.features().isEmpty(), "Safe areas around natural shafts should remain usable");
|
||||
verify(aroundShafts, pierced);
|
||||
require(NaturalHydrology.create(0, (x, y, z) -> -1).cells().isEmpty(), "An empty world must not acquire a guaranteed basin");
|
||||
var roof = NaturalHydrology.create(0, (x, y, z) ->
|
||||
Math.hypot(x, z) < 220 && (y == 240 || y >= 145 && y <= 200) ? 0.45F : -0.45F);
|
||||
require(roof.cells().isEmpty(), "Do not create underground lakes below a thin higher roof");
|
||||
var slab = NaturalHydrology.create(0, (x, y, z) ->
|
||||
Math.hypot(x, z) < 220 && y >= 199 && y <= 200 ? 0.45F : -0.45F);
|
||||
require(slab.cells().isEmpty(), "A thin floating slab cannot safely support water");
|
||||
var flat = NaturalHydrology.create(0, (x, y, z) ->
|
||||
Math.hypot(x, z) < 220 && y >= 145 && y <= 200 ? 0.45F : -0.45F);
|
||||
verify(flat, (x, y, z) -> Math.hypot(x, z) < 220 && y >= 145 && y <= 200 ? 0.45F : -0.45F);
|
||||
require(!flat.springs().isEmpty(), "A natural cliff over the void must exercise rock spring placement");
|
||||
long wet = flat.cells().stream().filter(NaturalHydrology.Cell::hasWater).count();
|
||||
require(flat.cells().size() > wet * 2, "Sediment sheets must extend beyond a narrow one-block ring");
|
||||
require(flat.cells().stream().anyMatch(cell -> cell.material() == NaturalHydrology.ShoreMaterial.STONE),
|
||||
"Some natural shores should be rocky");
|
||||
require(flat.cells().stream().anyMatch(cell -> cell.material() == NaturalHydrology.ShoreMaterial.GRASS),
|
||||
"Some natural shores should retain grassy terrain");
|
||||
require(flat.cells().stream().anyMatch(cell -> cell.sedimentDepth() == 5), "Some deposits should be five blocks thick");
|
||||
require(flat.features().stream().anyMatch(feature -> feature.kind() == NaturalHydrology.Kind.STREAM),
|
||||
"The controlled safe plateau must exercise the stream planner");
|
||||
System.out.println("NaturalHydrologySmoke: shallow closed water, broad layered shores, deterministic rock springs and bounded flows passed.");
|
||||
}
|
||||
|
||||
private static void verify(NaturalHydrology.Plan plan, NaturalHydrology.Sampler terrain) {
|
||||
Map<String, NaturalHydrology.Cell> byChunks = new HashMap<>();
|
||||
// Reverse chunk traversal must reconstruct the same cells without generating neighbours.
|
||||
for (int cx = 18; cx >= -18; cx--) for (int cz = 18; cz >= -18; cz--) {
|
||||
var chunk = plan.cellsInChunk(cx, cz);
|
||||
NaturalHydrology.Cell previous = null;
|
||||
for (var cell : chunk) {
|
||||
require(cell.x() >> 4 == cx && cell.z() >> 4 == cz, "A chunk slice writes outside its chunk");
|
||||
require(previous == null || previous.x() < cell.x() || previous.x() == cell.x() && previous.z() < cell.z(),
|
||||
"Cell order must be stable for seeded shore vegetation");
|
||||
previous = cell;
|
||||
require(byChunks.put(cell.x() + "," + cell.z(), cell) == null, "A cell belongs to multiple chunk slices");
|
||||
}
|
||||
}
|
||||
require(byChunks.size() == plan.cells().size(), "Chunk slices omitted planned cells");
|
||||
for (var cell : plan.cells()) {
|
||||
require(cell.equals(byChunks.get(cell.x() + "," + cell.z())), "Chunk traversal changed the plan");
|
||||
for (int y = cell.bedY() - cell.sedimentDepth() - 1; y <= cell.bedY(); y++) {
|
||||
require(terrain.sample(cell.x(), y, cell.z()) > 0, "The entire sediment layer and two supporting blocks must already exist");
|
||||
}
|
||||
require(cell.sedimentDepth() >= 3 && cell.sedimentDepth() <= 5, "Sediment thickness must vary between 3 and 5");
|
||||
if (!cell.hasWater()) {
|
||||
require(cell.carveTop() - cell.bedY() >= 0 && cell.carveTop() - cell.bedY() <= 2,
|
||||
"Dry terraces must remove at most two blocks");
|
||||
continue;
|
||||
}
|
||||
require(cell.waterY() - cell.bedY() >= 1 && cell.waterY() - cell.bedY() <= NaturalHydrology.MAX_DEPTH, "Invalid shallow-water depth");
|
||||
require(cell.carveTop() - cell.bedY() <= NaturalHydrology.MAX_CARVE, "Excavation exceeds four blocks");
|
||||
for (int y = cell.carveTop() + 1; y < 384; y++) {
|
||||
require(terrain.sample(cell.x(), y, cell.z()) <= 0, "Water placed below a natural roof");
|
||||
}
|
||||
for (int[] d : CARDINALS) {
|
||||
int x = cell.x() + d[0], z = cell.z() + d[1];
|
||||
var neighbor = plan.cellAt(x, z);
|
||||
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) {
|
||||
boolean plannedWater = neighbor != null && neighbor.hasWater()
|
||||
&& y > neighbor.bedY() && y <= neighbor.waterY();
|
||||
boolean carvedAir = neighbor != null && y > neighbor.bedY() && y <= neighbor.carveTop()
|
||||
&& !plannedWater;
|
||||
require(plannedWater || !carvedAir && terrain.sample(x, y, z) > 0,
|
||||
"A water cell has an open horizontal outlet, including excavated banks");
|
||||
}
|
||||
}
|
||||
}
|
||||
for (var spring : plan.springs()) {
|
||||
var source = spring.source();
|
||||
var outlet = spring.outlet();
|
||||
require(terrain.sample(source.x(), source.y(), source.z()) > 0, "Spring must replace existing rock");
|
||||
require(terrain.sample(source.x(), source.y() + 1, source.z()) > 0, "Rock spring must have a natural roof");
|
||||
require(Math.abs(source.x() - outlet.x()) + Math.abs(source.z() - outlet.z()) == 1
|
||||
&& source.y() == outlet.y(), "Spring outlet must be adjacent to its source");
|
||||
for (int[] direction : CARDINALS) {
|
||||
int x = source.x() + direction[0], z = source.z() + direction[1];
|
||||
if (x == outlet.x() && z == outlet.z()) continue;
|
||||
require(terrain.sample(x, source.y(), z) > 0, "A spring niche must have three natural lateral walls");
|
||||
}
|
||||
for (int y = source.y() - 3; y < source.y(); y++) {
|
||||
require(terrain.sample(source.x(), y, source.z()) > 0, "Source needs an intact natural rock floor");
|
||||
}
|
||||
for (int y = source.y(); y >= source.y() - 8; y--) {
|
||||
require(terrain.sample(outlet.x(), y, outlet.z()) <= 0, "Waterfall needs an exposed natural drop");
|
||||
}
|
||||
for (var cell : plan.cells()) {
|
||||
require(Math.hypot(source.x() - cell.x(), source.z() - cell.z()) >= 32,
|
||||
"Rock springs must stay away from modified closed-water terrain");
|
||||
}
|
||||
require(spring.flowBounds().minY() <= source.y() - 12, "Spring requires a real descending route");
|
||||
require(spring.flowPath().getFirst().equals(source), "Flow path starts at the single carved source");
|
||||
NaturalHydrology.Position previous = null;
|
||||
for (var position : spring.flowPath()) {
|
||||
require(plan.allowsSpringFlow(position.x(), position.y(), position.z()), "Path must lie inside the flow envelope");
|
||||
require(position.equals(source) || terrain.sample(position.x(), position.y(), position.z()) <= 0,
|
||||
"Waterfall path must use existing air; no cliff excavation or rock filling");
|
||||
if (previous != null) require(position.y() <= previous.y()
|
||||
&& Math.abs(position.x() - previous.x()) + Math.abs(position.y() - previous.y())
|
||||
+ Math.abs(position.z() - previous.z()) == 1, "Flow path must be contiguous and never ascend");
|
||||
previous = position;
|
||||
}
|
||||
require(plan.springsInChunk(source.x() >> 4, source.z() >> 4).contains(spring), "Spring belongs to its source chunk");
|
||||
require(!plan.allowsSpringFlow(10000, source.y(), 10000), "Bare-terrain prediction must exclude unrelated void water");
|
||||
}
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/** Small lava supply: existing rock, dry access, natural containment, and no mandatory fallback. */
|
||||
public final class StarterLavaDepositSmoke {
|
||||
private static final int[][] DIRECTIONS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
|
||||
public static void main(String[] args) {
|
||||
var noWater = NaturalHydrology.create(0, (x, y, z) -> -1);
|
||||
NaturalHydrology.Sampler cliff = (x, y, z) -> Math.hypot(x, z) < 220
|
||||
&& y >= 100 && y <= (x <= 0 ? 240 : 200) ? 1 : -1;
|
||||
var first = StarterLavaDeposit.create(0, cliff, noWater);
|
||||
var replay = StarterLavaDeposit.create(0, cliff, noWater);
|
||||
var otherSeed = StarterLavaDeposit.create(42, cliff, noWater);
|
||||
require(first.lavaCells().size() == 18, "A thick natural cliff should provide one small deposit");
|
||||
require(first.cells().equals(replay.cells()) && first.supports().equals(replay.supports())
|
||||
&& first.access().equals(replay.access()), "The same seed must replay the entire plan");
|
||||
require(!first.cells().equals(otherSeed.cells()), "The seed must influence which suitable ledge is chosen");
|
||||
verify(first, cliff, noWater);
|
||||
verify(otherSeed, cliff, noWater);
|
||||
try {
|
||||
first.cells().clear();
|
||||
throw new AssertionError("Plan cells must be immutable");
|
||||
} catch (UnsupportedOperationException expected) {
|
||||
// Passed.
|
||||
}
|
||||
require(StarterLavaDeposit.create(0, (x, y, z) -> -1, noWater).cells().isEmpty(), "No lava in the void");
|
||||
require(StarterLavaDeposit.create(0, (x, y, z) -> 1, noWater).cells().isEmpty(), "No sealed underground pocket");
|
||||
require(StarterLavaDeposit.create(0, (x, y, z) -> y < 199 ? -1 : cliff.sample(x, y, z), noWater)
|
||||
.cells().isEmpty(), "Thin natural floors must not receive a manufactured foundation");
|
||||
require(StarterLavaDeposit.create(0, (x, y, z) -> y == 198 && Math.floorMod(x, 2) == 0
|
||||
? -1 : cliff.sample(x, y, z), noWater).cells().isEmpty(), "Perforated floors must reject the deposit");
|
||||
NaturalHydrology.Sampler thinRoof = (x, y, z) -> Math.hypot(x, z) < 220
|
||||
&& y >= 100 && y <= (x <= 0 ? 207 : 200) ? 1 : -1;
|
||||
require(StarterLavaDeposit.create(0, thinRoof, noWater).cells().isEmpty(), "No exposed or thinly covered lava");
|
||||
// Exercise real water exclusions as well; no lava is an acceptable result on a crowded cliff.
|
||||
var water = NaturalHydrology.create(0, cliff);
|
||||
verify(StarterLavaDeposit.create(0, cliff, water), cliff, water);
|
||||
System.out.println("StarterLavaDepositSmoke: deterministic 18-source pocket, natural floor/walls/roof, walkable access, water separation and no fallback passed.");
|
||||
}
|
||||
|
||||
private static void verify(StarterLavaDeposit.Plan plan, NaturalHydrology.Sampler terrain, NaturalHydrology.Plan water) {
|
||||
Map<StarterLavaDeposit.Position, StarterLavaDeposit.Cell> changes = new HashMap<>();
|
||||
Set<StarterLavaDeposit.Position> supports = new HashSet<>();
|
||||
for (int cx = 12; cx >= -12; cx--) for (int cz = 12; cz >= -12; cz--) {
|
||||
for (var cell : plan.cellsInChunk(cx, cz)) {
|
||||
require(cell.x() >> 4 == cx && cell.z() >> 4 == cz, "A modification crossed its owning chunk");
|
||||
var pos = new StarterLavaDeposit.Position(cell.x(), cell.y(), cell.z());
|
||||
require(changes.put(pos, cell) == null, "Repeated modification in chunk slices");
|
||||
}
|
||||
for (var support : plan.supportsInChunk(cx, cz)) {
|
||||
require(support.x() >> 4 == cx && support.z() >> 4 == cz, "A support crossed its owning chunk");
|
||||
require(supports.add(support), "Repeated support in chunk slices");
|
||||
}
|
||||
}
|
||||
require(changes.size() == plan.cells().size() && supports.size() == plan.supports().size(), "Chunk order lost data");
|
||||
for (var support : supports) {
|
||||
require(!changes.containsKey(support), "A natural support is also being excavated");
|
||||
require(terrain.sample(support.x(), support.y(), support.z()) > 0, "Manufactured rock support");
|
||||
}
|
||||
require(plan.lavaCells().size() == 0 || plan.lavaCells().size() == StarterLavaDeposit.LAVA_VOLUME,
|
||||
"The planner must place at most one 18-source pocket");
|
||||
for (var cell : plan.cells()) {
|
||||
require(cell.y() >= 0 && cell.y() < 384 && Math.hypot(cell.x(), cell.z()) < StarterLavaDeposit.RADIUS,
|
||||
"Lava excavation exceeded island bounds");
|
||||
require(terrain.sample(cell.x(), cell.y(), cell.z()) > 0, "A modification replaced natural air");
|
||||
for (var shore : water.cells()) require(Math.hypot(cell.x() - shore.x(), cell.z() - shore.z()) >= 32,
|
||||
"Lava or its access intersects the water exclusion distance");
|
||||
for (var spring : water.springs()) {
|
||||
var b = spring.flowBounds();
|
||||
double dx = Math.max(Math.max(b.minX() - cell.x(), 0), cell.x() - b.maxX());
|
||||
double dz = Math.max(Math.max(b.minZ() - cell.z(), 0), cell.z() - b.maxZ());
|
||||
require(Math.hypot(dx, dz) >= 32, "A waterfall permission volume can reach the deposit");
|
||||
}
|
||||
if (!cell.lava()) continue;
|
||||
var below = new StarterLavaDeposit.Position(cell.x(), cell.y() - 1, cell.z());
|
||||
require(retainedSolidOrLava(below, changes, terrain), "Lava has an open floor");
|
||||
for (int[] direction : DIRECTIONS) require(retainedSolidOrLava(new StarterLavaDeposit.Position(
|
||||
cell.x() + direction[0], cell.y(), cell.z() + direction[1]), changes, terrain), "Lava has a side outlet");
|
||||
}
|
||||
if (plan.lavaCells().isEmpty()) {
|
||||
require(plan.access().isEmpty() && plan.cells().isEmpty() && plan.supports().isEmpty(), "An empty result left partial excavation");
|
||||
return;
|
||||
}
|
||||
var entry = plan.access().orElseThrow();
|
||||
for (int y = entry.y(); y < 384; y++) require(terrain.sample(entry.x(), y, entry.z()) <= 0,
|
||||
"Access does not start from naturally open air");
|
||||
// A player can walk from the outdoor ledge to the dry lip next to the lava, without digging
|
||||
// or crossing a fluid. Merely opening an isolated cavity would not satisfy this check.
|
||||
Set<StarterLavaDeposit.Position> seen = new HashSet<>();
|
||||
ArrayDeque<StarterLavaDeposit.Position> queue = new ArrayDeque<>();
|
||||
seen.add(entry); queue.add(entry);
|
||||
boolean reached = false;
|
||||
while (!queue.isEmpty() && seen.size() <= 1600) {
|
||||
var at = queue.removeFirst();
|
||||
for (int[] direction : DIRECTIONS) {
|
||||
var floor = new StarterLavaDeposit.Position(at.x() + direction[0], at.y() - 1, at.z() + direction[1]);
|
||||
var fluid = changes.get(floor);
|
||||
reached |= fluid != null && fluid.lava();
|
||||
var next = new StarterLavaDeposit.Position(at.x() + direction[0], at.y(), at.z() + direction[1]);
|
||||
if (Math.abs(next.x() - entry.x()) > 18 || Math.abs(next.z() - entry.z()) > 18 || seen.contains(next)) continue;
|
||||
if (!air(next, changes, terrain) || !air(new StarterLavaDeposit.Position(next.x(), next.y() + 1, next.z()), changes, terrain)) continue;
|
||||
if (changes.containsKey(floor) || terrain.sample(floor.x(), floor.y(), floor.z()) <= 0) continue;
|
||||
seen.add(next); queue.addLast(next);
|
||||
}
|
||||
}
|
||||
require(reached, "The pocket has no dry, two-block-high access from the natural ledge");
|
||||
}
|
||||
|
||||
private static boolean air(StarterLavaDeposit.Position at, Map<StarterLavaDeposit.Position, StarterLavaDeposit.Cell> changes,
|
||||
NaturalHydrology.Sampler terrain) {
|
||||
var change = changes.get(at);
|
||||
return change == null ? terrain.sample(at.x(), at.y(), at.z()) <= 0 : !change.lava();
|
||||
}
|
||||
|
||||
private static boolean retainedSolidOrLava(StarterLavaDeposit.Position at,
|
||||
Map<StarterLavaDeposit.Position, StarterLavaDeposit.Cell> changes, NaturalHydrology.Sampler terrain) {
|
||||
var change = changes.get(at);
|
||||
return change == null ? terrain.sample(at.x(), at.y(), at.z()) > 0 : change.lava();
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Determinism and containment checks independent of Minecraft, using controlled floating terrain. */
|
||||
public final class SurfaceHydrologySmoke {
|
||||
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
|
||||
public static void main(String[] args) {
|
||||
SurfaceHydrology.Sampler terrain = (x, y, z) -> {
|
||||
int top = 200 + (int) Math.round(2 * Math.sin(x / 24.0) + 2 * Math.cos(z / 31.0));
|
||||
return Math.hypot(x, z) < 220 && y <= top && y >= 145 ? 0.45F : -0.45F;
|
||||
};
|
||||
var first = SurfaceHydrology.create(0, terrain);
|
||||
var replay = SurfaceHydrology.create(0, terrain);
|
||||
var changed = SurfaceHydrology.create(42, terrain);
|
||||
require(!first.features().isEmpty(), "The safe rolling surface should contain surface water");
|
||||
require(List.copyOf(first.cells()).equals(List.copyOf(replay.cells())) && first.features().equals(replay.features()),
|
||||
"The same seed and density must replay exactly");
|
||||
require(!List.copyOf(first.cells()).equals(List.copyOf(changed.cells())), "The world seed must influence water placement");
|
||||
verify(first, terrain);
|
||||
verify(changed, terrain);
|
||||
SurfaceHydrology.Sampler pierced = (x, y, z) ->
|
||||
Math.floorMod(x, 37) < 2 && Math.floorMod(z, 41) < 2 ? -0.45F : terrain.sample(x, y, z);
|
||||
var aroundShafts = SurfaceHydrology.create(0, pierced);
|
||||
require(!aroundShafts.features().isEmpty(), "Safe areas around natural shafts should remain usable");
|
||||
verify(aroundShafts, pierced);
|
||||
require(SurfaceHydrology.create(0, (x, y, z) -> -1).cells().isEmpty(), "An empty world must not acquire a guaranteed basin");
|
||||
var roof = SurfaceHydrology.create(0, (x, y, z) ->
|
||||
Math.hypot(x, z) < 220 && (y == 240 || y >= 145 && y <= 200) ? 0.45F : -0.45F);
|
||||
require(roof.cells().isEmpty(), "Do not create underground lakes below a thin higher roof");
|
||||
var slab = SurfaceHydrology.create(0, (x, y, z) ->
|
||||
Math.hypot(x, z) < 220 && y >= 199 && y <= 200 ? 0.45F : -0.45F);
|
||||
require(slab.cells().isEmpty(), "A thin floating slab cannot safely support water");
|
||||
var flat = SurfaceHydrology.create(0, (x, y, z) ->
|
||||
Math.hypot(x, z) < 220 && y >= 145 && y <= 200 ? 0.45F : -0.45F);
|
||||
require(flat.features().stream().anyMatch(feature -> feature.kind() == SurfaceHydrology.Kind.STREAM),
|
||||
"The controlled safe plateau must exercise the stream planner");
|
||||
System.out.println("SurfaceHydrologySmoke: deterministic lakes/stream, sealed floors/walls, chunk order, no roof/void/thin-slab water passed.");
|
||||
}
|
||||
|
||||
private static void verify(SurfaceHydrology.Plan plan, SurfaceHydrology.Sampler terrain) {
|
||||
Map<String, SurfaceHydrology.Cell> byChunks = new HashMap<>();
|
||||
// Reverse chunk traversal must reconstruct the same cells without generating neighbours.
|
||||
for (int cx = 12; cx >= -12; cx--) for (int cz = 12; cz >= -12; cz--) {
|
||||
var chunk = plan.cellsInChunk(cx, cz);
|
||||
SurfaceHydrology.Cell previous = null;
|
||||
for (var cell : chunk) {
|
||||
require(cell.x() >> 4 == cx && cell.z() >> 4 == cz, "A chunk slice writes outside its chunk");
|
||||
require(previous == null || previous.x() < cell.x() || previous.x() == cell.x() && previous.z() < cell.z(),
|
||||
"Cell order must be stable for seeded shore vegetation");
|
||||
previous = cell;
|
||||
require(byChunks.put(cell.x() + "," + cell.z(), cell) == null, "A cell belongs to multiple chunk slices");
|
||||
}
|
||||
}
|
||||
require(byChunks.size() == plan.cells().size(), "Chunk slices omitted planned cells");
|
||||
for (var cell : plan.cells()) {
|
||||
require(cell.equals(byChunks.get(cell.x() + "," + cell.z())), "Chunk traversal changed the plan");
|
||||
for (int y = cell.bedY() - 2; y <= cell.bedY(); y++) {
|
||||
require(terrain.sample(cell.x(), y, cell.z()) > 0, "Bed material requires three existing solid blocks");
|
||||
}
|
||||
if (!cell.hasWater()) continue;
|
||||
require(cell.waterY() - cell.bedY() >= 1 && cell.waterY() - cell.bedY() <= 4, "Invalid shallow-water depth");
|
||||
require(cell.carveTop() - cell.bedY() <= SurfaceHydrology.MAX_CARVE, "Excavation exceeds six blocks");
|
||||
for (int y = cell.carveTop() + 1; y < 384; y++) {
|
||||
require(terrain.sample(cell.x(), y, cell.z()) <= 0, "Water placed below a natural roof");
|
||||
}
|
||||
for (int[] d : CARDINALS) {
|
||||
int x = cell.x() + d[0], z = cell.z() + d[1];
|
||||
var neighbor = plan.cellAt(x, z);
|
||||
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) {
|
||||
boolean plannedWater = neighbor != null && neighbor.hasWater()
|
||||
&& y > neighbor.bedY() && y <= neighbor.waterY();
|
||||
boolean carvedAir = neighbor != null && y > neighbor.bedY() && y <= neighbor.carveTop()
|
||||
&& !plannedWater;
|
||||
require(plannedWater || !carvedAir && terrain.sample(x, y, z) > 0,
|
||||
"A water cell has an open horizontal outlet, including excavated banks");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -51,7 +51,7 @@ Pour produire un fichier importable par un lanceur compatible Modrinth :
|
||||
|
||||
```sh
|
||||
cd build/packwiz # depuis la racine du dépôt
|
||||
packwiz modrinth export --output ../Sanctuary-0.1.0-alpha.3.mrpack
|
||||
packwiz modrinth export --output ../Sanctuary-0.1.0-alpha.6.mrpack
|
||||
```
|
||||
|
||||
L'export contient le JAR Sanctuary local et la référence vérifiée de Fabric API.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "0.1.0-alpha.3"
|
||||
version = "0.1.0-alpha.6"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
Reference in New Issue
Block a user