Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f816ffc640 | ||
|
|
f30a350240 | ||
|
|
a26b72fabd | ||
|
|
ef5d1eaad9 |
@@ -1,5 +1,53 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.0-alpha.11 — 2026-09-09
|
||||
|
||||
- Deux choix à la création : **Petit** (512 blocs de diamètre nominal)
|
||||
et **Grand** (environ 724 blocs), Grand par défaut. Leur aire nominale varie
|
||||
d’un facteur deux, sur une hauteur de 384 blocs. Les repères internes 5/10
|
||||
ne désignent pas un nombre garanti de joueurs.
|
||||
- Nouveau réglage `sanctuary:population_10` pour Grand. Les anciens formats
|
||||
20/100 disparaissent des choix publics, mais restent chargeables avec leurs
|
||||
identifiants et leur relief enregistrés. Aucune île existante n’est réduite.
|
||||
- Cache des coins d’interpolation conservant l’arithmétique exacte du moteur,
|
||||
borné à 192 Kio par thread. Les plans régionaux déjà calculés restent lisibles
|
||||
pendant le calcul d’une nouvelle région ; le sampler est compilé une seule
|
||||
fois par plan.
|
||||
- À géométrie 20 identique, création et 289 chunks : 407,43 → 62,54 s
|
||||
(−84,65 %). Exploration de 80 chunks : 103,14 → 14,05 s. Le nouveau Grand
|
||||
atteint 62,67 s sur le protocole de départ. Une seule mesure par version,
|
||||
graine 0, M1/8 Gio : ce sont des temps serveur jusqu’à FULL, pas des FPS
|
||||
ni une garantie de délai en jeu.
|
||||
|
||||
Validation : `check build assemblePack`, douze tests moteur pour Petit et
|
||||
Grand, 1 800 ticks de fluides, persistance des quatre références et
|
||||
3 183 804 comparaisons bit à bit de densités réelles, plus 122 528 synthétiques.
|
||||
Les sources historiques de forme et les anciens réglages sont conservés.
|
||||
Voir [Validation](docs/testing.md) et [Distribution](docs/packwiz.md).
|
||||
Le canal stable est publié et la même instance Prism est synchronisée deux fois,
|
||||
avec les 364 fichiers personnels et réglages suivis conservés.
|
||||
|
||||
## 0.1.0-alpha.10 — 2026-09-09
|
||||
|
||||
- Trois tailles de départ : **Sanctuary (20 joueurs)** par défaut, plus les
|
||||
variantes 5 et 100 joueurs. Diamètres nominaux de 512, 1 024 et environ
|
||||
2 290 blocs, hauteur de 384 blocs ; aucune limite de connexions modifiée.
|
||||
- Nouvelles générations `sanctuary:population_5`, `population_20` et
|
||||
`population_100`. Forêts, cavités, décorations et plans d’eau/lave couvrent
|
||||
la nouvelle emprise par régions locales ; une structure hydraulique qui
|
||||
dépasse son domaine est écartée en entier.
|
||||
- Capacité persistante sur disque, ancien réglage alpha.9 conservé ;
|
||||
212 anciens fichiers de production inchangés et huit fichiers de liaison
|
||||
ou d’interface adaptés. Aucune ancienne île agrandie ou régénérée.
|
||||
|
||||
Validation : `check build assemblePack` réussi, puis onze tests moteur
|
||||
réussis pour chacun des profils 5/20/100 sur la graine `0`, avec 1 800 ticks
|
||||
réels. Le MRpack et le ZIP d’amorçage sont vérifiés localement. Les mesures
|
||||
sont limitées aux chunks inspectés ; elles ne constituent ni un inventaire
|
||||
global ni une simulation de cent joueurs. Le premier plan local reste coûteux
|
||||
et la génération en masse reste à optimiser. Publication et synchronisations
|
||||
du canal sont consignées séparément dans [Distribution](docs/packwiz.md).
|
||||
|
||||
## 0.1.0-alpha.9 — 2026-09-09
|
||||
|
||||
- Cavités luxuriantes éclairées par leur végétation, secteurs à spéléothèmes
|
||||
|
||||
@@ -51,7 +51,7 @@ décrits dans la vision ne sont pas encore implémentés.
|
||||
profonde, puis plus haut si nécessaire. Une à deux sources profondes
|
||||
supplémentaires peuvent former des coulées, vérifiées par les ticks du moteur.
|
||||
|
||||
L'île a une emprise nominale de 512 blocs de diamètre, avec une bordure sculptée
|
||||
Jusqu’à l’alpha.9, 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 océans, les ruines et
|
||||
@@ -86,6 +86,40 @@ anciennes sauvegardes alpha.5. Aucun lac, groupe de terrasses, geyser ou stock
|
||||
de ressources n’est garanti sur chaque graine. Voir le ticket local
|
||||
[WG-10](docs/backlog.md).
|
||||
|
||||
## Alpha.10, historique : îles pour 5, 20 ou 100 joueurs
|
||||
|
||||
L’alpha.10 proposait trois tailles, avec **20 joueurs par défaut**.
|
||||
Leur aire nominale est proportionnelle à la capacité choisie, sur la base
|
||||
de l’île de 512 blocs pour cinq joueurs. La hauteur reste de 384 blocs.
|
||||
|
||||
| Choix à la création | Identifiant du preset | Capacité | Diamètre nominal |
|
||||
| --- | --- | --- | --- |
|
||||
| Sanctuary 5 joueurs | `sanctuary:sanctuary_5` | 5 | 512 blocs |
|
||||
| Sanctuary, choix par défaut | `sanctuary:sanctuary` | 20 | 1 024 blocs |
|
||||
| Sanctuary 100 joueurs | `sanctuary:sanctuary_100` | 100 | Environ 2 290 blocs |
|
||||
|
||||
Ces capacités dimensionnent l’île ; elles ne fixent pas la limite de
|
||||
connexions du serveur. Le contour réel dépend du relief et de la graine,
|
||||
et le stock de ressources reste celui de la génération. La taille se choisit
|
||||
pour un **nouveau monde** : aucune ancienne île n’est agrandie et aucune
|
||||
sauvegarde n’est régénérée lors de la mise à jour.
|
||||
Les paramètres sauvegardés sont distincts : `sanctuary:population_5`,
|
||||
`sanctuary:population_20` et `sanctuary:population_100`.
|
||||
|
||||
**Les trois profils passent les onze tests moteur sur la graine `0`, avec
|
||||
1 800 ticks réels de fluides.** Le choix de capacité persiste après écriture
|
||||
et relecture sur disque. La vérification conserve 212 anciens fichiers de
|
||||
production strictement identiques ; huit fichiers d’intégration ou d’interface
|
||||
accueillent les nouveaux choix. La première planification locale reste coûteuse,
|
||||
environ 83–88 secondes pour les premiers essais 5/20 sur cette configuration.
|
||||
La génération en masse reste à optimiser. Ces essais ne simulent pas
|
||||
100 connexions et ne donnent aucune garantie de performances multijoueurs.
|
||||
Les diamètres nominaux ne permettent pas d’extrapoler les stocks ou l’aire
|
||||
réellement habitable. La version est publiée et la même instance Prism a été
|
||||
synchronisée deux fois, avec 363 fichiers personnels et réglages suivis conservés.
|
||||
Voir [WG-11](docs/backlog.md), [Validation](docs/testing.md) et
|
||||
[Distribution](docs/packwiz.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
|
||||
@@ -94,6 +128,37 @@ en jeu reste à construire. Voir [Expansion et ressources](docs/expansion.md).
|
||||
Les détails du terrain et de ses limites sont dans [Génération](docs/worldgen.md),
|
||||
et la provenance dans [Audit 26.2](docs/migration-26.2.md).
|
||||
|
||||
## Alpha.11 : Petit, Grand et génération plus rapide
|
||||
|
||||
Les anciens formats sont trop grands et éloignent les constructions. Le
|
||||
nouveau choix de départ se limite à **Petit** et **Grand**, avec **Grand par
|
||||
défaut**. Petit garde un diamètre nominal de 512 blocs ; Grand mesure environ
|
||||
724 blocs, soit deux fois l’aire nominale de Petit. La hauteur reste de
|
||||
384 blocs. Les repères internes 5 et 10 servent au calcul de surface : ils
|
||||
ne sont ni les noms des choix ni une promesse de capacité multijoueur.
|
||||
|
||||
Grand utilise le nouveau réglage `sanctuary:population_10`. Les anciennes
|
||||
parties `population_5`, `population_20` et `population_100` gardent leurs
|
||||
paramètres ; les formats 20/100 sont retirés des choix publics de création,
|
||||
sans réduire les mondes déjà enregistrés. Aucun monde personnel n’est ouvert
|
||||
pour les essais.
|
||||
|
||||
Le ticket [WG-12](docs/backlog.md) réduit les calculs répétés de terrain et
|
||||
permet de lire un plan régional déjà prêt pendant le calcul du suivant.
|
||||
À géométrie 20 identique, la création puis 289 chunks autour du spawn passent
|
||||
de 407,43 à 62,54 secondes (−84,65 %) ; 80 chunks d’exploration passent de
|
||||
103,14 à 14,05 secondes. Le nouveau Grand atteint 62,67 secondes sur le même
|
||||
protocole de départ. Ces essais serveur sur une graine et une machine ne
|
||||
mesurent pas le rendu du client ni une charge multijoueur.
|
||||
|
||||
`check build assemblePack` passe, ainsi que douze tests moteur pour chacun
|
||||
des deux choix, avec 1 800 ticks de fluides. Plus de trois millions de
|
||||
comparaisons de densité confirment l’équivalence de calcul. Les mesures et
|
||||
limites sont dans [Validation](docs/testing.md) ; publication et synchronisation
|
||||
de la même instance sont consignées dans [Distribution](docs/packwiz.md).
|
||||
L’alpha.11 est publiée ; la même instance Prism a été synchronisée deux fois,
|
||||
avec les 364 fichiers personnels et réglages suivis conservés.
|
||||
|
||||
## Versions et prérequis
|
||||
|
||||
Au 8 septembre 2026, la cible disponible est **26.3-pre-2**. Le dépôt ne prétend
|
||||
@@ -108,7 +173,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.9 |
|
||||
| Sanctuary / pack | 0.1.0-alpha.11 |
|
||||
|
||||
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
|
||||
@@ -135,7 +200,7 @@ décrits dans [Validation](docs/testing.md).
|
||||
|
||||
Résultats :
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.9.jar` : mod à installer avec
|
||||
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.10.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).
|
||||
|
||||
+109
@@ -14,6 +14,10 @@ faire avant de clôturer tous leurs critères. WG-04 à WG-07 restent ouverts,
|
||||
avec les incréments de l’île initiale décrits ci-dessous. WG-08 est validé ;
|
||||
WG-09 est livré en alpha.8, avec mesures moteur et mise à jour Prism vérifiées.
|
||||
WG-10 est livré en alpha.9, avec mesures moteur et synchronisation Prism vérifiées.
|
||||
WG-11 est livré en alpha.10 : tailles 5/20/100 validées sur la graine 0,
|
||||
release publiée et même instance Prism synchronisée.
|
||||
WG-12 est livré en alpha.11 : Petit/Grand, génération accélérée et même instance
|
||||
Prism synchronisée deux fois ; voir [Distribution](packwiz.md).
|
||||
|
||||
Résultat visé : un client et un serveur Fabric compatibles peuvent ouvrir un monde Sanctuary, plusieurs joueurs y arrivent sur une même île sûre et les chunks extérieurs restent vides. La génération est reproductible et ses limites sont documentées.
|
||||
|
||||
@@ -310,6 +314,111 @@ réussissent, avec 260 fichiers personnels et réglages conservés. Ce ticket
|
||||
local n’est pas une issue distante publiée ; l’essai visuel et l’équilibrage
|
||||
restent ouverts.
|
||||
|
||||
### WG-11 — Tailles d’île selon la capacité — alpha.10 livrée
|
||||
|
||||
**Branche :** `codex/player-capacity-presets`.
|
||||
|
||||
**But :** choisir une île initiale adaptée à un groupe de 5, 20 ou 100 joueurs,
|
||||
avec 20 joueurs par défaut, tout en conservant l’ambiance et les anciennes parties.
|
||||
|
||||
**Critères d’acceptation :**
|
||||
|
||||
- La création d’un nouveau monde propose trois choix identifiables en FR/EN :
|
||||
Sanctuary par défaut pour 20 joueurs et des variantes pour 5 et 100 joueurs.
|
||||
Les presets `sanctuary:sanctuary_5`, `sanctuary:sanctuary` et
|
||||
`sanctuary:sanctuary_100` sélectionnent respectivement les paramètres
|
||||
`sanctuary:population_5`, `sanctuary:population_20` et
|
||||
`sanctuary:population_100`, qui doivent persister dans le monde sauvegardé.
|
||||
- L’aire nominale suit le rapport capacité/5. Les diamètres de référence sont
|
||||
512, 1 024 et environ 2 290 blocs ; la hauteur reste de 384 blocs. Le contour
|
||||
sculpté dépend de la graine et n’est pas remplacé par un cercle ou un cylindre.
|
||||
- Le choix dimensionne la génération, sans modifier `max-players`, imposer
|
||||
un quota de ressources ni redimensionner l’île lorsque des joueurs arrivent.
|
||||
- Les tailles conservent un extérieur vide, un spawn naturel, les forêts et
|
||||
l’écologie des cavités. Les eaux et les décors doivent être contrôlés dans
|
||||
les vrais chunks de la taille testée, pas seulement extrapolés depuis l’alpha.9.
|
||||
- Les réglages et identifiants des générations précédentes restent séparés.
|
||||
Une mise à jour du pack n’agrandit pas un ancien monde, ne convertit pas son
|
||||
générateur et ne régénère aucun chunk.
|
||||
- La validation progresse de 5 à 20 puis 100 joueurs dans des mondes de
|
||||
développement neufs. Chaque résultat précise capacité, graine, emprise
|
||||
réellement inspectée et durée observée. Un petit échantillon ne valide pas
|
||||
l’intégralité d’une grande île.
|
||||
- Les tests de génération ne sont pas présentés comme des connexions de
|
||||
joueurs simulées ni comme une promesse de performances multijoueurs.
|
||||
- Après validation : `check build assemblePack`, artefacts immuables vérifiés,
|
||||
deux synchronisations isolées puis deux dans la même instance Prism,
|
||||
avec sauvegardes et réglages conservés.
|
||||
|
||||
**État au 9 septembre 2026 :** implémentation et validation moteur terminées
|
||||
sur la graine `0` pour les profils 5, 20 et 100. Les onze tests requis passent
|
||||
dans chaque monde, avec les fluides pendant 1 800 ticks et la persistance réelle
|
||||
des réglages sur disque. Le build et les artefacts sont vérifiés ; la release
|
||||
et le canal sont publiés. Deux synchronisations isolées puis deux dans la même
|
||||
instance Prism réussissent, avec 363 fichiers personnels et réglages conservés.
|
||||
Les références figurent dans [Distribution](packwiz.md).
|
||||
Les anciens mondes restent inchangés. Les observations ne couvrent pas toute
|
||||
l’île de 100 joueurs, et le coût initial d’un plan régional reste notable.
|
||||
Ce ticket local n’est pas une issue distante publiée.
|
||||
|
||||
### WG-12 — Petit, Grand et génération plus rapide — alpha.11 livrée
|
||||
|
||||
**Branche :** `codex/worldgen-performance`.
|
||||
|
||||
**Problème :** les grands formats éloignent trop les constructions. Les essais
|
||||
signalent aussi plusieurs minutes de chargement puis des chunks difficiles à
|
||||
utiliser. Le ticket rapproche les deux choix de départ et optimise les calculs,
|
||||
avec des preuves distinctes pour la taille et la vitesse.
|
||||
|
||||
**Critères d’acceptation :**
|
||||
|
||||
- Proposer seulement **Petit** et **Grand** dans la création, avec **Grand par
|
||||
défaut**, et libellés FR/EN. Petit garde le diamètre nominal de 512 blocs ;
|
||||
Grand vise environ 724 blocs, soit une aire nominale deux fois supérieure.
|
||||
La hauteur reste de 384 blocs. Les nombres 5/10 ne servent que de repères
|
||||
internes de surface, sans capacité de joueurs garantie.
|
||||
- Associer Petit à `sanctuary:sanctuary_5` / `sanctuary:population_5` et Grand
|
||||
à `sanctuary:sanctuary` / nouveau réglage `sanctuary:population_10`.
|
||||
Garder les anciens réglages `population_20` et `population_100` chargeables,
|
||||
tout en cachant leurs formats dans les choix publics de création.
|
||||
- Ne pas modifier le relief d’une sauvegarde existante, son identifiant ou
|
||||
son format. Les changements du défaut de création ne convertissent pas
|
||||
un monde. Aucune sauvegarde personnelle n’est ouverte pendant les essais.
|
||||
- Pour l’optimisation, **conserver les résultats à paramètres identiques** :
|
||||
densités, plans et blocs utiles, ordre déterministe et protections de l’eau,
|
||||
des terrasses et de la lave. Le nouveau relief Grand a une clé distincte ;
|
||||
la réduction de surface ne compte pas comme gain de calcul à géométrie égale.
|
||||
- Mesurer séparément démarrage initial, planification et génération d’un lot
|
||||
explicite de chunks. Fixer machine, Java, mémoire, graine, paramètres,
|
||||
coordonnées et état des caches pour comparer alpha.10 et alpha.11.
|
||||
- Viser **120 secondes au plus de démarrage initial** pour les choix publics
|
||||
et **au moins 30 % de gain** sur les phases comparées à géométrie identique.
|
||||
Un temps de planificateur seul ne prouve pas l’entrée dans un monde jouable.
|
||||
Préciser les bornes de chronométrage et ne pas promettre ces seuils pour
|
||||
tout matériel ou pour une charge de cent joueurs.
|
||||
- Valider Petit et Grand, plus un témoin de compatibilité et de performance
|
||||
sur l’ancien profil 20. Les anciennes mesures 100 restent historiques ;
|
||||
ce format n’est plus un choix public ni le centre du plan de validation.
|
||||
Examiner le premier accès, le cache réutilisé et les chunks d’exploration.
|
||||
- Conserver les contrôles moteur de fluides et de réentrée sans marqueur,
|
||||
puis exécuter `check build assemblePack` sur les sources finales. Garder
|
||||
visibles les limites et toute cible de performance non atteinte.
|
||||
- Après validation seulement : artefacts immuables, canal packwiz stable,
|
||||
deux synchronisations isolées puis deux dans la même instance Prism. Les
|
||||
mondes, réglages et mods personnels, même désactivés, doivent être préservés.
|
||||
|
||||
**État :** livré. Artefacts immuables publiés, canal stable avancé, deux
|
||||
synchronisations isolées et deux dans la même instance Prism, avec 364 fichiers
|
||||
personnels et réglages suivis conservés. Tailles et code validés. `check build assemblePack` passe, ainsi
|
||||
que douze tests moteur sur Petit et Grand (graine 0, 1 800 ticks), les contrôles
|
||||
de persistance et l’équivalence bit à bit. Le benchmark ancien20 comparable
|
||||
baisse de 84,65 % au départ et de 86,38 % en exploration. Grand atteint
|
||||
62,67 secondes pour création + 289 chunks FULL. Cette mesure serveur ne
|
||||
valide pas le rendu ni un temps de chargement universel. Voir
|
||||
[Validation](testing.md), [résultats](benchmarks/alpha11-generation.json) et
|
||||
[Distribution](packwiz.md) pour le reçu de publication et de synchronisation.
|
||||
Ce ticket local n’est pas une issue distante publiée.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -0,0 +1,654 @@
|
||||
{
|
||||
"ticket": "WG-12",
|
||||
"version": "0.1.0-alpha.11",
|
||||
"date": "2026-09-09",
|
||||
"protocol": {
|
||||
"machine": "Apple M1, 8 GiB RAM, macOS",
|
||||
"minecraft": "26.3-pre-2",
|
||||
"fabric_loader": "0.19.5",
|
||||
"fabric_api": "0.160.0+26.3",
|
||||
"baseline_source_commit": "a26b72f",
|
||||
"production_files_changed_in_baseline": 0,
|
||||
"runs_per_configuration": 1,
|
||||
"world": "fresh ignored development world per run",
|
||||
"workload": "289 synchronous FULL chunks in concentric rings around natural spawn, then 80 FULL chunks in an eastward corridor. Minecraft world creation timed separately from startup log.",
|
||||
"scope": "Server generation up to FULL. Does not establish ticking readiness, postprocessing completion, client rendering, FPS, multiplayer capacity or a guaranteed user loading time.",
|
||||
"baseline_scope_correction": "The raw alpha.10 report incorrectly says postprocessing is included; FULL alone does not guarantee this. Workload code is identical; alpha.11 corrects only that explanatory string.",
|
||||
"build_time_excluded": true,
|
||||
"comparison": "Only legacy20 versus legacy20 measures the algorithm gain. Grand10 changes geometry; its exploration timing cannot be used for that comparison.",
|
||||
"frozen_runtime_files": 272,
|
||||
"frozen_runtime_manifest_sha256": "1c2b433417a5d9e6a219e9ee19781574538d2167c774e14ed2bf254d277d4482",
|
||||
"baseline_runtime_manifest_sha256": "d4860072d1896f0dfd784a5876f1253367b7818a2023f1910ba1612b6f0e7103",
|
||||
"benchmark_source_sha256": "408711c97d65de63af4e0a1eac90e6edb5bf2df2b70c43772a3fdef7347d75d1"
|
||||
},
|
||||
"runs": {
|
||||
"alpha10_legacy20": {
|
||||
"version": "0.1.0-alpha.10",
|
||||
"players": 20,
|
||||
"seed": 0,
|
||||
"spawn": [
|
||||
0,
|
||||
252,
|
||||
0
|
||||
],
|
||||
"java": "25.0.3",
|
||||
"processors": 8,
|
||||
"max_heap_bytes": 2147483648,
|
||||
"spawn_area_ms": 310664.163542,
|
||||
"spawn_area_full_chunks": 289,
|
||||
"spawn_area_nonempty_chunks": 289,
|
||||
"rings": [
|
||||
{
|
||||
"elapsed_ms": 2.609,
|
||||
"cumulative_chunks": 1,
|
||||
"radius": 0
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 5468.709333,
|
||||
"cumulative_chunks": 9,
|
||||
"radius": 1
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 17601.560917,
|
||||
"cumulative_chunks": 25,
|
||||
"radius": 2
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 27397.566458,
|
||||
"cumulative_chunks": 49,
|
||||
"radius": 3
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 36562.853333,
|
||||
"cumulative_chunks": 81,
|
||||
"radius": 4
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 44462.775334,
|
||||
"cumulative_chunks": 121,
|
||||
"radius": 5
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 52423.80275,
|
||||
"cumulative_chunks": 169,
|
||||
"radius": 6
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 60416.705916,
|
||||
"cumulative_chunks": 225,
|
||||
"radius": 7
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 66303.128125,
|
||||
"cumulative_chunks": 289,
|
||||
"radius": 8
|
||||
}
|
||||
],
|
||||
"spawn_area_chunk_latency_ms": {
|
||||
"median": 993.3335,
|
||||
"p95": 1392.158958,
|
||||
"max": 2560.16575
|
||||
},
|
||||
"exploration_ms": 103141.834334,
|
||||
"exploration_full_chunks": 80,
|
||||
"exploration_steps": [
|
||||
{
|
||||
"elapsed_ms": 5146.88675,
|
||||
"chunk_x": 9
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 5087.495583,
|
||||
"chunk_x": 10
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 5229.782834,
|
||||
"chunk_x": 11
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 5046.559,
|
||||
"chunk_x": 12
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4810.5415,
|
||||
"chunk_x": 13
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 28755.95025,
|
||||
"chunk_x": 14
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4763.415834,
|
||||
"chunk_x": 15
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4772.209166,
|
||||
"chunk_x": 16
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4826.460208,
|
||||
"chunk_x": 17
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4857.054416,
|
||||
"chunk_x": 18
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4947.188167,
|
||||
"chunk_x": 19
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 5168.714916,
|
||||
"chunk_x": 20
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4897.048167,
|
||||
"chunk_x": 21
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 5286.23025,
|
||||
"chunk_x": 22
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4865.694458,
|
||||
"chunk_x": 23
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4661.793875,
|
||||
"chunk_x": 24
|
||||
}
|
||||
],
|
||||
"exploration_chunk_latency_ms": {
|
||||
"median": 925.461875,
|
||||
"p95": 1273.829625,
|
||||
"max": 25131.068417
|
||||
},
|
||||
"regions_cached": 2,
|
||||
"passed": true,
|
||||
"world_creation_ms": 96766,
|
||||
"world_creation_plus_spawn_area_ms": 407430.163542,
|
||||
"raw_report_sha256": "d70c4ac49f37470d4db15b9200e26da8ac9d4d3b1b008e7ec03e67deafcc0f93",
|
||||
"log_sha256": "5b1df55f7a167003c089e80fe1fb824d087c137e3662bac84af3ae0b325b8392"
|
||||
},
|
||||
"alpha11_legacy20": {
|
||||
"version": "0.1.0-alpha.11",
|
||||
"players": 20,
|
||||
"seed": 0,
|
||||
"spawn": [
|
||||
0,
|
||||
252,
|
||||
0
|
||||
],
|
||||
"java": "25.0.3",
|
||||
"processors": 8,
|
||||
"max_heap_bytes": 2147483648,
|
||||
"spawn_area_ms": 43647.054375,
|
||||
"spawn_area_full_chunks": 289,
|
||||
"spawn_area_nonempty_chunks": 289,
|
||||
"rings": [
|
||||
{
|
||||
"cumulative_chunks": 1,
|
||||
"elapsed_ms": 1.965667,
|
||||
"radius": 0
|
||||
},
|
||||
{
|
||||
"cumulative_chunks": 9,
|
||||
"elapsed_ms": 870.4725,
|
||||
"radius": 1
|
||||
},
|
||||
{
|
||||
"cumulative_chunks": 25,
|
||||
"elapsed_ms": 2972.902334,
|
||||
"radius": 2
|
||||
},
|
||||
{
|
||||
"cumulative_chunks": 49,
|
||||
"elapsed_ms": 4480.355667,
|
||||
"radius": 3
|
||||
},
|
||||
{
|
||||
"cumulative_chunks": 81,
|
||||
"elapsed_ms": 5120.230375,
|
||||
"radius": 4
|
||||
},
|
||||
{
|
||||
"cumulative_chunks": 121,
|
||||
"elapsed_ms": 6290.425208,
|
||||
"radius": 5
|
||||
},
|
||||
{
|
||||
"cumulative_chunks": 169,
|
||||
"elapsed_ms": 7381.384417,
|
||||
"radius": 6
|
||||
},
|
||||
{
|
||||
"cumulative_chunks": 225,
|
||||
"elapsed_ms": 7839.425333,
|
||||
"radius": 7
|
||||
},
|
||||
{
|
||||
"cumulative_chunks": 289,
|
||||
"elapsed_ms": 8677.269209,
|
||||
"radius": 8
|
||||
}
|
||||
],
|
||||
"spawn_area_chunk_latency_ms": {
|
||||
"p95": 280.6,
|
||||
"median": 135.289625,
|
||||
"max": 699.612708
|
||||
},
|
||||
"exploration_ms": 14051.250167,
|
||||
"exploration_full_chunks": 80,
|
||||
"exploration_steps": [
|
||||
{
|
||||
"chunk_x": 9,
|
||||
"elapsed_ms": 750.152875
|
||||
},
|
||||
{
|
||||
"chunk_x": 10,
|
||||
"elapsed_ms": 680.650584
|
||||
},
|
||||
{
|
||||
"chunk_x": 11,
|
||||
"elapsed_ms": 729.180958
|
||||
},
|
||||
{
|
||||
"chunk_x": 12,
|
||||
"elapsed_ms": 725.223
|
||||
},
|
||||
{
|
||||
"chunk_x": 13,
|
||||
"elapsed_ms": 540.200125
|
||||
},
|
||||
{
|
||||
"chunk_x": 14,
|
||||
"elapsed_ms": 5554.177667
|
||||
},
|
||||
{
|
||||
"chunk_x": 15,
|
||||
"elapsed_ms": 473.982208
|
||||
},
|
||||
{
|
||||
"chunk_x": 16,
|
||||
"elapsed_ms": 471.05575
|
||||
},
|
||||
{
|
||||
"chunk_x": 17,
|
||||
"elapsed_ms": 513.109667
|
||||
},
|
||||
{
|
||||
"chunk_x": 18,
|
||||
"elapsed_ms": 533.284292
|
||||
},
|
||||
{
|
||||
"chunk_x": 19,
|
||||
"elapsed_ms": 503.983917
|
||||
},
|
||||
{
|
||||
"chunk_x": 20,
|
||||
"elapsed_ms": 513.126292
|
||||
},
|
||||
{
|
||||
"chunk_x": 21,
|
||||
"elapsed_ms": 508.7555
|
||||
},
|
||||
{
|
||||
"chunk_x": 22,
|
||||
"elapsed_ms": 587.331416
|
||||
},
|
||||
{
|
||||
"chunk_x": 23,
|
||||
"elapsed_ms": 467.628834
|
||||
},
|
||||
{
|
||||
"chunk_x": 24,
|
||||
"elapsed_ms": 494.771375
|
||||
}
|
||||
],
|
||||
"exploration_chunk_latency_ms": {
|
||||
"p95": 186.482333,
|
||||
"median": 103.648708,
|
||||
"max": 5153.6295
|
||||
},
|
||||
"regions_cached": 2,
|
||||
"passed": true,
|
||||
"world_creation_ms": 18888,
|
||||
"world_creation_plus_spawn_area_ms": 62535.054375,
|
||||
"raw_report_sha256": "4799180bd61ec4ffc93d6a1a7ccb5440eb880f7dc08a345429b77db10cc17ab0",
|
||||
"log_sha256": "a9347297608528e719d6780f3dd2f2588bb516e06ab771ca6da6ffdda404f7a4"
|
||||
},
|
||||
"alpha11_grand10": {
|
||||
"version": "0.1.0-alpha.11",
|
||||
"players": 10,
|
||||
"seed": 0,
|
||||
"spawn": [
|
||||
0,
|
||||
252,
|
||||
0
|
||||
],
|
||||
"java": "25.0.3",
|
||||
"processors": 8,
|
||||
"max_heap_bytes": 2147483648,
|
||||
"spawn_area_ms": 44291.159875,
|
||||
"spawn_area_full_chunks": 289,
|
||||
"spawn_area_nonempty_chunks": 289,
|
||||
"rings": [
|
||||
{
|
||||
"radius": 0,
|
||||
"cumulative_chunks": 1,
|
||||
"elapsed_ms": 42.387459
|
||||
},
|
||||
{
|
||||
"radius": 1,
|
||||
"cumulative_chunks": 9,
|
||||
"elapsed_ms": 1474.290584
|
||||
},
|
||||
{
|
||||
"radius": 2,
|
||||
"cumulative_chunks": 25,
|
||||
"elapsed_ms": 3180.279791
|
||||
},
|
||||
{
|
||||
"radius": 3,
|
||||
"cumulative_chunks": 49,
|
||||
"elapsed_ms": 4474.062208
|
||||
},
|
||||
{
|
||||
"radius": 4,
|
||||
"cumulative_chunks": 81,
|
||||
"elapsed_ms": 5043.704042
|
||||
},
|
||||
{
|
||||
"radius": 5,
|
||||
"cumulative_chunks": 121,
|
||||
"elapsed_ms": 6367.417708
|
||||
},
|
||||
{
|
||||
"radius": 6,
|
||||
"cumulative_chunks": 169,
|
||||
"elapsed_ms": 7285.252542
|
||||
},
|
||||
{
|
||||
"radius": 7,
|
||||
"cumulative_chunks": 225,
|
||||
"elapsed_ms": 7824.965875
|
||||
},
|
||||
{
|
||||
"radius": 8,
|
||||
"cumulative_chunks": 289,
|
||||
"elapsed_ms": 8577.641208
|
||||
}
|
||||
],
|
||||
"spawn_area_chunk_latency_ms": {
|
||||
"max": 665.612875,
|
||||
"p95": 274.9215,
|
||||
"median": 136.067375
|
||||
},
|
||||
"exploration_ms": 7684.81575,
|
||||
"exploration_full_chunks": 80,
|
||||
"exploration_steps": [
|
||||
{
|
||||
"elapsed_ms": 739.863,
|
||||
"chunk_x": 9
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 724.49625,
|
||||
"chunk_x": 10
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 720.754584,
|
||||
"chunk_x": 11
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 695.608958,
|
||||
"chunk_x": 12
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 522.1695,
|
||||
"chunk_x": 13
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 1112.798042,
|
||||
"chunk_x": 14
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 430.820667,
|
||||
"chunk_x": 15
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 416.082834,
|
||||
"chunk_x": 16
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 400.90425,
|
||||
"chunk_x": 17
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 414.387708,
|
||||
"chunk_x": 18
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 394.878458,
|
||||
"chunk_x": 19
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 393.643834,
|
||||
"chunk_x": 20
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 391.553625,
|
||||
"chunk_x": 21
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 279.306334,
|
||||
"chunk_x": 22
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 25.634541,
|
||||
"chunk_x": 23
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 18.210167,
|
||||
"chunk_x": 24
|
||||
}
|
||||
],
|
||||
"exploration_chunk_latency_ms": {
|
||||
"max": 760.323917,
|
||||
"p95": 198.353916,
|
||||
"median": 85.88425
|
||||
},
|
||||
"regions_cached": 2,
|
||||
"passed": true,
|
||||
"world_creation_ms": 18376,
|
||||
"world_creation_plus_spawn_area_ms": 62667.159875,
|
||||
"raw_report_sha256": "5284e0ef88217f01d51f9040bf13f652de8276dc3f9844dc14569848b227d05a",
|
||||
"log_sha256": "3a4c84bb6ea1614f8fc9c85f472612750149996e2f7658c83afe398b43afe5f6"
|
||||
}
|
||||
},
|
||||
"same_geometry_reduction_percent": {
|
||||
"world_creation_plus_spawn_area_ms": 84.65134396742975,
|
||||
"spawn_area_ms": 85.95040577665497,
|
||||
"exploration_ms": 86.37676917641545
|
||||
},
|
||||
"targets": {
|
||||
"minimum_reduction_percent": 30,
|
||||
"grand_creation_plus_spawn_area_max_ms": 120000,
|
||||
"same_geometry_reduction_passed": true,
|
||||
"grand_generation_proxy_passed": true
|
||||
},
|
||||
"density_equivalence": {
|
||||
"comparison": "Float.floatToRawIntBits; no tolerance",
|
||||
"reference_commit": "a26b72f",
|
||||
"scope": "Real compiled scalar/bulk densities, 4 capacities x 3 seeds (5/20/100 compatibility and new10 using the same parameterized algorithm), signed coordinates, region/coast/height boundaries, context switches and concurrency; no block changes",
|
||||
"reference_source_sha256": "fcc5a8a39b7c4d181d74ce4f9183a1ca93646137955ac64bab9d22e6db0752a1",
|
||||
"profiles": [
|
||||
{
|
||||
"seed": 0,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 5,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "28ec59cba36174fd"
|
||||
},
|
||||
{
|
||||
"seed": 42,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 5,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "efd1e8084800ed5c"
|
||||
},
|
||||
{
|
||||
"seed": 8675309,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 5,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "91bea1db755d9aca"
|
||||
},
|
||||
{
|
||||
"seed": 0,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 10,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "b7bab667baf73361"
|
||||
},
|
||||
{
|
||||
"seed": 42,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 10,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "42aeaf160360e41c"
|
||||
},
|
||||
{
|
||||
"seed": 8675309,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 10,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "cb85f95333e040c0"
|
||||
},
|
||||
{
|
||||
"seed": 0,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 20,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "175524360755dd88"
|
||||
},
|
||||
{
|
||||
"seed": 42,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 20,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "4cc8aec467a2d164"
|
||||
},
|
||||
{
|
||||
"seed": 8675309,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 20,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "5a3f16e426e5a64e"
|
||||
},
|
||||
{
|
||||
"seed": 0,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 100,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "83cda17336772fa7"
|
||||
},
|
||||
{
|
||||
"seed": 42,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 100,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "e7aa7f52022cc6aa"
|
||||
},
|
||||
{
|
||||
"seed": 8675309,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 100,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "de4b8f52d249a9bd"
|
||||
}
|
||||
],
|
||||
"concurrent_workers": 3,
|
||||
"concurrent_comparisons": 115668,
|
||||
"passed": true,
|
||||
"adversarial": {
|
||||
"aligned_scalar_path_preserved": true,
|
||||
"passed": true,
|
||||
"bitwise_comparisons": 122528,
|
||||
"concurrent_workers": 3,
|
||||
"repeated_cell_corner_batches": 1
|
||||
}
|
||||
},
|
||||
"persistence": {
|
||||
"default_players": 10,
|
||||
"profiles": [
|
||||
{
|
||||
"restored_settings": "sanctuary:population_5",
|
||||
"players": 5,
|
||||
"disk_round_trip": true,
|
||||
"preset": "sanctuary:sanctuary_5"
|
||||
},
|
||||
{
|
||||
"restored_settings": "sanctuary:population_10",
|
||||
"players": 10,
|
||||
"disk_round_trip": true,
|
||||
"preset": "sanctuary:sanctuary"
|
||||
},
|
||||
{
|
||||
"restored_settings": "sanctuary:population_20",
|
||||
"players": 20,
|
||||
"disk_round_trip": true,
|
||||
"preset": "sanctuary:sanctuary_20"
|
||||
},
|
||||
{
|
||||
"restored_settings": "sanctuary:population_100",
|
||||
"players": 100,
|
||||
"disk_round_trip": true,
|
||||
"preset": "sanctuary:sanctuary_100"
|
||||
}
|
||||
],
|
||||
"passed": true,
|
||||
"visible_presets": [
|
||||
"sanctuary:sanctuary_5",
|
||||
"sanctuary:sanctuary"
|
||||
],
|
||||
"seed": 0,
|
||||
"legacy_settings_preserved": "sanctuary:sanctuary_cavern",
|
||||
"omitted_density_capacity_preserved": 20
|
||||
},
|
||||
"limits": [
|
||||
"Single seed and one timed run per configuration; repeat to estimate variance.",
|
||||
"A new cold region still has a synchronous planning cost; observed legacy20 maximum exploration chunk latency is about 5.15 s.",
|
||||
"No personal saved world was opened or regenerated."
|
||||
],
|
||||
"engine_checks": {
|
||||
"5": {
|
||||
"mod_version": "0.1.0-alpha.11",
|
||||
"seed": 0,
|
||||
"settings": "sanctuary:population_5",
|
||||
"inspected_full_chunks": 110,
|
||||
"retained_water_blocks": 5656,
|
||||
"fluid_ticks": 1800,
|
||||
"tested_springs": 1,
|
||||
"tested_spills": 1,
|
||||
"tested_lava_falls": 1,
|
||||
"postprocessing_reentry_witnesses": 3,
|
||||
"passed": true,
|
||||
"scope": "Selected basins, terraces, springs and ecology patches in explicit regions; no whole-island inventory or multiplayer load test",
|
||||
"raw_report_sha256": "cfaa0a532b50399ab28808f51a871363b198a67d8c096ac13f7a011bd87ff9d0"
|
||||
},
|
||||
"10": {
|
||||
"mod_version": "0.1.0-alpha.11",
|
||||
"seed": 0,
|
||||
"settings": "sanctuary:population_10",
|
||||
"inspected_full_chunks": 125,
|
||||
"retained_water_blocks": 5923,
|
||||
"fluid_ticks": 1800,
|
||||
"tested_springs": 1,
|
||||
"tested_spills": 1,
|
||||
"tested_lava_falls": 1,
|
||||
"postprocessing_reentry_witnesses": 3,
|
||||
"passed": true,
|
||||
"scope": "Selected basins, terraces, springs and ecology patches in explicit regions; no whole-island inventory or multiplayer load test",
|
||||
"raw_report_sha256": "aaed5b38821197c479a8acb7f2a201c64f290666c039d0b15047340e403e028c"
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
-9
@@ -5,6 +5,20 @@ registre persistant des continents, leur déblocage et leur génération dans le
|
||||
huit directions restent à implémenter. Le relevé de ressources décrit plus bas
|
||||
est un outil de développement séparé, exécuté dans le monde jetable des GameTests.
|
||||
|
||||
## Taille de l’île initiale — alpha.11
|
||||
|
||||
Deux choix de départ sont proposés : **Petit** et **Grand**, Grand par défaut.
|
||||
Petit garde un diamètre nominal de 512 blocs ; Grand vise environ 724 blocs,
|
||||
soit deux fois l’aire nominale de Petit. La hauteur reste de 384 blocs. Les
|
||||
repères internes 5/10 servent au calcul de surface, sans nom de capacité affiché
|
||||
ni promesse sur le nombre de joueurs, les minerais ou l’aire constructible.
|
||||
|
||||
Cette taille se choisit pour un nouveau monde et ne remplace pas le futur
|
||||
déblocage collectif de continents. Grand utilise `population_10` ; les anciennes
|
||||
sauvegardes 5/20/100 conservent leurs réglages et leurs dimensions. Les formats
|
||||
20/100 sont cachés de la création. Les mesures de l’alpha.10 conservées plus bas
|
||||
restent historiques ; les contrôles alpha.11 sont dans [Validation](testing.md).
|
||||
|
||||
## Les huit directions climatiques
|
||||
|
||||
Sanctuary Island constitue le point de départ **tempéré, légèrement humide**.
|
||||
@@ -135,16 +149,32 @@ n'est inclus dans ces comptes de blocs.
|
||||
```
|
||||
|
||||
Les modes sont transmis à la propriété JVM de test
|
||||
`sanctuary.test.resourceSurvey`. Le mode `island` couvre **1 444 chunks** :
|
||||
`X` et `Z` de **−304 inclus à 304 exclu**, correspondant à l'enveloppe de terrain
|
||||
de rayon 288 et une marge de décoration de 16 blocs. Un halo d'un chunk porte
|
||||
la préparation à **1 600 chunks FULL** ; seuls les 1 444 chunks demandés sont
|
||||
comptés. Ce relevé complet est plus coûteux et ne s'exécute pas par défaut.
|
||||
La marge devra être revue si une future génération peut produire des structures
|
||||
ou décorations au-delà de cette enveloppe.
|
||||
`sanctuary.test.resourceSurvey`. La propriété Gradle `-PsanctuaryTestPlayers`
|
||||
choisit le profil interne. L’alpha.11 utilise `5` pour Petit et `10` pour Grand,
|
||||
ce dernier par défaut ; `20` et `100` servent à la compatibilité historique.
|
||||
Le mode `island` calcule l’enveloppe du profil actif, avec 16 blocs de marge de
|
||||
décoration et un halo de préparation d’un chunk. Les valeurs ci-dessous sont
|
||||
les bornes calculées lors de l’alpha.10, pas des relevés exécutés ni un nombre
|
||||
de chunks déjà validé pour le nouveau Grand :
|
||||
|
||||
| Profil | Chunks comptés par `island` | Chunks FULL préparés avec halo |
|
||||
| --- | --- | --- |
|
||||
| 5 | 1 444 | 1 600 |
|
||||
| 20 | 4 900 | 5 184 |
|
||||
| 100 | 22 500 | 23 104 |
|
||||
|
||||
Un relevé de toute l’emprise peut être très coûteux et ne s’exécute pas par
|
||||
défaut. La marge devra être revue si une future génération produit des
|
||||
structures ou décorations qui la dépassent. Les diagnostics alpha.10 courants
|
||||
restent des inspections ciblées ; leurs nombres de minerais ne sont pas ceux
|
||||
d’un relevé `island` complet.
|
||||
|
||||
Les nouveaux fichiers sont nommés
|
||||
`mods/sanctuary/build/run/gameTest/diagnostics/resource-survey-players-<capacité>-seed-<graine>-<mode>.json`.
|
||||
Sur un générateur historique, le nom émis par l’outil actuel porte `legacy`.
|
||||
Les anciens chemins `resource-survey-seed-…` cités plus bas désignent uniquement
|
||||
les relevés archivés de leurs versions respectives.
|
||||
|
||||
Le JSON est écrit dans
|
||||
`mods/sanctuary/build/run/gameTest/diagnostics/resource-survey-seed-<graine>-<mode>.json`.
|
||||
Il contient :
|
||||
|
||||
- La graine, la version Minecraft, le preset et les réglages de génération,
|
||||
|
||||
+35
-8
@@ -16,27 +16,33 @@ de l'ancien pack 26.2.
|
||||
## Livrer une mise à jour
|
||||
|
||||
Dernière livraison vérifiée le 9 septembre 2026 :
|
||||
[Sanctuary 0.1.0-alpha.9](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.9),
|
||||
depuis le commit source `40b6555`. Le JAR, le `.mrpack` et le ZIP d’amorçage
|
||||
[Sanctuary 0.1.0-alpha.11](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.11),
|
||||
depuis le commit source `f30a350`. Le JAR, le `.mrpack` et le ZIP d’amorçage
|
||||
Prism sont publiés et leurs téléchargements publics vérifiés. Le canal stable
|
||||
pointe sur cette version. SHA-256 du JAR :
|
||||
|
||||
```text
|
||||
150149852bf5afd0bb3096e6cd8953e69b23634859c10f294088a44aca538aff
|
||||
f075ad8a0b97cd53e4891273adbee5f79f04ab3984442950d5d85f58d2ded589
|
||||
```
|
||||
|
||||
Le véritable installateur packwiz a réussi deux fois dans un dossier de
|
||||
développement neuf, puis deux fois dans l’instance Sanctuary Beta existante.
|
||||
Le second passage laisse les fichiers gérés identiques. Un seul JAR Sanctuary
|
||||
alpha.9 est actif ; les 260 fichiers personnels et réglages suivis conservent
|
||||
leurs hashes. L’ancien JAR et les manifestes locaux sont sauvegardés hors de
|
||||
`mods/`. Aucun monde existant n’a été ouvert, converti ou régénéré.
|
||||
alpha.11 est actif ; les 364 fichiers personnels et réglages suivis conservent
|
||||
leurs hashes, y compris les fichiers des mods tiers et leurs configurations.
|
||||
L’ancien JAR et les manifestes locaux sont sauvegardés hors de `mods/`, dans
|
||||
`sanctuary-backups/before-0.1.0-alpha.11/` de cette même instance.
|
||||
Aucun monde existant n’a été ouvert, converti ou régénéré par la livraison.
|
||||
Les reçus locaux ignorés sont `build/alpha11-isolated-validation.json` et
|
||||
`build/alpha11-prism-validation.json` ; les listes de hashes avant et après
|
||||
figurent dans le dossier de sauvegarde. Les preuves du build, des deux tailles
|
||||
et de performance figurent dans [Validation](testing.md).
|
||||
|
||||
Après les vérifications, le commit et le push de la branche du ticket, le script
|
||||
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.9.mrpack
|
||||
python3 scripts/publish_pack.py --notes-file chemin/vers/notes.md --asset build/Sanctuary-0.1.0-alpha.11.mrpack
|
||||
```
|
||||
|
||||
Les options sont facultatives. Ce script **publie** sur le Git configuré dans
|
||||
@@ -55,7 +61,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.9/sanctuary-0.1.0-alpha.9.jar
|
||||
python3 scripts/pack.py release https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.11/sanctuary-0.1.0-alpha.11.jar
|
||||
```
|
||||
|
||||
4. Pousser le commit source vérifié. Créer une release Gitea correspondant à ce
|
||||
@@ -161,6 +167,27 @@ alpha.8 conservent `sanctuary:sanctuary_rift` ; leur contenu n’est pas retouch
|
||||
par une mise à jour de la même instance Prism. Les anciennes sauvegardes et
|
||||
les réglages ont été conservés lors des deux synchronisations de livraison.
|
||||
|
||||
L’alpha.10 propose des choix de taille pour 5, 20 et 100 joueurs, avec
|
||||
20 par défaut. Ils s’appliquent à un **nouveau monde**. Les profils sauvegardés `sanctuary:population_5`,
|
||||
`sanctuary:population_20` et `sanctuary:population_100` sont séparés des
|
||||
anciens réglages, notamment `sanctuary:sanctuary_cavern` de l’alpha.9.
|
||||
La mise à jour de la même instance Prism n’agrandit aucune île
|
||||
existante, ne modifie pas les paramètres de génération enregistrés et ne
|
||||
nécessite pas de réimporter le ZIP d’amorçage. Les fichiers de sauvegarde,
|
||||
captures, packs graphiques et réglages personnels suivis ont conservé leurs
|
||||
hashes lors des deux synchronisations de livraison.
|
||||
|
||||
L’alpha.11 propose deux choix : **Petit** et **Grand**, Grand par défaut.
|
||||
Le preset `sanctuary:sanctuary` crée un nouveau monde `population_10` ;
|
||||
Petit garde `sanctuary:sanctuary_5` / `population_5`. Les anciens mondes
|
||||
20/100 restent chargeables à leur taille enregistrée, alors que leurs formats
|
||||
sont cachés de la création. Les formats de sauvegarde sont inchangés.
|
||||
|
||||
L’optimisation conserve les résultats pour un même réglage. La livraison a
|
||||
préservé les fichiers personnels suivis, les mods tiers et leurs configurations.
|
||||
L’instance n’a pas été réimportée et aucun mod tiers n’a été retiré. Les
|
||||
sauvegardes personnelles sont restées fermées pendant les essais et la synchronisation.
|
||||
|
||||
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).
|
||||
|
||||
+158
-5
@@ -54,7 +54,10 @@ Deux adaptations y sont nécessaires, vérifiées contre les classes Minecraft
|
||||
|
||||
- `GameTestServer` sélectionne normalement `minecraft:flat_all_dimensions`.
|
||||
Le mixin de test sélectionne directement le preset de production
|
||||
`sanctuary:sanctuary`, sans recopier ses JSON.
|
||||
`sanctuary:sanctuary`, sans recopier ses JSON. L’alpha.11 utilise le défaut
|
||||
Grand (`-PsanctuaryTestPlayers=10`) et Petit (`5`). Les profils historiques
|
||||
restent accessibles aux diagnostics de compatibilité, séparément de la liste
|
||||
publique de création.
|
||||
- Le framework désactive normalement les structures. Le test les active dans
|
||||
`WorldOptions` pour examiner les chunks après toutes les étapes de génération.
|
||||
La seed du serveur de test est `0` par défaut ; `-PsanctuaryTestSeed` est transmis
|
||||
@@ -65,8 +68,8 @@ Le test capture donc le spawn juste avant ce déplacement, après l'initialisati
|
||||
normale du monde, et examine les coordonnées absolues de l'île.
|
||||
|
||||
Les scénarios suivants couvrent le socle et les générations historiques ;
|
||||
les contrôles validés pour l’alpha.9 et les résultats historiques
|
||||
propres à l’alpha.8 figurent plus bas :
|
||||
les contrôles validés pour l’alpha.10 et les résultats historiques
|
||||
des générations précédentes figurent plus bas :
|
||||
|
||||
1. Le vrai générateur Sanctuary est chargé et le spawn collectif repose sur une
|
||||
surface déjà présente dans la densité naturelle, avec un sol plein de 3×3
|
||||
@@ -95,8 +98,9 @@ propres à l’alpha.8 figurent plus bas :
|
||||
9. Les anciennes clés gardent leur traitement d’origine. Seuls les nouveaux
|
||||
paramètres activent le traitement ajouté pour leur génération.
|
||||
|
||||
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
|
||||
La recherche de spawn parcourt l'île finie par anneaux de quatre blocs,
|
||||
jusqu’à la limite de terrain du profil choisi. Les anciens réglages conservent
|
||||
leur borne de 288 blocs. Elle examine d'abord la hauteur brute pour éviter de
|
||||
générer entièrement les colonnes vides, puis vérifie le sol et les dégagements
|
||||
après décoration. Elle préfère une zone naturelle de 3×3 ; si aucune n'est trouvée,
|
||||
elle utilise le premier emplacement naturel sûr d'une colonne avec deux blocs
|
||||
@@ -190,6 +194,155 @@ 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.
|
||||
|
||||
## Petit, Grand et optimisation alpha.11 — 9 septembre 2026
|
||||
|
||||
`check build assemblePack` réussit sur les sources finales avec Grand, puis
|
||||
la suite moteur réussit également sur Petit. Douze tests requis passent dans
|
||||
chaque monde neuf, graine `0`. Les mondes personnels restent fermés.
|
||||
|
||||
| Choix | Chunks FULL inspectés | Eau retenue | Ticks de fluides |
|
||||
| --- | --- | --- | --- |
|
||||
| Petit (`population_5`) | 110 | 5 656 | 1 800 |
|
||||
| Grand (`population_10`, défaut) | 125 | 5 923 | 1 800 |
|
||||
|
||||
Les deux essais vérifient les sources, déversoirs, coulées de lave et témoins
|
||||
de réentrée après post-traitement. La persistance réelle sur disque préserve
|
||||
les quatre références 5/10/20/100, le défaut Grand, les deux seuls choix publics
|
||||
et l’ancien défaut 20 lorsque le champ `players` est absent d’une densité sauvée.
|
||||
|
||||
L’oracle alpha.10 effectue **3 183 804 comparaisons bit à bit** de densités
|
||||
compilées réelles : références 5/10/20/100, graines `0`, `42`, `8675309`,
|
||||
volumes, coordonnées signées, frontières et accès concurrents. Pour 10,
|
||||
l’oracle utilise l’ancien algorithme paramétré à cette nouvelle taille ;
|
||||
aucun ancien preset 10 n’est supposé. Les tests synthétiques ajoutent
|
||||
122 528 comparaisons sur les arrondis, collisions et changements de contexte.
|
||||
Les expressions de datapacks non reconnues gardent le sampler vanilla.
|
||||
Un test de concurrence vérifie qu’un plan prêt reste lisible pendant un
|
||||
calcul lent, sans publier de plan incomplet ni conserver un échec en cache.
|
||||
|
||||
### Mesure à géométrie identique et nouveau Grand
|
||||
|
||||
Machine : Apple M1, 8 Gio de RAM, Java 25.0.3, heap 2 Gio, huit processeurs
|
||||
visibles, Minecraft 26.3-pre-2 / Fabric 0.19.5. **Une exécution par configuration**,
|
||||
graine `0`, nouveau monde à chaque fois, spawn `(0,252,0)`. La référence
|
||||
alpha.10 est le commit `a26b72f` avec uniquement le harnais de mesure ajouté :
|
||||
aucun fichier de production modifié. Aucun autre serveur ou build simultané.
|
||||
|
||||
| Phase serveur | Alpha.10, ancien 20 | Alpha.11, ancien 20 | Alpha.11, Grand 10 |
|
||||
| --- | --- | --- | --- |
|
||||
| Création du monde et spawn | 96,766 s | 18,888 s | 18,376 s |
|
||||
| 289 chunks autour du spawn | 310,664 s | 43,647 s | 44,291 s |
|
||||
| Somme création + zone | 407,430 s | 62,535 s | 62,667 s |
|
||||
| 80 chunks d’exploration | 103,142 s | 14,051 s | 7,685 s |
|
||||
|
||||
À géométrie 20 identique, la somme initiale baisse de **84,65 %** et
|
||||
l’exploration de **86,38 %**. Le gain minimal de 30 % est dépassé sur ce
|
||||
protocole. Grand respecte le proxy de 120 secondes pour créer le monde et
|
||||
produire la zone demandée. Sa surface différente ne compte pas comme preuve
|
||||
du gain algorithmique. La latence maximale d’un chunk d’exploration sur
|
||||
l’ancien 20 passe de 25,13 à 5,15 secondes : une région froide conserve donc
|
||||
un coût ponctuel. Le cache ne rend pas toute exploration instantanée.
|
||||
|
||||
Le benchmark demande 289 chunks `FULL` par anneaux jusqu’au rayon huit,
|
||||
puis 80 dans un corridor vers l’est. Il distingue la création relevée dans
|
||||
`Time elapsed` des phases mesurées dans le test. **FULL ne garantit pas le
|
||||
statut de simulation ni la fin du post-traitement.** Le libellé du rapport
|
||||
brut alpha.10 affirmait par erreur inclure ce dernier ; seul ce texte a été
|
||||
corrigé dans le harnais alpha.11, sans changer les demandes de chunks.
|
||||
Ni rendu client, ni réseau, ni FPS, ni délai d’entrée visuellement confirmé,
|
||||
ni capacité multijoueur ne sont mesurés. Une graine et une répétition ne
|
||||
mesurent pas la variance sur toutes les îles ou machines.
|
||||
|
||||
Les [résultats structurés et hashes](benchmarks/alpha11-generation.json)
|
||||
conservent les phases, anneaux, latences, périmètres et preuves moteur.
|
||||
Les journaux bruts locaux sont `build/alpha11-baseline-20.log`,
|
||||
`build/alpha11-optimized-20.log`, `build/alpha11-grand-10.log`,
|
||||
`build/alpha11-check-build.log` et `build/alpha11-small-5.log`.
|
||||
|
||||
Reproduire le benchmark sur cette version (utiliser `10` pour Grand) :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryBenchmark=true -PsanctuaryTestPlayers=20 -PsanctuaryTestSeed=0 -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --console=plain
|
||||
```
|
||||
|
||||
L’option sélectionne le harnais de performance à la place de la suite moteur,
|
||||
jamais dans le JAR livré. Le build obligatoire reste une exécution normale :
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack -PsanctuaryTestPlayers=10 -PsanctuaryTestSeed=0 -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --console=plain
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryTestPlayers=5 -PsanctuaryTestSeed=0 -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --console=plain
|
||||
```
|
||||
|
||||
Les tableaux alpha.10 suivants restent historiques.
|
||||
|
||||
## Contrôles et validation alpha.10 — 9 septembre 2026
|
||||
|
||||
Les profils `sanctuary:population_5`, `population_20` et `population_100`
|
||||
passent chacun les onze tests requis sur la graine `0`, avec **1 800 ticks de
|
||||
fluides de production**. Aucun réveil manuel des sources naturelles n’est
|
||||
ajouté par le diagnostic. `check build assemblePack` passe sur le profil 5 ;
|
||||
les profils 20 puis 100 sont ensuite exécutés dans des mondes de test neufs.
|
||||
|
||||
| Capacité | Diamètre nominal | Régions inspectées | Chunks FULL | Régions en cache au spawn → après inspection | Durée de la commande |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 5 | 512 | (0, 0) | 110 | 1 → 1 | 3m 46s |
|
||||
| 20 | 1 024 | (0, 0), (1, 0) | 185 | 1 → 2 | 6m 31s |
|
||||
| 100 | environ 2 290 | (0, 0), (1, 1), (2, 0) | 290 | 1 → 4 | 11m 37s |
|
||||
|
||||
La durée du profil 5 inclut les tâches de construction ; celles des profils
|
||||
20/100 concernent `runGameTest`. Ces temps ne mesurent ni un tick en régime
|
||||
établi ni une charge multijoueur. Le calcul du premier plan central reste
|
||||
coûteux sur cette configuration : 5 joueurs : 87.710 s; 20 joueurs : 82.834 s; 100 joueurs : 85.761 s.
|
||||
La génération en masse reste à optimiser. Le cache commence localement et
|
||||
ne construit pas d’emblée toutes les régions de l’île.
|
||||
|
||||
| Capacité | Colonnes de sédiments sélectionnées | Blocs d’eau retenus | Sources de lave retenues | Sources d’eau / débordements / coulées de lave testés | Témoins de réentrée sans marqueur |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 5 | 3196 | 5656 | 19 | 1 / 1 / 1 | 3 |
|
||||
| 20 | 3367 | 6319 | 20 | 2 / 3 / 2 | 3 |
|
||||
| 100 | 4325 | 8106 | 57 | 3 / 4 / 3 | 3 |
|
||||
|
||||
Les témoins vérifient les fonds et sédiments après décoration, la conservation
|
||||
des sources, les composantes d’écoulement réellement descendantes et les
|
||||
supports de lave naturels. Les débordements sélectionnés rejoignent leur bassin
|
||||
cible ou leur chute terminale. Le rappel du post-traitement sans marqueur
|
||||
préserve une modification ultérieure de l’exutoire. Les nombres décrivent
|
||||
uniquement les éléments sélectionnés dans les chunks indiqués.
|
||||
|
||||
| Capacité | Pas horizontal du relevé de densité | Colonnes avec roche | Colonnes vides | Colonnes avec roche au-delà de l’ancienne emprise |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 5 | 9 | 1015 | 3210 | 0 |
|
||||
| 20 | 17 | 1753 | 2472 | 790 |
|
||||
| 100 | 37 | 2375 | 1850 | 2170 |
|
||||
|
||||
Ces échantillons de densité attestent des masses naturelles, du vide et de
|
||||
l’extension réelle des grandes tailles. Ils ne mesurent pas toute l’aire
|
||||
habitable. Les comptes de blocs, troncs et minerais des régions inspectées
|
||||
sont des constats locaux avant l’intervalle fluide ; ils ne doivent être
|
||||
extrapolés ni en stocks de l’île entière ni en ressources par joueur.
|
||||
|
||||
Le test de persistance utilise l’écriture et la lecture officielles de
|
||||
Minecraft 26.3-pre-2 dans des dossiers temporaires. Il vérifie les trois clés
|
||||
de paramètres enregistrées, la graine, **Sanctuary = 20 par défaut**, et un
|
||||
témoin alpha.9 qui conserve `sanctuary:sanctuary_cavern`. Le smoke pur régional
|
||||
vérifie le rejet atomique des bassins et groupes, les dépendances de sources,
|
||||
les limites, les traductions signées et les index de cellules multihauteurs.
|
||||
Le gel porte sur 267 fichiers de production ; 212 anciens fichiers restent
|
||||
strictement identiques, huit fichiers de liaison/interface sont modifiés.
|
||||
|
||||
Preuves locales ignorées :
|
||||
|
||||
- `build/alpha10-check-build-final.log`, puis `build/alpha10-players20-seed0.log`
|
||||
et `build/alpha10-players100-seed0.log`.
|
||||
- `mods/sanctuary/build/run/gameTest/diagnostics/population-<capacité>-seed-0-engine.json`
|
||||
et `population-<capacité>-seed-0-density.json` avec leurs CSV.
|
||||
- `mods/sanctuary/build/run/gameTest/diagnostics/population-persistence-seed-0/round-trip.json`.
|
||||
|
||||
Le JAR, le MRpack et le ZIP d’amorçage sont vérifiés localement. La publication
|
||||
et les synchronisations de la même instance Prism sont suivies séparément dans
|
||||
[Distribution](packwiz.md). Aucun test de cent joueurs simultanés ni garantie
|
||||
de performances n’est annoncé. Les preuves alpha.9 suivantes restent historiques.
|
||||
|
||||
## Contrôles et validation alpha.9 — 9 septembre 2026
|
||||
|
||||
Les mondes neufs des graines `0`, `42` et `8675309` utilisent les paramètres
|
||||
|
||||
@@ -50,6 +50,20 @@ 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.
|
||||
|
||||
Le choix de taille initiale se limite à **Petit** et **Grand**, avec Grand
|
||||
par défaut. Petit prend une référence de 512 blocs de diamètre ; Grand vise
|
||||
environ 724 blocs, soit deux fois son aire nominale, sur une hauteur de
|
||||
384 blocs. Les repères internes 5/10 ne constituent ni les noms affichés ni
|
||||
une promesse de capacité multijoueur. Ce resserrement des tailles doit limiter
|
||||
les distances entre constructions.
|
||||
|
||||
L’alpha.11 implémente ces deux choix et l’optimisation des chargements. Les
|
||||
anciens formats 20/100 sont masqués à la création mais restent chargeables
|
||||
avec leurs paramètres enregistrés ; aucune sauvegarde n’est réduite.
|
||||
Les mesures serveur et leurs limites sont consignées dans [Validation](testing.md).
|
||||
Ce dimensionnement ne remplace pas l’expansion collective ni ne garantit
|
||||
une aire habitable ou un stock de ressources par joueur.
|
||||
|
||||
L'île initiale doit être **tempérée et légèrement humide**, avec quelques zones
|
||||
plus sèches, notamment de larges plages et dépôts irréguliers de sable, gravier,
|
||||
pierre et terre enherbée. De petits étangs ou lacs de surface
|
||||
|
||||
+96
-12
@@ -5,18 +5,43 @@ entourée de vide sans répétition d'îles à grande distance. C'est une premi
|
||||
tranche de génération pour Minecraft **26.3-pre-2 / Fabric**, pas encore le
|
||||
système d'expansion collective.
|
||||
|
||||
L’alpha.9 utilise les paramètres `sanctuary:sanctuary_cavern`, pour de nouveaux
|
||||
mondes uniquement. Sa validation moteur passe sur les trois graines de référence. L’alpha.8 conserve
|
||||
`sanctuary:sanctuary_rift` et ses résultats sur trois graines avec 1 800 ticks
|
||||
de fluides ; ils ne valident pas la génération v9. Les observations de chaque
|
||||
version restent séparées dans [Validation](testing.md).
|
||||
L’alpha.11 propose deux choix de création, **Petit** et **Grand**, avec Grand
|
||||
par défaut. Les anciennes tailles 20/100 restent chargeables dans les sauvegardes.
|
||||
Les observations de chaque version restent dans [Validation](testing.md).
|
||||
|
||||
## Petit et Grand — alpha.11
|
||||
|
||||
| Choix public | Diamètre nominal | Aire nominale relative | Preset | Réglage enregistré |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Petit | 512 blocs | 1 | `sanctuary:sanctuary_5` | `sanctuary:population_5` |
|
||||
| Grand, défaut | Environ 724 blocs | 2 | `sanctuary:sanctuary` | `sanctuary:population_10` |
|
||||
|
||||
La hauteur reste de 384 blocs. Les repères internes 5/10 décrivent le calcul
|
||||
de surface ; ils ne sont ni des noms affichés ni une capacité promise.
|
||||
Le relief réel garde ses trous, ses surplombs et sa bordure sculptée.
|
||||
|
||||
Le nouveau Grand reçoit sa propre clé `population_10`. Les anciens mondes
|
||||
`population_5`, `population_20` et `population_100` restent chargeables avec
|
||||
leurs paramètres ; les formats 20/100 sont cachés dans les choix publics.
|
||||
Changer la valeur du preset de création n’agrandit ni ne réduit une sauvegarde.
|
||||
Les générations antérieures restent distinctes, dont `sanctuary_cavern` et
|
||||
`sanctuary_rift`.
|
||||
|
||||
L’optimisation réutilise les coins d’interpolation avec la même arithmétique
|
||||
et borne ce cache à 192 Kio par thread, sans retenir fortement un monde.
|
||||
Les plans prêts restent accessibles pendant le calcul d’une autre région ;
|
||||
le sampler compilé est réutilisé dans ce calcul. Les contrôles d’équivalence,
|
||||
de persistance et les mesures à géométrie identique sont dans [Validation](testing.md).
|
||||
Les règles de forme, d’hydrologie et les anciens réglages restent inchangés.
|
||||
|
||||
## Créer un monde
|
||||
|
||||
En solo : créer un **nouveau** monde et choisir le type de monde **Sanctuary**
|
||||
dans les options de génération. Les types de monde vanilla restent disponibles.
|
||||
La création solo propose **Sanctuary (Petit)** et **Sanctuary (Grand)**,
|
||||
ce dernier étant le défaut Sanctuary.
|
||||
Les types de monde vanilla restent disponibles.
|
||||
|
||||
Sur un nouveau serveur Fabric disposant du mod et de Fabric API :
|
||||
Sur un nouveau serveur Fabric disposant de cette version et de Fabric API,
|
||||
le choix Grand utilise :
|
||||
|
||||
```properties
|
||||
level-type=sanctuary:sanctuary
|
||||
@@ -24,14 +49,15 @@ level-name=sanctuary
|
||||
level-seed=0
|
||||
```
|
||||
|
||||
La seed est libre. Modifier `level-type` ne convertit pas un monde existant :
|
||||
utiliser un nouveau `level-name` pour tester une autre génération. Les fichiers
|
||||
de test et les sauvegardes personnelles doivent rester distincts.
|
||||
Pour Petit, utiliser `level-type=sanctuary:sanctuary_5`. Choisir un nouveau
|
||||
`level-name` pour tester l’autre taille. La seed est libre ; ces exemples ne
|
||||
convertissent aucun monde déjà enregistré et n’autorisent aucune régénération.
|
||||
Les fichiers de test et les sauvegardes personnelles restent distincts.
|
||||
|
||||
## Socle de génération conservé
|
||||
|
||||
Les points suivants décrivent le socle livré jusqu’à l’alpha.8. Les évolutions
|
||||
alpha.9 et leur état de validation sont précisés dans la section v9 ci-dessous.
|
||||
alpha.9 et alpha.10 sont précisées dans les sections v9 et v10 ci-dessous.
|
||||
|
||||
- Le générateur reste celui de Minecraft, `minecraft:noise`.
|
||||
- La forme reprend le champ de bruit flottant actif en 26.2 : `old_blended_noise`
|
||||
@@ -77,6 +103,64 @@ chaque seed. L’accès à chaque progression vanilla et les ressources renouvel
|
||||
nécessitent des vérifications concrètes. Les stocks réels sont relevés après la
|
||||
génération, sans corriger une île pour atteindre un quota.
|
||||
|
||||
## Capacité de l’île — génération v10 historique
|
||||
|
||||
Lors de l’alpha.10, trois tailles étaient accessibles à la création d’un monde.
|
||||
Le choix **Sanctuary** visait 20 joueurs par défaut, accompagné de variantes
|
||||
pour 5 et 100 joueurs. Le tableau suivant conserve cette association historique ;
|
||||
l’alpha.11 utilise le nouveau défaut Grand indiqué plus haut :
|
||||
|
||||
| Preset de création | Paramètres enregistrés dans le monde |
|
||||
| --- | --- |
|
||||
| `sanctuary:sanctuary_5` | `sanctuary:population_5` |
|
||||
| `sanctuary:sanctuary` | `sanctuary:population_20` |
|
||||
| `sanctuary:sanctuary_100` | `sanctuary:population_100` |
|
||||
|
||||
Les nouveaux réglages restent distincts des identifiants enregistrés dans
|
||||
les anciennes sauvegardes. Modifier le preset
|
||||
sélectionné pour une nouvelle partie ne convertit pas un monde existant.
|
||||
|
||||
| Capacité de référence | Facteur horizontal | Aire nominale relative | Diamètre nominal | Hauteur |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 5 joueurs | 1 | 1 | 512 blocs | 384 blocs |
|
||||
| 20 joueurs, défaut | 2 | 4 | 1 024 blocs | 384 blocs |
|
||||
| 100 joueurs | √20, environ 4,472 | 20 | Environ 2 290 blocs | 384 blocs |
|
||||
|
||||
Le facteur horizontal vaut `√(capacité / 5)` : doubler le diamètre multiplie
|
||||
l’aire nominale par quatre. Cette relation décrit l’enveloppe de référence,
|
||||
pas la surface réellement constructible après les trous, les falaises et
|
||||
la sculpture de la bordure. Aucun cylindre central ni remplissage de secours
|
||||
ne doit être ajouté pour obtenir l’aire théorique. La hauteur de dimension
|
||||
reste `Y=0..383`.
|
||||
|
||||
La capacité choisie reste un paramètre de génération. Elle ne modifie pas
|
||||
`max-players`, n’agrandit pas l’île à chaque connexion et ne fixe aucun quota
|
||||
de ressources. Les anciens mondes jusqu’à l’alpha.9 gardent leur génération
|
||||
et leur taille ; leurs chunks ne sont ni remplacés ni régénérés. Le projet
|
||||
de continents déverrouillables reste séparé de ces tailles de départ.
|
||||
|
||||
Le bruit naturel reste échantillonné aux coordonnées du monde ; seule
|
||||
l’enveloppe extérieure change de taille. Les plans hydrauliques sont construits
|
||||
à la demande dans des régions de 512 blocs, avec une graine régionale distincte.
|
||||
Les marges d’admission sont de 16 blocs pour l’eau et 32 pour la lave. Bassins,
|
||||
berges et groupes de terrasses sont admis entiers ; les sources dépendant d’une
|
||||
coupe écartée sont retirées aussi. Aucun trajet n’est tronqué à la frontière.
|
||||
Les couches superposées et leurs sources sont ensuite traduites ensemble.
|
||||
|
||||
Les forêts, minerais et décorations utilisent toute l’emprise du profil.
|
||||
La recherche facultative d’un arbre remarquable reste unique et centrale.
|
||||
Les sources et exutoires n’agissent que pendant la génération, avec les
|
||||
marqueurs vanilla consommés ; recharger un ancien chunk ne rouvre pas une bouche.
|
||||
|
||||
Les trois tailles passent onze tests requis sur la graine `0`, avec 1 800 ticks
|
||||
de fluides et un aller-retour réel des paramètres sur disque. Le fichier
|
||||
`data/minecraft/world_gen_settings.dat` conserve la clé du profil choisi.
|
||||
212 anciens fichiers de production sont strictement identiques, tandis que
|
||||
huit fichiers d’intégration ou d’interface exposent les nouvelles générations.
|
||||
Le premier calcul d’une région reste coûteux ; les résultats ne sont pas un
|
||||
test de cent connexions ni une garantie de génération rapide de toute l’île.
|
||||
Voir les emprises et durées dans [Validation](testing.md).
|
||||
|
||||
## Cavités et terrasses d’eau — génération v9
|
||||
|
||||
Le nouveau réglage est `sanctuary:sanctuary_cavern`, avec le biome source
|
||||
|
||||
+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.9
|
||||
pack_version=0.1.0-alpha.9
|
||||
mod_version=0.1.0-alpha.11
|
||||
pack_version=0.1.0-alpha.11
|
||||
maven_group=fr.koka.sanctuary
|
||||
|
||||
@@ -33,10 +33,21 @@ tasks.register('cleanGameTestWorld', Delete) {
|
||||
tasks.named('runGameTest') {
|
||||
dependsOn('cleanGameTestWorld')
|
||||
systemProperty('sanctuary.test.seed', providers.gradleProperty('sanctuaryTestSeed').getOrElse('0'))
|
||||
systemProperty('sanctuary.test.players', providers.gradleProperty('sanctuaryTestPlayers').getOrElse('10'))
|
||||
systemProperty('sanctuary.test.resourceSurvey', providers.gradleProperty('sanctuaryResourceSurvey').getOrElse('sample'))
|
||||
systemProperty('sanctuary.test.densityMaps', providers.gradleProperty('sanctuaryDensityMaps').getOrElse('true'))
|
||||
}
|
||||
|
||||
tasks.named('processGametestResources') {
|
||||
def benchmark = providers.gradleProperty('sanctuaryBenchmark').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('benchmark', benchmark)
|
||||
filesMatching('fabric.mod.json') {
|
||||
expand(gametest_entrypoint: benchmark
|
||||
? 'fr.koka.sanctuary.gametest.WorldgenPerformanceGameTests'
|
||||
: 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests')
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
minecraft "com.mojang:minecraft:${rootProject.minecraft_version}"
|
||||
implementation "net.fabricmc:fabric-loader:${rootProject.loader_version}"
|
||||
@@ -203,3 +214,30 @@ tasks.register('cavernLavaSmoke', JavaExec) {
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.CavernLavaSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('cavernLavaSmoke') }
|
||||
|
||||
tasks.register('populationCapacitySmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Check supported capacities, area scaling, finite boundaries and regional ownership.'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.PopulationCapacitySmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('populationCapacitySmoke') }
|
||||
|
||||
tasks.register('populationRegionPlanSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Check atomic regional admission, hydraulic dependencies and translated chunk indices.'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.PopulationRegionPlanSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('populationRegionPlanSmoke') }
|
||||
|
||||
tasks.register('populationPlanCacheSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Check that completed regions remain readable while new regions are planned.'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.PopulationPlanCacheSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('populationPlanCacheSmoke') }
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
// Frozen test oracle from a26b72f PopulationIslandDensity.java.
|
||||
// Original SHA-256: fcc5a8a39b7c4d181d74ce4f9183a1ca93646137955ac64bab9d22e6db0752a1
|
||||
// Only package/imports/class name differ; do not optimize this reference.
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.*;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import net.minecraft.util.Interval;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.*;
|
||||
|
||||
/** Capacity changes the envelope; all natural noise is still sampled at world coordinates. */
|
||||
public record Alpha10PopulationDensityReference(DensityFunction terrain, DensityFunction distortion,
|
||||
DensityFunction sculpt, DensityFunction detail, DensityFunction underside,
|
||||
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity) implements DensityFunction {
|
||||
public static final MapCodec<Alpha10PopulationDensityReference> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||
DensityFunction.CODEC.fieldOf("terrain").forGetter(Alpha10PopulationDensityReference::terrain),
|
||||
DensityFunction.CODEC.fieldOf("distortion").forGetter(Alpha10PopulationDensityReference::distortion),
|
||||
DensityFunction.CODEC.fieldOf("sculpt").forGetter(Alpha10PopulationDensityReference::sculpt),
|
||||
DensityFunction.CODEC.fieldOf("detail").forGetter(Alpha10PopulationDensityReference::detail),
|
||||
DensityFunction.CODEC.fieldOf("underside").forGetter(Alpha10PopulationDensityReference::underside),
|
||||
DensityFunction.CODEC.fieldOf("rift_warp").forGetter(Alpha10PopulationDensityReference::riftWarp),
|
||||
DensityFunction.CODEC.fieldOf("rift_detail").forGetter(Alpha10PopulationDensityReference::riftDetail),
|
||||
IslandCapacity.CODEC.optionalFieldOf("players", IslandCapacity.TWENTY).forGetter(Alpha10PopulationDensityReference::capacity)
|
||||
).apply(instance, Alpha10PopulationDensityReference::new));
|
||||
|
||||
@Override public DensitySampler compileSampler(CompileContext context) {
|
||||
var raw = terrain.compileSampler(context); var edge = distortion.compileSampler(context);
|
||||
var coarse = sculpt.compileSampler(context); var fine = detail.compileSampler(context);
|
||||
var bottom = underside.compileSampler(context); var warp = riftWarp.compileSampler(context);
|
||||
var fractureDetail = riftDetail.compileSampler(context);
|
||||
long seed = context.createRandom(SanctuaryMod.id("population_rifts_alpha10")).nextLong();
|
||||
var layouts = new ConcurrentHashMap<IslandCapacity.Region, RiftShape.Layout>();
|
||||
return new DensitySampler() {
|
||||
@Override public float sampleValue(SamplerContext sample, int x, int y, int z) {
|
||||
if (Math.hypot((double) x, z) >= capacity.terrainLimit() || y <= 0 || y >= 384) return -1;
|
||||
int localY = y - 64;
|
||||
float natural = PopulationIslandShape.raisedDensity(x, y, z, raw.sampleValue(sample, x, localY, z),
|
||||
edge.sampleValue(sample, x, 0, z), coarse.sampleValue(sample, x, localY, z),
|
||||
fine.sampleValue(sample, x, localY, z), bottom.sampleValue(sample, x, localY, z), 64, 384, capacity);
|
||||
if (natural <= 0) return natural;
|
||||
var region = capacity.regionAt(x, z);
|
||||
var layout = layouts.computeIfAbsent(region, r -> RiftShape.layout(regionSeed(seed, r)));
|
||||
int rx = x - region.originX(), rz = z - region.originZ();
|
||||
if (!RiftShape.mayAffect(layout, rx, rz)) return natural;
|
||||
return RiftShape.density(layout, rx, y, rz, natural,
|
||||
warp.sampleValue(sample, x, y, z), fractureDetail.sampleValue(sample, x, y, z));
|
||||
}
|
||||
@Override public void sampleVolume(SamplerContext sample, DensityBuffer buffer, DensityVolume volume) {
|
||||
DensitySampler.sampleVolumeNaive(sample, buffer, volume, this);
|
||||
}
|
||||
};
|
||||
}
|
||||
public static long regionSeed(long seed, IslandCapacity.Region region) {
|
||||
long value = seed ^ ((long) region.x() << 32) ^ (region.z() & 0xffffffffL) ^ 0xA10CA7E1L;
|
||||
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
|
||||
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
|
||||
return value ^ (value >>> 31);
|
||||
}
|
||||
@Override public DensityFunction rewriteChildren(DfRewriteRule rule) {
|
||||
return new Alpha10PopulationDensityReference(rule.rewrite(terrain), rule.rewrite(distortion), rule.rewrite(sculpt),
|
||||
rule.rewrite(detail), rule.rewrite(underside), rule.rewrite(riftWarp), rule.rewrite(riftDetail), capacity);
|
||||
}
|
||||
@Override public Interval range() { return Interval.of(-1, 1); }
|
||||
@Override public int domainAxes() { return ALL_AXES; }
|
||||
@Override public MapCodec<Alpha10PopulationDensityReference> codec() { return CODEC; }
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.ExactInterpolationChecks;
|
||||
import fr.koka.sanctuary.worldgen.ExactInterpolationSampler;
|
||||
import fr.koka.sanctuary.worldgen.IslandCapacity;
|
||||
import fr.koka.sanctuary.worldgen.PopulationIslandDensity;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Executors;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DensityBuffer;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DensitySampler;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DensityVolume;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
|
||||
/** Compares production against the frozen alpha.10 implementation; no chunks are created. */
|
||||
final class PopulationDensityEquivalenceDiagnostics {
|
||||
private record Pair(int players, long seed, DensitySampler reference, DensitySampler actual) {}
|
||||
|
||||
static void verify(GameTestHelper helper) throws Exception {
|
||||
Map<String, Object> adversarial = ExactInterpolationChecks.verify();
|
||||
var registries = helper.getLevel().registryAccess();
|
||||
var noises = registries.lookupOrThrow(Registries.NOISE);
|
||||
var densities = registries.lookupOrThrow(Registries.DENSITY_FUNCTION);
|
||||
var settings = registries.lookupOrThrow(Registries.NOISE_SETTINGS);
|
||||
List<Map<String, Object>> reports = new ArrayList<>();
|
||||
List<Pair> pairs = new ArrayList<>();
|
||||
for (int players : new int[]{5, 10, 20, 100}) {
|
||||
var key = SanctuaryMod.id("population_" + players);
|
||||
var registered = densities.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, key)).value();
|
||||
helper.assertTrue(registered instanceof PopulationIslandDensity, "Compare the real registered Population density");
|
||||
var density = (PopulationIslandDensity) registered;
|
||||
helper.assertTrue(ExactInterpolationSampler.supportsTerrain(density.terrain()),
|
||||
"The shipped coordinate-only terrain must actually use the exact cache, not the fallback");
|
||||
var reference = new Alpha10PopulationDensityReference(density.terrain(), density.distortion(), density.sculpt(),
|
||||
density.detail(), density.underside(), density.riftWarp(), density.riftDetail(), density.capacity());
|
||||
for (long seed : new long[]{0, 42, 8675309}) {
|
||||
var state = RandomState.create(noises, seed,
|
||||
settings.getOrThrow(ResourceKey.create(Registries.NOISE_SETTINGS, key)).value());
|
||||
var pair = new Pair(players, seed, state.getSampler(reference), state.getSampler(density));
|
||||
pairs.add(pair);
|
||||
SamplerContext context = SamplerContext.builder().enableCaches().build();
|
||||
long comparisons = 0, digest = 0xcbf29ce484222325L;
|
||||
for (var volume : volumes(IslandCapacity.fromPlayers(players))) {
|
||||
DensityBuffer expected = DensityBuffer.createUnpooled(volume.size());
|
||||
DensityBuffer actual = DensityBuffer.createUnpooled(volume.size());
|
||||
pair.reference().sampleVolume(SamplerContext.EMPTY_UNCACHED, expected, volume);
|
||||
pair.actual().sampleVolume(context, actual, volume);
|
||||
for (int z = 0; z < volume.sizeZ(); z++) for (int x = 0; x < volume.sizeX(); x++) for (int y = 0; y < volume.sizeY(); y++) {
|
||||
int index = volume.indexUnchecked(x, y, z);
|
||||
int wx = volume.blockX(x), wy = volume.blockY(y), wz = volume.blockZ(z);
|
||||
same(pair, expected.get(index), actual.get(index), wx, wy, wz, "bulk");
|
||||
same(pair, expected.get(index), pair.actual().sampleValue(context, wx, wy, wz), wx, wy, wz, "scalar after bulk");
|
||||
comparisons += 2;
|
||||
digest = (digest ^ Float.floatToRawIntBits(expected.get(index))) * 0x100000001b3L;
|
||||
}
|
||||
}
|
||||
Random order = new Random(seed ^ players);
|
||||
int limit = density.capacity().terrainLimit();
|
||||
for (int i = 0; i < 512; i++) {
|
||||
int x = order.nextInt(limit * 2 + 33) - limit - 16;
|
||||
int y = order.nextInt(400) - 8;
|
||||
int z = order.nextInt(limit * 2 + 33) - limit - 16;
|
||||
float expected = pair.reference().sampleValue(SamplerContext.EMPTY_UNCACHED, x, y, z);
|
||||
same(pair, expected, pair.actual().sampleValue(SamplerContext.EMPTY_UNCACHED, x, y, z), x, y, z, "uncached context");
|
||||
same(pair, expected, pair.actual().sampleValue(context, x, y, z), x, y, z, "context switch");
|
||||
comparisons += 2;
|
||||
}
|
||||
reports.add(Map.of("players", players, "seed", seed, "bitwise_comparisons", comparisons,
|
||||
"reference_density_digest", Long.toUnsignedString(digest, 16), "optimized_path_supported", true));
|
||||
SanctuaryMod.LOGGER.info("Exact density equivalence: players {}, seed {}, {} bitwise comparisons", players, seed, comparisons);
|
||||
}
|
||||
}
|
||||
long concurrentComparisons = 0;
|
||||
try (var executor = Executors.newFixedThreadPool(3)) {
|
||||
List<Callable<Long>> tasks = new ArrayList<>();
|
||||
for (int worker = 0; worker < 3; worker++) {
|
||||
int id = worker;
|
||||
tasks.add(() -> {
|
||||
long count = 0;
|
||||
SamplerContext local = SamplerContext.builder().enableCaches().build();
|
||||
// All workers share the same samplers and alternate worlds/seeds.
|
||||
// Only their scratch buffers and sampler contexts are thread-local.
|
||||
for (int round = 0; round < 3; round++) for (var pair : pairs) {
|
||||
var volume = new DensityVolume(9, 17, 7, -13 + id, 89 + round, -7 - id);
|
||||
DensityBuffer actual = DensityBuffer.createUnpooled(volume.size());
|
||||
pair.actual().sampleVolume(local, actual, volume);
|
||||
for (int z = 0; z < volume.sizeZ(); z++) for (int x = 0; x < volume.sizeX(); x++) for (int y = 0; y < volume.sizeY(); y++) {
|
||||
int wx = volume.blockX(x), wy = volume.blockY(y), wz = volume.blockZ(z);
|
||||
same(pair, pair.reference().sampleValue(local, wx, wy, wz),
|
||||
actual.get(volume.indexUnchecked(x, y, z)), wx, wy, wz, "concurrent bulk");
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
});
|
||||
}
|
||||
for (var result : executor.invokeAll(tasks)) concurrentComparisons += result.get();
|
||||
}
|
||||
Path report = Path.of("diagnostics", "population-alpha10-density-equivalence.json");
|
||||
Files.createDirectories(report.getParent());
|
||||
Files.writeString(report, new GsonBuilder().setPrettyPrinting().create().toJson(Map.of(
|
||||
"reference_commit", "a26b72f", "reference_source_sha256", "fcc5a8a39b7c4d181d74ce4f9183a1ca93646137955ac64bab9d22e6db0752a1",
|
||||
"comparison", "Float.floatToRawIntBits; no tolerance", "profiles", reports, "adversarial", adversarial,
|
||||
"concurrent_comparisons", concurrentComparisons, "concurrent_workers", 3, "passed", true,
|
||||
"scope", "Real compiled scalar/bulk densities, 4 capacities x 3 seeds (5/20/100 compatibility and new10 using the same parameterized algorithm), signed coordinates, region/coast/height boundaries, context switches and concurrency; no block changes")) + "\n");
|
||||
}
|
||||
|
||||
private static List<DensityVolume> volumes(IslandCapacity capacity) {
|
||||
int limit = capacity.terrainLimit(), middle = (int) capacity.radius() / 2;
|
||||
return List.of(
|
||||
new DensityVolume(16, 384, 16, -8, 0, -8),
|
||||
new DensityVolume(19, 11, 17, -263, 61, -259),
|
||||
new DensityVolume(17, 17, 17, 249, 124, 249),
|
||||
new DensityVolume(13, 21, 11, 761, 167, -9),
|
||||
new DensityVolume(13, 21, 11, -775, 103, 503),
|
||||
new DensityVolume(17, 49, 5, limit - 12, 96, -2),
|
||||
new DensityVolume(17, 49, 5, -limit - 4, 96, -2),
|
||||
new DensityVolume(12, 33, 12, middle - 7, 90, -middle / 2 - 5),
|
||||
new DensityVolume(5, 13, 5, -2, -4, -2),
|
||||
new DensityVolume(5, 13, 5, -2, 378, -2),
|
||||
new DensityVolume(9, 13, 7, -257, 61, -9, 3, 5, 2));
|
||||
}
|
||||
|
||||
private static void same(Pair pair, float expected, float actual, int x, int y, int z, String phase) {
|
||||
if (Float.floatToRawIntBits(expected) != Float.floatToRawIntBits(actual))
|
||||
throw new AssertionError("Density changed from alpha10 in " + phase + ": players=" + pair.players() + ", seed=" + pair.seed()
|
||||
+ ", position=" + x + "," + y + "," + z + ", expected=" + Float.toHexString(expected) + ", actual=" + Float.toHexString(actual));
|
||||
}
|
||||
}
|
||||
+613
@@ -0,0 +1,613 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.*;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
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.MultifaceBlock;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
/** Bounded engine probes at the centre AND in newly available terrain. No whole-island scan. */
|
||||
public final class PopulationDiagnostics {
|
||||
public static int regionsAtInitialSpawn;
|
||||
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST};
|
||||
private PopulationDiagnostics() {}
|
||||
|
||||
static void verifyLegacyIsolation(GameTestHelper helper) {
|
||||
var level = helper.getLevel();
|
||||
var generator = (NoiseBasedChunkGenerator) level.getChunkSource().getGenerator();
|
||||
var capacity = IslandCapacity.forGenerator(generator);
|
||||
helper.assertTrue(capacity != null && capacity.players() == Integer.getInteger("sanctuary.test.players", 10),
|
||||
"The engine must create the requested production world preset");
|
||||
helper.assertFalse(HydrologyRuntime.enabled(generator) || NaturalHydrologyRuntime.enabled(generator)
|
||||
|| LayeredHydrologyRuntime.enabled(generator) || WoodlandHydrologyRuntime.enabled(generator)
|
||||
|| RiftHydrologyRuntime.enabled(generator) || CavernHydrologyRuntime.enabled(generator),
|
||||
"Only the population hydrology pipeline may run with the new settings");
|
||||
var settings = level.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS);
|
||||
for (var key : List.of(SanctuarySpawn.SETTINGS, SanctuarySpawn.RAISED_SETTINGS, SanctuarySpawn.HYDROLOGY_SETTINGS,
|
||||
SanctuarySpawn.NATURAL_SETTINGS, SanctuarySpawn.LAYERED_SETTINGS, SanctuarySpawn.WOODLAND_SETTINGS,
|
||||
SanctuarySpawn.RIFT_SETTINGS, SanctuarySpawn.CAVERN_SETTINGS)) {
|
||||
var old = new NoiseBasedChunkGenerator(generator.getBiomeSource(), settings.getOrThrow(key));
|
||||
helper.assertFalse(PopulationHydrologyRuntime.enabled(old), "A saved settings key must stay isolated: " + key);
|
||||
helper.assertFalse(new PopulationGrovesFeature().place(level, old, RandomSource.create(0), BlockPos.ZERO),
|
||||
"New trees must refuse older saved worlds");
|
||||
helper.assertFalse(new PopulationDecorationsFeature().place(level, old, RandomSource.create(0), BlockPos.ZERO),
|
||||
"New cave decorations must refuse older saved worlds");
|
||||
}
|
||||
helper.assertTrue(generator.getBiomeSource() instanceof PopulationIslandBiomeSource,
|
||||
"The biome palette must use the new versioned resource family");
|
||||
helper.assertTrue(generator.getBiomeSource().possibleBiomes().size() == 10
|
||||
&& generator.getBiomeSource().possibleBiomes().stream().allMatch(b -> b.getRegisteredName().startsWith("sanctuary:population_")),
|
||||
"All ten layers of the ecology must remain available at each capacity");
|
||||
}
|
||||
|
||||
static void verifyDensity(GameTestHelper helper) throws IOException {
|
||||
var level = helper.getLevel();
|
||||
var generator = (NoiseBasedChunkGenerator) level.getChunkSource().getGenerator();
|
||||
var capacity = IslandCapacity.forGenerator(generator);
|
||||
var density = level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION).getOrThrow(
|
||||
ResourceKey.create(Registries.DENSITY_FUNCTION, SanctuaryMod.id("population_" + capacity.players()))).value();
|
||||
helper.assertTrue(density instanceof PopulationIslandDensity, "Inspect the actual compiled population density");
|
||||
var island = (PopulationIslandDensity) density;
|
||||
helper.assertTrue(island.capacity() == capacity, "Noise settings and density must agree on saved capacity");
|
||||
var random = level.getChunkSource().randomState();
|
||||
var replay = RandomState.create(level.registryAccess().lookupOrThrow(Registries.NOISE), level.getSeed(), generator.generatorSettings().value());
|
||||
int step = Math.max(8, (int) Math.ceil(capacity.terrainLimit() / 32.0));
|
||||
int extent = (int) Math.ceil(capacity.terrainLimit() / (double) step) * step;
|
||||
int solidColumns = 0, outerColumns = 0, emptyColumns = 0;
|
||||
List<int[]> landCoordinates = new ArrayList<>();
|
||||
Set<Integer> bottomHeights = new TreeSet<>();
|
||||
StringBuilder map = new StringBuilder("x,z,bottom_y,top_y\n");
|
||||
for (int x = -extent; x <= extent; x += step) for (int z = -extent; z <= extent; z += step) {
|
||||
int bottom = -1, top = -1;
|
||||
for (int y = 8; y < 384; y += 8) {
|
||||
float value = random.sampleBlockValueUncached(density, x, y, z);
|
||||
float raw = random.sampleBlockValueUncached(island.terrain(), x, y - 64, z);
|
||||
helper.assertTrue(Float.isFinite(value), "Density remains finite at every sampled position");
|
||||
helper.assertTrue(value <= 0 || raw > 0, "Enlarging the envelope must never invent terrain inside natural holes");
|
||||
if (value > 0) { if (bottom < 0) bottom = y; top = y; }
|
||||
if (x % (step * 8) == 0 && z % (step * 8) == 0)
|
||||
helper.assertTrue(Float.floatToIntBits(value) == Float.floatToIntBits(replay.sampleBlockValueUncached(density, x, y, z)),
|
||||
"Seed and capacity must replay identically without depending on region request order");
|
||||
}
|
||||
if (top >= 0) {
|
||||
solidColumns++;
|
||||
landCoordinates.add(new int[]{x, z});
|
||||
bottomHeights.add(bottom);
|
||||
if (Math.hypot(x, z) > IslandShape.TERRAIN_LIMIT + 16) outerColumns++;
|
||||
helper.assertTrue(Math.hypot(x, z) < capacity.terrainLimit(), "Terrain stays inside its finite declared boundary");
|
||||
} else emptyColumns++;
|
||||
map.append(x).append(',').append(z).append(',').append(bottom).append(',').append(top).append('\n');
|
||||
}
|
||||
helper.assertTrue(solidColumns > 100 && emptyColumns > 100 && bottomHeights.size() >= 4,
|
||||
"The sampled island must contain natural masses, holes and a varied underside");
|
||||
int additionalColumns = 0;
|
||||
if (capacity == IslandCapacity.TEN) {
|
||||
var small = level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION).getOrThrow(
|
||||
ResourceKey.create(Registries.DENSITY_FUNCTION, SanctuaryMod.id("population_5"))).value();
|
||||
for (int[] coordinate : landCoordinates) {
|
||||
boolean presentInSmall = false;
|
||||
for (int y = 8; y < 384 && !presentInSmall; y += 8)
|
||||
presentInSmall = random.sampleBlockValueUncached(small, coordinate[0], y, coordinate[1]) > 0;
|
||||
if (!presentInSmall) additionalColumns++;
|
||||
}
|
||||
helper.assertTrue(additionalColumns > 0,
|
||||
"Large must add real land columns compared with Small on the same seed and sample grid");
|
||||
} else helper.assertTrue(capacity.players() == 5 ? outerColumns == 0 : outerColumns > 100,
|
||||
"The historical large capacities must contain land beyond the old island boundary");
|
||||
Path directory = Path.of("diagnostics");
|
||||
Files.createDirectories(directory);
|
||||
String prefix = "population-" + capacity.players() + "-seed-" + level.getSeed();
|
||||
Files.writeString(directory.resolve(prefix + "-density.csv"), map.toString());
|
||||
Files.writeString(directory.resolve(prefix + "-density.json"), json(Map.of("players", capacity.players(),
|
||||
"seed", level.getSeed(), "step", step, "solid_columns", solidColumns, "empty_columns", emptyColumns,
|
||||
"solid_columns_beyond_old_boundary", outerColumns, "underside_heights", bottomHeights,
|
||||
"scope", "sparse actual density samples; no resource extrapolation", "passed", true,
|
||||
"additional_columns_vs_small", capacity == IslandCapacity.TEN ? additionalColumns : -1)));
|
||||
SanctuaryMod.LOGGER.info("Capacity {} density seed {}: {} land columns, {} beyond the old envelope",
|
||||
capacity.players(), level.getSeed(), solidColumns, outerColumns);
|
||||
}
|
||||
|
||||
static void verifyExterior(GameTestHelper helper) {
|
||||
var level = helper.getLevel();
|
||||
var capacity = IslandCapacity.forGenerator(level.getChunkSource().getGenerator());
|
||||
int edge = Math.floorDiv(capacity.terrainLimit() + 48, 16) + 1;
|
||||
for (int[] point : new int[][]{{edge,0},{-edge-1,0},{0,edge},{0,-edge-1},{256,0},{-257,0}}) {
|
||||
var chunk = full(helper, level, point[0], point[1]);
|
||||
for (var section : chunk.getSections()) helper.assertTrue(section.hasOnlyAir(),
|
||||
"Completed terrain and decoration must remain void beyond this capacity: " + chunk.getPos());
|
||||
}
|
||||
}
|
||||
|
||||
static void verifyLowerMargin(GameTestHelper helper) {
|
||||
var level = helper.getLevel();
|
||||
for (int[] point : new int[][]{{0,0},{-8,0},{8,0},{0,-8},{0,8}}) {
|
||||
var chunk = full(helper, level, point[0], point[1]);
|
||||
for (int x = 0; x < 16; x++) for (int z = 0; z < 16; z++) for (int y = 0; y < 16; y++) {
|
||||
var pos = new BlockPos(point[0]*16+x, y, point[1]*16+z);
|
||||
var block = chunk.getBlockState(pos);
|
||||
helper.assertTrue(block.isAir() || block.getFluidState().is(FluidTags.WATER) || block.getFluidState().is(FluidTags.LAVA),
|
||||
"The increased horizontal size must not add a lower floor: " + pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void start(GameTestHelper helper) throws IOException {
|
||||
var context = new Context(helper);
|
||||
try { context.prepare(); }
|
||||
catch (RuntimeException | Error | IOException e) { context.cleanup(); throw e; }
|
||||
helper.onEachTick(() -> {
|
||||
try { context.tick(); }
|
||||
catch (RuntimeException | Error e) { context.cleanup(); throw e; }
|
||||
});
|
||||
}
|
||||
|
||||
private static final class Context {
|
||||
final GameTestHelper helper;
|
||||
final ServerLevel level;
|
||||
final NoiseBasedChunkGenerator generator;
|
||||
final IslandCapacity capacity;
|
||||
final List<PopulationHydrologyRuntime.RegionPlan> regions = new ArrayList<>();
|
||||
final Map<Long, LevelChunk> chunks = new TreeMap<>();
|
||||
final Set<Long> forced = new HashSet<>();
|
||||
final Set<BlockPos> expectedWater = new HashSet<>();
|
||||
final Map<BlockPos, PopulationHydrology.Cell> selectedCells = new LinkedHashMap<>();
|
||||
final Map<BlockPos, BlockState> naturalLogs = new HashMap<>();
|
||||
final Set<BlockPos> declaredWater = new HashSet<>();
|
||||
final Set<BlockPos> expectedLava = new HashSet<>();
|
||||
final Set<BlockPos> lavaSupports = new HashSet<>();
|
||||
final List<PopulationLavaDeposit.Cell> pocketCells = new ArrayList<>();
|
||||
final List<PopulationLavaFlows.Fall> lavaFalls = new ArrayList<>();
|
||||
final List<Map<String,Object>> flowReports = new ArrayList<>();
|
||||
int reentryWitnesses;
|
||||
final List<PopulationHydrology.Spring> springs = new ArrayList<>();
|
||||
final List<PopulationHydrology.Spill> spills = new ArrayList<>();
|
||||
final List<Map<String,Object>> regionReports = new ArrayList<>();
|
||||
long startTime;
|
||||
boolean running, closed;
|
||||
|
||||
Context(GameTestHelper helper) {
|
||||
this.helper = helper; level = helper.getLevel();
|
||||
generator = (NoiseBasedChunkGenerator) level.getChunkSource().getGenerator();
|
||||
capacity = IslandCapacity.forGenerator(generator);
|
||||
}
|
||||
|
||||
void prepare() throws IOException {
|
||||
helper.assertTrue(regionsAtInitialSpawn > 0 && regionsAtInitialSpawn <= 4,
|
||||
"Initial spawn must plan only nearby regions, not materialize the large island: " + regionsAtInitialSpawn);
|
||||
List<IslandCapacity.Region> targets = new ArrayList<>();
|
||||
targets.add(new IslandCapacity.Region(0,0));
|
||||
if (capacity.players() == 20) targets.add(new IslandCapacity.Region(1,0));
|
||||
if (capacity.players() == 100) {
|
||||
targets.add(new IslandCapacity.Region(1,1));
|
||||
targets.add(new IslandCapacity.Region(2,0));
|
||||
}
|
||||
Set<Long> requested = new TreeSet<>();
|
||||
for (var target : targets) {
|
||||
SanctuaryMod.LOGGER.info("Capacity {}: inspecting hydrology in region {}", capacity.players(), target);
|
||||
var region = PopulationHydrologyRuntime.regionPlan(generator, level.getChunkSource().randomState(), target);
|
||||
regions.add(region);
|
||||
var plan = region.water();
|
||||
checkOwnership(region);
|
||||
var ids = new HashSet<Long>();
|
||||
plan.features().stream().filter(f -> f.kind() == PopulationHydrology.Kind.LAKE || f.kind() == PopulationHydrology.Kind.POND)
|
||||
.max(Comparator.comparingInt(PopulationHydrology.Feature::waterCells)).ifPresent(f -> ids.add(f.id()));
|
||||
if (!plan.terraces().isEmpty()) ids.addAll(plan.terraces().getFirst().basinIds());
|
||||
if (ids.isEmpty()) plan.features().stream().findFirst().ifPresent(f -> ids.add(f.id()));
|
||||
for (var cell : plan.cells()) if (ids.contains(cell.featureId())) {
|
||||
selectedCells.put(new BlockPos(cell.x(), cell.bedY(), cell.z()), cell);
|
||||
request(requested, cell.x() >> 4, cell.z() >> 4, 1);
|
||||
if (cell.hasWater()) for (int y = cell.bedY()+1; y <= cell.waterY(); y++) expectedWater.add(new BlockPos(cell.x(),y,cell.z()));
|
||||
}
|
||||
for (var spill : plan.spills()) if (ids.contains(spill.fromFeatureId())) {
|
||||
spills.add(spill); request(requested, spill.flowBounds());
|
||||
}
|
||||
if (!plan.springs().isEmpty()) {
|
||||
var spring = plan.springs().getFirst(); springs.add(spring);
|
||||
request(requested, spring.flowBounds()); expectedWater.add(pos(spring.source()));
|
||||
}
|
||||
// Inspect the finite pocket and at most one volcanic fall per selected region.
|
||||
var lava = region.lava();
|
||||
Set<BlockPos> allFallSources = new HashSet<>(), allFallSupports = new HashSet<>();
|
||||
for (var fall : lava.falls()) {
|
||||
allFallSources.add(lavaPos(fall.source()));
|
||||
fall.supports().forEach(p -> allFallSupports.add(lavaPos(p)));
|
||||
}
|
||||
for (var cell : lava.cells()) {
|
||||
BlockPos p = new BlockPos(cell.x(), cell.y(), cell.z());
|
||||
if (allFallSources.contains(p)) continue;
|
||||
pocketCells.add(cell);
|
||||
request(requested, cell.x() >> 4, cell.z() >> 4, 1);
|
||||
if (cell.lava()) expectedLava.add(p);
|
||||
}
|
||||
for (var support : lava.supports()) if (!allFallSupports.contains(lavaPos(support))) {
|
||||
lavaSupports.add(lavaPos(support));
|
||||
request(requested, support.x() >> 4, support.z() >> 4, 1);
|
||||
}
|
||||
if (!lava.falls().isEmpty()) {
|
||||
var fall = lava.falls().getFirst(); lavaFalls.add(fall);
|
||||
request(requested, fall.flowBounds()); expectedLava.add(lavaPos(fall.source()));
|
||||
fall.supports().forEach(p -> lavaSupports.add(lavaPos(p)));
|
||||
}
|
||||
// A small, independently selected patch proves full ecology beyond the former radius.
|
||||
int ox = target.originX(), oz = target.originZ();
|
||||
int bestX = ox, bestZ = oz; int highest = -1;
|
||||
for (int dx = -144; dx <= 144; dx += 48) for (int dz = -144; dz <= 144; dz += 48) {
|
||||
int x = ox+dx, z=oz+dz;
|
||||
int top = generator.getBaseHeight(x,z,net.minecraft.world.level.levelgen.Heightmap.Types.WORLD_SURFACE,
|
||||
level,level.getChunkSource().randomState());
|
||||
if (top > highest) { highest=top; bestX=x; bestZ=z; }
|
||||
}
|
||||
helper.assertTrue(highest > 48, "Every inspected region must contain real natural terrain");
|
||||
request(requested, bestX >> 4, bestZ >> 4, 2);
|
||||
regionReports.add(new LinkedHashMap<>(Map.of("region_x",target.x(),"region_z",target.z(),
|
||||
"planned_features",plan.features().size(),"planned_cells",plan.cells().size(),
|
||||
"planned_springs",plan.springs().size(),"planned_terraces",plan.terraces().size(),
|
||||
"ecology_patch_x",bestX,"ecology_patch_z",bestZ)));
|
||||
}
|
||||
helper.assertTrue(!expectedWater.isEmpty(), "The test must inspect retained production water");
|
||||
if (capacity.players() > 10) helper.assertTrue(regions.stream().skip(1).anyMatch(r -> !r.water().cells().isEmpty()),
|
||||
"Hydrology must occur on newly available terrain, not only around the old central island");
|
||||
SanctuaryMod.LOGGER.info("Capacity {}: preparing {} bounded FULL chunks",capacity.players(),requested.size());
|
||||
for (long packed : requested) {
|
||||
var p=ChunkPos.unpack(packed);
|
||||
if (level.setChunkForced(p.x(),p.z(),true)) forced.add(packed);
|
||||
}
|
||||
for (long packed : requested) {
|
||||
var p=ChunkPos.unpack(packed); chunks.put(packed,full(helper,level,p.x(),p.z()));
|
||||
}
|
||||
for (long packed : requested) level.waitForEntities(ChunkPos.unpack(packed),0);
|
||||
declaredWater.addAll(expectedWater);
|
||||
captureNaturalLogs();
|
||||
captureEcology();
|
||||
export(false,0);
|
||||
}
|
||||
|
||||
void checkOwnership(PopulationHydrologyRuntime.RegionPlan region) {
|
||||
var owner=region.region(); var plan=region.water();
|
||||
Set<Long> ids=new HashSet<>();
|
||||
for(var feature:plan.features()) helper.assertTrue(ids.add(feature.id()),"Feature IDs must be unique within a region");
|
||||
for(var cell:plan.cells()) {
|
||||
helper.assertTrue(cell.x()>=owner.minX() && cell.x()<=owner.maxX() && cell.z()>=owner.minZ() && cell.z()<=owner.maxZ(),
|
||||
"Hydrology must never be truncated at or written across region ownership: "+cell);
|
||||
helper.assertTrue(ids.contains(cell.featureId()),"Each retained cell needs its complete owning feature");
|
||||
for(int y=cell.bedY()-cell.sedimentDepth()-1;y<=cell.bedY();y++)
|
||||
helper.assertTrue(level.getChunkSource().randomState().sampleBlockValueUncached(
|
||||
generator.generatorSettings().value().noiseRouter().finalDensity(),cell.x(),y,cell.z())>0,
|
||||
"Regional translation must retain the real natural support below sediment: "+cell);
|
||||
}
|
||||
for(var terrace:plan.terraces()) helper.assertTrue(ids.containsAll(terrace.basinIds()),
|
||||
"Admission must retain or reject a complete group of water terraces");
|
||||
for(var spill:plan.spills()) helper.assertTrue(ids.contains(spill.fromFeatureId())
|
||||
&& (spill.toFeatureId()==-1 || ids.contains(spill.toFeatureId())),
|
||||
"No cascade may refer to a basin removed by region admission");
|
||||
for(var bounds:region.lava().flowBounds()) helper.assertTrue(bounds.minX()>=owner.minX() && bounds.maxX()<=owner.maxX()
|
||||
&& bounds.minZ()>=owner.minZ() && bounds.maxZ()<=owner.maxZ(),"Volcanic flows must respect the same ownership");
|
||||
}
|
||||
|
||||
void captureEcology() {
|
||||
for (int i=0;i<regions.size();i++) {
|
||||
var region=regions.get(i).region(); var counts=new TreeMap<String,Long>();
|
||||
long logs=0,ores=0,nonAir=0;
|
||||
for(var chunk:chunks.values()) {
|
||||
int x=chunk.getPos().getMinBlockX(),z=chunk.getPos().getMinBlockZ();
|
||||
if (!capacity.regionAt(x,z).equals(region)) continue;
|
||||
for(var section:chunk.getSections()) section.getStates().count((state,count)->{
|
||||
if(!state.isAir()) counts.merge(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(),(long)count,Long::sum);
|
||||
});
|
||||
}
|
||||
for(var entry:counts.entrySet()) {
|
||||
nonAir+=entry.getValue();
|
||||
if(entry.getKey().endsWith("_log")) logs+=entry.getValue();
|
||||
if(entry.getKey().endsWith("_ore")) ores+=entry.getValue();
|
||||
}
|
||||
helper.assertTrue(nonAir>1000,"Distant FULL chunks must contain the new island");
|
||||
helper.assertTrue(logs>0,"Trees must decorate each inspected region, including outside the old envelope");
|
||||
helper.assertTrue(ores>0,"Ore features must also decorate the new distant terrain");
|
||||
regionReports.get(i).put("observed_blocks",counts);
|
||||
regionReports.get(i).put("observed_logs",logs);
|
||||
regionReports.get(i).put("observed_ores",ores);
|
||||
}
|
||||
}
|
||||
|
||||
void tick() {
|
||||
if(closed || running || helper.getTick()<20) return;
|
||||
if(!chunks.keySet().stream().allMatch(level::isPositionTickingWithEntitiesLoaded)) {
|
||||
helper.assertTrue(helper.getTick()<800,"All forced inspection chunks must become ready for real fluid ticks");
|
||||
return;
|
||||
}
|
||||
running=true; startTime=level.getGameTime();
|
||||
verifyWater();
|
||||
verifyFinishedShell();
|
||||
verifyLava();
|
||||
helper.runAfterDelay(1800,()->{
|
||||
try {
|
||||
helper.assertTrue(chunks.keySet().stream().allMatch(level::isPositionTickingWithEntitiesLoaded),
|
||||
"Chunks must remain scheduler-ready throughout the measured fluid interval");
|
||||
verifyWater();
|
||||
verifyFinishedShell();
|
||||
verifyLava();
|
||||
for (var spring : springs) verifySpring(spring);
|
||||
for(var spill:spills) {
|
||||
Set<BlockPos> destination=null;
|
||||
if(spill.toFeatureId()!=-1) {
|
||||
destination=new HashSet<>();
|
||||
for(var region:regions) for(var cell:region.water().cells()) if(cell.featureId()==spill.toFeatureId() && cell.hasWater())
|
||||
destination.add(new BlockPos(cell.x(),cell.waterY(),cell.z()));
|
||||
}
|
||||
int drop=connectedDescent(pos(spill.source()),pos(spill.outlet()),destination);
|
||||
helper.assertTrue(drop >= (destination==null?12:4),"Each retained cascade must connect downhill to its basin or fall: "+spill);
|
||||
}
|
||||
for (var fall : lavaFalls) {
|
||||
int descent = connectedDescent(lavaPos(fall.source()), lavaPos(fall.outlet()), null, true);
|
||||
helper.assertTrue(descent >= 8, "A translated volcanic fall must descend through real lava ticks: " + fall.source());
|
||||
}
|
||||
verifyPostProcessingReentry();
|
||||
export(true,level.getGameTime()-startTime);
|
||||
helper.succeed();
|
||||
} catch (RuntimeException | Error failure) {
|
||||
try { export(false, level.getGameTime() - startTime); }
|
||||
catch (IOException e) { failure.addSuppressed(e); }
|
||||
throw failure;
|
||||
} catch(IOException e) {throw new IllegalStateException(e);}
|
||||
finally {cleanup();}
|
||||
});
|
||||
}
|
||||
|
||||
PopulationHydrology.Plan waterPlanAt(int x, int z) {
|
||||
var owner = capacity.regionAt(x, z);
|
||||
return regions.stream().filter(r -> r.region().equals(owner)).map(PopulationHydrologyRuntime.RegionPlan::water)
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
|
||||
PopulationHydrology.Cell cellAt(BlockPos p) {
|
||||
var plan = waterPlanAt(p.getX(), p.getZ());
|
||||
return plan == null ? null : plan.cellAt(p.getX(), p.getY(), p.getZ());
|
||||
}
|
||||
|
||||
float natural(BlockPos p) {
|
||||
return level.getChunkSource().randomState().sampleBlockValueUncached(
|
||||
generator.generatorSettings().value().noiseRouter().finalDensity(), p.getX(), p.getY(), p.getZ());
|
||||
}
|
||||
|
||||
void captureNaturalLogs() {
|
||||
for (BlockPos p : declaredWater) {
|
||||
BlockState state = level.getBlockState(p);
|
||||
if (state.getFluidState().is(FluidTags.WATER) && state.getFluidState().isSource()) continue;
|
||||
var cell = cellAt(p);
|
||||
boolean horizontal = state.is(BlockTags.LOGS) && state.hasProperty(BlockStateProperties.AXIS)
|
||||
&& state.getValue(BlockStateProperties.AXIS) != Direction.Axis.Y;
|
||||
boolean lowerWater = declaredWater.contains(p.below())
|
||||
&& level.getFluidState(p.below()).is(FluidTags.WATER) && level.getFluidState(p.below()).isSource();
|
||||
boolean bed = cell != null && cell.bedY() == p.getY() - 1
|
||||
&& level.getBlockState(p.below()).isCollisionShapeFullBlock(level, p.below());
|
||||
boolean beside = Arrays.stream(HORIZONTAL).map(p::relative).anyMatch(side -> declaredWater.contains(side)
|
||||
&& level.getFluidState(side).is(FluidTags.WATER) && level.getFluidState(side).isSource());
|
||||
helper.assertTrue(cell != null && horizontal && p.getY() == cell.waterY() && (lowerWater || bed) && beside,
|
||||
"Only a supported horizontal surface log beside real retained source water may occupy a planned water voxel: " + p + "=" + state);
|
||||
naturalLogs.put(p.immutable(), state); expectedWater.remove(p);
|
||||
}
|
||||
helper.assertTrue(expectedWater.size() + naturalLogs.size() == declaredWater.size(),
|
||||
"Every selected water voxel must have an exact retained source or recorded log");
|
||||
}
|
||||
|
||||
void verifyFinishedShell() {
|
||||
for (var cell : selectedCells.values()) {
|
||||
var plan = waterPlanAt(cell.x(), cell.z());
|
||||
var kind = plan.features().stream().filter(f -> f.id() == cell.featureId()).findFirst().orElseThrow().kind();
|
||||
int cut = !cell.hasWater() ? kind == PopulationHydrology.Kind.RIVER ? PopulationHydrology.MAX_RIVER_BANK_CARVE : 2
|
||||
: kind == PopulationHydrology.Kind.TERRACE ? PopulationTerraces.MAX_CARVE
|
||||
: kind == PopulationHydrology.Kind.LAKE ? PopulationHydrology.MAX_LAKE_CARVE : PopulationHydrology.MAX_CARVE;
|
||||
helper.assertTrue(cell.sedimentDepth() >= 3 && cell.sedimentDepth() <= 5 && cell.carveTop() - cell.bedY() <= cut,
|
||||
"Regional sediment and carving must keep their feature-specific budgets: " + cell);
|
||||
for (int d = 0; d <= cell.sedimentDepth() + 1; d++) {
|
||||
BlockPos p = new BlockPos(cell.x(), cell.bedY() - d, cell.z());
|
||||
BlockState actual = level.getBlockState(p);
|
||||
helper.assertTrue(natural(p) > 0 && actual.isCollisionShapeFullBlock(level, p),
|
||||
"Every sediment layer and its two natural supports must remain full after generation and ticks: " + p + "=" + actual);
|
||||
if (d >= cell.sedimentDepth()) continue;
|
||||
BlockState expected = PopulationMaterials.sedimentBlock(cell, level.getSeed(), d);
|
||||
boolean ore = actual.is(Blocks.COAL_ORE) || actual.is(Blocks.DEEPSLATE_COAL_ORE)
|
||||
|| actual.is(Blocks.IRON_ORE) || actual.is(Blocks.DEEPSLATE_IRON_ORE)
|
||||
|| actual.is(Blocks.COPPER_ORE) || actual.is(Blocks.DEEPSLATE_COPPER_ORE)
|
||||
|| actual.is(Blocks.GOLD_ORE) || actual.is(Blocks.DEEPSLATE_GOLD_ORE)
|
||||
|| actual.is(Blocks.REDSTONE_ORE) || actual.is(Blocks.DEEPSLATE_REDSTONE_ORE)
|
||||
|| actual.is(Blocks.LAPIS_ORE) || actual.is(Blocks.DEEPSLATE_LAPIS_ORE)
|
||||
|| actual.is(Blocks.DIAMOND_ORE) || actual.is(Blocks.DEEPSLATE_DIAMOND_ORE)
|
||||
|| actual.is(Blocks.EMERALD_ORE) || actual.is(Blocks.DEEPSLATE_EMERALD_ORE);
|
||||
helper.assertTrue(actual.equals(expected) || expected.is(BlockTags.BASE_STONE_OVERWORLD) && ore
|
||||
|| expected.is(Blocks.GRASS_BLOCK) && actual.is(Blocks.DIRT),
|
||||
"The selected regional sediment profile must survive decoration: " + p + " expected=" + expected + " actual=" + actual);
|
||||
}
|
||||
if (!cell.hasWater()) continue;
|
||||
int maxDepth = kind == PopulationHydrology.Kind.TERRACE ? PopulationTerraces.MAX_DEPTH : PopulationHydrology.MAX_DEPTH;
|
||||
helper.assertTrue(cell.waterY() > cell.bedY() && cell.waterY() - cell.bedY() <= maxDepth,
|
||||
"Retained water must respect its feature depth limit");
|
||||
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) for (Direction direction : HORIZONTAL) {
|
||||
BlockPos wall = new BlockPos(cell.x(), y, cell.z()).relative(direction);
|
||||
var next = cellAt(wall);
|
||||
boolean plannedWater = next != null && next.hasWater() && y > next.bedY() && y <= next.waterY();
|
||||
if (plannedWater || plan.isSpillOpening(wall.getX(), wall.getY(), wall.getZ())) continue;
|
||||
helper.assertTrue(natural(wall) > 0 && level.getBlockState(wall).isCollisionShapeFullBlock(level, wall),
|
||||
"A retained basin must preserve its actual natural underwater shell, except its declared outlet: " + wall);
|
||||
}
|
||||
if (kind == PopulationHydrology.Kind.TERRACE) {
|
||||
for (int y = cell.carveTop() + 1; y <= cell.carveTop() + 4; y++)
|
||||
helper.assertTrue(natural(new BlockPos(cell.x(), y, cell.z())) <= 0, "A cave terrace needs four natural headroom blocks");
|
||||
boolean roof = false;
|
||||
for (int y = cell.carveTop() + 5; y < 384 && !roof; y++) roof = natural(new BlockPos(cell.x(), y, cell.z())) > 0;
|
||||
helper.assertTrue(roof, "A lower terrace must remain below an existing natural roof");
|
||||
} else for (int y = cell.carveTop() + 1; y < 384; y++)
|
||||
helper.assertTrue(natural(new BlockPos(cell.x(), y, cell.z())) <= 0, "A surface basin must be exposed to the natural sky");
|
||||
}
|
||||
}
|
||||
|
||||
void verifyLava() {
|
||||
for (BlockPos p : expectedLava) helper.assertTrue(natural(p) > 0
|
||||
&& level.getFluidState(p).is(FluidTags.LAVA) && level.getFluidState(p).isSource(),
|
||||
"Every inspected lava source must replace natural rock and survive actual fluid ticks: " + p);
|
||||
for (BlockPos p : lavaSupports) {
|
||||
BlockState block = level.getBlockState(p);
|
||||
helper.assertTrue(natural(p) > 0 && block.isCollisionShapeFullBlock(level, p) && !block.ignitedByLava(),
|
||||
"A selected volcanic niche must keep its actual nonflammable natural support: " + p);
|
||||
}
|
||||
for (var cell : pocketCells) if (!cell.lava()) {
|
||||
BlockPos p = new BlockPos(cell.x(), cell.y(), cell.z());
|
||||
BlockState block = level.getBlockState(p);
|
||||
// Wall lichen can light the excavated chamber without obstructing its dry access.
|
||||
helper.assertTrue(natural(p) > 0 && (block.isAir() || block.is(Blocks.GLOW_LICHEN))
|
||||
&& block.getCollisionShape(level, p).isEmpty() && block.getFluidState().isEmpty(),
|
||||
"The lava pocket must keep its excavated two-block approach and chamber accessible: " + p + " = " + block);
|
||||
}
|
||||
}
|
||||
|
||||
void verifySpring(PopulationHydrology.Spring spring) {
|
||||
var plan = waterPlanAt(spring.outlet().x(), spring.outlet().z());
|
||||
var channel = plan == null ? null : plan.cellAt(spring.outlet().x(), spring.outlet().z());
|
||||
if (channel != null && plan.isRiverCell(channel) && spring.outlet().y() > channel.waterY()
|
||||
&& spring.outlet().y() <= channel.carveTop()) {
|
||||
int expectedDrop = spring.source().y() - channel.waterY();
|
||||
helper.assertTrue(expectedDrop >= 5, "A river inlet must start above its retained channel");
|
||||
Set<BlockPos> destination = new HashSet<>();
|
||||
for (var cell : plan.cells()) if (cell.featureId() == channel.featureId() && cell.hasWater())
|
||||
destination.add(new BlockPos(cell.x(), cell.waterY(), cell.z()));
|
||||
helper.assertTrue(connectedDescent(pos(spring.source()), pos(spring.outlet()), destination) >= expectedDrop,
|
||||
"The translated inlet must actually connect downhill into its river");
|
||||
} else {
|
||||
int predicted = spring.source().y() - spring.flowPath().stream().mapToInt(PopulationHydrology.Position::y).min().orElse(spring.source().y());
|
||||
helper.assertTrue(connectedDescent(pos(spring.source()), pos(spring.outlet()), null) >= Math.max(8, Math.min(16, predicted)),
|
||||
"An independent spring must actually descend through vanilla ticks: " + spring.source());
|
||||
}
|
||||
}
|
||||
|
||||
void verifyPostProcessingReentry() {
|
||||
var outlets = new LinkedHashSet<BlockPos>();
|
||||
if (!springs.isEmpty()) outlets.add(pos(springs.getFirst().outlet()));
|
||||
if (!spills.isEmpty()) outlets.add(pos(spills.getFirst().outlet()));
|
||||
if (!lavaFalls.isEmpty()) outlets.add(lavaPos(lavaFalls.getFirst().outlet()));
|
||||
for (BlockPos outlet : outlets) {
|
||||
LevelChunk chunk = chunks.get(ChunkPos.pack(outlet));
|
||||
helper.assertTrue(chunk != null && Arrays.stream(chunk.getPostProcessing()).allMatch(m -> m == null || m.isEmpty()),
|
||||
"Re-entry uses a finished inspected chunk with consumed generation markers");
|
||||
helper.assertTrue(PopulationSpringOutlets.capturePending(level, chunk).isEmpty(),
|
||||
"An already generated chunk has no pending population outlet work");
|
||||
BlockState original = level.getBlockState(outlet);
|
||||
BlockState witness = Blocks.GLOW_LICHEN.defaultBlockState().setValue(MultifaceBlock.getFaceProperty(Direction.UP), true);
|
||||
try {
|
||||
level.setBlock(outlet, witness, 2);
|
||||
chunk.postProcessGeneration(level);
|
||||
helper.assertTrue(level.getBlockState(outlet).equals(witness),
|
||||
"Post-processing re-entry must preserve a later construction at a completed outlet: " + outlet);
|
||||
} finally { level.setBlock(outlet, original, 2); }
|
||||
helper.assertTrue(level.getBlockState(outlet).equals(original), "Restore the development-world witness exactly");
|
||||
reentryWitnesses++;
|
||||
}
|
||||
}
|
||||
|
||||
boolean isFluid(BlockPos p, boolean lava) {
|
||||
return lava ? level.getFluidState(p).is(FluidTags.LAVA) : level.getFluidState(p).is(FluidTags.WATER);
|
||||
}
|
||||
|
||||
void verifyWater() {
|
||||
for (var entry : naturalLogs.entrySet()) {
|
||||
BlockPos p = entry.getKey();
|
||||
helper.assertTrue(level.getBlockState(p).equals(entry.getValue()), "A recorded natural log must retain its exact state");
|
||||
helper.assertTrue(level.getBlockState(p.below()).isCollisionShapeFullBlock(level, p.below())
|
||||
|| level.getFluidState(p.below()).is(FluidTags.WATER) && level.getFluidState(p.below()).isSource(),
|
||||
"A horizontal log must retain its original bed or source water underneath");
|
||||
helper.assertTrue(Arrays.stream(HORIZONTAL).map(p::relative).anyMatch(side -> expectedWater.contains(side)
|
||||
&& level.getFluidState(side).is(FluidTags.WATER) && level.getFluidState(side).isSource()),
|
||||
"A horizontal log must remain beside actual retained water");
|
||||
}
|
||||
for(var p:expectedWater) helper.assertTrue(level.getFluidState(p).is(FluidTags.WATER)
|
||||
&& level.getFluidState(p).isSource(),"Retained water must survive generation and vanilla fluid ticks: "+p+"="+level.getBlockState(p));
|
||||
}
|
||||
|
||||
int connectedDescent(BlockPos source,BlockPos outlet,Set<BlockPos> destination) {
|
||||
return connectedDescent(source, outlet, destination, false);
|
||||
}
|
||||
|
||||
int connectedDescent(BlockPos source,BlockPos outlet,Set<BlockPos> destination, boolean lava) {
|
||||
helper.assertTrue(source.distManhattan(outlet)==1,"A natural outlet must adjoin its source");
|
||||
helper.assertTrue(isFluid(outlet, lava),"The production post-processing must leave a flowing outlet: "+outlet);
|
||||
var seen=new HashSet<BlockPos>(); var queue=new ArrayDeque<BlockPos>();
|
||||
seen.add(outlet);queue.add(outlet);int drop=0;boolean reached=destination==null;
|
||||
while(!queue.isEmpty() && seen.size()<150000) {
|
||||
var at=queue.removeFirst(); drop=Math.max(drop,source.getY()-at.getY());
|
||||
if(destination!=null && destination.contains(at)) reached=true;
|
||||
for(var direction:new Direction[]{Direction.DOWN,Direction.NORTH,Direction.SOUTH,Direction.EAST,Direction.WEST}) {
|
||||
var next=at.relative(direction);
|
||||
if(next.getY()<0 || !chunks.containsKey(ChunkPos.pack(next)) || seen.contains(next))continue;
|
||||
if(isFluid(next, lava)) {seen.add(next);queue.addLast(next);}
|
||||
}
|
||||
}
|
||||
helper.assertTrue(queue.isEmpty(), "The finite flow inspection must exhaust its search budget");
|
||||
helper.assertTrue(reached,"The actual downhill water component must reach its declared receiving basin");
|
||||
flowReports.add(Map.of("source", source, "outlet", outlet, "fluid", lava ? "lava" : "water",
|
||||
"actual_descending_blocks", seen.size(), "actual_drop", drop, "reaches_destination", reached));
|
||||
return drop;
|
||||
}
|
||||
|
||||
void export(boolean passed,long elapsed) throws IOException {
|
||||
Path output=Path.of("diagnostics","population-"+capacity.players()+"-seed-"+level.getSeed()+"-engine.json");
|
||||
Files.createDirectories(output.getParent());
|
||||
var report=new LinkedHashMap<String,Object>();
|
||||
report.put("players",capacity.players());report.put("seed",level.getSeed());
|
||||
report.put("mod_version",FabricLoader.getInstance().getModContainer("sanctuary").orElseThrow().getMetadata().getVersion().getFriendlyString());
|
||||
report.put("settings",generator.generatorSettings().getRegisteredName());
|
||||
report.put("regions_at_initial_spawn",regionsAtInitialSpawn);
|
||||
report.put("regions_cached_after_inspection",PopulationHydrologyRuntime.cachedRegionCount(level.getChunkSource().randomState()));
|
||||
report.put("regions",regionReports);report.put("inspected_full_chunks",chunks.size());
|
||||
report.put("retained_water_blocks",expectedWater.size());report.put("tested_springs",springs.size());
|
||||
report.put("tested_spills",spills.size());report.put("fluid_ticks",elapsed);
|
||||
report.put("selected_sediment_columns", selectedCells.size());
|
||||
report.put("natural_horizontal_logs", naturalLogs.size());
|
||||
report.put("retained_lava_sources", expectedLava.size()); report.put("tested_lava_falls", lavaFalls.size());
|
||||
report.put("flows", flowReports); report.put("postprocessing_reentry_witnesses", reentryWitnesses);
|
||||
report.put("scope","Selected basins, terraces, springs and ecology patches in explicit regions; no whole-island inventory or multiplayer load test");
|
||||
report.put("passed",passed);Files.writeString(output,json(report));
|
||||
if(passed) SanctuaryMod.LOGGER.info("Capacity {} seed {} passed: {} FULL chunks, {} retained water blocks, {} fluid ticks",
|
||||
capacity.players(),level.getSeed(),chunks.size(),expectedWater.size(),elapsed);
|
||||
}
|
||||
void cleanup() {
|
||||
if(closed)return;closed=true;
|
||||
for(long key:forced) {var p=ChunkPos.unpack(key);level.setChunkForced(p.x(),p.z(),false);}
|
||||
}
|
||||
}
|
||||
|
||||
private static String json(Object value) {return new GsonBuilder().setPrettyPrinting().create().toJson(value)+"\n";}
|
||||
private static BlockPos pos(PopulationHydrology.Position p) {return new BlockPos(p.x(),p.y(),p.z());}
|
||||
private static BlockPos lavaPos(PopulationLavaDeposit.Position p) {return new BlockPos(p.x(),p.y(),p.z());}
|
||||
private static LevelChunk full(GameTestHelper helper,ServerLevel level,int x,int z) {
|
||||
var chunk=level.getChunkSource().getChunk(x,z,ChunkStatus.FULL,true);
|
||||
helper.assertTrue(chunk instanceof LevelChunk,"The inspected chunk must be fully generated");return(LevelChunk)chunk;
|
||||
}
|
||||
private static void request(Set<Long> requested,int x,int z,int halo) {
|
||||
for(int dx=-halo;dx<=halo;dx++)for(int dz=-halo;dz<=halo;dz++)requested.add(ChunkPos.pack(x+dx,z+dz));
|
||||
}
|
||||
private static void request(Set<Long> requested,PopulationHydrology.FlowBounds b) {
|
||||
for(int x=(b.minX()>>4)-1;x<=(b.maxX()>>4)+1;x++)for(int z=(b.minZ()>>4)-1;z<=(b.maxZ()>>4)+1;z++)requested.add(ChunkPos.pack(x,z));
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.IslandCapacity;
|
||||
import fr.koka.sanctuary.worldgen.PopulationIslandDensity;
|
||||
import fr.koka.sanctuary.worldgen.SanctuarySpawn;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
import net.minecraft.tags.WorldPresetTags;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.WorldDimensions;
|
||||
import net.minecraft.world.level.levelgen.WorldGenSettings;
|
||||
import net.minecraft.world.level.levelgen.WorldOptions;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** Uses the real 26.3 saved-data writer and reader; never opens the user's saves. */
|
||||
final class PopulationPersistenceDiagnostics {
|
||||
static void verify(GameTestHelper helper) throws IOException {
|
||||
var level = helper.getLevel();
|
||||
var access = level.registryAccess();
|
||||
var presets = access.lookupOrThrow(Registries.WORLD_PRESET);
|
||||
var standard = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET,
|
||||
SanctuaryMod.id("sanctuary"))).value();
|
||||
helper.assertTrue(IslandCapacity.forGenerator(standard.createWorldDimensions().overworld()) == IslandCapacity.TEN,
|
||||
"The public Sanctuary preset defaults to the new large geometry");
|
||||
var visible = presets.getOrThrow(WorldPresetTags.NORMAL).stream().map(holder -> holder.getRegisteredName())
|
||||
.filter(name -> name.startsWith("sanctuary:")).collect(Collectors.toSet());
|
||||
helper.assertTrue(visible.equals(Set.of("sanctuary:sanctuary", "sanctuary:sanctuary_5")),
|
||||
"World creation must expose only small and large Sanctuary presets");
|
||||
var oldDensity = (PopulationIslandDensity) access.lookupOrThrow(Registries.DENSITY_FUNCTION)
|
||||
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, SanctuaryMod.id("population_20"))).value();
|
||||
var ops = RegistryOps.create(JsonOps.INSTANCE, access);
|
||||
var serialized = PopulationIslandDensity.CODEC.codec().encodeStart(ops, oldDensity).getOrThrow().getAsJsonObject();
|
||||
serialized.remove("players");
|
||||
helper.assertTrue(PopulationIslandDensity.CODEC.codec().parse(ops, serialized).getOrThrow().capacity() == IslandCapacity.TWENTY,
|
||||
"An omitted alpha10 density field must keep its historical default, independently of the new world selector");
|
||||
List<Map<String, Object>> results = new ArrayList<>();
|
||||
Path testRoot = Path.of("diagnostics", "population-persistence-seed-" + level.getSeed()).toAbsolutePath();
|
||||
Files.createDirectories(testRoot);
|
||||
LevelStorageSource storage = LevelStorageSource.createDefault(testRoot);
|
||||
for (int players : new int[]{5, 10, 20, 100}) {
|
||||
String presetId = players == 10 ? "sanctuary" : "sanctuary_" + players;
|
||||
WorldDimensions dimensions = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET,
|
||||
SanctuaryMod.id(presetId))).value().createWorldDimensions();
|
||||
helper.assertTrue(IslandCapacity.forGenerator(dimensions.overworld()) == IslandCapacity.fromPlayers(players),
|
||||
"The selectable world type must resolve its declared capacity");
|
||||
var original = new WorldGenSettings(new WorldOptions(level.getSeed(), true, false), dimensions);
|
||||
String name = "players-" + players;
|
||||
try (var save = storage.createAccess(name)) {
|
||||
LevelStorageSource.writeWorldGenSettings(access, storage.getLevelPath(name), original);
|
||||
var restored = LevelStorageSource.readExistingSavedData(save, access, WorldGenSettings.TYPE).getOrThrow();
|
||||
var generator = (NoiseBasedChunkGenerator) restored.dimensions().overworld();
|
||||
helper.assertTrue(IslandCapacity.forGenerator(generator) == IslandCapacity.fromPlayers(players),
|
||||
"Saved generator settings must preserve the chosen capacity after reading the world from disk");
|
||||
helper.assertTrue(generator.generatorSettings().getRegisteredName().equals("sanctuary:population_" + players),
|
||||
"The saved world must retain its explicit versioned settings key");
|
||||
helper.assertTrue(restored.options().seed() == level.getSeed(), "The seed must survive storage unchanged");
|
||||
results.add(Map.of("players", players, "preset", "sanctuary:" + presetId,
|
||||
"restored_settings", generator.generatorSettings().getRegisteredName(), "disk_round_trip", true));
|
||||
}
|
||||
}
|
||||
var old = new NoiseBasedChunkGenerator(standard.createWorldDimensions().overworld().getBiomeSource(),
|
||||
access.lookupOrThrow(Registries.NOISE_SETTINGS).getOrThrow(SanctuarySpawn.CAVERN_SETTINGS));
|
||||
var savedOld = new WorldGenSettings(new WorldOptions(level.getSeed(), true, false),
|
||||
standard.createWorldDimensions().replaceOverworldGenerator(access, old));
|
||||
try (var save = storage.createAccess("legacy-alpha9")) {
|
||||
LevelStorageSource.writeWorldGenSettings(access, storage.getLevelPath("legacy-alpha9"), savedOld);
|
||||
var restored = LevelStorageSource.readExistingSavedData(save, access, WorldGenSettings.TYPE).getOrThrow();
|
||||
var generator = (NoiseBasedChunkGenerator) restored.dimensions().overworld();
|
||||
helper.assertTrue(generator.generatorSettings().is(SanctuarySpawn.CAVERN_SETTINGS)
|
||||
&& IslandCapacity.forGenerator(generator) == null,
|
||||
"An existing alpha.9 settings reference must not acquire the new default capacity");
|
||||
}
|
||||
Files.writeString(testRoot.resolve("round-trip.json"), new GsonBuilder().setPrettyPrinting().create().toJson(Map.of(
|
||||
"seed", level.getSeed(), "default_players", 10, "profiles", results,
|
||||
"legacy_settings_preserved", "sanctuary:sanctuary_cavern", "visible_presets", visible,
|
||||
"omitted_density_capacity_preserved", 20, "passed", true)) + "\n");
|
||||
}
|
||||
}
|
||||
+7
-4
@@ -4,6 +4,7 @@ import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.IslandShape;
|
||||
import fr.koka.sanctuary.worldgen.IslandCapacity;
|
||||
import fr.koka.sanctuary.worldgen.SanctuarySpawn;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.SharedConstants;
|
||||
@@ -58,7 +59,9 @@ public final class ResourceSurveyDiagnostics {
|
||||
long initialGameTime = level.getGameTime();
|
||||
int centreX = initialSpawn.pos().getX() >> 4;
|
||||
int centreZ = initialSpawn.pos().getZ() >> 4;
|
||||
int envelopeRadius = IslandShape.TERRAIN_LIMIT + DECORATION_MARGIN;
|
||||
IslandCapacity capacity = IslandCapacity.forGenerator(level.getChunkSource().getGenerator());
|
||||
int terrainRadius = capacity == null ? IslandShape.TERRAIN_LIMIT : capacity.terrainLimit();
|
||||
int envelopeRadius = terrainRadius + DECORATION_MARGIN;
|
||||
int minimumIslandChunk = Math.floorDiv(-envelopeRadius, 16);
|
||||
int maximumIslandChunk = Math.floorDiv(envelopeRadius - 1, 16);
|
||||
int minimumX = mode.equals("island") ? minimumIslandChunk : centreX - 1;
|
||||
@@ -135,7 +138,7 @@ public final class ResourceSurveyDiagnostics {
|
||||
report.put("mode", mode);
|
||||
report.put("seed", level.getSeed());
|
||||
report.put("minecraft_version", SharedConstants.getCurrentVersion().name());
|
||||
report.put("generator", Map.of("preset", "sanctuary:sanctuary", "noise_settings", settingsHolder.getRegisteredName(),
|
||||
report.put("generator", Map.of("preset", capacity == null || capacity.players() == 20 ? "sanctuary:sanctuary" : "sanctuary:sanctuary_" + capacity.players(), "noise_settings", settingsHolder.getRegisteredName(),
|
||||
"mod_version", modVersion, "version", "sanctuary@" + modVersion,
|
||||
"noise_min_y", settings.noiseSettings().minY(),
|
||||
"noise_max_y_exclusive", settings.noiseSettings().minY() + settings.noiseSettings().height()));
|
||||
@@ -149,7 +152,7 @@ public final class ResourceSurveyDiagnostics {
|
||||
"min_y_inclusive", level.getMinY(), "max_y_exclusive", level.getMinY() + level.getHeight(),
|
||||
"min_chunk_x_inclusive", minimumX, "max_chunk_x_inclusive", maximumX,
|
||||
"min_chunk_z_inclusive", minimumZ, "max_chunk_z_inclusive", maximumZ));
|
||||
report.put("declared_island_envelope", Map.of("terrain_radius_blocks", IslandShape.TERRAIN_LIMIT,
|
||||
report.put("declared_island_envelope", Map.of("terrain_radius_blocks", terrainRadius,
|
||||
"decoration_margin_blocks", DECORATION_MARGIN, "bounding_rectangle_radius_blocks", envelopeRadius));
|
||||
report.put("completeness", Map.of("requested_chunks", expectedChunks, "inspected_full_chunks", chunks.size(),
|
||||
"complete_within_requested_bounds", true, "covers_declared_island_envelope", mode.equals("island"),
|
||||
@@ -172,7 +175,7 @@ public final class ResourceSurveyDiagnostics {
|
||||
"scope", "snapshot after FULL generation; no resource quotas or terrain correction applied"));
|
||||
Path directory = Path.of("diagnostics");
|
||||
Files.createDirectories(directory);
|
||||
Path output = directory.resolve("resource-survey-seed-" + level.getSeed() + "-" + mode + ".json");
|
||||
Path output = directory.resolve("resource-survey-" + (capacity == null ? "legacy" : "players-" + capacity.players()) + "-seed-" + level.getSeed() + "-" + mode + ".json");
|
||||
Files.writeString(output, JSON.toJson(report) + "\n");
|
||||
SanctuaryMod.LOGGER.info("Resource survey {} complete: {} FULL chunks, {} non-air blocks; {}",
|
||||
mode, chunks.size(), totalCells - air, output.toAbsolutePath());
|
||||
|
||||
+37
-8
@@ -1,6 +1,8 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.SanctuarySpawn;
|
||||
import fr.koka.sanctuary.worldgen.IslandCapacity;
|
||||
import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.NaturalHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.LayeredHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.WoodlandHydrologyRuntime;
|
||||
@@ -30,10 +32,23 @@ import net.minecraft.world.level.storage.LevelData;
|
||||
public final class SanctuaryWorldGameTests {
|
||||
public static LevelData.RespawnData initialSpawn;
|
||||
|
||||
@GameTest(maxTicks = 3000)
|
||||
public void optimizedDensityPreservesSavedTerrain(GameTestHelper helper) throws Exception {
|
||||
PopulationDensityEquivalenceDiagnostics.verify(helper);
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 200)
|
||||
public void populationChoiceSurvivesSavingTheWorld(GameTestHelper helper) throws java.io.IOException {
|
||||
PopulationPersistenceDiagnostics.verify(helper);
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 3000)
|
||||
public void generatedSurfaceWaterRemainsContained(GameTestHelper helper) throws java.io.IOException {
|
||||
var generator = (NoiseBasedChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
if (CavernHydrologyRuntime.enabled(generator)) CavernDiagnostics.start(helper);
|
||||
if (PopulationHydrologyRuntime.enabled(generator)) PopulationDiagnostics.start(helper);
|
||||
else if (CavernHydrologyRuntime.enabled(generator)) CavernDiagnostics.start(helper);
|
||||
else if (RiftHydrologyRuntime.enabled(generator)) RiftDiagnostics.start(helper);
|
||||
else if (WoodlandHydrologyRuntime.enabled(generator)) WoodlandDiagnostics.start(helper);
|
||||
else if (LayeredHydrologyRuntime.enabled(generator)) LayeredHydrologyDiagnostics.start(helper);
|
||||
@@ -44,7 +59,8 @@ public final class SanctuaryWorldGameTests {
|
||||
@GameTest(maxTicks = 200)
|
||||
public void hydrologyDoesNotActivateForSavedLegacySettings(GameTestHelper helper) {
|
||||
var generator = (NoiseBasedChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
if (CavernHydrologyRuntime.enabled(generator)) CavernDiagnostics.verifyLegacyIsolation(helper);
|
||||
if (PopulationHydrologyRuntime.enabled(generator)) PopulationDiagnostics.verifyLegacyIsolation(helper);
|
||||
else if (CavernHydrologyRuntime.enabled(generator)) CavernDiagnostics.verifyLegacyIsolation(helper);
|
||||
else if (RiftHydrologyRuntime.enabled(generator)) RiftDiagnostics.verifyLegacyIsolation(helper);
|
||||
else if (WoodlandHydrologyRuntime.enabled(generator)) WoodlandDiagnostics.verifyLegacyIsolation(helper);
|
||||
else if (LayeredHydrologyRuntime.enabled(generator)) LayeredHydrologyDiagnostics.verifyLegacyIsolation(helper);
|
||||
@@ -61,12 +77,14 @@ public final class SanctuaryWorldGameTests {
|
||||
|
||||
helper.assertTrue(initialSpawn != null, "Capture the world's initial spawn before the test grid moves it");
|
||||
BlockPos spawn = initialSpawn.pos();
|
||||
helper.assertTrue(Math.hypot((double) spawn.getX(), (double) spawn.getZ()) < IslandShape.TERRAIN_LIMIT,
|
||||
var generator = (NoiseBasedChunkGenerator) level.getChunkSource().getGenerator();
|
||||
IslandCapacity capacity = IslandCapacity.forGenerator(generator);
|
||||
int terrainLimit = capacity == null ? IslandShape.TERRAIN_LIMIT : capacity.terrainLimit();
|
||||
helper.assertTrue(Math.hypot((double) spawn.getX(), (double) spawn.getZ()) < terrainLimit,
|
||||
"Shared spawn must lie on the finite natural island: " + spawn);
|
||||
helper.assertTrue(spawn.getY() > 1 && spawn.getY() < level.getMaxY() - 1,
|
||||
"Shared spawn must be inside the dimension height");
|
||||
var settings = level.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||
.getOrThrow(SanctuarySpawn.RAISED_SETTINGS).value();
|
||||
var settings = generator.generatorSettings().value();
|
||||
float spawnFloorDensity = level.getChunkSource().randomState().sampleBlockValueUncached(
|
||||
settings.noiseRouter().finalDensity(), spawn.getX(), spawn.getY() - 1, spawn.getZ());
|
||||
helper.assertTrue(spawnFloorDensity > 0.0F,
|
||||
@@ -92,7 +110,9 @@ public final class SanctuaryWorldGameTests {
|
||||
|
||||
@GameTest(maxTicks = 200)
|
||||
public void sculptOnlyRemovesTerrain(GameTestHelper helper) throws java.io.IOException {
|
||||
DensityDiagnostics.verifyAndExport(helper, initialSpawn);
|
||||
if (IslandCapacity.forGenerator(helper.getLevel().getChunkSource().getGenerator()) != null)
|
||||
PopulationDiagnostics.verifyDensity(helper);
|
||||
else DensityDiagnostics.verifyAndExport(helper, initialSpawn);
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@@ -120,6 +140,11 @@ public final class SanctuaryWorldGameTests {
|
||||
|
||||
@GameTest(maxTicks = 200)
|
||||
public void completedLowerMarginIsVoid(GameTestHelper helper) {
|
||||
if (IslandCapacity.forGenerator(helper.getLevel().getChunkSource().getGenerator()) != null) {
|
||||
PopulationDiagnostics.verifyLowerMargin(helper);
|
||||
helper.succeed();
|
||||
return;
|
||||
}
|
||||
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);
|
||||
@@ -178,6 +203,11 @@ public final class SanctuaryWorldGameTests {
|
||||
|
||||
@GameTest(maxTicks = 200)
|
||||
public void completedExteriorChunksAreVoid(GameTestHelper helper) {
|
||||
if (IslandCapacity.forGenerator(helper.getLevel().getChunkSource().getGenerator()) != null) {
|
||||
PopulationDiagnostics.verifyExterior(helper);
|
||||
helper.succeed();
|
||||
return;
|
||||
}
|
||||
ServerLevel level = helper.getLevel();
|
||||
// First row is just beyond the declared terrain/decorations envelope. The last
|
||||
// samples catch the legacy bug where the infinite archipelago returned farther out.
|
||||
@@ -201,8 +231,7 @@ public final class SanctuaryWorldGameTests {
|
||||
public void compiledWorldDensityUsesTheSeed(GameTestHelper helper) {
|
||||
ServerLevel level = helper.getLevel();
|
||||
var registries = level.registryAccess();
|
||||
NoiseGeneratorSettings settings = registries.lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||
.getOrThrow(SanctuarySpawn.RAISED_SETTINGS).value();
|
||||
NoiseGeneratorSettings settings = ((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator()).generatorSettings().value();
|
||||
var noises = registries.lookupOrThrow(Registries.NOISE);
|
||||
RandomState first = RandomState.create(noises, 0L, settings);
|
||||
RandomState replay = RandomState.create(noises, 0L, settings);
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.IslandCapacity;
|
||||
import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
|
||||
/** Opt-in, repeatable server generation workload; it does not measure client rendering or FPS. */
|
||||
public final class WorldgenPerformanceGameTests {
|
||||
@GameTest(maxTicks = 3000)
|
||||
public void spawnAreaAndExploration(GameTestHelper helper) throws Exception {
|
||||
ServerLevel level = helper.getLevel();
|
||||
var capacity = IslandCapacity.forGenerator(level.getChunkSource().getGenerator());
|
||||
var spawn = SanctuaryWorldGameTests.initialSpawn.pos();
|
||||
int cx = spawn.getX() >> 4, cz = spawn.getZ() >> 4;
|
||||
var report = new LinkedHashMap<String, Object>();
|
||||
report.put("version", FabricLoader.getInstance().getModContainer("sanctuary").orElseThrow()
|
||||
.getMetadata().getVersion().getFriendlyString());
|
||||
report.put("players", capacity.players()); report.put("seed", level.getSeed());
|
||||
report.put("spawn", List.of(spawn.getX(), spawn.getY(), spawn.getZ()));
|
||||
report.put("java", System.getProperty("java.version"));
|
||||
report.put("processors", Runtime.getRuntime().availableProcessors());
|
||||
report.put("max_heap_bytes", Runtime.getRuntime().maxMemory());
|
||||
List<Map<String, Object>> rings = new ArrayList<>();
|
||||
List<Long> chunkTimes = new ArrayList<>();
|
||||
int count = 0, land = 0;
|
||||
long areaStart = System.nanoTime();
|
||||
// Centre outwards, 17x17 FULL chunks: a fixed eight-chunk radius around natural spawn.
|
||||
for (int radius = 0; radius <= 8; radius++) {
|
||||
long start = System.nanoTime();
|
||||
for (int x = -radius; x <= radius; x++) for (int z = -radius; z <= radius; z++) {
|
||||
if (Math.max(Math.abs(x), Math.abs(z)) != radius) continue;
|
||||
long before = System.nanoTime();
|
||||
var chunk = full(helper, level, cx + x, cz + z);
|
||||
chunkTimes.add(System.nanoTime() - before);
|
||||
count++;
|
||||
if (java.util.Arrays.stream(chunk.getSections()).anyMatch(s -> !s.hasOnlyAir())) land++;
|
||||
}
|
||||
long elapsed = System.nanoTime() - start;
|
||||
rings.add(Map.of("radius", radius, "elapsed_ms", millis(elapsed), "cumulative_chunks", count));
|
||||
SanctuaryMod.LOGGER.info("Generation benchmark {} seed {} ring {}: {} FULL chunks, {} ms",
|
||||
capacity.players(), level.getSeed(), radius, count, millis(elapsed));
|
||||
}
|
||||
report.put("spawn_area_ms", millis(System.nanoTime() - areaStart));
|
||||
report.put("spawn_area_full_chunks", count); report.put("spawn_area_nonempty_chunks", land);
|
||||
report.put("rings", rings); report.put("spawn_area_chunk_latency_ms", latency(chunkTimes));
|
||||
|
||||
// Continue east through a regional boundary, with a five-chunk-wide corridor.
|
||||
// This exposes stalls during exploration instead of timing only the initial region.
|
||||
List<Map<String, Object>> steps = new ArrayList<>();
|
||||
chunkTimes.clear();
|
||||
long exploreStart = System.nanoTime();
|
||||
for (int x = 9; x <= 24; x++) {
|
||||
long start = System.nanoTime();
|
||||
for (int z = -2; z <= 2; z++) {
|
||||
long before = System.nanoTime();
|
||||
full(helper, level, cx + x, cz + z);
|
||||
chunkTimes.add(System.nanoTime() - before);
|
||||
}
|
||||
long elapsed = System.nanoTime() - start;
|
||||
steps.add(Map.of("chunk_x", cx + x, "elapsed_ms", millis(elapsed)));
|
||||
SanctuaryMod.LOGGER.info("Generation benchmark {} seed {} exploration column {}: {} ms",
|
||||
capacity.players(), level.getSeed(), cx + x, millis(elapsed));
|
||||
}
|
||||
report.put("exploration_ms", millis(System.nanoTime() - exploreStart));
|
||||
report.put("exploration_full_chunks", 80); report.put("exploration_steps", steps);
|
||||
report.put("exploration_chunk_latency_ms", latency(chunkTimes));
|
||||
report.put("regions_cached", PopulationHydrologyRuntime.cachedRegionCount(level.getChunkSource().randomState()));
|
||||
report.put("scope", "Fresh development world; 289 FULL chunks around natural spawn, then 80 eastward FULL chunks. "
|
||||
+ "Synchronous server requests include dependent generation up to FULL, not guaranteed ticking readiness, client GPU, network, FPS or player load. "
|
||||
+ "World creation before the test is timed separately by Minecraft's startup log.");
|
||||
helper.assertTrue(count == 289 && land > 100, "The benchmark must generate actual playable terrain");
|
||||
report.put("passed", true);
|
||||
Path output = Path.of("diagnostics", "generation-performance-players-" + capacity.players() + "-seed-" + level.getSeed() + ".json");
|
||||
Files.createDirectories(output.getParent());
|
||||
Files.writeString(output, new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
private static LevelChunk full(GameTestHelper helper, ServerLevel level, int x, int z) {
|
||||
var chunk = level.getChunkSource().getChunk(x, z, ChunkStatus.FULL, true);
|
||||
helper.assertTrue(chunk instanceof LevelChunk, "Benchmark requires completed chunk " + new ChunkPos(x, z));
|
||||
return (LevelChunk) chunk;
|
||||
}
|
||||
|
||||
private static double millis(long nanos) { return nanos / 1_000_000.0; }
|
||||
|
||||
private static Map<String, Double> latency(List<Long> times) {
|
||||
var sorted = times.stream().sorted().toList();
|
||||
return Map.of("median", millis(sorted.get(sorted.size() / 2)),
|
||||
"p95", millis(sorted.get((int) Math.ceil(sorted.size() * .95) - 1)),
|
||||
"max", millis(sorted.getLast()));
|
||||
}
|
||||
}
|
||||
+11
-1
@@ -32,12 +32,22 @@ public abstract class SanctuaryGameTestServerMixin {
|
||||
@Redirect(method = "lambda$create$1", at = @At(value = "FIELD", target =
|
||||
"Lnet/minecraft/world/level/levelgen/presets/WorldPresets;FLAT_ALL_DIMENSIONS:Lnet/minecraft/resources/ResourceKey;"))
|
||||
private static ResourceKey<WorldPreset> sanctuary$selectActualPreset() {
|
||||
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary"));
|
||||
int players = Integer.getInteger("sanctuary.test.players", 10);
|
||||
String preset = switch (players) {
|
||||
case 5 -> "sanctuary_5";
|
||||
case 10 -> "sanctuary";
|
||||
case 20 -> "sanctuary_20";
|
||||
case 100 -> "sanctuary_100";
|
||||
default -> throw new IllegalArgumentException("Test geometry must be 5, 10, 20 or 100");
|
||||
};
|
||||
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(preset));
|
||||
}
|
||||
|
||||
@Inject(method = "startTests", at = @At("HEAD"))
|
||||
private void sanctuary$captureInitialSpawn(ServerLevel level, CallbackInfo ci) {
|
||||
// The framework moves its own spawn to a random test grid after this point.
|
||||
SanctuaryWorldGameTests.initialSpawn = level.getRespawnData();
|
||||
fr.koka.sanctuary.gametest.PopulationDiagnostics.regionsAtInitialSpawn =
|
||||
fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime.cachedRegionCount(level.getChunkSource().randomState());
|
||||
}
|
||||
}
|
||||
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.util.Interval;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.util.context.ContextKey;
|
||||
import net.minecraft.util.context.ContextMap;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.*;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.op.InterpolatedFunction;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.op.UnaryFunction;
|
||||
import net.minecraft.world.level.levelgen.synth.Noise;
|
||||
import net.minecraft.world.level.levelgen.synth.NormalNoise;
|
||||
|
||||
/** Adversarial checks against the actual vanilla scalar implementation, requiring no chunks. */
|
||||
public final class ExactInterpolationChecks {
|
||||
private static final ContextKey<Float> SHIFT = ContextKey.vanilla("sanctuary_test_interpolation_shift");
|
||||
private static final ContextKey<float[]> MUTABLE = ContextKey.vanilla("sanctuary_test_mutable_interpolation_shift");
|
||||
private static final float[] NO_MUTATION = {0};
|
||||
private static final DensityFunction.CompileContext COMPILE = new DensityFunction.CompileContext() {
|
||||
@Override public Noise createNoiseSampler(Holder<NormalNoise> noise) { throw new AssertionError("No noise needed"); }
|
||||
@Override public RandomSource createRandom(Identifier id) { throw new AssertionError("No random needed"); }
|
||||
@Override public RandomSource createEndIslandRandom() { throw new AssertionError("No End needed"); }
|
||||
};
|
||||
|
||||
private ExactInterpolationChecks() {}
|
||||
|
||||
public static Map<String, Object> verify() throws Exception {
|
||||
long checked = 0;
|
||||
for (int[] sizes : new int[][]{{8, 4}, {3, 5}, {1, 1}}) {
|
||||
var input = new Probe(31);
|
||||
DensityFunction expression = expression(input, sizes[0], sizes[1]);
|
||||
var reference = expression.compileSampler(COMPILE);
|
||||
var cached = cached(input, sizes[0], sizes[1]);
|
||||
SamplerContext first = context(0), second = context(0.0625f);
|
||||
var points = new ArrayList<int[]>();
|
||||
for (int x = -13; x <= 13; x++) for (int y = -7; y <= 11; y++) for (int z = -9; z <= 9; z++)
|
||||
points.add(new int[]{x, y, z});
|
||||
Collections.shuffle(points, new Random(73));
|
||||
for (var context : List.of(first, second, first)) for (var p : points) {
|
||||
same(reference.sampleValue(context, p[0], p[1], p[2]), cached.sampleValue(context, p[0], p[1], p[2]), "scalar/context");
|
||||
checked++;
|
||||
}
|
||||
var volume = new DensityVolume(13, 17, 11, -19, -9, -17, 3, 2, 5);
|
||||
DensityBuffer actual = DensityBuffer.createUnpooled(volume.size());
|
||||
cached.sampleVolume(first, actual, volume);
|
||||
for (int z = 0; z < volume.sizeZ(); z++) for (int x = 0; x < volume.sizeX(); x++) for (int y = 0; y < volume.sizeY(); y++) {
|
||||
same(reference.sampleValue(first, volume.blockX(x), volume.blockY(y), volume.blockZ(z)),
|
||||
actual.get(volume.indexUnchecked(x, y, z)), "non-unit volume");
|
||||
checked++;
|
||||
}
|
||||
}
|
||||
|
||||
// A scalar and a bulk input intentionally differ in Probe. This catches accidental
|
||||
// replacement of the aligned scalar branch with one of the cached lattice corners.
|
||||
var counted = new Probe(91);
|
||||
var cached = cached(counted, 8, 4);
|
||||
var stable = context(0);
|
||||
for (int repeat = 0; repeat < 2; repeat++)
|
||||
for (int x = 1; x < 8; x++) for (int y = 1; y < 4; y++) for (int z = 1; z < 8; z++) cached.sampleValue(stable, x, y, z);
|
||||
require(counted.volumeCalls.get() == 1, "A reused cell must sample its eight corners only once");
|
||||
int alignedBefore = counted.scalarCalls.get();
|
||||
cached.sampleValue(stable, 0, 0, 0);
|
||||
require(counted.scalarCalls.get() == alignedBefore + 1, "Aligned nodes retain their separate scalar input path");
|
||||
|
||||
var reference = expression(counted, 8, 4).compileSampler(COMPILE);
|
||||
// More distinct cells than the bounded cache holds; collisions and replacement must
|
||||
// remain invisible to both forward and reverse traversals.
|
||||
for (int direction : new int[]{1, -1}) for (int index = 0; index < 9000; index++) {
|
||||
int i = direction > 0 ? index : 8999 - index;
|
||||
int x = (i % 113 - 56) * 8 + 1, y = ((i / 113) % 97 - 48) * 4 + 1, z = (i % 67 - 33) * 8 + 3;
|
||||
same(reference.sampleValue(stable, x, y, z), cached.sampleValue(stable, x, y, z), "cache replacement");
|
||||
checked++;
|
||||
}
|
||||
var other = cached(new Probe(999), 8, 4);
|
||||
var otherReference = expression(new Probe(999), 8, 4).compileSampler(COMPILE);
|
||||
for (int i = 0; i < 128; i++) {
|
||||
same(reference.sampleValue(stable, i - 67, 21, -3), cached.sampleValue(stable, i - 67, 21, -3), "sampler identity A");
|
||||
same(otherReference.sampleValue(stable, i - 67, 21, -3), other.sampleValue(stable, i - 67, 21, -3), "sampler identity B");
|
||||
checked += 2;
|
||||
}
|
||||
try (var executor = Executors.newFixedThreadPool(3)) {
|
||||
List<Callable<Integer>> tasks = new ArrayList<>();
|
||||
for (int worker = 0; worker < 3; worker++) {
|
||||
final int id = worker;
|
||||
tasks.add(() -> {
|
||||
var local = context(id * 0.03125f);
|
||||
for (int i = 0; i < 3000; i++) {
|
||||
int x = (i * 17 + id) % 251 - 125, y = i % 384, z = (i * 37 - id) % 257 - 128;
|
||||
same(reference.sampleValue(local, x, y, z), cached.sampleValue(local, x, y, z), "concurrent shared sampler");
|
||||
}
|
||||
return 3000;
|
||||
});
|
||||
}
|
||||
for (var result : executor.invokeAll(tasks)) checked += result.get();
|
||||
}
|
||||
var plain = new ProbeFunction(new Probe(42));
|
||||
require(ExactInterpolationSampler.compileTerrain(plain, COMPILE) == plain.probe,
|
||||
"Unrecognized expressions must preserve their original sampler");
|
||||
require(!ExactInterpolationSampler.supportsTerrain(expression(new Probe(42), 8, 4)),
|
||||
"An opaque/contextual input must not be cached merely because its outer expression matches");
|
||||
float[] payload = {0};
|
||||
var mutable = SamplerContext.builder().setUserFields(ContextMap.builder().set(MUTABLE, payload).build()).build();
|
||||
var fallback = ExactInterpolationSampler.compileTerrain(expression(new Probe(42), 8, 4), COMPILE);
|
||||
float before = fallback.sampleValue(mutable, 1, 2, 3);
|
||||
payload[0] = 0.125f;
|
||||
float after = fallback.sampleValue(mutable, 1, 2, 3);
|
||||
require(Float.floatToRawIntBits(before) != Float.floatToRawIntBits(after),
|
||||
"The fallback must observe mutable context payloads even when context identity stays the same");
|
||||
|
||||
var nestedInput = new Probe(817);
|
||||
var nested = cached(nestedInput, 8, 4);
|
||||
var nestedReference = expression(new Probe(817), 8, 4).compileSampler(COMPILE);
|
||||
var outerInput = new Probe(83);
|
||||
outerInput.nested = nested;
|
||||
var outer = cached(outerInput, 8, 4);
|
||||
var outerReference = expression(outerInput, 8, 4).compileSampler(COMPILE);
|
||||
for (int i = 0; i < 128; i++) {
|
||||
int x = i - 65;
|
||||
same(outerReference.sampleValue(stable, x, 23, -7), outer.sampleValue(stable, x, 23, -7), "nested outer request");
|
||||
same(nestedReference.sampleValue(stable, x, 23, -7), nested.sampleValue(stable, x, 23, -7), "nested cache identity");
|
||||
checked += 2;
|
||||
}
|
||||
return Map.of("bitwise_comparisons", checked, "repeated_cell_corner_batches", 1,
|
||||
"concurrent_workers", 3, "aligned_scalar_path_preserved", true, "passed", true);
|
||||
}
|
||||
|
||||
private static DensityFunction expression(Probe probe, int xz, int y) {
|
||||
return new UnaryFunction(UnaryFunction.Type.SQUEEZE, new InterpolatedFunction(new ProbeFunction(probe), xz, y));
|
||||
}
|
||||
|
||||
private static DensitySampler cached(Probe probe, int xz, int y) {
|
||||
return new UnaryFunction.SqueezeSampler(new ExactInterpolationSampler(probe, xz, y));
|
||||
}
|
||||
|
||||
private static SamplerContext context(float shift) {
|
||||
return SamplerContext.builder().setUserFields(ContextMap.builder().set(SHIFT, shift).build()).build();
|
||||
}
|
||||
|
||||
private record ProbeFunction(Probe probe) implements DensityFunction {
|
||||
@Override public DensitySampler compileSampler(CompileContext context) { return probe; }
|
||||
@Override public DensityFunction rewriteChildren(DfRewriteRule rule) { return this; }
|
||||
@Override public Interval range() { return Interval.of(-2, 2); }
|
||||
@Override public int domainAxes() { return ALL_AXES; }
|
||||
@Override public MapCodec<ProbeFunction> codec() { return MapCodec.unit(this); }
|
||||
}
|
||||
|
||||
private static final class Probe implements DensitySampler {
|
||||
final int seed;
|
||||
final AtomicInteger scalarCalls = new AtomicInteger(), volumeCalls = new AtomicInteger();
|
||||
DensitySampler nested;
|
||||
Probe(int seed) { this.seed = seed; }
|
||||
private float value(SamplerContext context, int x, int y, int z) {
|
||||
int hash = (x * 73428767 ^ y * 912931 ^ z * 4382893 ^ seed);
|
||||
hash ^= hash >>> 15;
|
||||
float[] mutable = context.getFieldOrDefault(MUTABLE, NO_MUTATION);
|
||||
return Float.intBitsToFloat(0x3e000000 | (hash & 0x007fffff)) - 0.19f
|
||||
+ context.getFieldOrDefault(SHIFT, 0f) + mutable[0];
|
||||
}
|
||||
@Override public float sampleValue(SamplerContext context, int x, int y, int z) {
|
||||
scalarCalls.incrementAndGet();
|
||||
return value(context, x, y, z) + 0.03125f;
|
||||
}
|
||||
@Override public void sampleVolume(SamplerContext context, DensityBuffer buffer, DensityVolume volume) {
|
||||
volumeCalls.incrementAndGet();
|
||||
if (nested != null) nested.sampleValue(context, volume.minBlockX() + 1, volume.minBlockY() + 1, volume.minBlockZ() + 1);
|
||||
for (int z = 0; z < volume.sizeZ(); z++) for (int x = 0; x < volume.sizeX(); x++) for (int y = 0; y < volume.sizeY(); y++)
|
||||
buffer.set(volume.indexUnchecked(x, y, z), value(context, volume.blockX(x), volume.blockY(y), volume.blockZ(z)));
|
||||
}
|
||||
}
|
||||
|
||||
private static void same(float expected, float actual, String label) {
|
||||
if (Float.floatToRawIntBits(expected) != Float.floatToRawIntBits(actual))
|
||||
throw new AssertionError(label + ": expected " + Float.toHexString(expected) + ", actual " + Float.toHexString(actual));
|
||||
}
|
||||
private static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); }
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
"environment": "*",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"entrypoints": {
|
||||
"fabric-gametest": ["fr.koka.sanctuary.gametest.SanctuaryWorldGameTests"],
|
||||
"fabric-gametest": ["${gametest_entrypoint}"],
|
||||
"fabric-client-gametest": ["fr.koka.sanctuary.gametest.SanctuaryClientRenderTests"]
|
||||
},
|
||||
"mixins": ["sanctuary-gametest.mixins.json"],
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
package fr.koka.sanctuary;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.CavernIslandBiomeSource;
|
||||
import fr.koka.sanctuary.worldgen.PopulationIslandDensity;
|
||||
import fr.koka.sanctuary.worldgen.PopulationIslandBiomeSource;
|
||||
import fr.koka.sanctuary.worldgen.PopulationGrovesFeature;
|
||||
import fr.koka.sanctuary.worldgen.PopulationDecorationsFeature;
|
||||
import fr.koka.sanctuary.worldgen.PopulationShoreSugarCaneFeature;
|
||||
import fr.koka.sanctuary.worldgen.CavernGrovesFeature;
|
||||
import fr.koka.sanctuary.worldgen.CavernDecorationsFeature;
|
||||
import fr.koka.sanctuary.worldgen.CavernShoreSugarCaneFeature;
|
||||
@@ -36,6 +41,11 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("main_island"), MainIslandDensity.CODEC);
|
||||
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("population_island"), PopulationIslandDensity.CODEC);
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("population_island"), PopulationIslandBiomeSource.CODEC);
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("population_groves"), PopulationGrovesFeature.CODEC);
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("population_decorations"), PopulationDecorationsFeature.CODEC);
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("population_shore_sugar_cane"), PopulationShoreSugarCaneFeature.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);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package fr.koka.sanctuary.mixin;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.CavernSpringOutlets;
|
||||
import fr.koka.sanctuary.worldgen.PopulationSpringOutlets;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
@@ -12,16 +13,23 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
/** Finish only pending generation markers; completed saved chunks are never repaired on reload. */
|
||||
@Mixin(LevelChunk.class)
|
||||
public abstract class CavernSpringOutletMixin {
|
||||
@Unique
|
||||
private PopulationSpringOutlets.Pending sanctuary$pendingPopulationOutlets = PopulationSpringOutlets.Pending.EMPTY;
|
||||
|
||||
@Unique
|
||||
private CavernSpringOutlets.Pending sanctuary$pendingCavernOutlets = CavernSpringOutlets.Pending.EMPTY;
|
||||
|
||||
@Inject(method = "postProcessGeneration", at = @At("HEAD"))
|
||||
private void sanctuary$captureCavernOutlets(ServerLevel level, CallbackInfo callback) {
|
||||
sanctuary$pendingPopulationOutlets = PopulationSpringOutlets.capturePending(level, (LevelChunk) (Object) this);
|
||||
sanctuary$pendingCavernOutlets = CavernSpringOutlets.capturePending(level, (LevelChunk) (Object) this);
|
||||
}
|
||||
|
||||
@Inject(method = "postProcessGeneration", at = @At("TAIL"))
|
||||
private void sanctuary$finishCavernOutlets(ServerLevel level, CallbackInfo callback) {
|
||||
var population = sanctuary$pendingPopulationOutlets;
|
||||
sanctuary$pendingPopulationOutlets = PopulationSpringOutlets.Pending.EMPTY;
|
||||
PopulationSpringOutlets.finish(level, (LevelChunk) (Object) this, population);
|
||||
var pending = sanctuary$pendingCavernOutlets;
|
||||
sanctuary$pendingCavernOutlets = CavernSpringOutlets.Pending.EMPTY;
|
||||
CavernSpringOutlets.finish(level, (LevelChunk) (Object) this, pending);
|
||||
|
||||
@@ -6,6 +6,7 @@ import fr.koka.sanctuary.worldgen.LayeredHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.WoodlandHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.RiftHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.CavernHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import net.minecraft.core.Holder;
|
||||
@@ -30,7 +31,12 @@ public abstract class SurfaceHydrologyMixin {
|
||||
StructureManager structures, BiomeManager biomeManager, WorldGenRegion region,
|
||||
Set<Holder<Biome>> biomes, CallbackInfoReturnable<CompletableFuture<ChunkAccess>> callback) {
|
||||
var generator = (NoiseBasedChunkGenerator) (Object) this;
|
||||
if (CavernHydrologyRuntime.enabled(generator)) {
|
||||
if (PopulationHydrologyRuntime.enabled(generator)) {
|
||||
callback.setReturnValue(callback.getReturnValue().thenApply(generated -> {
|
||||
PopulationHydrologyRuntime.apply(generator, randomState, generated);
|
||||
return generated;
|
||||
}));
|
||||
} else if (CavernHydrologyRuntime.enabled(generator)) {
|
||||
callback.setReturnValue(callback.getReturnValue().thenApply(generated -> {
|
||||
CavernHydrologyRuntime.apply(generator, randomState, generated);
|
||||
return generated;
|
||||
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Arrays;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DensityBuffer;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DensityFunction;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DensitySampler;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DensityVolume;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DfRewriteRule;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.generator.ConstantFunction;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.generator.GradientFunction;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.generator.NoiseFunction;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.op.BinaryFunction;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.op.ClampFunction;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.op.InterpolatedFunction;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.op.LerpFunction;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.op.SliceFunction;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.op.UnaryFunction;
|
||||
import net.minecraft.world.level.levelgen.synth.BlendedNoise;
|
||||
|
||||
/** Reuses lattice corners without changing 26.3-pre-2's scalar interpolation arithmetic.
|
||||
* Vanilla's bulk interpolation changes operation order and accumulates Y increments, so it
|
||||
* cannot replace scalar sampling in a generator whose existing worlds must remain exact. */
|
||||
public final class ExactInterpolationSampler implements DensitySampler {
|
||||
private static final int CACHE_SIZE = 4096;
|
||||
// One numeric cache per worker, not one cache per world. Both identities are weak:
|
||||
// neither a sampler's captured noise graph nor a context's world fields are retained.
|
||||
private static final ThreadLocal<CornerCache> CORNERS = ThreadLocal.withInitial(CornerCache::new);
|
||||
|
||||
private final DensitySampler input;
|
||||
private final int cellSizeXz;
|
||||
private final int cellSizeY;
|
||||
|
||||
ExactInterpolationSampler(DensitySampler input, int cellSizeXz, int cellSizeY) {
|
||||
this.input = input;
|
||||
this.cellSizeXz = cellSizeXz;
|
||||
this.cellSizeY = cellSizeY;
|
||||
}
|
||||
|
||||
/** Only the known Population terrain expression is optimized. Other data-pack expressions
|
||||
* retain their own sampler, and the squeeze operation remains the actual vanilla class. */
|
||||
public static DensitySampler compileTerrain(DensityFunction terrain, DensityFunction.CompileContext context) {
|
||||
if (supportsTerrain(terrain)) {
|
||||
var interpolated = (InterpolatedFunction) ((UnaryFunction) terrain).input();
|
||||
return new UnaryFunction.SqueezeSampler(new ExactInterpolationSampler(
|
||||
interpolated.input().compileSampler(context), interpolated.cellSizeXz(), interpolated.cellSizeY()));
|
||||
}
|
||||
return terrain.compileSampler(context);
|
||||
}
|
||||
|
||||
/** Also used by the engine equivalence check to prove the shipped graphs take this path. */
|
||||
public static boolean supportsTerrain(DensityFunction terrain) {
|
||||
return terrain instanceof UnaryFunction unary && unary.type() == UnaryFunction.Type.SQUEEZE
|
||||
&& unary.input() instanceof InterpolatedFunction interpolated && coordinateOnly(interpolated.input(), 0);
|
||||
}
|
||||
|
||||
private static boolean coordinateOnly(DensityFunction function, int depth) {
|
||||
if (depth > 64) return false;
|
||||
function = DfRewriteRule.INLINE_REFERENCE.rewrite(function);
|
||||
if (function instanceof ConstantFunction || function instanceof GradientFunction || function instanceof BlendedNoise) return true;
|
||||
if (function instanceof NoiseFunction noise) return coordinateOnly(noise.shiftX(), depth + 1)
|
||||
&& coordinateOnly(noise.shiftY(), depth + 1) && coordinateOnly(noise.shiftZ(), depth + 1);
|
||||
if (function instanceof BinaryFunction binary) return coordinateOnly(binary.left(), depth + 1)
|
||||
&& coordinateOnly(binary.right(), depth + 1);
|
||||
if (function instanceof LerpFunction lerp) return coordinateOnly(lerp.alpha(), depth + 1)
|
||||
&& coordinateOnly(lerp.first(), depth + 1) && coordinateOnly(lerp.second(), depth + 1);
|
||||
if (function instanceof UnaryFunction unary) return coordinateOnly(unary.input(), depth + 1);
|
||||
if (function instanceof ClampFunction clamp) return coordinateOnly(clamp.input(), depth + 1);
|
||||
if (function instanceof SliceFunction slice) return coordinateOnly(slice.input(), depth + 1);
|
||||
// Contextual blending, prepared caches and third-party density functions are deliberately
|
||||
// opaque. Identity of a SamplerContext does not prove that its payload cannot mutate.
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override public float sampleValue(SamplerContext context, int x, int y, int z) {
|
||||
int offsetX = Math.floorMod(x, cellSizeXz);
|
||||
int offsetY = Math.floorMod(y, cellSizeY);
|
||||
int offsetZ = Math.floorMod(z, cellSizeXz);
|
||||
// The vanilla aligned path is scalar; a lattice value from sampleVolume is not
|
||||
// assumed to have the same bits as this separate input.sampleValue path.
|
||||
if (offsetX == 0 && offsetY == 0 && offsetZ == 0) return input.sampleValue(context, x, y, z);
|
||||
|
||||
int originX = x - offsetX, originY = y - offsetY, originZ = z - offsetZ;
|
||||
CornerCache cache = CORNERS.get();
|
||||
cache.select(this, context);
|
||||
int slot = cache.slot(originX, originY, originZ);
|
||||
if (!cache.contains(slot, originX, originY, originZ)) {
|
||||
DensityVolume lattice = new DensityVolume(2, 2, 2, originX, originY, originZ,
|
||||
cellSizeXz, cellSizeY, cellSizeXz);
|
||||
try (var buffer = context.acquireBuffer(lattice)) {
|
||||
// Keep the precise 2x2x2 request used by InterpolatedFunction.sampleValue:
|
||||
// batching a larger lattice can change the input noise's rounding too.
|
||||
input.sampleVolume(context, buffer, lattice);
|
||||
// A nested input sampler could itself use this thread's cache. Restore our
|
||||
// identity after it returns, before publishing our corners into the cache.
|
||||
cache.select(this, context);
|
||||
int first = slot * 8;
|
||||
for (int i = 0; i < 8; i++) cache.values[first + i] = buffer.get(i);
|
||||
cache.store(slot, originX, originY, originZ);
|
||||
}
|
||||
}
|
||||
int first = slot * 8;
|
||||
// DensityVolume packs Y fastest, then X, then Z. lerp3 expects the corners in
|
||||
// X, Y, Z order; retain vanilla's float divisions, not reciprocal multiplication.
|
||||
return Mth.lerp3((float) offsetX / cellSizeXz, (float) offsetY / cellSizeY, (float) offsetZ / cellSizeXz,
|
||||
cache.values[first], cache.values[first + 2], cache.values[first + 1], cache.values[first + 3],
|
||||
cache.values[first + 4], cache.values[first + 6], cache.values[first + 5], cache.values[first + 7]);
|
||||
}
|
||||
|
||||
@Override public void sampleVolume(SamplerContext context, DensityBuffer buffer, DensityVolume volume) {
|
||||
DensitySampler.sampleVolumeNaive(context, buffer, volume, this);
|
||||
}
|
||||
|
||||
/** Direct-mapped and bounded: 192 KiB of primitive arrays per participating thread.
|
||||
* A collision only recomputes a cell; it never substitutes a different cell's corners. */
|
||||
private static final class CornerCache {
|
||||
private WeakReference<ExactInterpolationSampler> sampler = new WeakReference<>(null);
|
||||
private WeakReference<SamplerContext> context = new WeakReference<>(null);
|
||||
private final int[] xs = new int[CACHE_SIZE], ys = new int[CACHE_SIZE], zs = new int[CACHE_SIZE];
|
||||
private final int[] generations = new int[CACHE_SIZE];
|
||||
private final float[] values = new float[CACHE_SIZE * 8];
|
||||
private int generation;
|
||||
|
||||
void select(ExactInterpolationSampler selected, SamplerContext selectedContext) {
|
||||
if (sampler.get() == selected && context.get() == selectedContext) return;
|
||||
sampler = new WeakReference<>(selected);
|
||||
context = new WeakReference<>(selectedContext);
|
||||
if (++generation == 0) {
|
||||
Arrays.fill(generations, 0);
|
||||
generation = 1;
|
||||
}
|
||||
}
|
||||
|
||||
int slot(int x, int y, int z) {
|
||||
int hash = x * 0x9e3779b9 ^ Integer.rotateLeft(y * 0x85ebca6b, 11)
|
||||
^ Integer.rotateLeft(z * 0xc2b2ae35, 22);
|
||||
hash = (hash ^ (hash >>> 16)) * 0x7feb352d;
|
||||
return (hash ^ (hash >>> 15)) & (CACHE_SIZE - 1);
|
||||
}
|
||||
|
||||
boolean contains(int slot, int x, int y, int z) {
|
||||
return generations[slot] == generation && xs[slot] == x && ys[slot] == y && zs[slot] == z;
|
||||
}
|
||||
|
||||
void store(int slot, int x, int y, int z) {
|
||||
xs[slot] = x; ys[slot] = y; zs[slot] = z;
|
||||
generations[slot] = generation;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.DataResult;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
|
||||
/** Saved geometry references. Small/large use FIVE/TEN; TWENTY/HUNDRED remain loadable. */
|
||||
public enum IslandCapacity {
|
||||
FIVE(5, 256), TEN(10, 256 * Math.sqrt(2)), TWENTY(20, 512), HUNDRED(100, 256 * Math.sqrt(20));
|
||||
|
||||
public static final int REGION_SIZE = 512;
|
||||
public static final Codec<IslandCapacity> CODEC = Codec.INT.comapFlatMap(players -> {
|
||||
for (var capacity : values()) if (capacity.players == players) return DataResult.success(capacity);
|
||||
return DataResult.error(() -> "Unknown saved island size; supported references are 5, 10, 20 and 100");
|
||||
}, IslandCapacity::players);
|
||||
private final int players;
|
||||
private final double radius;
|
||||
IslandCapacity(int players, double radius) { this.players = players; this.radius = radius; }
|
||||
public int players() { return players; }
|
||||
public double radius() { return radius; }
|
||||
public int terrainLimit() { return (int) Math.ceil(radius + 32); }
|
||||
public static IslandCapacity fromPlayers(int players) {
|
||||
for (var capacity : values()) if (capacity.players == players) return capacity;
|
||||
throw new IllegalArgumentException("Unknown saved island size; supported references are 5, 10, 20 and 100");
|
||||
}
|
||||
public static IslandCapacity forGenerator(ChunkGenerator generator) {
|
||||
if (!(generator instanceof NoiseBasedChunkGenerator noise)) return null;
|
||||
var settings = noise.generatorSettings();
|
||||
if (settings.is(SanctuarySpawn.POPULATION_5_SETTINGS)) return FIVE;
|
||||
if (settings.is(SanctuarySpawn.POPULATION_10_SETTINGS)) return TEN;
|
||||
if (settings.is(SanctuarySpawn.POPULATION_20_SETTINGS)) return TWENTY;
|
||||
if (settings.is(SanctuarySpawn.POPULATION_100_SETTINGS)) return HUNDRED;
|
||||
return null;
|
||||
}
|
||||
public boolean areaExists(int chunkX, int chunkZ) {
|
||||
double x = Math.max(0, Math.max((long) chunkX * 16, -((long) chunkX * 16 + 15)));
|
||||
double z = Math.max(0, Math.max((long) chunkZ * 16, -((long) chunkZ * 16 + 15)));
|
||||
return Math.hypot(x, z) < terrainLimit();
|
||||
}
|
||||
public Region regionAt(int x, int z) {
|
||||
return new Region((int) Math.floorDiv((long) x + 256, REGION_SIZE),
|
||||
(int) Math.floorDiv((long) z + 256, REGION_SIZE));
|
||||
}
|
||||
public record Region(int x, int z) {
|
||||
public int originX() { return Math.multiplyExact(x, REGION_SIZE); }
|
||||
public int originZ() { return Math.multiplyExact(z, REGION_SIZE); }
|
||||
public int minX() { return originX() - 256; }
|
||||
public int minZ() { return originZ() - 256; }
|
||||
public int maxX() { return originX() + 255; }
|
||||
public int maxZ() { return originZ() + 255; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
/** Alpha.10 keeps the upper woodland climate and specializes sheltered lower layers. */
|
||||
public final class PopulationBiomePalette {
|
||||
public enum Kind {
|
||||
OAK_FOREST, BIRCH_FOREST, CLEARING, DRY_WOODLAND, ROCKY_HEATH,
|
||||
DARK_GROVE, BAMBOO_GROVE, SULFUR_DEPTHS, LUSH_CAVES, DRIPSTONE_CAVES
|
||||
}
|
||||
|
||||
private PopulationBiomePalette() {}
|
||||
|
||||
public static Kind select(int blockY, float variation, float moisture) {
|
||||
double elevation = blockY - Math.clamp(variation, -1.0F, 1.0F) * 16
|
||||
- Math.clamp(moisture, -1.0F, 1.0F) * 10;
|
||||
// Small climate pockets replace alpha.8's broad sulfur province. Neither
|
||||
// mineral pockets nor cave vegetation can take over the upper forest.
|
||||
if (blockY < 176 && elevation < 168 && variation > 0.22F && variation < 0.42F
|
||||
&& moisture > -0.42F && moisture < -0.12F) return Kind.SULFUR_DEPTHS;
|
||||
if (variation > 0.48F) return Kind.ROCKY_HEATH;
|
||||
if (blockY < 184 && elevation < 180) {
|
||||
if (moisture > 0.05F && variation > -0.32F) return Kind.LUSH_CAVES;
|
||||
if (moisture < -0.18F && variation < 0.22F) return Kind.DRIPSTONE_CAVES;
|
||||
}
|
||||
if (elevation < 214) {
|
||||
if (variation < -0.45F && moisture > 0.40F) return Kind.BAMBOO_GROVE;
|
||||
double shelteredMoisture = moisture + Math.min(0.55, (214 - elevation) / 160);
|
||||
if (shelteredMoisture > -0.08) return Kind.DARK_GROVE;
|
||||
}
|
||||
if (moisture < -0.55F && variation > -0.25F) return Kind.DRY_WOODLAND;
|
||||
if (Math.abs(variation) < 0.09F && moisture < 0.35F) return Kind.CLEARING;
|
||||
if (variation < -0.33F) return Kind.BIRCH_FOREST;
|
||||
return Kind.OAK_FOREST;
|
||||
}
|
||||
}
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.AmethystClusterBlock;
|
||||
import net.minecraft.world.level.block.CaveVines;
|
||||
import net.minecraft.world.level.block.GrowingPlantHeadBlock;
|
||||
import net.minecraft.world.level.block.PotentSulfurBlock;
|
||||
import net.minecraft.world.level.block.SpeleothemBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.PotentSulfurState;
|
||||
import net.minecraft.world.level.block.state.properties.SpeleothemThickness;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.feature.Feature;
|
||||
import net.minecraft.world.level.levelgen.feature.GeodeFeature;
|
||||
|
||||
/** Decorations in existing lower cavities only. All writes belong to the owner's chunk. */
|
||||
public final class PopulationDecorationsFeature implements Feature {
|
||||
public static final MapCodec<PopulationDecorationsFeature> CODEC = MapCodec.unit(PopulationDecorationsFeature::new);
|
||||
private static final ResourceKey<Feature> GEODE = ResourceKey.create(Registries.FEATURE, SanctuaryMod.id("population_amethyst_geode"));
|
||||
private static final Direction[] SIDES = {Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST};
|
||||
public enum Kind { LUSH, DRIPSTONE, SULFUR, GEODE }
|
||||
public record Candidate(Kind kind, BlockPos floor, BlockPos ceiling) {
|
||||
public Candidate { floor = floor.immutable(); ceiling = ceiling.immutable(); }
|
||||
}
|
||||
|
||||
@Override public MapCodec<PopulationDecorationsFeature> codec() { return CODEC; }
|
||||
|
||||
/** Pure, bounded probes for development diagnostics; no chunk loads or successful-placement promise. */
|
||||
public static List<Candidate> candidates(NoiseBasedChunkGenerator generator, RandomState state, int chunkX, int chunkZ) {
|
||||
if (!PopulationHydrologyRuntime.enabled(generator)
|
||||
|| !PopulationHydrologyRuntime.capacity(generator).areaExists(chunkX, chunkZ)) return List.of();
|
||||
var density = generator.generatorSettings().value().noiseRouter().finalDensity();
|
||||
WoodlandGroveGeometry.Sampler sampler = (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z);
|
||||
var biomes = generator.getBiomeSource().createUncachedResolver(state);
|
||||
List<Candidate> found = new ArrayList<>();
|
||||
for (int[] offset : new int[][]{{4, 4}, {4, 12}, {12, 4}, {12, 12}, {8, 8}}) {
|
||||
int x = chunkX * 16 + offset[0], z = chunkZ * 16 + offset[1], floors = 0;
|
||||
for (int y = 182; y >= 40; y--) {
|
||||
if (sampler.sample(x, y, z) <= 0 || sampler.sample(x, y + 1, z) > 0
|
||||
|| sampler.sample(x, y + 2, z) > 0 || sampler.sample(x, y - 1, z) <= 0
|
||||
|| sampler.sample(x, y - 2, z) <= 0) continue;
|
||||
int roof = y + 3;
|
||||
while (roof <= Math.min(230, y + 48) && sampler.sample(x, roof, z) <= 0) roof++;
|
||||
if (roof > Math.min(230, y + 48) || roof - y < 6
|
||||
|| sampler.sample(x, roof + 1, z) <= 0) continue;
|
||||
BlockPos floor = new BlockPos(x, y, z), ceiling = new BlockPos(x, roof, z);
|
||||
var biome = biomes.getNoiseBiome(x >> 2, (y + 1) >> 2, z >> 2);
|
||||
if (biome.is(PopulationIslandBiomeSource.LUSH_CAVES)) found.add(new Candidate(Kind.LUSH, floor, ceiling));
|
||||
else if (biome.is(PopulationIslandBiomeSource.DRIPSTONE_CAVES)) found.add(new Candidate(Kind.DRIPSTONE, floor, ceiling));
|
||||
else if (biome.is(PopulationIslandBiomeSource.SULFUR_DEPTHS) && y < 176)
|
||||
found.add(new Candidate(Kind.SULFUR, floor, ceiling));
|
||||
if (offset[0] == 8 && offset[1] == 8 && Math.floorMod(chunkSeed(state.seed(), chunkX, chunkZ, 0x9501L), 8) == 0)
|
||||
found.add(new Candidate(Kind.GEODE, floor, ceiling));
|
||||
if (++floors >= 3) break;
|
||||
}
|
||||
}
|
||||
// Geodes keep their entry free before this feature's moss, grass or pointed
|
||||
// dripstone can occupy it. Stable sorting preserves every other candidate order.
|
||||
found.sort(java.util.Comparator.comparingInt(candidate -> candidate.kind() == Kind.GEODE ? 0 : 1));
|
||||
return List.copyOf(found);
|
||||
}
|
||||
|
||||
@Override public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource ignored, BlockPos origin) {
|
||||
if (!(generator instanceof NoiseBasedChunkGenerator noise) || !PopulationHydrologyRuntime.enabled(noise)) return false;
|
||||
var state = level.getLevel().getChunkSource().randomState();
|
||||
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
|
||||
WoodlandGroveGeometry.Sampler sampler = (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z);
|
||||
int cx = origin.getX() >> 4, cz = origin.getZ() >> 4;
|
||||
boolean changed = false, geyser = false, geode = false;
|
||||
for (Candidate candidate : candidates(noise, state, cx, cz)) {
|
||||
BlockPos floor = candidate.floor();
|
||||
if (!stable(level, sampler, floor) || !level.getBlockState(floor.above()).isAir()) continue;
|
||||
long seed = WoodlandGroveGeometry.seed(state.seed(), floor.getX(), floor.getY(), floor.getZ(), 0x9502L);
|
||||
switch (candidate.kind()) {
|
||||
case LUSH -> changed |= lush(level, noise, state, sampler, candidate, seed);
|
||||
case DRIPSTONE -> changed |= dripstone(level, noise, state, sampler, candidate, seed);
|
||||
case SULFUR -> {
|
||||
if (!geyser && Math.floorMod(chunkSeed(state.seed(), cx, cz, 0x9503L), 2) == 0)
|
||||
changed |= geyser = geyser(level, noise, state, sampler, candidate);
|
||||
}
|
||||
case GEODE -> { if (!geode) changed |= geode = geode(level, noise, state, sampler, candidate, seed); }
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private boolean lush(WorldGenLevel level, NoiseBasedChunkGenerator noise, RandomState state,
|
||||
WoodlandGroveGeometry.Sampler sampler, Candidate site, long seed) {
|
||||
BlockPos floor = site.floor(), roof = site.ceiling();
|
||||
int gap = roof.getY() - floor.getY();
|
||||
if (PopulationHydrologyRuntime.protects(noise, state, floor, 3, gap + 2, 3)) return false;
|
||||
boolean changed = false;
|
||||
for (int dx = -2; dx <= 2; dx++) for (int dz = -2; dz <= 2; dz++) {
|
||||
if (dx * dx + dz * dz > 5) continue;
|
||||
BlockPos p = floor.offset(dx, 0, dz);
|
||||
if (!stable(level, sampler, p) || !stable(level, sampler, p.below()) || !stable(level, sampler, p.below(2))
|
||||
|| !level.getBlockState(p.above()).isAir()) continue;
|
||||
setBlock(level, p, Blocks.MOSS_BLOCK.defaultBlockState());
|
||||
long local = WoodlandGroveGeometry.seed(seed, p.getX(), p.getY(), p.getZ(), 0x9504L);
|
||||
BlockState plant = (Math.floorMod(local, 5) == 0 ? Blocks.AZALEA : Math.floorMod(local, 3) == 0
|
||||
? Blocks.MOSS_CARPET : Blocks.SHORT_GRASS).defaultBlockState();
|
||||
if (plant.canSurvive(level, p.above())) setBlock(level, p.above(), plant);
|
||||
changed = true;
|
||||
}
|
||||
// Real cave-vine heads and bodies remain harvestable and luminous via BERRIES.
|
||||
int length = Math.min(gap - 3, 3 + Math.floorMod(seed, 6));
|
||||
if (stable(level, sampler, roof) && stable(level, sampler, roof.above())) {
|
||||
boolean free = true;
|
||||
for (int d = 1; d <= length; d++) free &= level.getBlockState(roof.below(d)).isAir();
|
||||
if (free) for (int d = 1; d <= length; d++) {
|
||||
BlockPos p = roof.below(d);
|
||||
BlockState vine = (d == length ? Blocks.CAVE_VINES : Blocks.CAVE_VINES_PLANT).defaultBlockState()
|
||||
.setValue(CaveVines.BERRIES, d == length || Math.floorMod(seed + d, 3) == 0);
|
||||
if (d == length) vine = vine.setValue(GrowingPlantHeadBlock.AGE, 25);
|
||||
if (!vine.canSurvive(level, p)) break;
|
||||
setBlock(level, p, vine); changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private boolean dripstone(WorldGenLevel level, NoiseBasedChunkGenerator noise, RandomState state,
|
||||
WoodlandGroveGeometry.Sampler sampler, Candidate site, long seed) {
|
||||
BlockPos floor = site.floor(), roof = site.ceiling();
|
||||
int gap = roof.getY() - floor.getY();
|
||||
if (PopulationHydrologyRuntime.protects(noise, state, floor, 1, gap + 2, 3)) return false;
|
||||
int length = Math.min((gap - 3) / 2, 1 + Math.floorMod(seed, 4));
|
||||
boolean changed = spike(level, sampler, floor, Direction.UP, length);
|
||||
changed |= spike(level, sampler, roof, Direction.DOWN, length);
|
||||
return changed;
|
||||
}
|
||||
|
||||
private boolean spike(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler, BlockPos support, Direction direction, int length) {
|
||||
if (length < 1 || !stable(level, sampler, support) || !stable(level, sampler, support.relative(direction.getOpposite()))) return false;
|
||||
for (int d = 1; d <= length; d++) {
|
||||
BlockPos p = support.relative(direction, d);
|
||||
if (!level.getBlockState(p).isAir() || sampler.sample(p.getX(), p.getY(), p.getZ()) > 0) return false;
|
||||
}
|
||||
setBlock(level, support, Blocks.DRIPSTONE_BLOCK.defaultBlockState());
|
||||
for (int d = 1; d <= length; d++) {
|
||||
SpeleothemThickness thickness = d == length ? SpeleothemThickness.TIP
|
||||
: d == length - 1 ? SpeleothemThickness.FRUSTUM : d == 1 ? SpeleothemThickness.BASE : SpeleothemThickness.MIDDLE;
|
||||
BlockState pointed = Blocks.POINTED_DRIPSTONE.defaultBlockState().setValue(SpeleothemBlock.TIP_DIRECTION, direction)
|
||||
.setValue(SpeleothemBlock.THICKNESS, thickness);
|
||||
setBlock(level, support.relative(direction, d), pointed);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean geyser(WorldGenLevel level, NoiseBasedChunkGenerator noise, RandomState state,
|
||||
WoodlandGroveGeometry.Sampler sampler, Candidate site) {
|
||||
BlockPos water = site.floor(), potent = water.below(), heat = water.below(2);
|
||||
if (site.ceiling().getY() - water.getY() < 8
|
||||
|| PopulationHydrologyRuntime.protects(noise, state, water, 2, 8, 5)) return false;
|
||||
for (int d = 0; d <= 4; d++) if (!stable(level, sampler, water.below(d))) return false;
|
||||
for (Direction side : SIDES) for (int d = 0; d <= 2; d++)
|
||||
if (!stable(level, sampler, water.below(d).relative(side))) return false;
|
||||
for (int d = 1; d <= 7; d++) if (!level.getBlockState(water.above(d)).isAir()) return false;
|
||||
// One source, four original rock walls, original deep support. Vanilla's
|
||||
// periodic geyser ticker requires magma below potent sulfur and source water above.
|
||||
setBlock(level, heat, Blocks.MAGMA_BLOCK.defaultBlockState());
|
||||
setBlock(level, water, Blocks.WATER.defaultBlockState());
|
||||
setBlock(level, potent, Blocks.POTENT_SULFUR.defaultBlockState().setValue(PotentSulfurBlock.STATE, PotentSulfurState.DORMANT));
|
||||
level.getChunk(water).markPosForPostProcessing(water);
|
||||
level.getChunk(potent).markPosForPostProcessing(potent);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean geode(WorldGenLevel level, NoiseBasedChunkGenerator noise, RandomState state,
|
||||
WoodlandGroveGeometry.Sampler sampler, Candidate site, long seed) {
|
||||
BlockPos origin = site.floor().offset(-1, -9, -1);
|
||||
// Include the vanilla write range, its crystal neighbours and the entrance.
|
||||
// This asymmetric envelope is centred explicitly so its positive edge is protected too.
|
||||
if (origin.getY() - 6 < 24 || !level.getBlockState(site.floor().above(2)).isAir()
|
||||
|| PopulationHydrologyRuntime.protects(noise, state, origin.offset(1, -6, 1), 7, 17, 0)) return false;
|
||||
Map<BlockPos, BlockState> before = new LinkedHashMap<>();
|
||||
for (int dx = -6; dx <= 8; dx++) for (int dz = -6; dz <= 8; dz++) for (int dy = -6; dy <= 11; dy++) {
|
||||
BlockPos p = origin.offset(dx, dy, dz);
|
||||
BlockState original = level.getBlockState(p);
|
||||
// No block-entity data or fluid scheduler state needs to be restored.
|
||||
if (original.hasBlockEntity() || !original.getFluidState().isEmpty()) return false;
|
||||
before.put(p, original);
|
||||
}
|
||||
Feature feature = level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(GEODE).value();
|
||||
if (!(feature instanceof GeodeFeature geode) || geode.minGenOffset() != -5 || geode.maxGenOffset() != 7) return false;
|
||||
boolean accepted = false;
|
||||
try {
|
||||
if (!geode.place(level, noise, RandomSource.create(seed), origin) || !naturalGeodeChanges(level, sampler, before)) return false;
|
||||
var entrance = geodeEntrance(level, sampler, site.floor().above(), origin, before);
|
||||
if (entrance.isEmpty()) return false;
|
||||
for (BlockPos p : entrance) if (!level.getBlockState(p).isAir()) setBlock(level, p, Blocks.AIR.defaultBlockState());
|
||||
if (!naturalGeodeChanges(level, sampler, before)) return false;
|
||||
accepted = true;
|
||||
return true;
|
||||
} finally {
|
||||
if (!accepted) before.forEach((p, original) -> {
|
||||
if (!level.getBlockState(p).equals(original)) setBlock(level, p, original);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Test the actual vanilla mask, not unused bounding-box corners. A geode may
|
||||
* excavate original rock but cannot fill original air or remove earlier decoration. */
|
||||
private static boolean naturalGeodeChanges(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler,
|
||||
Map<BlockPos, BlockState> before) {
|
||||
for (var entry : before.entrySet()) {
|
||||
BlockPos p = entry.getKey();
|
||||
BlockState original = entry.getValue(), current = level.getBlockState(p);
|
||||
if (original.equals(current)) continue;
|
||||
if (!carvableOriginal(level, sampler, p, original)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean carvableOriginal(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler,
|
||||
BlockPos p, BlockState original) {
|
||||
return original != null && original.isCollisionShapeFullBlock(level, p)
|
||||
&& !original.hasBlockEntity() && original.getFluidState().isEmpty()
|
||||
&& !original.is(BlockTags.LOGS) && !original.is(BlockTags.LEAVES)
|
||||
&& !original.is(Blocks.BUDDING_AMETHYST) && !original.is(Blocks.MAGMA_BLOCK)
|
||||
&& sampler.sample(p.getX(), p.getY(), p.getZ()) > 0;
|
||||
}
|
||||
|
||||
private static List<BlockPos> geodeEntrance(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler,
|
||||
BlockPos start, BlockPos origin, Map<BlockPos, BlockState> before) {
|
||||
var previous = new java.util.HashMap<BlockPos, BlockPos>();
|
||||
var distances = new java.util.HashMap<BlockPos, Integer>();
|
||||
var queue = new java.util.ArrayDeque<BlockPos>();
|
||||
queue.add(start); distances.put(start, 0);
|
||||
BlockPos destination = null;
|
||||
while (!queue.isEmpty()) {
|
||||
BlockPos p = queue.removeFirst();
|
||||
int distance = distances.get(p);
|
||||
boolean adjacentBudding = false;
|
||||
for (Direction direction : Direction.values())
|
||||
adjacentBudding |= level.getBlockState(p.relative(direction)).is(Blocks.BUDDING_AMETHYST)
|
||||
|| level.getBlockState(p.above().relative(direction)).is(Blocks.BUDDING_AMETHYST);
|
||||
if (p.getY() <= start.getY() - 3 && level.getBlockState(p).isAir()
|
||||
&& level.getBlockState(p.above()).isAir() && adjacentBudding) { destination = p; break; }
|
||||
if (distance >= 16) continue;
|
||||
for (Direction direction : Direction.values()) {
|
||||
BlockPos next = p.relative(direction);
|
||||
if (Math.abs(next.getX() - start.getX()) > 6 || Math.abs(next.getZ() - start.getZ()) > 6
|
||||
|| next.getY() < origin.getY() - 5 || next.getY() > start.getY()
|
||||
|| distances.containsKey(next) || !before.containsKey(next) || !before.containsKey(next.above())) continue;
|
||||
if (!entranceBlock(level, sampler, next, before) || !entranceBlock(level, sampler, next.above(), before)) continue;
|
||||
distances.put(next, distance + 1); previous.put(next, p); queue.addLast(next);
|
||||
}
|
||||
}
|
||||
if (destination == null) return List.of();
|
||||
var passage = new java.util.LinkedHashSet<BlockPos>();
|
||||
for (BlockPos p = destination; p != null; p = previous.get(p)) { passage.add(p); passage.add(p.above()); }
|
||||
return List.copyOf(passage);
|
||||
}
|
||||
|
||||
private static boolean entranceBlock(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler,
|
||||
BlockPos p, Map<BlockPos, BlockState> before) {
|
||||
BlockState current = level.getBlockState(p);
|
||||
if (current.isAir()) return true;
|
||||
// Keep every budding block and attached crystal. Search around them instead.
|
||||
if (current.is(Blocks.BUDDING_AMETHYST) || current.is(Blocks.AMETHYST_CLUSTER)
|
||||
|| current.is(Blocks.SMALL_AMETHYST_BUD) || current.is(Blocks.MEDIUM_AMETHYST_BUD)
|
||||
|| current.is(Blocks.LARGE_AMETHYST_BUD)) return false;
|
||||
for (Direction direction : Direction.values()) {
|
||||
BlockState neighbour = level.getBlockState(p.relative(direction));
|
||||
if (neighbour.getBlock() instanceof AmethystClusterBlock
|
||||
&& neighbour.getValue(AmethystClusterBlock.FACING) == direction) return false;
|
||||
}
|
||||
return carvableOriginal(level, sampler, p, before.get(p));
|
||||
}
|
||||
|
||||
private static boolean stable(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler, BlockPos p) {
|
||||
BlockState state = level.getBlockState(p);
|
||||
return sampler.sample(p.getX(), p.getY(), p.getZ()) > 0 && state.isCollisionShapeFullBlock(level, p)
|
||||
&& state.getFluidState().isEmpty() && !state.hasBlockEntity() && !state.is(BlockTags.LOGS)
|
||||
&& !state.is(BlockTags.LEAVES) && !state.is(Blocks.BUDDING_AMETHYST) && !state.is(Blocks.MAGMA_BLOCK);
|
||||
}
|
||||
|
||||
private static long chunkSeed(long seed, int x, int z, long salt) {
|
||||
return WoodlandGroveGeometry.seed(seed, x * 16, 0, z * 16, salt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.WeakHashMap;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.data.worldgen.features.TreeFeatures;
|
||||
import net.minecraft.data.worldgen.features.VegetationFeatures;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.VegetationBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.feature.Feature;
|
||||
import net.minecraft.world.level.levelgen.feature.AbstractHugeMushroomFeature;
|
||||
|
||||
/** Alpha.10: sheltered vanilla mushroom trees and dark oak on natural lower floors,
|
||||
* plus one globally selected remarkable tree.
|
||||
* Placement runs only in new generation. No world journal or mutable placement counter is retained. */
|
||||
public final class PopulationGrovesFeature implements Feature {
|
||||
public static final MapCodec<PopulationGrovesFeature> CODEC = MapCodec.unit(PopulationGrovesFeature::new);
|
||||
private static final Map<RandomState, Optional<RemarkableTree>> REMARKABLE =
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
|
||||
public record RemarkableTree(BlockPos base, String featureId, int radius, int height) {
|
||||
public RemarkableTree { base = base.immutable(); }
|
||||
}
|
||||
private record Species(ResourceKey<Feature> key, int width, int radius, int height) {}
|
||||
private record Candidate(int x, int z, long rank) {}
|
||||
@FunctionalInterface
|
||||
private interface Protection {
|
||||
boolean intersects(BlockPos base, int radius, int height, int rootDepth);
|
||||
}
|
||||
private static final Species CHERRY = new Species(TreeFeatures.CHERRY, 1, 9, 15);
|
||||
private static final Species[] SPECIES = {CHERRY, CHERRY, CHERRY, CHERRY,
|
||||
new Species(TreeFeatures.SPRUCE, 1, 5, 15), new Species(TreeFeatures.JUNGLE_TREE_NO_VINE, 1, 5, 14),
|
||||
new Species(TreeFeatures.ACACIA, 1, 7, 14), new Species(TreeFeatures.MANGROVE, 1, 10, 20),
|
||||
new Species(TreeFeatures.PALE_OAK_BONEMEAL, 2, 6, 12)};
|
||||
|
||||
@Override public MapCodec<PopulationGrovesFeature> codec() { return CODEC; }
|
||||
|
||||
public static Optional<RemarkableTree> remarkablePlan(ServerLevel level) {
|
||||
if (!(level.getChunkSource().getGenerator() instanceof NoiseBasedChunkGenerator noise)) return Optional.empty();
|
||||
return remarkablePlan(noise, level.getChunkSource().randomState());
|
||||
}
|
||||
|
||||
public static Optional<RemarkableTree> remarkablePlan(NoiseBasedChunkGenerator noise, RandomState state) {
|
||||
if (!PopulationHydrologyRuntime.enabled(noise)) return Optional.empty();
|
||||
return REMARKABLE.computeIfAbsent(state, ignored -> {
|
||||
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
|
||||
WoodlandGroveGeometry.Sampler sampler = (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z);
|
||||
Protection protection = (base, radius, height, rootDepth) ->
|
||||
PopulationHydrologyRuntime.protects(noise, state, base, radius, height, rootDepth);
|
||||
Species species = SPECIES[Math.floorMod(WoodlandGroveGeometry.seed(state.seed(), 0, 0, 0, 0x70EEL), SPECIES.length)];
|
||||
var candidates = new ArrayList<Candidate>();
|
||||
// One global candidate stays in the starter centre; ordinary groves use the full capacity.
|
||||
for (int x = -152; x <= 152; x += 16) for (int z = -152; z <= 152; z += 16) {
|
||||
if (Math.hypot(x, z) <= 168) candidates.add(new Candidate(x, z,
|
||||
WoodlandGroveGeometry.seed(state.seed(), x, 0, z, 0x71AFL)));
|
||||
}
|
||||
candidates.sort(Comparator.comparingLong(Candidate::rank).thenComparingInt(Candidate::x).thenComparingInt(Candidate::z));
|
||||
for (Candidate candidate : candidates) {
|
||||
int bed = WoodlandGroveGeometry.highestSurface(sampler, candidate.x(), candidate.z());
|
||||
if (!WoodlandGroveGeometry.naturalFootprint(sampler, candidate.x(), bed, candidate.z(), species.width())) continue;
|
||||
BlockPos base = new BlockPos(candidate.x(), bed + 1, candidate.z());
|
||||
if (blocked(protection, base, species.radius() + 1, species.height(), rootDepth(species))) continue;
|
||||
if (!WoodlandGroveGeometry.treeRoom(sampler, base.getX(), base.getY(), base.getZ(),
|
||||
species.width(), species.radius(), species.height())) continue;
|
||||
return Optional.of(new RemarkableTree(base, species.key().identifier().toString(), species.radius(), species.height()));
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource ignored, BlockPos origin) {
|
||||
if (!(generator instanceof NoiseBasedChunkGenerator noise) || !PopulationHydrologyRuntime.enabled(noise)) return false;
|
||||
int minX = (origin.getX() >> 4) * 16, minZ = (origin.getZ() >> 4) * 16;
|
||||
if (!PopulationHydrologyRuntime.capacity(noise).areaExists(minX >> 4, minZ >> 4)) return false;
|
||||
var state = level.getLevel().getChunkSource().randomState();
|
||||
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
|
||||
WoodlandGroveGeometry.Sampler sampler = (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z);
|
||||
Protection protection = (base, radius, height, rootDepth) ->
|
||||
PopulationHydrologyRuntime.protects(noise, state, base, radius, height, rootDepth);
|
||||
boolean changed = false;
|
||||
int[] heights = new int[256];
|
||||
Arrays.fill(heights, Integer.MIN_VALUE);
|
||||
var remarkable = remarkablePlan(noise, state);
|
||||
if (remarkable.isPresent()) {
|
||||
var tree = remarkable.get();
|
||||
BlockPos base = tree.base();
|
||||
if ((base.getX() >> 4) == (minX >> 4) && (base.getZ() >> 4) == (minZ >> 4)) {
|
||||
for (Species species : SPECIES) if (species.key().identifier().toString().equals(tree.featureId())) {
|
||||
changed |= tree(level, generator, sampler, state.seed(), base, species, protection, 0x723BL);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// The scarce tall rooms usually go to dark oak first; one quarter of chunks
|
||||
// give mushroom trees first choice. This changes priority, never attempt budgets
|
||||
// or natural fit requirements, and uses a separate deterministic chunk seed.
|
||||
boolean mushroomsFirst = Math.floorMod(WoodlandGroveGeometry.seed(state.seed(), minX, 0, minZ, 0x8503L), 4) == 0;
|
||||
if (mushroomsFirst) changed |= mushroomTrees(level, generator, sampler, state.seed(), protection, minX, minZ, heights);
|
||||
// Four spaced origins per chunk; the 2x2 trunk and full canopy still have to fit the cave.
|
||||
for (int x = minX + 4; x < minX + 16; x += 8) for (int z = minZ + 4; z < minZ + 16; z += 8) {
|
||||
int highest = localTop(level, sampler, protection, minX, minZ, x, z, heights), placed = 0;
|
||||
for (int bed = highest - 8; bed >= 40; bed--) {
|
||||
BlockPos base = new BlockPos(x, bed + 1, z);
|
||||
if (!ground(level, base.below()) || !level.getBlockState(base).isAir()) continue;
|
||||
var biome = level.getBiome(base);
|
||||
boolean dark = biome.is(PopulationIslandBiomeSource.DARK_GROVE);
|
||||
boolean bamboo = biome.is(PopulationIslandBiomeSource.BAMBOO_GROVE);
|
||||
if (!dark && !bamboo) continue;
|
||||
long placementSeed = WoodlandGroveGeometry.seed(state.seed(), x, bed, z, 0x730DL);
|
||||
if (dark) {
|
||||
if (tree(level, generator, sampler, state.seed(), base,
|
||||
new Species(TreeFeatures.DARK_OAK, 2, 6, 12), protection, 0x730DL)) { changed = true; placed++; }
|
||||
} else if (Math.floorMod(placementSeed, 8) == 0) {
|
||||
if (tree(level, generator, sampler, state.seed(), base,
|
||||
new Species(TreeFeatures.JUNGLE_TREE_NO_VINE, 1, 5, 14), protection, 0x735BL)) { changed = true; placed++; }
|
||||
} else if (Math.floorMod(placementSeed, 3) == 0
|
||||
&& WoodlandGroveGeometry.naturalFootprint(sampler, x, bed, z, 1)
|
||||
&& WoodlandGroveGeometry.treeRoom(sampler, x, bed + 1, z, 1, 3, 16)
|
||||
&& !blocked(protection, base, 4, 17) && actualRoom(level, base, 1, 3, 16)) {
|
||||
setBlock(level, base.below(), Blocks.PODZOL.defaultBlockState());
|
||||
changed |= level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(VegetationFeatures.BAMBOO_NO_PODZOL)
|
||||
.value().place(level, generator, RandomSource.create(placementSeed), base);
|
||||
placed++;
|
||||
}
|
||||
if (placed >= 2) break;
|
||||
}
|
||||
}
|
||||
if (!mushroomsFirst) changed |= mushroomTrees(level, generator, sampler, state.seed(), protection, minX, minZ, heights);
|
||||
// Coherent ground patches: about 40% moss, 40% podzol, 20% mycelium.
|
||||
// Small mushrooms are accents (one opportunity per hundred columns), not the canopy.
|
||||
for (int x = minX; x < minX + 16; x++) for (int z = minZ; z < minZ + 16; z++) {
|
||||
int highest = localTop(level, sampler, protection, minX, minZ, x, z, heights), floors = 0;
|
||||
long detail = WoodlandGroveGeometry.seed(state.seed(), x, 0, z, 0x843EL);
|
||||
boolean smallMushroomPlaced = false;
|
||||
for (int bed = highest - 8; bed >= 32; bed--) {
|
||||
BlockPos floor = new BlockPos(x, bed, z), plant = floor.above();
|
||||
if (!ground(level, floor) || !level.getBlockState(plant).isAir()) continue;
|
||||
var biome = level.getBiome(plant);
|
||||
if (!biome.is(PopulationIslandBiomeSource.DARK_GROVE) && !biome.is(PopulationIslandBiomeSource.BAMBOO_GROVE)) continue;
|
||||
if (!WoodlandGroveGeometry.naturalFootprint(sampler, x, bed, z, 1) || blocked(protection, plant, 1, 2)) continue;
|
||||
long patch = WoodlandGroveGeometry.seed(state.seed(), Math.floorDiv(x, 7), 0, Math.floorDiv(z, 7), 0x742DL);
|
||||
int soilKind = Math.floorMod(patch, 5);
|
||||
BlockState soil = (soilKind < 2 ? Blocks.MOSS_BLOCK : soilKind < 4 ? Blocks.PODZOL : Blocks.MYCELIUM).defaultBlockState();
|
||||
setBlock(level, floor, soil);
|
||||
if (!smallMushroomPlaced && Math.floorMod(detail, 100) == 0) {
|
||||
BlockState mushroom = (Math.floorMod(detail >>> 8, 2) == 0 ? Blocks.BROWN_MUSHROOM : Blocks.RED_MUSHROOM).defaultBlockState();
|
||||
if (mushroom.canSurvive(level, plant)) {
|
||||
setBlock(level, plant, mushroom);
|
||||
smallMushroomPlaced = true;
|
||||
}
|
||||
}
|
||||
changed = true;
|
||||
if (++floors >= 3) break;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private boolean mushroomTrees(WorldGenLevel level, ChunkGenerator generator, WoodlandGroveGeometry.Sampler sampler,
|
||||
long seed, Protection protection, int minX, int minZ, int[] heights) {
|
||||
boolean changed = false;
|
||||
RandomSource mushroomSites = RandomSource.create(WoodlandGroveGeometry.seed(seed, minX, 0, minZ, 0x8501L));
|
||||
int mushroomAttempts = 1 + mushroomSites.nextInt(3);
|
||||
for (int attempt = 0; attempt < mushroomAttempts; attempt++) {
|
||||
int x = minX + 3 + mushroomSites.nextInt(10), z = minZ + 3 + mushroomSites.nextInt(10);
|
||||
int highest = localTop(level, sampler, protection, minX, minZ, x, z, heights);
|
||||
for (int bed = highest - 8; bed >= 32; bed--) {
|
||||
BlockPos base = new BlockPos(x, bed + 1, z);
|
||||
if (!ground(level, base.below()) || !level.getBlockState(base).isAir()
|
||||
|| !level.getBiome(base).is(PopulationIslandBiomeSource.DARK_GROVE)) continue;
|
||||
long mushroomSeed = WoodlandGroveGeometry.seed(seed, x, bed, z, 0x8502L + attempt);
|
||||
var key = Math.floorMod(mushroomSeed, 2) == 0 ? TreeFeatures.HUGE_BROWN_MUSHROOM : TreeFeatures.HUGE_RED_MUSHROOM;
|
||||
Feature feature = level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(key).value();
|
||||
if (!(feature instanceof AbstractHugeMushroomFeature mushroom)) break;
|
||||
int height = mushroom.getTreeHeight(RandomSource.create(mushroomSeed));
|
||||
int radius = mushroom.foliageRadius();
|
||||
if (!mushroomRoom(level, sampler, protection, base, radius, height)) continue;
|
||||
BlockState originalFloor = level.getBlockState(base.below());
|
||||
setBlock(level, base.below(), (Math.floorMod(mushroomSeed, 5) == 0 ? Blocks.MYCELIUM : Blocks.PODZOL).defaultBlockState());
|
||||
// Vanilla consumes its height draw first. Replaying the same random seed
|
||||
// gives precisely the height whose natural envelope was checked above.
|
||||
boolean placed = mushroom.place(level, generator, RandomSource.create(mushroomSeed), base);
|
||||
if (!placed) setBlock(level, base.below(), originalFloor);
|
||||
changed |= placed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private boolean tree(WorldGenLevel level, ChunkGenerator generator, WoodlandGroveGeometry.Sampler sampler, long seed,
|
||||
BlockPos base, Species species, Protection protection, long salt) {
|
||||
if (blocked(protection, base, species.radius() + 1, species.height(), rootDepth(species))
|
||||
|| !WoodlandGroveGeometry.naturalFootprint(sampler, base.getX(), base.getY() - 1, base.getZ(), species.width())
|
||||
|| !actualRoom(level, base, species.width(), species.radius(), species.height())
|
||||
|| !WoodlandGroveGeometry.treeRoom(sampler, base.getX(), base.getY(), base.getZ(), species.width(), species.radius(), species.height())) return false;
|
||||
for (int dx = 0; dx < species.width(); dx++) for (int dz = 0; dz < species.width(); dz++) {
|
||||
BlockPos floor = base.offset(dx, -1, dz);
|
||||
if (!ground(level, floor)) return false;
|
||||
}
|
||||
for (int dx = 0; dx < species.width(); dx++) for (int dz = 0; dz < species.width(); dz++) {
|
||||
setBlock(level, base.offset(dx, -1, dz), Blocks.PODZOL.defaultBlockState());
|
||||
}
|
||||
return level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(species.key()).value()
|
||||
.place(level, generator, RandomSource.create(WoodlandGroveGeometry.seed(seed, base.getX(), base.getY(), base.getZ(), salt)), base);
|
||||
}
|
||||
|
||||
private static boolean actualRoom(WorldGenLevel level, BlockPos base, int width, int radius, int height) {
|
||||
for (int y = 0; y <= height; y++) {
|
||||
int from = y < 3 ? 0 : -radius, to = y < 3 ? width - 1 : radius;
|
||||
for (int dx = from; dx <= to; dx++) for (int dz = from; dz <= to; dz++) {
|
||||
BlockState current = level.getBlockState(base.offset(dx, y, dz));
|
||||
if (!current.getFluidState().isEmpty() || !(current.isAir() || current.is(BlockTags.LEAVES)
|
||||
|| current.getBlock() instanceof VegetationBlock)) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean ground(WorldGenLevel level, BlockPos floor) {
|
||||
BlockState state = level.getBlockState(floor);
|
||||
if (state.is(Blocks.DEEPSLATE)) {
|
||||
// Deep humid ledges can support organic soil. Check every tree foot rather
|
||||
// than admitting this rock globally, including for the remarkable tree.
|
||||
var biome = level.getBiome(floor.above());
|
||||
return biome.is(PopulationIslandBiomeSource.DARK_GROVE) || biome.is(PopulationIslandBiomeSource.BAMBOO_GROVE);
|
||||
}
|
||||
return state.is(Blocks.STONE) || state.is(Blocks.DIRT) || state.is(Blocks.GRASS_BLOCK) || state.is(Blocks.PODZOL)
|
||||
|| state.is(Blocks.MYCELIUM) || state.is(Blocks.ROOTED_DIRT) || state.is(Blocks.COARSE_DIRT)
|
||||
|| state.is(Blocks.GRANITE) || state.is(Blocks.DIORITE) || state.is(Blocks.ANDESITE) || state.is(Blocks.TUFF)
|
||||
|| state.is(Blocks.COBBLESTONE) || state.is(Blocks.MOSSY_COBBLESTONE) || state.is(Blocks.PACKED_MUD);
|
||||
}
|
||||
|
||||
private static int localTop(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler, Protection protection,
|
||||
int minX, int minZ, int x, int z, int[] heights) {
|
||||
int index = (x - minX) * 16 + z - minZ;
|
||||
if (heights[index] != Integer.MIN_VALUE) return heights[index];
|
||||
// In an untouched column the heightmap bounds the original surface cheaply.
|
||||
// A protected water/lava column may have been carved: recover its raw top
|
||||
// without depending on the current hydrology plan's representation.
|
||||
int ceiling = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z) - 1;
|
||||
if (protection.intersects(new BlockPos(x, level.getMinY(), z), 0, level.getHeight() - 1, 0))
|
||||
ceiling = level.getMaxY();
|
||||
return heights[index] = WoodlandGroveGeometry.highestSurfaceBelow(sampler, x, z, ceiling);
|
||||
}
|
||||
|
||||
private static boolean mushroomRoom(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler, Protection protection,
|
||||
BlockPos base, int radius, int height) {
|
||||
if (blocked(protection, base, radius + 1, height + 1, 4)
|
||||
|| !WoodlandGroveGeometry.naturalFootprint(sampler, base.getX(), base.getY() - 1, base.getZ(), 1)) return false;
|
||||
for (int depth = 1; depth <= 3; depth++) {
|
||||
BlockPos floor = base.below(depth);
|
||||
if (!level.getBlockState(floor).isCollisionShapeFullBlock(level, floor)) return false;
|
||||
}
|
||||
// A complete conservative column around the cap handles both brown umbrellas
|
||||
// and the lower rounded red cap. Neither natural rock nor existing logs move.
|
||||
for (int y = 0; y <= height + 1; y++) for (int dx = -radius; dx <= radius; dx++) for (int dz = -radius; dz <= radius; dz++) {
|
||||
BlockPos p = base.offset(dx, y, dz);
|
||||
BlockState current = level.getBlockState(p);
|
||||
if (sampler.sample(p.getX(), p.getY(), p.getZ()) > 0 || !current.getFluidState().isEmpty()
|
||||
|| !(current.isAir() || current.is(BlockTags.LEAVES))) return false;
|
||||
}
|
||||
// Being below another surface is not enough: the same column needs an actual
|
||||
// original rock ceiling above the whole mushroom, not an open hillside.
|
||||
for (int y = base.getY() + height + 2; y <= level.getMaxY(); y++)
|
||||
if (sampler.sample(base.getX(), y, base.getZ()) > 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int rootDepth(Species species) { return species.key().equals(TreeFeatures.MANGROVE) ? 16 : 4; }
|
||||
|
||||
private static boolean blocked(Protection protection, BlockPos base, int radius, int height) {
|
||||
return blocked(protection, base, radius, height, 4);
|
||||
}
|
||||
|
||||
private static boolean blocked(Protection protection, BlockPos base, int radius, int height, int rootDepth) {
|
||||
return protection.intersects(base, radius, height, rootDepth);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+239
@@ -0,0 +1,239 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.util.*;
|
||||
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;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
|
||||
/** Alpha.10 plans only regions touched by generation or a requested protection volume. */
|
||||
public final class PopulationHydrologyRuntime {
|
||||
public record RegionPlan(IslandCapacity.Region region, PopulationHydrology.Plan water, PopulationLavaDeposit.Plan lava) {}
|
||||
private static final Map<RandomState, PopulationPlanCache<IslandCapacity.Region, RegionPlan>> WORLDS =
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
private PopulationHydrologyRuntime() {}
|
||||
public static boolean enabled(NoiseBasedChunkGenerator generator) { return IslandCapacity.forGenerator(generator) != null; }
|
||||
public static IslandCapacity capacity(NoiseBasedChunkGenerator generator) {
|
||||
return Objects.requireNonNull(IslandCapacity.forGenerator(generator), "Population generation is not enabled");
|
||||
}
|
||||
private static PopulationPlanCache<IslandCapacity.Region, RegionPlan> regions(RandomState state) {
|
||||
synchronized (WORLDS) { return WORLDS.computeIfAbsent(state, ignored -> new PopulationPlanCache<>()); }
|
||||
}
|
||||
public static int cachedRegionCount(RandomState state) {
|
||||
PopulationPlanCache<IslandCapacity.Region, RegionPlan> regions;
|
||||
synchronized (WORLDS) { regions = WORLDS.get(state); }
|
||||
if (regions == null) return 0;
|
||||
return regions.size();
|
||||
}
|
||||
public static RegionPlan regionPlan(NoiseBasedChunkGenerator generator, RandomState state, IslandCapacity.Region region) {
|
||||
var capacity = capacity(generator);
|
||||
var regions = regions(state);
|
||||
// Only cache misses wait for the bounded planner. Completed regions stay readable
|
||||
// while another region is being computed; no neighbouring plan is built recursively.
|
||||
return regions.getOrCreate(region, () -> {
|
||||
long start = System.nanoTime();
|
||||
var density = generator.generatorSettings().value().noiseRouter().finalDensity();
|
||||
var sampler = state.getSampler(density);
|
||||
var memo = new SignMemo((x, y, z) -> {
|
||||
long wx = (long) x + region.originX(), wz = (long) z + region.originZ();
|
||||
if (Math.hypot((double) wx, wz) >= capacity.terrainLimit()) return -1;
|
||||
return sampler.sampleValue(SamplerContext.EMPTY_UNCACHED, (int) wx, y, (int) wz);
|
||||
});
|
||||
long seed = PopulationIslandDensity.regionSeed(state.seed(), region);
|
||||
var water = PopulationHydrology.create(seed, memo).admitted();
|
||||
var lava = PopulationLavaDeposit.create(seed, memo, water).admitted();
|
||||
var result = new RegionPlan(region, water.translated(region.originX(), region.originZ()),
|
||||
lava.translated(region.originX(), region.originZ()));
|
||||
SanctuaryMod.LOGGER.info("Population {} seed {} region {},{}: {} features, {} water columns, {} lava sources, {} density probes, {} ms",
|
||||
capacity.players(), state.seed(), region.x(), region.z(), water.features().size(), water.cells().size(),
|
||||
lava.lavaCells().size(), memo.samples, (System.nanoTime() - start) / 1_000_000);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
public static RegionPlan regionPlan(ServerLevel level, IslandCapacity.Region region) {
|
||||
return regionPlan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(), level.getChunkSource().randomState(), region);
|
||||
}
|
||||
public static List<RegionPlan> plansIntersecting(NoiseBasedChunkGenerator generator, RandomState state,
|
||||
int minX, int minZ, int maxX, int maxZ) {
|
||||
var capacity = capacity(generator);
|
||||
var first = capacity.regionAt(minX, minZ); var last = capacity.regionAt(maxX, maxZ);
|
||||
List<RegionPlan> plans = new ArrayList<>();
|
||||
for (int x = first.x(); x <= last.x(); x++) for (int z = first.z(); z <= last.z(); z++) {
|
||||
var region = new IslandCapacity.Region(x, z);
|
||||
double nearestX = Math.max(0, Math.max(region.minX(), -region.maxX()));
|
||||
double nearestZ = Math.max(0, Math.max(region.minZ(), -region.maxZ()));
|
||||
if (Math.hypot(nearestX, nearestZ) < capacity.terrainLimit()) plans.add(regionPlan(generator, state, region));
|
||||
}
|
||||
return List.copyOf(plans);
|
||||
}
|
||||
public static List<PopulationHydrology.Cell> cellsInChunk(NoiseBasedChunkGenerator generator, RandomState state, int cx, int cz) {
|
||||
if (!capacity(generator).areaExists(cx, cz)) return List.of();
|
||||
return regionPlan(generator, state, capacity(generator).regionAt(cx * 16, cz * 16)).water().cellsInChunk(cx, cz);
|
||||
}
|
||||
public static List<PopulationHydrology.Cell> cellsAt(NoiseBasedChunkGenerator generator, RandomState state, int x, int z) {
|
||||
if (Math.hypot((double) x, z) >= capacity(generator).terrainLimit()) return List.of();
|
||||
return regionPlan(generator, state, capacity(generator).regionAt(x, z)).water().cellsAt(x, z);
|
||||
}
|
||||
public static PopulationHydrology.Cell cellAt(NoiseBasedChunkGenerator generator, RandomState state, int x, int y, int z) {
|
||||
for (var cell : cellsAt(generator, state, x, z)) if (y >= cell.bedY() - cell.sedimentDepth() - 1
|
||||
&& y <= Math.max(cell.waterY(), cell.carveTop())) return cell;
|
||||
return null;
|
||||
}
|
||||
/** Only the sign is observed by all local planners. The two bit masks are populated lazily
|
||||
* and released after one region; there is no dense whole-island height-volume scan. */
|
||||
private static final class SignMemo implements PopulationHydrology.Sampler {
|
||||
final PopulationHydrology.Sampler original;
|
||||
final Map<Long, long[]> columns = new HashMap<>();
|
||||
long samples;
|
||||
SignMemo(PopulationHydrology.Sampler original) { this.original = original; }
|
||||
@Override public float sample(int x, int y, int z) {
|
||||
if (y < 0 || y >= 384) return -1;
|
||||
long key = ((long) x << 32) | (z & 0xffffffffL);
|
||||
long[] bits = columns.computeIfAbsent(key, ignored -> new long[12]);
|
||||
int word = y >>> 6; long mask = 1L << (y & 63);
|
||||
if ((bits[word] & mask) == 0) {
|
||||
samples++;
|
||||
if (original.sample(x, y, z) > 0) bits[word + 6] |= mask;
|
||||
bits[word] |= mask;
|
||||
}
|
||||
return (bits[word + 6] & mask) != 0 ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
public static void apply(NoiseBasedChunkGenerator generator, RandomState randomState, ChunkAccess chunk) {
|
||||
if (!enabled(generator)) return;
|
||||
var chunkPos = chunk.getPos();
|
||||
if (!capacity(generator).areaExists(chunkPos.x(), chunkPos.z())) return;
|
||||
var region = regionPlan(generator, randomState, capacity(generator).regionAt(chunkPos.getMinBlockX(), chunkPos.getMinBlockZ()));
|
||||
var plan = region.water();
|
||||
var cells = plan.cellsInChunk(chunkPos.x(), chunkPos.z());
|
||||
var springs = plan.springsInChunk(chunkPos.x(), chunkPos.z());
|
||||
var outlets = new java.util.ArrayList<>(plan.springs().stream().map(PopulationHydrology.Spring::outlet)
|
||||
.filter(outlet -> (outlet.x() >> 4) == chunkPos.x() && (outlet.z() >> 4) == chunkPos.z()).toList());
|
||||
for (var spill : plan.spills()) {
|
||||
var outlet = spill.outlet();
|
||||
if ((outlet.x() >> 4) == chunkPos.x() && (outlet.z() >> 4) == chunkPos.z()) outlets.add(outlet);
|
||||
}
|
||||
var lava = region.lava();
|
||||
for (var fall : lava.falls()) {
|
||||
var outlet = fall.outlet();
|
||||
if ((outlet.x() >> 4) == chunkPos.x() && (outlet.z() >> 4) == chunkPos.z())
|
||||
outlets.add(new PopulationHydrology.Position(outlet.x(), outlet.y(), outlet.z()));
|
||||
}
|
||||
var lavaCells = lava.cellsInChunk(chunkPos.x(), chunkPos.z());
|
||||
var lavaSupports = lava.supportsInChunk(chunkPos.x(), chunkPos.z());
|
||||
if (cells.isEmpty() && springs.isEmpty() && outlets.isEmpty() && lavaCells.isEmpty() && lavaSupports.isEmpty()) return;
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
// Validate all replacements and two intact support layers before modifying this chunk.
|
||||
for (var cell : cells) {
|
||||
for (int y = cell.bedY() - cell.sedimentDepth() - 1; y <= cell.bedY(); y++) {
|
||||
requireSolid(chunk, pos.set(cell.x(), y, cell.z()), randomState.seed());
|
||||
}
|
||||
}
|
||||
for (var spring : springs) {
|
||||
var source = spring.source();
|
||||
requireSolid(chunk, pos.set(source.x(), source.y(), source.z()), randomState.seed());
|
||||
}
|
||||
for (var support : lavaSupports) {
|
||||
requireSolid(chunk, pos.set(support.x(), support.y(), support.z()), randomState.seed());
|
||||
if (chunk.getBlockState(pos).ignitedByLava()) {
|
||||
throw new IllegalStateException("Lava pocket requires nonflammable natural support at " + pos);
|
||||
}
|
||||
}
|
||||
for (var cell : lavaCells) {
|
||||
requireSolid(chunk, pos.set(cell.x(), cell.y(), cell.z()), randomState.seed());
|
||||
}
|
||||
for (var cell : cells) {
|
||||
for (int depth = 0; depth < cell.sedimentDepth(); depth++) {
|
||||
pos.set(cell.x(), cell.bedY() - depth, cell.z());
|
||||
BlockState material = PopulationMaterials.sedimentBlock(cell, randomState.seed(), depth);
|
||||
chunk.setBlockState(pos, material, 0);
|
||||
}
|
||||
for (int y = cell.bedY() + 1; y <= Math.max(cell.waterY(), cell.carveTop()); y++) {
|
||||
pos.set(cell.x(), y, cell.z());
|
||||
chunk.setBlockState(pos, cell.hasWater() && y <= cell.waterY()
|
||||
? Blocks.WATER.defaultBlockState() : Blocks.AIR.defaultBlockState(), 0);
|
||||
if (cell.hasWater() && y <= cell.waterY()) chunk.markPosForPostProcessing(pos);
|
||||
}
|
||||
}
|
||||
for (var spring : springs) {
|
||||
var source = spring.source();
|
||||
pos.set(source.x(), source.y(), source.z());
|
||||
chunk.setBlockState(pos, Blocks.WATER.defaultBlockState(), 0);
|
||||
// Only the source is generated. Vanilla fluid ticks create the descending waterfall.
|
||||
chunk.markPosForPostProcessing(pos);
|
||||
}
|
||||
for (var cell : lavaCells) {
|
||||
pos.set(cell.x(), cell.y(), cell.z());
|
||||
chunk.setBlockState(pos, (cell.lava() ? Blocks.LAVA : Blocks.AIR).defaultBlockState(), 0);
|
||||
if (cell.lava()) chunk.markPosForPostProcessing(pos);
|
||||
}
|
||||
// This existing vanilla queue is consumed after neighbouring chunks finish decoration.
|
||||
// Mark the outlet in its own chunk, including when its source belongs to a neighbour.
|
||||
for (var outlet : outlets) {
|
||||
pos.set(outlet.x(), outlet.y(), outlet.z());
|
||||
chunk.markPosForPostProcessing(pos);
|
||||
}
|
||||
Heightmap.primeHeightmaps(chunk, EnumSet.of(Heightmap.Types.WORLD_SURFACE_WG,
|
||||
Heightmap.Types.OCEAN_FLOOR_WG));
|
||||
}
|
||||
|
||||
/** Reserve water, sediment and hot-rock volumes before placing vegetation. */
|
||||
public static boolean protects(NoiseBasedChunkGenerator generator, RandomState randomState, BlockPos base,
|
||||
int radius, int height, int rootDepth) {
|
||||
for (RegionPlan region : plansIntersecting(generator, randomState, base.getX() - radius, base.getZ() - radius,
|
||||
base.getX() + radius, base.getZ() + radius)) {
|
||||
if (protects(region, base, radius, height, rootDepth)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean protects(RegionPlan region, BlockPos base, int radius, int height, int rootDepth) {
|
||||
var water = region.water();
|
||||
var lava = region.lava();
|
||||
int low = base.getY() - rootDepth, high = base.getY() + height;
|
||||
for (int x = base.getX() - radius; x <= base.getX() + radius; x++) {
|
||||
for (int z = base.getZ() - radius; z <= base.getZ() + radius; z++) {
|
||||
for (var cell : water.cellsAt(x, z))
|
||||
if (low <= cell.carveTop() && high >= cell.bedY() - cell.sedimentDepth() - 1) return true;
|
||||
}
|
||||
}
|
||||
for (var spring : water.springs()) {
|
||||
var bounds = spring.flowBounds();
|
||||
if (base.getX() + radius >= bounds.minX() - 2 && base.getX() - radius <= bounds.maxX() + 2
|
||||
&& base.getZ() + radius >= bounds.minZ() - 2 && base.getZ() - radius <= bounds.maxZ() + 2
|
||||
&& low <= bounds.maxY() + 2 && high >= bounds.minY() - 2) return true;
|
||||
var source = spring.source();
|
||||
if (Math.abs(base.getX() - source.x()) <= radius + 3 && Math.abs(base.getZ() - source.z()) <= radius + 3
|
||||
&& low <= source.y() + 3 && high >= source.y() - 3) return true;
|
||||
}
|
||||
for (var bounds : lava.flowBounds()) {
|
||||
if (base.getX() + radius >= bounds.minX() - 8 && base.getX() - radius <= bounds.maxX() + 8
|
||||
&& base.getZ() + radius >= bounds.minZ() - 8 && base.getZ() - radius <= bounds.maxZ() + 8
|
||||
&& low <= bounds.maxY() + 8 && high >= bounds.minY() - 8) return true;
|
||||
}
|
||||
for (var spill : water.spills()) {
|
||||
var bounds = spill.flowBounds();
|
||||
if (base.getX() + radius >= bounds.minX() - 3 && base.getX() - radius <= bounds.maxX() + 3
|
||||
&& base.getZ() + radius >= bounds.minZ() - 3 && base.getZ() - radius <= bounds.maxZ() + 3
|
||||
&& low <= bounds.maxY() + 3 && high >= bounds.minY() - 3) return true;
|
||||
}
|
||||
for (var cell : lava.cells()) if (Math.abs(base.getX() - cell.x()) <= radius + 2 && Math.abs(base.getZ() - cell.z()) <= radius + 2
|
||||
&& low <= cell.y() + 2 && high >= cell.y() - 2) return true;
|
||||
for (var support : lava.supports()) if (Math.abs(base.getX() - support.x()) <= radius + 2 && Math.abs(base.getZ() - support.z()) <= radius + 2
|
||||
&& low <= support.y() + 2 && high >= support.y() - 2) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void requireSolid(ChunkAccess chunk, BlockPos pos, long seed) {
|
||||
if (!chunk.getBlockState(pos).isCollisionShapeFullBlock(chunk, pos)) {
|
||||
throw new IllegalStateException("Population hydrology differs from terrain at " + pos + " for seed " + seed);
|
||||
}
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.util.stream.Stream;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeResolver;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Climate;
|
||||
|
||||
/** Seeded alpha.10 rift climate, separate from all saved earlier biome-source codecs. */
|
||||
public final class PopulationIslandBiomeSource extends BiomeSource {
|
||||
public static final ResourceKey<Biome> OAK_FOREST = key("population_oak_forest");
|
||||
public static final ResourceKey<Biome> BIRCH_FOREST = key("population_birch_forest");
|
||||
public static final ResourceKey<Biome> CLEARING = key("population_clearing");
|
||||
public static final ResourceKey<Biome> DRY_WOODLAND = key("population_dry_woodland");
|
||||
public static final ResourceKey<Biome> ROCKY_HEATH = key("population_rocky_heath");
|
||||
public static final ResourceKey<Biome> DARK_GROVE = key("population_dark_grove");
|
||||
public static final ResourceKey<Biome> BAMBOO_GROVE = key("population_bamboo_grove");
|
||||
public static final ResourceKey<Biome> SULFUR_DEPTHS = key("population_sulfur_depths");
|
||||
public static final ResourceKey<Biome> LUSH_CAVES = key("population_lush_caves");
|
||||
public static final ResourceKey<Biome> DRIPSTONE_CAVES = key("population_dripstone_caves");
|
||||
|
||||
public static final MapCodec<PopulationIslandBiomeSource> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||
Biome.CODEC.fieldOf("oak_forest").forGetter(source -> source.oakForest),
|
||||
Biome.CODEC.fieldOf("birch_forest").forGetter(source -> source.birchForest),
|
||||
Biome.CODEC.fieldOf("clearing").forGetter(source -> source.clearing),
|
||||
Biome.CODEC.fieldOf("dry_woodland").forGetter(source -> source.dryWoodland),
|
||||
Biome.CODEC.fieldOf("rocky_heath").forGetter(source -> source.rockyHeath),
|
||||
Biome.CODEC.fieldOf("dark_grove").forGetter(source -> source.darkGrove),
|
||||
Biome.CODEC.fieldOf("bamboo_grove").forGetter(source -> source.bambooGrove),
|
||||
Biome.CODEC.fieldOf("sulfur_depths").forGetter(source -> source.sulfurDepths),
|
||||
Biome.CODEC.fieldOf("lush_caves").forGetter(source -> source.lushCaves),
|
||||
Biome.CODEC.fieldOf("dripstone_caves").forGetter(source -> source.dripstoneCaves)
|
||||
).apply(instance, PopulationIslandBiomeSource::new));
|
||||
|
||||
private final Holder<Biome> oakForest;
|
||||
private final Holder<Biome> birchForest;
|
||||
private final Holder<Biome> clearing;
|
||||
private final Holder<Biome> dryWoodland;
|
||||
private final Holder<Biome> rockyHeath;
|
||||
private final Holder<Biome> darkGrove;
|
||||
private final Holder<Biome> bambooGrove;
|
||||
private final Holder<Biome> sulfurDepths;
|
||||
private final Holder<Biome> lushCaves;
|
||||
private final Holder<Biome> dripstoneCaves;
|
||||
|
||||
public PopulationIslandBiomeSource(Holder<Biome> oakForest, Holder<Biome> birchForest, Holder<Biome> clearing,
|
||||
Holder<Biome> dryWoodland, Holder<Biome> rockyHeath, Holder<Biome> darkGrove,
|
||||
Holder<Biome> bambooGrove, Holder<Biome> sulfurDepths, Holder<Biome> lushCaves, Holder<Biome> dripstoneCaves) {
|
||||
this.oakForest = oakForest;
|
||||
this.birchForest = birchForest;
|
||||
this.clearing = clearing;
|
||||
this.dryWoodland = dryWoodland;
|
||||
this.rockyHeath = rockyHeath;
|
||||
this.darkGrove = darkGrove;
|
||||
this.bambooGrove = bambooGrove;
|
||||
this.sulfurDepths = sulfurDepths;
|
||||
this.lushCaves = lushCaves;
|
||||
this.dripstoneCaves = dripstoneCaves;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<PopulationIslandBiomeSource> codec() { return CODEC; }
|
||||
|
||||
@Override
|
||||
protected Stream<Holder<Biome>> collectPossibleBiomes() {
|
||||
return Stream.of(oakForest, birchForest, clearing, dryWoodland, rockyHeath, darkGrove, bambooGrove, sulfurDepths, lushCaves, dripstoneCaves);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiomeResolver createResolver(Climate.Sampler sampler) {
|
||||
return (quartX, quartY, quartZ) -> {
|
||||
int x = quartX * 4, y = quartY * 4, z = quartZ * 4;
|
||||
float variation = sampler.temperature().sampleValue(x, 0, z);
|
||||
float moisture = sampler.humidity().sampleValue(x, 0, z);
|
||||
return switch (PopulationBiomePalette.select(y, variation, moisture)) {
|
||||
case OAK_FOREST -> oakForest;
|
||||
case BIRCH_FOREST -> birchForest;
|
||||
case CLEARING -> clearing;
|
||||
case DRY_WOODLAND -> dryWoodland;
|
||||
case ROCKY_HEATH -> rockyHeath;
|
||||
case DARK_GROVE -> darkGrove;
|
||||
case BAMBOO_GROVE -> bambooGrove;
|
||||
case SULFUR_DEPTHS -> sulfurDepths;
|
||||
case LUSH_CAVES -> lushCaves;
|
||||
case DRIPSTONE_CAVES -> dripstoneCaves;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
private static ResourceKey<Biome> key(String path) {
|
||||
return ResourceKey.create(Registries.BIOME, SanctuaryMod.id(path));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import net.minecraft.util.Interval;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.*;
|
||||
|
||||
/** Capacity changes the envelope; all natural noise is still sampled at world coordinates. */
|
||||
public record PopulationIslandDensity(DensityFunction terrain, DensityFunction distortion,
|
||||
DensityFunction sculpt, DensityFunction detail, DensityFunction underside,
|
||||
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity) implements DensityFunction {
|
||||
public static final MapCodec<PopulationIslandDensity> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||
DensityFunction.CODEC.fieldOf("terrain").forGetter(PopulationIslandDensity::terrain),
|
||||
DensityFunction.CODEC.fieldOf("distortion").forGetter(PopulationIslandDensity::distortion),
|
||||
DensityFunction.CODEC.fieldOf("sculpt").forGetter(PopulationIslandDensity::sculpt),
|
||||
DensityFunction.CODEC.fieldOf("detail").forGetter(PopulationIslandDensity::detail),
|
||||
DensityFunction.CODEC.fieldOf("underside").forGetter(PopulationIslandDensity::underside),
|
||||
DensityFunction.CODEC.fieldOf("rift_warp").forGetter(PopulationIslandDensity::riftWarp),
|
||||
DensityFunction.CODEC.fieldOf("rift_detail").forGetter(PopulationIslandDensity::riftDetail),
|
||||
IslandCapacity.CODEC.optionalFieldOf("players", IslandCapacity.TWENTY).forGetter(PopulationIslandDensity::capacity)
|
||||
).apply(instance, PopulationIslandDensity::new));
|
||||
|
||||
@Override public DensitySampler compileSampler(CompileContext context) {
|
||||
var raw = ExactInterpolationSampler.compileTerrain(terrain, context); var edge = distortion.compileSampler(context);
|
||||
var coarse = sculpt.compileSampler(context); var fine = detail.compileSampler(context);
|
||||
var bottom = underside.compileSampler(context); var warp = riftWarp.compileSampler(context);
|
||||
var fractureDetail = riftDetail.compileSampler(context);
|
||||
long seed = context.createRandom(SanctuaryMod.id("population_rifts_alpha10")).nextLong();
|
||||
var layouts = new ConcurrentHashMap<IslandCapacity.Region, RiftShape.Layout>();
|
||||
return new DensitySampler() {
|
||||
@Override public float sampleValue(SamplerContext sample, int x, int y, int z) {
|
||||
if (Math.hypot((double) x, z) >= capacity.terrainLimit() || y <= 0 || y >= 384) return -1;
|
||||
int localY = y - 64;
|
||||
float natural = PopulationIslandShape.raisedDensity(x, y, z, raw.sampleValue(sample, x, localY, z),
|
||||
edge.sampleValue(sample, x, 0, z), coarse.sampleValue(sample, x, localY, z),
|
||||
fine.sampleValue(sample, x, localY, z), bottom.sampleValue(sample, x, localY, z), 64, 384, capacity);
|
||||
if (natural <= 0) return natural;
|
||||
var region = capacity.regionAt(x, z);
|
||||
var layout = layouts.computeIfAbsent(region, r -> RiftShape.layout(regionSeed(seed, r)));
|
||||
int rx = x - region.originX(), rz = z - region.originZ();
|
||||
if (!RiftShape.mayAffect(layout, rx, rz)) return natural;
|
||||
return RiftShape.density(layout, rx, y, rz, natural,
|
||||
warp.sampleValue(sample, x, y, z), fractureDetail.sampleValue(sample, x, y, z));
|
||||
}
|
||||
@Override public void sampleVolume(SamplerContext sample, DensityBuffer buffer, DensityVolume volume) {
|
||||
DensitySampler.sampleVolumeNaive(sample, buffer, volume, this);
|
||||
}
|
||||
};
|
||||
}
|
||||
public static long regionSeed(long seed, IslandCapacity.Region region) {
|
||||
long value = seed ^ ((long) region.x() << 32) ^ (region.z() & 0xffffffffL) ^ 0xA10CA7E1L;
|
||||
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
|
||||
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
|
||||
return value ^ (value >>> 31);
|
||||
}
|
||||
@Override public DensityFunction rewriteChildren(DfRewriteRule rule) {
|
||||
return new PopulationIslandDensity(rule.rewrite(terrain), rule.rewrite(distortion), rule.rewrite(sculpt),
|
||||
rule.rewrite(detail), rule.rewrite(underside), rule.rewrite(riftWarp), rule.rewrite(riftDetail), capacity);
|
||||
}
|
||||
@Override public Interval range() { return Interval.of(-1, 1); }
|
||||
@Override public int domainAxes() { return ALL_AXES; }
|
||||
@Override public MapCodec<PopulationIslandDensity> codec() { return CODEC; }
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
/** Alpha.10 keeps local erosion scales while expanding only the outer envelope. */
|
||||
public final class PopulationIslandShape {
|
||||
private PopulationIslandShape() {}
|
||||
|
||||
/** Generation v3. The natural field and lateral sculpture are sampled in their original frame. */
|
||||
public static float raisedDensity(int x, int y, int z, float terrain, float distortion,
|
||||
float sculpt, float detail, float underside, int offset, int height, IslandCapacity capacity) {
|
||||
double distance = Math.hypot((double) x, (double) z);
|
||||
if (distance >= capacity.terrainLimit() || y <= 0 || y >= height) return -1.0F;
|
||||
double localY = (double) y - offset;
|
||||
double erosion = rimErosion(distance, localY, distortion, sculpt, detail, capacity);
|
||||
|
||||
// The former uniform lower gradient is absent from the new natural field. Let its seeded
|
||||
// lobes continue downwards, then sculpt them over a broad, spatially varying transition.
|
||||
// The extra margin below the translated island must not merely reveal the same flat cut.
|
||||
double bottom = -24.0 + Math.clamp(underside, -1.0F, 1.0F) * 16.0
|
||||
+ Math.clamp(sculpt, -1.0F, 1.0F) * 8.0 + Math.clamp(detail, -1.0F, 1.0F) * 4.0;
|
||||
double lower = Math.clamp((bottom + 72.0 - localY) / 72.0, 0.0, 1.0);
|
||||
erosion += 2.0 * lower * lower * lower * lower;
|
||||
// Upper silhouette is exactly the old one translated by offset. Absolute-world fades are
|
||||
// safety margins, outside the normal terrain, rather than the island's visible underside.
|
||||
erosion += 2.0 * smoothstep((localY - 224.0) / 32.0);
|
||||
erosion += 2.0 * smoothstep((24.0 - y) / 24.0);
|
||||
erosion += 2.0 * smoothstep((y - (height - 24.0)) / 24.0);
|
||||
return bounded(terrain, erosion);
|
||||
}
|
||||
|
||||
private static double rimErosion(double distance, double y, float distortion, float sculpt, float detail, IslandCapacity capacity) {
|
||||
double coast = Math.clamp(distortion, -1.0F, 1.0F);
|
||||
double coarse = Math.clamp(sculpt, -1.0F, 1.0F);
|
||||
double fine = Math.clamp(detail, -1.0F, 1.0F);
|
||||
|
||||
// Pass 1: a broad outline bends through the island's height with two truly 3D noises.
|
||||
// Keep the central field untouched even when a deeply recessed rim approaches it.
|
||||
double rimEnd = Math.min(capacity.terrainLimit(), capacity.radius() + coast * 16.0 + coarse * 28.0 + fine * 8.0);
|
||||
double rim = Math.clamp((distance - (capacity.radius() - 128)) / (rimEnd - (capacity.radius() - 128)), 0.0, 1.0);
|
||||
// Raising the density threshold gradually shrinks each natural lobe from all sides. Unlike
|
||||
// min(terrain, distance-to-edge), this never shears every altitude at one vertical contour.
|
||||
double erosion = 2.0 * rim * rim * rim * rim;
|
||||
|
||||
// A saturated natural lobe has almost no density gradient through its middle. Merely
|
||||
// raising its threshold can still leave a tall near-vertical wall. Retreat its upper and
|
||||
// lower rim faces as well, with a curved profile that disappears entirely at the interior.
|
||||
// This is an outer-rim cut, never an ellipsoid combined with (or added to) the natural field.
|
||||
double vertical = (y - (104.0 + coarse * 20.0)) / 88.0;
|
||||
erosion += 8.0 * rim * rim * vertical * vertical;
|
||||
|
||||
// Passes 2 and 3: broad recesses and smaller surface erosion, fading into the untouched
|
||||
// interior. Both subtract material; holes stay empty and detached fragments are never joined.
|
||||
double sculptWeight = smoothstep(rim);
|
||||
erosion += sculptWeight * (0.10 * (coarse + 1.0) * 0.5 + 0.04 * (fine + 1.0) * 0.5);
|
||||
|
||||
return erosion;
|
||||
}
|
||||
|
||||
private static float bounded(float terrain, double erosion) {
|
||||
return (float) Math.clamp(Math.clamp(terrain, -1.0F, 1.0F) - erosion, -1.0, 1.0);
|
||||
}
|
||||
|
||||
private static double smoothstep(double value) {
|
||||
double t = Math.clamp(value, 0.0, 1.0);
|
||||
return t * t * (3.0 - 2.0 * t);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/** Alpha.10 lava: an optional covered pocket and up to two deep rock outlets.
|
||||
* All sources replace natural rock; Minecraft alone produces the falling lava. */
|
||||
public final class PopulationLavaDeposit {
|
||||
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 PopulationLavaDeposit() {}
|
||||
|
||||
public record Position(int x, int y, int z) {}
|
||||
/** lava=false removes existing rock to open the two-block-high chamber or approach. */
|
||||
public record Cell(int x, int y, int z, boolean lava) {}
|
||||
|
||||
public static final class Plan {
|
||||
private final List<Cell> cells;
|
||||
private final List<Cell> lavaCells;
|
||||
private final List<Position> supports;
|
||||
private final Optional<Position> access;
|
||||
private final Map<Long, List<Cell>> chunks;
|
||||
private final Map<Long, List<Position>> supportChunks;
|
||||
private final List<PopulationLavaFlows.Fall> falls;
|
||||
private final Map<Long, List<PopulationLavaFlows.Fall>> fallChunks;
|
||||
private final List<Cell> pocketCells;
|
||||
private final Set<Position> pocketSupports;
|
||||
|
||||
private Plan(List<Cell> cells, Set<Position> supports, Position access) {
|
||||
this(cells, supports, access, List.of(), cells, supports);
|
||||
}
|
||||
|
||||
private Plan(List<Cell> cells, Set<Position> supports, Position access, List<PopulationLavaFlows.Fall> falls,
|
||||
List<Cell> pocketCells, Set<Position> pocketSupports) {
|
||||
this.pocketCells = pocketCells.stream().sorted(CELL_ORDER).toList();
|
||||
this.pocketSupports = Set.copyOf(pocketSupports);
|
||||
this.falls = List.copyOf(falls);
|
||||
Map<Long, List<PopulationLavaFlows.Fall>> groupedFalls = new HashMap<>();
|
||||
for (var fall : falls) groupedFalls.computeIfAbsent(
|
||||
key(fall.source().x() >> 4, fall.source().z() >> 4), ignored -> new ArrayList<>()).add(fall);
|
||||
groupedFalls.replaceAll((ignored, values) -> List.copyOf(values));
|
||||
fallChunks = Map.copyOf(groupedFalls);
|
||||
this.cells = cells.stream().sorted(CELL_ORDER).toList();
|
||||
lavaCells = this.cells.stream().filter(Cell::lava).toList();
|
||||
this.supports = supports.stream().sorted(POSITION_ORDER).toList();
|
||||
this.access = Optional.ofNullable(access);
|
||||
Map<Long, List<Cell>> byChunk = new HashMap<>();
|
||||
for (Cell cell : this.cells) byChunk.computeIfAbsent(key(cell.x() >> 4, cell.z() >> 4),
|
||||
ignored -> new ArrayList<>()).add(cell);
|
||||
byChunk.replaceAll((ignored, values) -> List.copyOf(values));
|
||||
chunks = Map.copyOf(byChunk);
|
||||
Map<Long, List<Position>> bySupportChunk = new HashMap<>();
|
||||
for (Position support : this.supports) bySupportChunk.computeIfAbsent(key(support.x() >> 4, support.z() >> 4),
|
||||
ignored -> new ArrayList<>()).add(support);
|
||||
bySupportChunk.replaceAll((ignored, values) -> List.copyOf(values));
|
||||
supportChunks = Map.copyOf(bySupportChunk);
|
||||
}
|
||||
|
||||
public List<Cell> cells() { return cells; }
|
||||
public List<Cell> lavaCells() { return lavaCells; }
|
||||
public List<Position> supports() { return supports; }
|
||||
public List<PopulationLavaFlows.Fall> falls() { return falls; }
|
||||
public List<PopulationLavaFlows.Fall> fallsInChunk(int x, int z) { return fallChunks.getOrDefault(key(x, z), List.of()); }
|
||||
public List<Position> flowSources() { return falls.stream().map(PopulationLavaFlows.Fall::source).toList(); }
|
||||
public List<PopulationHydrology.FlowBounds> flowBounds() { return falls.stream().map(PopulationLavaFlows.Fall::flowBounds).toList(); }
|
||||
/** Player feet on a natural ledge, with three blocks of headroom below a roof or sky. */
|
||||
public Optional<Position> access() { return access; }
|
||||
public List<Cell> cellsInChunk(int x, int z) { return chunks.getOrDefault(key(x, z), List.of()); }
|
||||
public List<Position> supportsInChunk(int x, int z) { return supportChunks.getOrDefault(key(x, z), List.of()); }
|
||||
|
||||
/** Keep a complete pocket or complete falls, with a 32-block regional safety margin. */
|
||||
public Plan admitted() {
|
||||
boolean keepPocket = pocketCells.stream().allMatch(c -> owned(c.x(), c.z()))
|
||||
&& pocketSupports.stream().allMatch(Plan::owned) && access.map(Plan::owned).orElse(true);
|
||||
List<PopulationLavaFlows.Fall> keptFalls = falls.stream().filter(f -> owned(f.source()) && owned(f.outlet())
|
||||
&& owned(f.flowBounds()) && f.supports().stream().allMatch(Plan::owned)
|
||||
&& f.flowPath().stream().allMatch(Plan::owned)).toList();
|
||||
return compose(keepPocket ? pocketCells : List.of(), keepPocket ? pocketSupports : Set.of(),
|
||||
keepPocket ? access.orElse(null) : null, keptFalls);
|
||||
}
|
||||
|
||||
/** IDs stay region-seeded; every position, flow envelope and chunk index moves together. */
|
||||
public Plan translated(int dx, int dz) {
|
||||
List<Cell> movedPocket = pocketCells.stream().map(c -> new Cell(Math.addExact(c.x(), dx), c.y(),
|
||||
Math.addExact(c.z(), dz), c.lava())).toList();
|
||||
Set<Position> movedSupports = pocketSupports.stream().map(p -> move(p, dx, dz))
|
||||
.collect(java.util.stream.Collectors.toUnmodifiableSet());
|
||||
List<PopulationLavaFlows.Fall> movedFalls = falls.stream().map(f -> new PopulationLavaFlows.Fall(f.id(),
|
||||
move(f.source(), dx, dz), move(f.outlet(), dx, dz), f.flowPath().stream().map(p -> move(p, dx, dz)).toList(),
|
||||
move(f.flowBounds(), dx, dz), f.supports().stream().map(p -> move(p, dx, dz)).toList())).toList();
|
||||
return compose(movedPocket, movedSupports, access.map(p -> move(p, dx, dz)).orElse(null), movedFalls);
|
||||
}
|
||||
|
||||
private static Plan compose(List<Cell> pocket, Set<Position> support, Position access,
|
||||
List<PopulationLavaFlows.Fall> selectedFalls) {
|
||||
List<Cell> combined = new ArrayList<>(pocket);
|
||||
Set<Position> combinedSupports = new HashSet<>(support);
|
||||
for (var fall : selectedFalls) {
|
||||
var source = fall.source();
|
||||
combined.add(new Cell(source.x(), source.y(), source.z(), true));
|
||||
combinedSupports.addAll(fall.supports());
|
||||
}
|
||||
return new Plan(combined, combinedSupports, access, selectedFalls, pocket, support);
|
||||
}
|
||||
|
||||
private static boolean owned(int x, int z) { return PopulationOwnership.contains(x, z, 32); }
|
||||
private static boolean owned(Position p) { return owned(p.x(), p.z()); }
|
||||
private static boolean owned(PopulationHydrology.FlowBounds b) {
|
||||
return PopulationOwnership.contains(b.minX(), b.minZ(), b.maxX(), b.maxZ(), 32);
|
||||
}
|
||||
private static Position move(Position p, int dx, int dz) {
|
||||
return new Position(Math.addExact(p.x(), dx), p.y(), Math.addExact(p.z(), dz));
|
||||
}
|
||||
private static PopulationHydrology.FlowBounds move(PopulationHydrology.FlowBounds b, int dx, int dz) {
|
||||
return new PopulationHydrology.FlowBounds(Math.addExact(b.minX(), dx), b.minY(), Math.addExact(b.minZ(), dz),
|
||||
Math.addExact(b.maxX(), dx), b.maxY(), Math.addExact(b.maxZ(), dz));
|
||||
}
|
||||
}
|
||||
|
||||
public static Plan create(long seed, PopulationHydrology.Sampler sampler, PopulationHydrology.Plan hydrology) {
|
||||
Plan pocket = new Search(seed, sampler, hydrology).find();
|
||||
List<PopulationLavaFlows.Fall> falls = PopulationLavaFlows.create(seed, sampler, hydrology, pocket);
|
||||
return Plan.compose(pocket.pocketCells, pocket.pocketSupports, pocket.access().orElse(null), falls);
|
||||
}
|
||||
|
||||
private record Candidate(int x, int z, int dx, int dz, double score) {}
|
||||
private record LowerCandidate(int x, int y, int z, int dx, int dz, double score) {}
|
||||
|
||||
private static final class Search {
|
||||
private final long seed;
|
||||
private final PopulationHydrology.Sampler sampler;
|
||||
private final PopulationHydrology.Plan hydrology;
|
||||
private final Map<Long, Integer> heights = new HashMap<>();
|
||||
|
||||
Search(long seed, PopulationHydrology.Sampler sampler, PopulationHydrology.Plan hydrology) {
|
||||
this.seed = seed;
|
||||
this.sampler = sampler;
|
||||
this.hydrology = hydrology;
|
||||
}
|
||||
|
||||
Plan find() {
|
||||
Plan lower = findLowerPocket();
|
||||
if (lower != null) return lower;
|
||||
List<Candidate> candidates = new ArrayList<>();
|
||||
for (int x = -184; x <= 184; x += 8) for (int z = -184; z <= 184; z += 8) {
|
||||
if (!inside(x, z, RADIUS - 8)) continue;
|
||||
int ledge = top(x, z);
|
||||
if (ledge < 64 || ledge > 300) continue;
|
||||
for (int[] direction : DIRECTIONS) {
|
||||
int cx = x - direction[0] * ENTRY_DISTANCE, cz = z - direction[1] * ENTRY_DISTANCE;
|
||||
if (!inside(cx, cz, RADIUS - 12) || !clearOfWater(cx, cz)) continue;
|
||||
int rise = top(cx, cz) - ledge;
|
||||
if (rise < 12) continue;
|
||||
candidates.add(new Candidate(x, z, direction[0], direction[1],
|
||||
random(seed, cx, cz) + Math.min(rise, 40) * 0.015));
|
||||
}
|
||||
}
|
||||
candidates.sort(Comparator.comparingDouble(Candidate::score).reversed()
|
||||
.thenComparingInt(Candidate::x).thenComparingInt(Candidate::z)
|
||||
.thenComparingInt(Candidate::dx).thenComparingInt(Candidate::dz));
|
||||
int attempts = 0;
|
||||
for (Candidate candidate : candidates) {
|
||||
if (attempts++ >= 192) break;
|
||||
// Refine the coarse edge, including a ledge lying between two eight-block samples.
|
||||
for (int along = 0; along < 8; along++) for (int tangent : new int[]{0, -3, 3}) {
|
||||
int x = candidate.x() - candidate.dx() * along - candidate.dz() * tangent;
|
||||
int z = candidate.z() - candidate.dz() * along + candidate.dx() * tangent;
|
||||
Plan plan = tryNiche(x, z, candidate.dx(), candidate.dz());
|
||||
if (plan != null) return plan;
|
||||
}
|
||||
}
|
||||
return new Plan(List.of(), Set.of(), null);
|
||||
}
|
||||
|
||||
/** Prefer a real lower ledge below an overhang. The chamber uses exactly the same rock
|
||||
* shell as the outdoor fallback; only the entrance may now have a natural ceiling. */
|
||||
private Plan findLowerPocket() {
|
||||
List<LowerCandidate> candidates = new ArrayList<>();
|
||||
for (int x = -176; x <= 176; x += 16) for (int z = -176; z <= 176; z += 16) {
|
||||
if (!inside(x, z, RADIUS - 8)) continue;
|
||||
for (int coarseY = 64; coarseY <= 160; coarseY += 8) {
|
||||
if (!solid(x, coarseY, z) || solid(x, coarseY + 8, z)) continue;
|
||||
int level = coarseY;
|
||||
for (int y = coarseY + 7; y > coarseY; y--) {
|
||||
if (solid(x, y, z)) { level = y; break; }
|
||||
}
|
||||
if (level > 160 || top(x, z) < level + 12) continue;
|
||||
for (int[] direction : DIRECTIONS) {
|
||||
int inwardX = x - direction[0] * 16, inwardZ = z - direction[1] * 16;
|
||||
if (!inside(inwardX, inwardZ, RADIUS - 12)
|
||||
|| !solid(inwardX, level + 2, inwardZ) || !solid(inwardX, level + 10, inwardZ)) continue;
|
||||
candidates.add(new LowerCandidate(x, level, z, direction[0], direction[1],
|
||||
random(seed + level + 0x108E2L, x - direction[0], z - direction[1])
|
||||
+ (160 - level) * .002));
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates.sort(Comparator.comparingDouble(LowerCandidate::score).reversed()
|
||||
.thenComparingInt(LowerCandidate::x).thenComparingInt(LowerCandidate::z)
|
||||
.thenComparingInt(LowerCandidate::y).thenComparingInt(LowerCandidate::dx).thenComparingInt(LowerCandidate::dz));
|
||||
int attempts = 0;
|
||||
for (LowerCandidate candidate : candidates) {
|
||||
if (attempts++ >= 128) break;
|
||||
for (int along = 0; along < 16; along++) for (int tangent : new int[]{0, -3, 3}) {
|
||||
int x = candidate.x() - candidate.dx() * along - candidate.dz() * tangent;
|
||||
int z = candidate.z() - candidate.dz() * along + candidate.dx() * tangent;
|
||||
for (int level = Math.min(160, candidate.y() + 5); level >= Math.max(64, candidate.y() - 5); level--) {
|
||||
// Reject solid cave walls and missing floors before expensive shell probes.
|
||||
if (!solid(x, level, z) || solid(x, level + 1, z)) continue;
|
||||
Plan plan = tryNiche(x, z, candidate.dx(), candidate.dz(), level, true);
|
||||
if (plan != null) return plan;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Plan tryNiche(int entryX, int entryZ, int dx, int dz) {
|
||||
return tryNiche(entryX, entryZ, dx, dz, top(entryX, entryZ), false);
|
||||
}
|
||||
|
||||
private Plan tryNiche(int entryX, int entryZ, int dx, int dz, int level, boolean underground) {
|
||||
int cx = entryX - dx * ENTRY_DISTANCE, cz = entryZ - dz * ENTRY_DISTANCE;
|
||||
if (!inside(cx, cz, RADIUS - 12) || level < 64 || level > (underground ? 160 : 300)
|
||||
|| top(cx, cz) < level + 12) return null;
|
||||
// A lower entrance has genuine overhead island rock, but does not need a sky view.
|
||||
if (underground && top(entryX, entryZ) < level + 12) return null;
|
||||
int airLimit = underground ? level + 3 : 383;
|
||||
for (int y = level + 1; y <= airLimit; y++) if (solid(entryX, y, entryZ)) return null;
|
||||
for (int y = level - 2; y <= level; y++) if (!solid(entryX, y, entryZ)) return null;
|
||||
if (underground ? !clearOfWater(cx, level, cz) : !clearOfWater(cx, cz)) return null;
|
||||
|
||||
List<Cell> cells = new ArrayList<>();
|
||||
Set<Position> changed = new HashSet<>();
|
||||
Set<Position> supports = new HashSet<>();
|
||||
for (int x = cx - 1; x <= cx + 1; x++) for (int z = cz - 1; z <= cz + 1; z++) {
|
||||
for (int y = level - 1; y <= level + 2; y++) {
|
||||
if (!solid(x, y, z)) return null;
|
||||
add(cells, changed, x, y, z, y <= level);
|
||||
}
|
||||
}
|
||||
// A dry lip separates the ledge from the pool. Only the air above it is opened.
|
||||
for (int distance = 2; distance < ENTRY_DISTANCE; distance++) {
|
||||
int x = cx + dx * distance, z = cz + dz * distance;
|
||||
for (int y = level + 1; y <= level + 2; y++) {
|
||||
if (solid(x, y, z)) add(cells, changed, x, y, z, false);
|
||||
}
|
||||
}
|
||||
// Every carved column must lie under eight continuous natural rock blocks. This
|
||||
// excludes soil-cap excavation at the entrance as well as exposed surface lava.
|
||||
Map<Long, Integer> highest = new HashMap<>();
|
||||
for (Cell cell : cells) highest.merge(key(cell.x(), cell.z()), cell.y(), Math::max);
|
||||
for (var column : highest.entrySet()) {
|
||||
int x = (int) (column.getKey() >> 32), z = (int) (long) column.getKey();
|
||||
for (int y = column.getValue() + 1; y <= column.getValue() + 8; y++) {
|
||||
if (!requireRock(supports, x, y, z)) return null;
|
||||
}
|
||||
}
|
||||
// Three intact blocks under the pool, two-block-thick side walls, and a solid chamber
|
||||
// surround prevent escape through an adjacent cave. No part of this shell is created.
|
||||
for (int x = cx - 3; x <= cx + 3; x++) for (int z = cz - 3; z <= cz + 3; z++) {
|
||||
for (int y = level - 4; y <= level + 2; y++) {
|
||||
if (changed.contains(new Position(x, y, z))) continue;
|
||||
if (!requireRock(supports, x, y, z)) return null;
|
||||
}
|
||||
}
|
||||
// The natural roof also covers a conservative envelope around local lava fire spread.
|
||||
// Its thickness keeps later surface vegetation well above the lava chamber.
|
||||
for (int x = cx - 4; x <= cx + 4; x++) for (int z = cz - 4; z <= cz + 4; z++) {
|
||||
for (int y = level + 3; y <= level + 10; y++) {
|
||||
if (!requireRock(supports, x, y, z)) return null;
|
||||
}
|
||||
}
|
||||
for (int distance = 2; distance <= ENTRY_DISTANCE; distance++) {
|
||||
int x = cx + dx * distance, z = cz + dz * distance;
|
||||
for (int y = level - 2; y <= level; y++) {
|
||||
if (!requireRock(supports, x, y, z)) return null;
|
||||
}
|
||||
}
|
||||
supports.removeAll(changed);
|
||||
return new Plan(cells, supports, new Position(entryX, level + 1, entryZ));
|
||||
}
|
||||
|
||||
private boolean requireRock(Set<Position> supports, int x, int y, int z) {
|
||||
if (!solid(x, y, z)) return false;
|
||||
supports.add(new Position(x, y, z));
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean clearOfWater(int x, int z) {
|
||||
// The extra ten blocks include both the entrance and the complete protected shell.
|
||||
int clearance = WATER_CLEARANCE + 10;
|
||||
for (var cell : hydrology.cells()) {
|
||||
long dx = x - cell.x(), dz = z - cell.z();
|
||||
if (dx * dx + dz * dz < (long) clearance * clearance) return false;
|
||||
}
|
||||
for (var spring : hydrology.springs()) {
|
||||
var bounds = spring.flowBounds();
|
||||
long dx = Math.max(Math.max((long) bounds.minX() - x, 0), (long) x - bounds.maxX());
|
||||
long dz = Math.max(Math.max((long) bounds.minZ() - z, 0), (long) z - bounds.maxZ());
|
||||
if (dx * dx + dz * dz < (long) clearance * clearance) return false;
|
||||
}
|
||||
for (var spill : hydrology.spills()) {
|
||||
var bounds = spill.flowBounds();
|
||||
long dx = Math.max(Math.max((long) bounds.minX() - x, 0), (long) x - bounds.maxX());
|
||||
long dz = Math.max(Math.max((long) bounds.minZ() - z, 0), (long) z - bounds.maxZ());
|
||||
if (dx * dx + dz * dz < (long) clearance * clearance) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean clearOfWater(int x, int level, int z) {
|
||||
// This box covers chamber, dry approach, intact shell and roof. A lake far above is
|
||||
// allowed; an actual waterfall crossing this level still excludes the entire pocket.
|
||||
var pocket = new PopulationHydrology.FlowBounds(x - 10, level - 4, z - 10, x + 10, level + 10, z + 10);
|
||||
for (var cell : hydrology.cells()) {
|
||||
var ground = new PopulationHydrology.FlowBounds(cell.x(), cell.bedY() - cell.sedimentDepth() - 1,
|
||||
cell.z(), cell.x(), Math.max(cell.carveTop(), cell.waterY()), cell.z());
|
||||
if (distanceSquared(pocket, ground) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
|
||||
}
|
||||
for (var spring : hydrology.springs()) {
|
||||
if (distanceSquared(pocket, spring.flowBounds()) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
|
||||
}
|
||||
for (var spill : hydrology.spills()) {
|
||||
if (distanceSquared(pocket, spill.flowBounds()) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static long distanceSquared(PopulationHydrology.FlowBounds a, PopulationHydrology.FlowBounds b) {
|
||||
long dx = Math.max(0, Math.max((long) a.minX() - b.maxX(), (long) b.minX() - a.maxX()));
|
||||
long dy = Math.max(0, Math.max((long) a.minY() - b.maxY(), (long) b.minY() - a.maxY()));
|
||||
long dz = Math.max(0, Math.max((long) a.minZ() - b.maxZ(), (long) b.minZ() - a.maxZ()));
|
||||
return dx * dx + dy * dy + dz * dz;
|
||||
}
|
||||
|
||||
private int top(int x, int z) {
|
||||
return heights.computeIfAbsent(key(x, z), ignored -> {
|
||||
for (int y = 383; y >= 32; y -= 4) {
|
||||
if (!solid(x, y, z)) continue;
|
||||
for (int exact = Math.min(383, y + 3); exact > y; exact--) if (solid(x, exact, z)) return exact;
|
||||
return y;
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
}
|
||||
|
||||
private boolean solid(int x, int y, int z) { return sampler.sample(x, y, z) > 0; }
|
||||
}
|
||||
|
||||
private static void add(List<Cell> cells, Set<Position> changed, int x, int y, int z, boolean lava) {
|
||||
if (changed.add(new Position(x, y, z))) cells.add(new Cell(x, y, z, lava));
|
||||
}
|
||||
|
||||
private static boolean inside(int x, int z, int radius) { return (long) x * x + (long) z * z < (long) radius * radius; }
|
||||
private static long key(int x, int z) { return ((long) x << 32) | (z & 0xffffffffL); }
|
||||
private static double random(long seed, int x, int z) {
|
||||
long value = seed ^ key(x, z) ^ 0x1A7A5EEDL;
|
||||
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
|
||||
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
|
||||
return ((value ^ (value >>> 31)) >>> 11) * 0x1.0p-53;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.PopulationLavaDeposit.Position;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/** Selects a few volcanic seeps in deep exposed rock. Sources replace rock; their outlets and
|
||||
* entire falling columns are left empty for vanilla lava ticks. No retaining wall is generated. */
|
||||
public final class PopulationLavaFlows {
|
||||
public static final int MIN_SOURCE_Y = 40;
|
||||
public static final int MAX_SOURCE_Y = 160;
|
||||
public static final int MAX_FALLS = 2;
|
||||
public static final int HORIZONTAL_REACH = 3;
|
||||
public static final int FIRE_CLEARANCE = 8;
|
||||
public static final int WATER_CLEARANCE = 24;
|
||||
private static final int RADIUS = 224;
|
||||
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
private static final Comparator<Position> POSITION_ORDER = Comparator.comparingInt(Position::x)
|
||||
.thenComparingInt(Position::z).thenComparingInt(Position::y);
|
||||
|
||||
private PopulationLavaFlows() {}
|
||||
|
||||
/** flowBounds predicts undecorated rock drainage; it does not override Minecraft physics.
|
||||
* supports contains only unchanged natural rock around the source, never the source itself. */
|
||||
public record Fall(long id, Position source, Position outlet, List<Position> flowPath,
|
||||
PopulationHydrology.FlowBounds flowBounds, List<Position> supports) {
|
||||
public Fall { flowPath = List.copyOf(flowPath); supports = List.copyOf(supports); }
|
||||
}
|
||||
|
||||
public static List<Fall> create(long seed, PopulationHydrology.Sampler sampler,
|
||||
PopulationHydrology.Plan water, PopulationLavaDeposit.Plan pocket) {
|
||||
return new Search(seed, sampler, water, pocket).find();
|
||||
}
|
||||
|
||||
private record Candidate(int x, int y, int z, int dx, int dz, double score) {}
|
||||
private record Node(Position at, int reach) {}
|
||||
private record Trace(List<Position> path, PopulationHydrology.FlowBounds bounds) {}
|
||||
|
||||
private static final class Search {
|
||||
private final long seed;
|
||||
private final PopulationHydrology.Sampler sampler;
|
||||
private final PopulationHydrology.Plan water;
|
||||
private final PopulationLavaDeposit.Plan pocket;
|
||||
private final List<Fall> falls = new ArrayList<>();
|
||||
|
||||
Search(long seed, PopulationHydrology.Sampler sampler, PopulationHydrology.Plan water, PopulationLavaDeposit.Plan pocket) {
|
||||
this.seed = seed; this.sampler = sampler; this.water = water; this.pocket = pocket;
|
||||
}
|
||||
|
||||
List<Fall> find() {
|
||||
List<Candidate> candidates = new ArrayList<>();
|
||||
// Coarse probes below the surface can see a lower overhang that a heightmap hides.
|
||||
for (int x = -216; x <= 216; x += 8) for (int z = -216; z <= 216; z += 8) {
|
||||
if ((long) x * x + (long) z * z >= (long) RADIUS * RADIUS) continue;
|
||||
for (int y = MIN_SOURCE_Y; y <= MAX_SOURCE_Y; y += 8) {
|
||||
if (!solid(x, y, z)) continue;
|
||||
for (int[] d : CARDINALS) {
|
||||
if (solid(x + d[0] * 4, y, z + d[1] * 4)) continue;
|
||||
double score = random(seed + y, x + d[0], z + d[1])
|
||||
+ (MAX_SOURCE_Y - y) * .002;
|
||||
candidates.add(new Candidate(x, y, z, d[0], d[1], score));
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates.sort(Comparator.comparingDouble(Candidate::score).reversed()
|
||||
.thenComparingInt(Candidate::x).thenComparingInt(Candidate::z)
|
||||
.thenComparingInt(Candidate::y).thenComparingInt(Candidate::dx).thenComparingInt(Candidate::dz));
|
||||
int attempts = 0;
|
||||
for (Candidate candidate : candidates) {
|
||||
if (falls.size() >= MAX_FALLS || attempts++ >= 160) break;
|
||||
boolean placed = false;
|
||||
for (int tangent : new int[]{0, -2, 2}) {
|
||||
for (int along = 0; along < 4; along++) {
|
||||
int x = candidate.x() + candidate.dx() * along - candidate.dz() * tangent;
|
||||
int z = candidate.z() + candidate.dz() * along + candidate.dx() * tangent;
|
||||
for (int dy : new int[]{0, 2, -2, 4, -4}) {
|
||||
Position source = new Position(x, candidate.y() + dy, z);
|
||||
if (source.y() < MIN_SOURCE_Y || source.y() > MAX_SOURCE_Y || !separated(source)) continue;
|
||||
List<Position> supports = niche(source, candidate.dx(), candidate.dz());
|
||||
if (supports == null) continue;
|
||||
Position outlet = new Position(x + candidate.dx(), source.y(), z + candidate.dz());
|
||||
Trace flow = trace(source, outlet);
|
||||
if (flow == null || flow.bounds().minY() > source.y() - 12 || !clearOfWater(flow.bounds())) continue;
|
||||
long id = mix(seed ^ (0x1A7AFA11L + falls.size() * 0x9E3779B97F4A7C15L));
|
||||
falls.add(new Fall(id, source, outlet, flow.path(), flow.bounds(), supports));
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
if (placed) break;
|
||||
}
|
||||
if (placed) break;
|
||||
}
|
||||
}
|
||||
return List.copyOf(falls);
|
||||
}
|
||||
|
||||
private boolean separated(Position source) {
|
||||
PopulationHydrology.FlowBounds point = bounds(source);
|
||||
if (!clearOfWater(point)) return false;
|
||||
for (Fall fall : falls) {
|
||||
if (distanceSquared(source, fall.source()) < 80L * 80
|
||||
|| distanceSquared(point, fall.flowBounds()) < 40L * 40) return false;
|
||||
}
|
||||
for (var cell : pocket.cells()) {
|
||||
if (distanceSquared(source, new Position(cell.x(), cell.y(), cell.z())) < 32L * 32) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private List<Position> niche(Position source, int dx, int dz) {
|
||||
if (!solid(source)) return null;
|
||||
Set<Position> supports = new HashSet<>();
|
||||
// The source sits in rock, with a short intact roof, floor and three back/side faces.
|
||||
// The spill column needs no floor: its absence is precisely what makes a cascade.
|
||||
for (int dy = -3; dy <= 8; dy++) {
|
||||
if (dy == 0) continue;
|
||||
Position at = new Position(source.x(), source.y() + dy, source.z());
|
||||
if (!solid(at)) return null;
|
||||
supports.add(at);
|
||||
}
|
||||
for (int[] d : CARDINALS) {
|
||||
if (d[0] == dx && d[1] == dz) continue;
|
||||
for (int depth = 1; depth <= 2; depth++) {
|
||||
Position at = new Position(source.x() + d[0] * depth, source.y(), source.z() + d[1] * depth);
|
||||
if (!solid(at)) return null;
|
||||
supports.add(at);
|
||||
}
|
||||
}
|
||||
for (int dy = 0; dy >= -8; dy--) {
|
||||
if (solid(source.x() + dx, source.y() + dy, source.z() + dz)) return null;
|
||||
}
|
||||
return supports.stream().sorted(POSITION_ORDER).toList();
|
||||
}
|
||||
|
||||
private boolean clearOfWater(PopulationHydrology.FlowBounds bounds) {
|
||||
for (var cell : water.cells()) {
|
||||
var column = new PopulationHydrology.FlowBounds(cell.x(), cell.bedY() - cell.sedimentDepth() - 1,
|
||||
cell.z(), cell.x(), Math.max(cell.carveTop(), cell.waterY()), cell.z());
|
||||
if (distanceSquared(bounds, column) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
|
||||
}
|
||||
for (var spring : water.springs()) {
|
||||
if (distanceSquared(bounds, spring.flowBounds()) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
|
||||
}
|
||||
for (var spill : water.spills()) {
|
||||
if (distanceSquared(bounds, spill.flowBounds()) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Descending lava resets its horizontal reach to three in this non-ultrawarm dimension.
|
||||
* Explore all downhill branches through rock, so selection does not depend on chunk order. */
|
||||
private Trace trace(Position source, Position outlet) {
|
||||
Map<Position, Integer> reach = new HashMap<>();
|
||||
Map<Position, Position> parent = new HashMap<>();
|
||||
ArrayDeque<Node> queue = new ArrayDeque<>();
|
||||
reach.put(source, HORIZONTAL_REACH);
|
||||
queue.addLast(new Node(source, HORIZONTAL_REACH));
|
||||
Position lowest = source;
|
||||
while (!queue.isEmpty()) {
|
||||
Node node = queue.removeFirst();
|
||||
Position at = node.at();
|
||||
if (node.reach() < reach.getOrDefault(at, -1)) continue;
|
||||
if (at.y() < lowest.y()) lowest = at;
|
||||
if (reach.size() > 12000 || Math.abs(at.x() - source.x()) > 64 || Math.abs(at.z() - source.z()) > 64) return null;
|
||||
if (at.y() <= 0) continue;
|
||||
Position below = new Position(at.x(), at.y() - 1, at.z());
|
||||
if (!flowSolid(below, source)) {
|
||||
enqueue(below, HORIZONTAL_REACH, at, reach, parent, queue);
|
||||
continue;
|
||||
}
|
||||
if (node.reach() <= 0) continue;
|
||||
for (int[] d : CARDINALS) {
|
||||
Position next = new Position(at.x() + d[0], at.y(), at.z() + d[1]);
|
||||
if (!flowSolid(next, source)) enqueue(next, node.reach() - 1, at, reach, parent, queue);
|
||||
}
|
||||
}
|
||||
if (!reach.containsKey(outlet)) return null;
|
||||
int minX = source.x(), maxX = minX, minY = source.y(), maxY = minY, minZ = source.z(), maxZ = minZ;
|
||||
for (Position at : reach.keySet()) {
|
||||
minX = Math.min(minX, at.x()); maxX = Math.max(maxX, at.x());
|
||||
minY = Math.min(minY, at.y()); maxY = Math.max(maxY, at.y());
|
||||
minZ = Math.min(minZ, at.z()); maxZ = Math.max(maxZ, at.z());
|
||||
}
|
||||
List<Position> path = new ArrayList<>();
|
||||
for (Position at = lowest; at != null; at = parent.get(at)) path.add(at);
|
||||
Collections.reverse(path);
|
||||
return new Trace(List.copyOf(path), new PopulationHydrology.FlowBounds(minX, minY, minZ, maxX, maxY, maxZ));
|
||||
}
|
||||
|
||||
private boolean flowSolid(Position at, Position source) {
|
||||
if (at.equals(source)) return false;
|
||||
var cell = water.cellAt(at.x(), at.y(), at.z());
|
||||
if (cell != null && at.y() > cell.bedY() && at.y() <= cell.carveTop()) return false;
|
||||
return solid(at);
|
||||
}
|
||||
|
||||
private boolean solid(Position at) { return solid(at.x(), at.y(), at.z()); }
|
||||
private boolean solid(int x, int y, int z) { return sampler.sample(x, y, z) > 0; }
|
||||
}
|
||||
|
||||
private static void enqueue(Position next, int available, Position from, Map<Position, Integer> reach,
|
||||
Map<Position, Position> parent, ArrayDeque<Node> queue) {
|
||||
if (available <= reach.getOrDefault(next, -1)) return;
|
||||
if (!reach.containsKey(next)) parent.put(next, from);
|
||||
reach.put(next, available);
|
||||
queue.addLast(new Node(next, available));
|
||||
}
|
||||
|
||||
private static PopulationHydrology.FlowBounds bounds(Position at) {
|
||||
return new PopulationHydrology.FlowBounds(at.x(), at.y(), at.z(), at.x(), at.y(), at.z());
|
||||
}
|
||||
|
||||
private static long distanceSquared(Position a, Position b) {
|
||||
long dx = a.x() - b.x(), dy = a.y() - b.y(), dz = a.z() - b.z();
|
||||
return dx * dx + dy * dy + dz * dz;
|
||||
}
|
||||
|
||||
private static long distanceSquared(PopulationHydrology.FlowBounds a, PopulationHydrology.FlowBounds b) {
|
||||
long dx = Math.max(0, Math.max((long) a.minX() - b.maxX(), (long) b.minX() - a.maxX()));
|
||||
long dy = Math.max(0, Math.max((long) a.minY() - b.maxY(), (long) b.minY() - a.maxY()));
|
||||
long dz = Math.max(0, Math.max((long) a.minZ() - b.maxZ(), (long) b.minZ() - a.maxZ()));
|
||||
return dx * dx + dy * dy + dz * dz;
|
||||
}
|
||||
|
||||
private static double random(long seed, int x, int z) {
|
||||
return (mix(seed ^ ((long) x << 32) ^ (z & 0xffffffffL)) >>> 11) * 0x1.0p-53;
|
||||
}
|
||||
|
||||
private static long mix(long value) {
|
||||
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
|
||||
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
|
||||
return value ^ (value >>> 31);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/** Alpha.10 shore deposits: broad continuous patches through the existing sediment thickness. */
|
||||
public final class PopulationMaterials {
|
||||
private PopulationMaterials() {}
|
||||
|
||||
public static BlockState sedimentBlock(PopulationHydrology.Cell cell, long seed, int depth) {
|
||||
if (cell.material() == PopulationHydrology.ShoreMaterial.GRASS) {
|
||||
return (depth == 0 ? Blocks.GRASS_BLOCK : Blocks.DIRT).defaultBlockState();
|
||||
}
|
||||
if (cell.material() == PopulationHydrology.ShoreMaterial.CLAY) return Blocks.CLAY.defaultBlockState();
|
||||
double patch = patch(seed, cell.x(), cell.z(), 32);
|
||||
Block material;
|
||||
if (cell.hasWater() && patch < 0.24) material = Blocks.CLAY;
|
||||
else if (patch < 0.53) material = Blocks.SAND;
|
||||
else if (patch < 0.68) material = Blocks.GRAVEL;
|
||||
else {
|
||||
double rock = patch(seed ^ 0x4c6179657273L, cell.x(), cell.z(), 40);
|
||||
material = rock < 0.28 ? Blocks.GRANITE : rock < 0.45 ? Blocks.DIORITE
|
||||
: rock < 0.65 ? Blocks.ANDESITE : Blocks.STONE;
|
||||
}
|
||||
return material.defaultBlockState();
|
||||
}
|
||||
|
||||
private static double patch(long seed, int x, int z, int scale) {
|
||||
int gx = Math.floorDiv(x, scale), gz = Math.floorDiv(z, scale);
|
||||
double tx = smooth(Math.floorMod(x, scale) / (double) scale);
|
||||
double tz = smooth(Math.floorMod(z, scale) / (double) scale);
|
||||
double near = lerp(value(seed, gx, gz), value(seed, gx + 1, gz), tx);
|
||||
double far = lerp(value(seed, gx, gz + 1), value(seed, gx + 1, gz + 1), tx);
|
||||
return lerp(near, far, tz);
|
||||
}
|
||||
|
||||
private static double value(long seed, int x, int z) {
|
||||
return (WoodlandGroveGeometry.seed(seed, x, 0, z, 0x53484f5245L) >>> 11) * 0x1.0p-53;
|
||||
}
|
||||
|
||||
private static double smooth(double t) { return t * t * (3 - 2 * t); }
|
||||
private static double lerp(double a, double b, double t) { return a + (b - a) * t; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
/** Local ownership of one 512-block planning region, with unused space at each edge. */
|
||||
final class PopulationOwnership {
|
||||
private PopulationOwnership() {}
|
||||
|
||||
static boolean contains(int x, int z, int margin) {
|
||||
return x >= -256 + margin && x <= 255 - margin
|
||||
&& z >= -256 + margin && z <= 255 - margin;
|
||||
}
|
||||
|
||||
static boolean contains(int minX, int minZ, int maxX, int maxZ, int margin) {
|
||||
return contains(minX, minZ, margin) && contains(maxX, maxZ, margin);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/** Completed plans remain readable while one new region uses temporary planning memory. */
|
||||
final class PopulationPlanCache<K, V> {
|
||||
private final ConcurrentHashMap<K, V> completed = new ConcurrentHashMap<>();
|
||||
|
||||
V getOrCreate(K key, Supplier<V> create) {
|
||||
V value = completed.get(key);
|
||||
if (value != null) return value;
|
||||
synchronized (this) {
|
||||
value = completed.get(key);
|
||||
if (value == null) {
|
||||
value = Objects.requireNonNull(create.get());
|
||||
completed.put(key, value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
int size() { return completed.size(); }
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
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 PopulationShoreSugarCaneFeature implements Feature {
|
||||
public static final MapCodec<PopulationShoreSugarCaneFeature> CODEC = MapCodec.unit(PopulationShoreSugarCaneFeature::new);
|
||||
|
||||
@Override
|
||||
public MapCodec<PopulationShoreSugarCaneFeature> codec() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource random, BlockPos origin) {
|
||||
if (!(generator instanceof NoiseBasedChunkGenerator noise) || !PopulationHydrologyRuntime.enabled(noise)) return false;
|
||||
var state = level.getLevel().getChunkSource().randomState();
|
||||
var cells = PopulationHydrologyRuntime.cellsInChunk(noise, state, origin.getX() >> 4, origin.getZ() >> 4);
|
||||
var cane = Blocks.SUGAR_CANE.defaultBlockState();
|
||||
boolean placed = false;
|
||||
for (var cell : cells) {
|
||||
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,111 @@
|
||||
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.10 outlet finishing, using vanilla's persisted and consumed generation queue. */
|
||||
public final class PopulationSpringOutlets {
|
||||
public record Pending(List<PopulationHydrology.Position> sources, List<PopulationHydrology.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 PopulationSpringOutlets() {}
|
||||
|
||||
/** 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)
|
||||
|| !PopulationHydrologyRuntime.enabled(noise)) return Pending.EMPTY;
|
||||
if (!PopulationHydrologyRuntime.capacity(noise).areaExists(chunk.getPos().x(), chunk.getPos().z())) 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 regional = PopulationHydrologyRuntime.regionPlan(noise, level.getChunkSource().randomState(),
|
||||
PopulationHydrologyRuntime.capacity(noise).regionAt(chunk.getPos().getMinBlockX(), chunk.getPos().getMinBlockZ()));
|
||||
var plan = regional.water();
|
||||
List<PopulationHydrology.Position> sources = new ArrayList<>(), outlets = new ArrayList<>();
|
||||
for (var spring : plan.springs()) {
|
||||
if (markedHere(chunk, spring.source())) sources.add(spring.source());
|
||||
if (markedHere(chunk, spring.outlet())) outlets.add(spring.outlet());
|
||||
}
|
||||
for (var spill : plan.spills()) {
|
||||
if (markedHere(chunk, spill.source())) sources.add(spill.source());
|
||||
if (markedHere(chunk, spill.outlet())) outlets.add(spill.outlet());
|
||||
}
|
||||
for (var fall : regional.lava().falls()) {
|
||||
var source = new PopulationHydrology.Position(fall.source().x(), fall.source().y(), fall.source().z());
|
||||
var outlet = new PopulationHydrology.Position(fall.outlet().x(), fall.outlet().y(), fall.outlet().z());
|
||||
if (markedHere(chunk, source)) sources.add(source);
|
||||
if (markedHere(chunk, outlet)) outlets.add(outlet);
|
||||
}
|
||||
return new Pending(sources, outlets);
|
||||
}
|
||||
|
||||
/** All neighbouring FEATURES have finished before a chunk reaches this stage. */
|
||||
public static void finish(ServerLevel level, LevelChunk chunk, Pending pending) {
|
||||
if (pending.isEmpty() || !(level.getChunkSource().getGenerator() instanceof NoiseBasedChunkGenerator noise)
|
||||
|| !PopulationHydrologyRuntime.enabled(noise)) return;
|
||||
var random = level.getChunkSource().randomState();
|
||||
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
|
||||
for (var outlet : pending.outlets()) {
|
||||
if (!owns(chunk, outlet)) throw new IllegalArgumentException("An outlet belongs to another chunk: " + outlet);
|
||||
var modified = PopulationHydrologyRuntime.cellAt(noise, random, outlet.x(), outlet.y(), outlet.z());
|
||||
boolean carved = modified != null && outlet.y() > modified.waterY() && outlet.y() > modified.bedY()
|
||||
&& outlet.y() <= modified.carveTop();
|
||||
if (!carved && random.sampleBlockValueUncached(density, outlet.x(), outlet.y(), outlet.z()) > 0) {
|
||||
throw new IllegalStateException("A spring outlet must already be natural air: " + outlet);
|
||||
}
|
||||
BlockPos pos = blockPos(outlet);
|
||||
BlockState current = chunk.getBlockState(pos);
|
||||
if (removableDecoration(current)) {
|
||||
// Only this declared air cell is reopened. Neighbour notification wakes the
|
||||
// adjacent source even if its own chunk was post-processed first.
|
||||
level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
|
||||
}
|
||||
}
|
||||
for (var source : pending.sources()) {
|
||||
if (!owns(chunk, source)) throw new IllegalArgumentException("A source belongs to another chunk: " + source);
|
||||
BlockPos pos = blockPos(source);
|
||||
BlockState current = chunk.getBlockState(pos);
|
||||
if (current.getFluidState().isSource()) {
|
||||
if (current.is(Blocks.WATER)) level.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(level));
|
||||
else if (current.is(Blocks.LAVA)) level.scheduleTick(pos, Fluids.LAVA, Fluids.LAVA.getTickDelay(level));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean markedHere(LevelChunk chunk, PopulationHydrology.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, PopulationHydrology.Position position) {
|
||||
return (position.x() >> 4) == chunk.getPos().x() && (position.z() >> 4) == chunk.getPos().z();
|
||||
}
|
||||
|
||||
private static BlockPos blockPos(PopulationHydrology.Position position) {
|
||||
return new BlockPos(position.x(), position.y(), position.z());
|
||||
}
|
||||
|
||||
private static boolean removableDecoration(BlockState state) {
|
||||
if (state.hasBlockEntity()) return false;
|
||||
return state.is(Blocks.GLOW_LICHEN) || state.is(Blocks.VINE) || state.is(Blocks.MOSS_CARPET)
|
||||
|| state.getBlock() instanceof VegetationBlock || state.is(BlockTags.LEAVES) || state.is(BlockTags.LOGS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.PopulationHydrology.*;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.PriorityQueue;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/** Small connected water terraces inside existing cave ledges. Each retained pool is excavated
|
||||
* in rock; one declared edge may overflow. No dam, shelf, cave fill or fluid column is generated. */
|
||||
public final class PopulationTerraces {
|
||||
public static final int MIN_Y = 56, MAX_Y = 208;
|
||||
public static final int MAX_GROUPS = 2, MAX_DEPTH = 3, MAX_CARVE = 5;
|
||||
private static final int RADIUS = 216, SEDIMENT = 3;
|
||||
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
private static final int[][] NEIGHBORS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}};
|
||||
|
||||
private PopulationTerraces() {}
|
||||
|
||||
public record Result(List<Cell> cells, List<Feature> features, List<Terrace> terraces,
|
||||
List<Spill> spills, Set<Position> predictedFlow) {
|
||||
public static final Result EMPTY = new Result(List.of(), List.of(), List.of(), List.of(), Set.of());
|
||||
public Result {
|
||||
cells = List.copyOf(cells); features = List.copyOf(features); terraces = List.copyOf(terraces);
|
||||
spills = List.copyOf(spills); predictedFlow = Set.copyOf(predictedFlow);
|
||||
}
|
||||
}
|
||||
|
||||
public static Result create(long seed, Sampler sampler, PopulationHydrology.Plan surface) {
|
||||
return new Search(seed, sampler, surface).build();
|
||||
}
|
||||
|
||||
private record Candidate(int x, int y, int z, int dx, int dz, double score) {}
|
||||
private record Floor(int top, int bottom, int ceiling) {}
|
||||
private record Node(int x, int z, double cost) {}
|
||||
private record FlowNode(Position at, int reach) {}
|
||||
private record Flow(Set<Position> volume, List<Position> path, FlowBounds bounds) {}
|
||||
private record Pool(Map<Long, Cell> cells, Position source, Position outlet) {}
|
||||
private static final Floor NO_FLOOR = new Floor(-1, -1, -1);
|
||||
|
||||
private static final class Search {
|
||||
private final long seed;
|
||||
private final Sampler sampler;
|
||||
private final PopulationHydrology.Plan surface;
|
||||
private final Map<Position, Floor> floors = new HashMap<>();
|
||||
private final List<Cell> acceptedCells = new ArrayList<>();
|
||||
private final List<Feature> features = new ArrayList<>();
|
||||
private final List<Terrace> terraces = new ArrayList<>();
|
||||
private final List<Spill> spills = new ArrayList<>();
|
||||
private final Set<Position> predicted = new HashSet<>();
|
||||
|
||||
Search(long seed, Sampler sampler, PopulationHydrology.Plan surface) {
|
||||
this.seed = seed; this.sampler = sampler; this.surface = surface;
|
||||
}
|
||||
|
||||
Result build() {
|
||||
List<Candidate> candidates = new ArrayList<>();
|
||||
for (int x = -208; x <= 208; x += 8) for (int z = -208; z <= 208; z += 8) {
|
||||
if (!inside(x, z)) continue;
|
||||
for (int y = 64; y <= MAX_Y; y += 8) {
|
||||
if (!solid(x, y, z) || solid(x, y + 8, z)) continue;
|
||||
int level = y;
|
||||
for (int exact = y + 7; exact > y; exact--) if (solid(x, exact, z)) { level = exact; break; }
|
||||
if (level > MAX_Y || floor(x, z, level) == NO_FLOOR) continue;
|
||||
for (int[] d : CARDINALS) if (!solid(x + d[0] * 4, level, z + d[1] * 4)) {
|
||||
candidates.add(new Candidate(x, level, z, d[0], d[1], random(seed + level, x + d[0], z + d[1])));
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates.sort(Comparator.comparingDouble(Candidate::score).reversed()
|
||||
.thenComparingInt(Candidate::x).thenComparingInt(Candidate::z).thenComparingInt(Candidate::y)
|
||||
.thenComparingInt(Candidate::dx).thenComparingInt(Candidate::dz));
|
||||
int attempts = 0;
|
||||
for (Candidate candidate : candidates) {
|
||||
if (terraces.size() >= MAX_GROUPS || attempts++ >= 160) break;
|
||||
boolean added = false;
|
||||
for (int tangent : new int[]{0, -2, 2}) {
|
||||
for (int along = 0; along < 4; along++) {
|
||||
int x = candidate.x() + candidate.dx() * along - candidate.dz() * tangent;
|
||||
int z = candidate.z() + candidate.dz() * along + candidate.dx() * tangent;
|
||||
for (int dy : new int[]{0, -1, 1, -2, 2}) {
|
||||
Position source = new Position(x, candidate.y() + dy, z);
|
||||
Position outlet = new Position(x + candidate.dx(), source.y(), z + candidate.dz());
|
||||
if (!mouth(source, outlet) || !separated(source)) continue;
|
||||
if (addGroup(source, outlet)) { added = true; break; }
|
||||
}
|
||||
if (added) break;
|
||||
}
|
||||
if (added) break;
|
||||
}
|
||||
}
|
||||
return new Result(acceptedCells, features, terraces, spills, predicted);
|
||||
}
|
||||
|
||||
private boolean addGroup(Position source, Position outlet) {
|
||||
long groupId = mix(seed ^ (0xA9CA7E2L + terraces.size() * 0x9E3779B97F4A7C15L));
|
||||
long upperId = mix(groupId + 1), lowerId = mix(groupId + 2);
|
||||
Pool upper = grow(source, source.y(), source, outlet, upperId);
|
||||
if (upper == null) return false;
|
||||
|
||||
// The first impact on a slope is often a thin lip. Follow natural drainage across
|
||||
// its ledges, then find a supported receiving pool at a genuinely lower level.
|
||||
// The same bounded fluid trace is used before and after carving the receiving pool.
|
||||
Flow drainage = trace(source, outlet, new ArrayList<>(upper.cells().values()));
|
||||
if (drainage == null) return false;
|
||||
List<Position> contacts = new ArrayList<>();
|
||||
for (Position at : drainage.volume()) {
|
||||
int level = at.y() - 1;
|
||||
if (level < MIN_Y || level > source.y() - 4 || !solid(at.x(), level, at.z())) continue;
|
||||
Floor ground = floor(at.x(), at.z(), level);
|
||||
if (ground != NO_FLOOR && ground.bottom() <= level - 7)
|
||||
contacts.add(new Position(at.x(), level, at.z()));
|
||||
}
|
||||
contacts.sort(Comparator.comparingInt(Position::y).reversed()
|
||||
.thenComparingInt(Position::x).thenComparingInt(Position::z));
|
||||
Pool lower = null;
|
||||
Position landing = null;
|
||||
int attempts = 0;
|
||||
for (Position contact : contacts) {
|
||||
if (attempts++ >= 48) break;
|
||||
lower = grow(contact, contact.y(), null, null, lowerId);
|
||||
if (lower != null) { landing = contact; break; }
|
||||
}
|
||||
if (lower == null) return false;
|
||||
List<Cell> groupCells = new ArrayList<>(upper.cells().values());
|
||||
groupCells.addAll(lower.cells().values());
|
||||
if (overlapping(groupCells)) return false;
|
||||
Flow inlet = trace(source, outlet, groupCells);
|
||||
if (inlet == null || !reaches(inlet, lower)) return false;
|
||||
|
||||
// If the same lower ledge has another natural edge, connect it to an open terminal
|
||||
// fall. Otherwise its second pool stays closed; the first inter-pool cascade remains.
|
||||
Pool drained = terminalPool(landing, lowerId);
|
||||
Flow terminal = null;
|
||||
if (drained != null) {
|
||||
List<Cell> replacement = new ArrayList<>(upper.cells().values());
|
||||
replacement.addAll(drained.cells().values());
|
||||
if (!overlapping(replacement)) {
|
||||
Flow in = trace(source, outlet, replacement);
|
||||
Flow out = trace(drained.source(), drained.outlet(), replacement);
|
||||
if (in != null && reaches(in, drained) && out != null
|
||||
&& out.bounds().minY() <= drained.source().y() - 12) {
|
||||
lower = drained; inlet = in; terminal = out; groupCells = replacement;
|
||||
}
|
||||
}
|
||||
}
|
||||
acceptedCells.addAll(groupCells);
|
||||
features.add(feature(upperId, upper, source));
|
||||
features.add(feature(lowerId, lower, landing));
|
||||
terraces.add(new Terrace(groupId, List.of(upperId, lowerId)));
|
||||
spills.add(new Spill(mix(groupId + 3), upperId, lowerId, source, outlet, inlet.path(), inlet.bounds()));
|
||||
predicted.addAll(inlet.volume());
|
||||
if (terminal != null) {
|
||||
spills.add(new Spill(mix(groupId + 4), lowerId, -1, lower.source(), lower.outlet(), terminal.path(), terminal.bounds()));
|
||||
predicted.addAll(terminal.volume());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Pool terminalPool(Position center, long id) {
|
||||
List<Position> candidates = new ArrayList<>();
|
||||
for (int dx = -12; dx <= 12; dx++) for (int dz = -12; dz <= 12; dz++) {
|
||||
if (dx * dx + dz * dz > 144) continue;
|
||||
candidates.add(new Position(center.x() + dx, center.y(), center.z() + dz));
|
||||
}
|
||||
candidates.sort(Comparator.comparingDouble((Position p) -> Math.hypot(p.x() - center.x(), p.z() - center.z()))
|
||||
.thenComparingInt(Position::x).thenComparingInt(Position::z));
|
||||
int tried = 0;
|
||||
for (Position source : candidates) for (int[] d : CARDINALS) {
|
||||
Position outlet = new Position(source.x() + d[0], source.y(), source.z() + d[1]);
|
||||
if (!mouth(source, outlet)) continue;
|
||||
if (tried++ >= 12) return null;
|
||||
Pool pool = grow(center, center.y(), source, outlet, id);
|
||||
if (pool != null) return pool;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Pool grow(Position center, int level, Position source, Position outlet, long id) {
|
||||
PriorityQueue<Node> queue = new PriorityQueue<>(Comparator.comparingDouble(Node::cost)
|
||||
.thenComparingInt(Node::x).thenComparingInt(Node::z));
|
||||
Map<Long, Double> costs = new HashMap<>();
|
||||
Set<Long> wet = new HashSet<>();
|
||||
long origin = key(center.x(), center.z());
|
||||
queue.add(new Node(center.x(), center.z(), 0)); costs.put(origin, 0.0);
|
||||
int target = 100 + (int) (random(seed + level, center.x(), center.z()) * 80);
|
||||
while (!queue.isEmpty() && wet.size() < target) {
|
||||
Node node = queue.remove(); long k = key(node.x(), node.z());
|
||||
if (wet.contains(k) || node.cost() > costs.getOrDefault(k, Double.POSITIVE_INFINITY)
|
||||
|| !poolColumn(node.x(), node.z(), level, source, outlet)) continue;
|
||||
wet.add(k);
|
||||
for (int[] d : NEIGHBORS) {
|
||||
int x = node.x() + d[0], z = node.z() + d[1]; long next = key(x, z);
|
||||
if (Math.hypot(x - center.x(), z - center.z()) > 16 || wet.contains(next)) continue;
|
||||
double cost = node.cost() + Math.hypot(d[0], d[1]) * (1 + noise(x, z, 11) * 1.8);
|
||||
if (cost >= costs.getOrDefault(next, Double.POSITIVE_INFINITY)) continue;
|
||||
costs.put(next, cost); queue.add(new Node(x, z, cost));
|
||||
}
|
||||
}
|
||||
wet = connected(wet, origin);
|
||||
if (wet.size() < 40 || source != null && !wet.contains(key(source.x(), source.z()))) return null;
|
||||
Map<Long, Cell> cells = new HashMap<>();
|
||||
for (long k : wet) {
|
||||
int x = (int) (k >> 32), z = (int) k;
|
||||
boolean edge = false;
|
||||
for (int[] d : CARDINALS) edge |= !wet.contains(key(x + d[0], z + d[1]));
|
||||
int depth = edge ? 1 : 2 + (noise(x, z, 9) > .6 ? 1 : 0);
|
||||
Floor floor = floor(x, z, level);
|
||||
int bed = level - depth;
|
||||
if (floor == NO_FLOOR || floor.top() - bed > MAX_CARVE || floor.bottom() > bed - SEDIMENT - 1) return null;
|
||||
ShoreMaterial material = noise(x + 101, z - 87, 19) > .42 ? ShoreMaterial.CLAY : ShoreMaterial.STONE;
|
||||
cells.put(k, new Cell(x, z, level, bed, floor.top(), material, id, SEDIMENT));
|
||||
}
|
||||
for (Cell cell : cells.values()) for (int[] d : CARDINALS) {
|
||||
int x = cell.x() + d[0], z = cell.z() + d[1];
|
||||
Cell next = cells.get(key(x, z));
|
||||
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) {
|
||||
if (next != null && y > next.bedY()) continue;
|
||||
if (source != null && cell.x() == source.x() && cell.z() == source.z()
|
||||
&& x == outlet.x() && z == outlet.z() && y == outlet.y()) continue;
|
||||
if (!solid(x, y, z)) return null;
|
||||
}
|
||||
}
|
||||
return new Pool(Map.copyOf(cells), source, outlet);
|
||||
}
|
||||
|
||||
private boolean poolColumn(int x, int z, int level, Position source, Position outlet) {
|
||||
if (!inside(x, z)) return false;
|
||||
Floor floor = floor(x, z, level);
|
||||
if (floor == NO_FLOOR || floor.bottom() > level - 7 || !available(x, z, level - 8, floor.top())) return false;
|
||||
for (int[] d : CARDINALS) {
|
||||
if (source != null && x == source.x() && z == source.z()
|
||||
&& x + d[0] == outlet.x() && z + d[1] == outlet.z()) continue;
|
||||
if (!solid(x + d[0], level, z + d[1])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Floor floor(int x, int z, int level) {
|
||||
return floors.computeIfAbsent(new Position(x, level, z), ignored -> {
|
||||
if (level < MIN_Y || level > MAX_Y) return NO_FLOOR;
|
||||
int top = -1;
|
||||
for (int y = level + 2; y >= level; y--) {
|
||||
if (solid(x, y, z)) { top = y; break; }
|
||||
}
|
||||
if (top < 0) return NO_FLOOR;
|
||||
for (int y = top + 1; y <= top + 4; y++) if (solid(x, y, z)) return NO_FLOOR;
|
||||
int ceiling = top + 5;
|
||||
while (ceiling < 384 && !solid(x, ceiling, z)) ceiling++;
|
||||
if (ceiling >= 384) return NO_FLOOR;
|
||||
int bottom = top;
|
||||
while (bottom > top - 16 && solid(x, bottom - 1, z)) bottom--;
|
||||
return new Floor(top, bottom, ceiling);
|
||||
});
|
||||
}
|
||||
|
||||
private boolean mouth(Position source, Position outlet) {
|
||||
if (source.y() < MIN_Y || source.y() > MAX_Y || !solid(source.x(), source.y(), source.z())) return false;
|
||||
// An adjacent natural step may receive the water before it descends again. Requiring
|
||||
// a vertical shaft here would discard the gentle ledges that form cave terraces.
|
||||
if (solid(outlet.x(), outlet.y(), outlet.z())) return false;
|
||||
return floor(source.x(), source.z(), source.y()) != NO_FLOOR;
|
||||
}
|
||||
|
||||
private boolean available(int x, int z, int low, int high) {
|
||||
for (Cell cell : surface.cellsAt(x, z)) if (low <= cell.carveTop() + 4
|
||||
&& high >= cell.bedY() - cell.sedimentDepth() - 5) return false;
|
||||
for (Cell cell : acceptedCells) if (cell.x() == x && cell.z() == z && low <= cell.carveTop() + 4
|
||||
&& high >= cell.bedY() - cell.sedimentDepth() - 5) return false;
|
||||
for (Spring spring : surface.springs()) if (inBounds(x, low, high, z, spring.flowBounds(), 6)) return false;
|
||||
for (Spill spill : spills) if (inBounds(x, low, high, z, spill.flowBounds(), 6)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean separated(Position source) {
|
||||
for (Spill spill : spills) if (Math.hypot(source.x() - spill.source().x(), source.z() - spill.source().z()) < 64) return false;
|
||||
return available(source.x(), source.z(), source.y() - 8, source.y() + 4);
|
||||
}
|
||||
|
||||
private static boolean overlapping(List<Cell> cells) {
|
||||
Map<Long, List<Cell>> columns = new HashMap<>();
|
||||
for (Cell cell : cells) {
|
||||
var existing = columns.computeIfAbsent(key(cell.x(), cell.z()), ignored -> new ArrayList<>());
|
||||
for (Cell old : existing) if (cell.bedY() - cell.sedimentDepth() - 1 <= old.carveTop()
|
||||
&& old.bedY() - old.sedimentDepth() - 1 <= cell.carveTop()) return true;
|
||||
existing.add(cell);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Flow trace(Position source, Position outlet, List<Cell> group) {
|
||||
Map<Long, List<Cell>> changes = new HashMap<>();
|
||||
Set<Position> retained = new HashSet<>();
|
||||
for (Cell cell : group) {
|
||||
changes.computeIfAbsent(key(cell.x(), cell.z()), ignored -> new ArrayList<>()).add(cell);
|
||||
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) retained.add(new Position(cell.x(), y, cell.z()));
|
||||
}
|
||||
Map<Position, Boolean> solidCache = new HashMap<>();
|
||||
Predicate<Position> open = at -> !solidCache.computeIfAbsent(at, p -> flowSolid(p, changes));
|
||||
Predicate<Position> canEnter = at -> !retained.contains(at) && open.test(at);
|
||||
Predicate<Position> hole = at -> open.test(new Position(at.x(), at.y() - 1, at.z()));
|
||||
Map<Position, Integer> reach = new HashMap<>(); Map<Position, Position> parent = new HashMap<>();
|
||||
ArrayDeque<FlowNode> queue = new ArrayDeque<>();
|
||||
reach.put(source, 7); queue.addLast(new FlowNode(source, 7)); Position lowest = source;
|
||||
while (!queue.isEmpty()) {
|
||||
FlowNode node = queue.removeFirst(); Position at = node.at();
|
||||
if (node.reach() < reach.getOrDefault(at, -1)) continue;
|
||||
if (at.y() < lowest.y()) lowest = at;
|
||||
if (reach.size() > 16000 || Math.abs(at.x() - source.x()) > 64 || Math.abs(at.z() - source.z()) > 64) return null;
|
||||
// Vanilla never replaces an existing source. Record the arriving contact,
|
||||
// but do not manufacture a route through the separately retained basin.
|
||||
if (retained.contains(at) && !at.equals(source)) continue;
|
||||
if (at.y() <= 0) continue;
|
||||
Position below = new Position(at.x(), at.y() - 1, at.z());
|
||||
if (open.test(below)) { enqueue(below, 7, at, reach, parent, queue); continue; }
|
||||
if (node.reach() <= 0) continue;
|
||||
for (Position next : downhillChoices(at, canEnter, hole))
|
||||
enqueue(next, node.reach() - 1, at, reach, parent, queue);
|
||||
}
|
||||
if (!reach.containsKey(outlet)) return null;
|
||||
int minX = source.x(), maxX = minX, minY = source.y(), maxY = minY, minZ = source.z(), maxZ = minZ;
|
||||
for (Position at : reach.keySet()) {
|
||||
minX = Math.min(minX, at.x()); maxX = Math.max(maxX, at.x()); minY = Math.min(minY, at.y()); maxY = Math.max(maxY, at.y());
|
||||
minZ = Math.min(minZ, at.z()); maxZ = Math.max(maxZ, at.z());
|
||||
}
|
||||
List<Position> path = new ArrayList<>();
|
||||
for (Position at = lowest; at != null; at = parent.get(at)) path.add(at);
|
||||
Collections.reverse(path);
|
||||
return new Flow(Set.copyOf(reach.keySet()), List.copyOf(path), new FlowBounds(minX, minY, minZ, maxX, maxY, maxZ));
|
||||
}
|
||||
|
||||
private boolean flowSolid(Position at, Map<Long, List<Cell>> changes) {
|
||||
for (Cell cell : changes.getOrDefault(key(at.x(), at.z()), List.of()))
|
||||
if (at.y() > cell.bedY() && at.y() <= cell.carveTop()) return false;
|
||||
Cell surfaceCell = surface.cellAt(at.x(), at.y(), at.z());
|
||||
if (surfaceCell != null && at.y() > surfaceCell.bedY() && at.y() <= surfaceCell.carveTop()) return false;
|
||||
return solid(at.x(), at.y(), at.z());
|
||||
}
|
||||
|
||||
private static boolean reaches(Flow flow, Pool pool) {
|
||||
return flow.volume().stream().anyMatch(at -> {
|
||||
Cell cell = pool.cells().get(key(at.x(), at.z()));
|
||||
return cell != null && at.y() > cell.bedY() && at.y() <= cell.waterY();
|
||||
});
|
||||
}
|
||||
|
||||
private static Feature feature(long id, Pool pool, Position center) {
|
||||
int minX = Integer.MAX_VALUE, minZ = minX, maxX = Integer.MIN_VALUE, maxZ = maxX;
|
||||
for (Cell cell : pool.cells().values()) {
|
||||
minX = Math.min(minX, cell.x()); maxX = Math.max(maxX, cell.x()); minZ = Math.min(minZ, cell.z()); maxZ = Math.max(maxZ, cell.z());
|
||||
}
|
||||
return new Feature(id, Kind.TERRACE, center.y(), List.of(new Point(center.x(), center.z())), pool.cells().size(),
|
||||
new Bounds(minX, minZ, maxX, maxZ));
|
||||
}
|
||||
|
||||
private boolean solid(int x, int y, int z) { return sampler.sample(x, y, z) > 0; }
|
||||
private double noise(int x, int z, int scale) {
|
||||
int gx = Math.floorDiv(x, scale), gz = Math.floorDiv(z, scale);
|
||||
double tx = smooth(Math.floorMod(x, scale) / (double) scale), tz = smooth(Math.floorMod(z, scale) / (double) scale);
|
||||
double a = random(seed, gx, gz) * (1 - tx) + random(seed, gx + 1, gz) * tx;
|
||||
double b = random(seed, gx, gz + 1) * (1 - tx) + random(seed, gx + 1, gz + 1) * tx;
|
||||
return a * (1 - tz) + b * tz;
|
||||
}
|
||||
}
|
||||
|
||||
/** WaterFluid's bare-rock downhill choice: consider holes through four recursive
|
||||
* steps, keep only the nearest directions (including ties), spread everywhere
|
||||
* only when no direction finds a hole. Retained sources cannot be entered. */
|
||||
static List<Position> downhillChoices(Position at, Predicate<Position> canEnter, Predicate<Position> hole) {
|
||||
List<Position> selected = new ArrayList<>();
|
||||
int best = 1000;
|
||||
for (int[] d : CARDINALS) {
|
||||
Position next = new Position(at.x() + d[0], at.y(), at.z() + d[1]);
|
||||
if (!canEnter.test(next)) continue;
|
||||
int cost = hole.test(next) ? 0 : slopeDistance(next, 1, -d[0], -d[1], canEnter, hole);
|
||||
if (cost < best) selected.clear();
|
||||
if (cost <= best) { selected.add(next); best = cost; }
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
private static int slopeDistance(Position at, int distance, int backX, int backZ,
|
||||
Predicate<Position> canEnter, Predicate<Position> hole) {
|
||||
int best = 1000;
|
||||
for (int[] d : CARDINALS) {
|
||||
if (d[0] == backX && d[1] == backZ) continue;
|
||||
Position next = new Position(at.x() + d[0], at.y(), at.z() + d[1]);
|
||||
if (!canEnter.test(next)) continue;
|
||||
if (hole.test(next)) return distance;
|
||||
if (distance < 4) best = Math.min(best,
|
||||
slopeDistance(next, distance + 1, -d[0], -d[1], canEnter, hole));
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private static void enqueue(Position next, int amount, Position from, Map<Position, Integer> reach,
|
||||
Map<Position, Position> parent, ArrayDeque<FlowNode> queue) {
|
||||
if (amount <= reach.getOrDefault(next, -1)) return;
|
||||
if (!reach.containsKey(next)) parent.put(next, from);
|
||||
reach.put(next, amount); queue.addLast(new FlowNode(next, amount));
|
||||
}
|
||||
private static Set<Long> connected(Set<Long> footprint, long origin) {
|
||||
if (!footprint.contains(origin)) return Set.of();
|
||||
Set<Long> seen = new HashSet<>(); ArrayDeque<Long> queue = new ArrayDeque<>();
|
||||
seen.add(origin); queue.add(origin);
|
||||
while (!queue.isEmpty()) {
|
||||
long at = queue.removeFirst(); int x = (int) (at >> 32), z = (int) at;
|
||||
for (int[] d : CARDINALS) {
|
||||
long next = key(x + d[0], z + d[1]);
|
||||
if (footprint.contains(next) && seen.add(next)) queue.addLast(next);
|
||||
}
|
||||
}
|
||||
return seen;
|
||||
}
|
||||
private static boolean inBounds(int x, int low, int high, int z, FlowBounds b, int margin) {
|
||||
return x >= b.minX() - margin && x <= b.maxX() + margin && z >= b.minZ() - margin && z <= b.maxZ() + margin
|
||||
&& low <= b.maxY() + margin && high >= b.minY() - margin;
|
||||
}
|
||||
private static boolean inside(int x, int z) { return (long) x * x + (long) z * z < (long) RADIUS * RADIUS; }
|
||||
private static long key(int x, int z) { return ((long) x << 32) | (z & 0xffffffffL); }
|
||||
private static double smooth(double t) { return t * t * (3 - 2 * t); }
|
||||
private static double random(long seed, int x, int z) { return (mix(seed ^ key(x, z)) >>> 11) * 0x1.0p-53; }
|
||||
private static long mix(long value) {
|
||||
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
|
||||
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
|
||||
return value ^ (value >>> 31);
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,15 @@ public final class SanctuarySpawn {
|
||||
public static final ResourceKey<NoiseGeneratorSettings> CAVERN_SETTINGS =
|
||||
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("sanctuary_cavern"));
|
||||
|
||||
public static final ResourceKey<NoiseGeneratorSettings> POPULATION_5_SETTINGS =
|
||||
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("population_5"));
|
||||
public static final ResourceKey<NoiseGeneratorSettings> POPULATION_10_SETTINGS =
|
||||
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("population_10"));
|
||||
public static final ResourceKey<NoiseGeneratorSettings> POPULATION_20_SETTINGS =
|
||||
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("population_20"));
|
||||
public static final ResourceKey<NoiseGeneratorSettings> POPULATION_100_SETTINGS =
|
||||
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("population_100"));
|
||||
|
||||
private SanctuarySpawn() {}
|
||||
|
||||
public static boolean usesSanctuaryGenerator(ServerLevel level) {
|
||||
@@ -47,7 +56,11 @@ public final class SanctuarySpawn {
|
||||
|| generator.generatorSettings().is(LAYERED_SETTINGS)
|
||||
|| generator.generatorSettings().is(WOODLAND_SETTINGS)
|
||||
|| generator.generatorSettings().is(RIFT_SETTINGS)
|
||||
|| generator.generatorSettings().is(CAVERN_SETTINGS));
|
||||
|| generator.generatorSettings().is(CAVERN_SETTINGS)
|
||||
|| generator.generatorSettings().is(POPULATION_5_SETTINGS)
|
||||
|| generator.generatorSettings().is(POPULATION_10_SETTINGS)
|
||||
|| generator.generatorSettings().is(POPULATION_20_SETTINGS)
|
||||
|| generator.generatorSettings().is(POPULATION_100_SETTINGS));
|
||||
}
|
||||
|
||||
/** Called only during first creation; saved spawns and /setworldspawn survive subsequent loads. */
|
||||
@@ -56,14 +69,16 @@ public final class SanctuarySpawn {
|
||||
return;
|
||||
}
|
||||
NoiseBasedChunkGenerator generator = (NoiseBasedChunkGenerator) level.getChunkSource().getGenerator();
|
||||
IslandCapacity capacity = IslandCapacity.forGenerator(generator);
|
||||
int terrainLimit = capacity == null ? IslandShape.TERRAIN_LIMIT : capacity.terrainLimit();
|
||||
BlockPos fallback = null;
|
||||
// Search the finite island from its centre outwards. Sampling base terrain first avoids
|
||||
// generating/decorating full chunks for the holes that can now cross the origin.
|
||||
for (int radius = 0; radius <= IslandShape.TERRAIN_LIMIT; radius += 4) {
|
||||
for (int radius = 0; radius <= terrainLimit; radius += 4) {
|
||||
for (int x = -radius; x <= radius; x += 4) {
|
||||
for (int z = -radius; z <= radius; z += 4) {
|
||||
if (Math.max(Math.abs(x), Math.abs(z)) != radius) continue;
|
||||
if (Math.hypot((double) x, (double) z) >= IslandShape.TERRAIN_LIMIT) continue;
|
||||
if (Math.hypot((double) x, (double) z) >= terrainLimit) continue;
|
||||
if (generator.getBaseHeight(x, z, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES,
|
||||
level, level.getChunkSource().randomState()) <= generator.getMinY()) continue;
|
||||
// Level.getHeight returns minY for an unloaded chunk; request full terrain first.
|
||||
@@ -86,7 +101,7 @@ public final class SanctuarySpawn {
|
||||
return;
|
||||
}
|
||||
throw new IllegalStateException("Sanctuary found no natural safe ground within "
|
||||
+ IslandShape.TERRAIN_LIMIT + " blocks for seed " + level.getSeed()
|
||||
+ terrainLimit + " blocks for seed " + level.getSeed()
|
||||
+ "; no artificial platform was created");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generator.sanctuary.sanctuary": "Sanctuary",
|
||||
"generator.sanctuary.sanctuary": "Sanctuary (Large)",
|
||||
"biome.sanctuary.starter_forest": "Sanctuary Forest",
|
||||
"biome.sanctuary.starter_forest_hydrology": "Sanctuary Forest and Shores",
|
||||
"biome.sanctuary.temperate_forest": "Sanctuary Temperate Forest",
|
||||
@@ -37,5 +37,18 @@
|
||||
"biome.sanctuary.cavern_bamboo_grove": "Bamboo Grove",
|
||||
"biome.sanctuary.cavern_sulfur_depths": "Sulfur Depths",
|
||||
"biome.sanctuary.cavern_lush_caves": "Sanctuary Lush Caves",
|
||||
"biome.sanctuary.cavern_dripstone_caves": "Sanctuary Dripstone Caves"
|
||||
"biome.sanctuary.cavern_dripstone_caves": "Sanctuary Dripstone Caves",
|
||||
"generator.sanctuary.sanctuary_5": "Sanctuary (Small)",
|
||||
"generator.sanctuary.sanctuary_100": "Sanctuary (100 players)",
|
||||
"biome.sanctuary.population_oak_forest": "Sanctuary Oak Woodland",
|
||||
"biome.sanctuary.population_birch_forest": "Sanctuary Birch Woodland",
|
||||
"biome.sanctuary.population_clearing": "Flowered Clearing",
|
||||
"biome.sanctuary.population_dry_woodland": "Dry Woodland",
|
||||
"biome.sanctuary.population_rocky_heath": "Rocky Heath",
|
||||
"biome.sanctuary.population_dark_grove": "Dark Undergrowth",
|
||||
"biome.sanctuary.population_bamboo_grove": "Bamboo Grove",
|
||||
"biome.sanctuary.population_sulfur_depths": "Sulfur Depths",
|
||||
"biome.sanctuary.population_lush_caves": "Sanctuary Lush Caves",
|
||||
"biome.sanctuary.population_dripstone_caves": "Sanctuary Dripstone Caves",
|
||||
"generator.sanctuary.sanctuary_20": "Sanctuary (20 players — legacy size)"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generator.sanctuary.sanctuary": "Sanctuary",
|
||||
"generator.sanctuary.sanctuary": "Sanctuary (Grand)",
|
||||
"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",
|
||||
@@ -37,5 +37,18 @@
|
||||
"biome.sanctuary.cavern_bamboo_grove": "Bosquet de bambous",
|
||||
"biome.sanctuary.cavern_sulfur_depths": "Profondeurs soufrées",
|
||||
"biome.sanctuary.cavern_lush_caves": "Grottes luxuriantes de Sanctuary",
|
||||
"biome.sanctuary.cavern_dripstone_caves": "Grottes de spéléothèmes de Sanctuary"
|
||||
"biome.sanctuary.cavern_dripstone_caves": "Grottes de spéléothèmes de Sanctuary",
|
||||
"generator.sanctuary.sanctuary_5": "Sanctuary (Petit)",
|
||||
"generator.sanctuary.sanctuary_100": "Sanctuary (100 joueurs)",
|
||||
"biome.sanctuary.population_oak_forest": "Chênaie de Sanctuary",
|
||||
"biome.sanctuary.population_birch_forest": "Bétulaie de Sanctuary",
|
||||
"biome.sanctuary.population_clearing": "Clairière fleurie",
|
||||
"biome.sanctuary.population_dry_woodland": "Bois sec",
|
||||
"biome.sanctuary.population_rocky_heath": "Lande rocheuse",
|
||||
"biome.sanctuary.population_dark_grove": "Sous-bois sombre",
|
||||
"biome.sanctuary.population_bamboo_grove": "Bosquet de bambous",
|
||||
"biome.sanctuary.population_sulfur_depths": "Profondeurs soufrées",
|
||||
"biome.sanctuary.population_lush_caves": "Grottes luxuriantes de Sanctuary",
|
||||
"biome.sanctuary.population_dripstone_caves": "Grottes de spéléothèmes de Sanctuary",
|
||||
"generator.sanctuary.sanctuary_20": "Sanctuary (20 joueurs — ancien format)"
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"replace": false,
|
||||
"values": [
|
||||
"sanctuary:sanctuary"
|
||||
"sanctuary:sanctuary",
|
||||
"sanctuary:sanctuary_5"
|
||||
]
|
||||
}
|
||||
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.sparse_jungle"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:wolf",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 2
|
||||
},
|
||||
"weight": 8
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.95,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:brown_mushroom_normal",
|
||||
"minecraft:red_mushroom_normal",
|
||||
"sanctuary:population_groves",
|
||||
"sanctuary:population_decorations",
|
||||
"sanctuary:population_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.8
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.forest"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.7,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:forest_flowers",
|
||||
"minecraft:wildflowers_birch_forest",
|
||||
"minecraft:trees_birch",
|
||||
"minecraft:patch_bush",
|
||||
"minecraft:flower_default",
|
||||
"minecraft:patch_grass_forest",
|
||||
"minecraft:brown_mushroom_normal",
|
||||
"minecraft:red_mushroom_normal",
|
||||
"minecraft:patch_pumpkin",
|
||||
"minecraft:patch_sugar_cane",
|
||||
"minecraft:patch_firefly_bush_near_water",
|
||||
"sanctuary:population_groves",
|
||||
"sanctuary:population_decorations",
|
||||
"sanctuary:population_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.7
|
||||
}
|
||||
+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.75,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:patch_tall_grass_2",
|
||||
"minecraft:patch_grass_meadow",
|
||||
"minecraft:flower_meadow",
|
||||
"minecraft:trees_meadow",
|
||||
"minecraft:wildflowers_meadow",
|
||||
"minecraft:flower_flower_forest",
|
||||
"sanctuary:population_groves",
|
||||
"sanctuary:population_decorations",
|
||||
"sanctuary:population_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.7
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.forest"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.9,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:brown_mushroom_normal",
|
||||
"minecraft:red_mushroom_normal",
|
||||
"sanctuary:population_groves",
|
||||
"sanctuary:population_decorations",
|
||||
"sanctuary:population_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.7
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.forest"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.55,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:brown_mushroom_normal",
|
||||
"minecraft:red_mushroom_normal",
|
||||
"sanctuary:population_groves",
|
||||
"sanctuary:population_decorations",
|
||||
"sanctuary:population_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.5
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.forest"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
},
|
||||
{
|
||||
"type": "minecraft:wolf",
|
||||
"count": 4,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.4,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:forest_flowers",
|
||||
"sanctuary:rift_sparse_trees",
|
||||
"minecraft:patch_bush",
|
||||
"minecraft:flower_default",
|
||||
"minecraft:patch_grass_forest",
|
||||
"minecraft:brown_mushroom_normal",
|
||||
"minecraft:red_mushroom_normal",
|
||||
"sanctuary:population_groves",
|
||||
"sanctuary:population_decorations",
|
||||
"sanctuary:population_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.75
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.forest"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.9,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:brown_mushroom_normal",
|
||||
"minecraft:red_mushroom_normal",
|
||||
"sanctuary:population_groves",
|
||||
"sanctuary:population_decorations",
|
||||
"sanctuary:population_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.5
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.forest"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
},
|
||||
{
|
||||
"type": "minecraft:wolf",
|
||||
"count": 4,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.8,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:forest_flowers",
|
||||
"minecraft:trees_birch_and_oak_leaf_litter",
|
||||
"minecraft:patch_bush",
|
||||
"minecraft:flower_default",
|
||||
"minecraft:patch_grass_forest",
|
||||
"minecraft:brown_mushroom_normal",
|
||||
"minecraft:red_mushroom_normal",
|
||||
"minecraft:patch_pumpkin",
|
||||
"minecraft:patch_sugar_cane",
|
||||
"minecraft:patch_firefly_bush_near_water",
|
||||
"sanctuary:population_groves",
|
||||
"sanctuary:population_decorations",
|
||||
"sanctuary:population_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.7
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.meadow"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:donkey",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 2,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 1
|
||||
},
|
||||
{
|
||||
"type": "minecraft:rabbit",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 2
|
||||
},
|
||||
"weight": 2
|
||||
},
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 2
|
||||
},
|
||||
"weight": 2
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.65,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"sanctuary:population_groves",
|
||||
"sanctuary:population_decorations",
|
||||
"sanctuary:population_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.7
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.forest"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
},
|
||||
{
|
||||
"type": "minecraft:wolf",
|
||||
"count": 4,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.5,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"sanctuary:population_groves",
|
||||
"sanctuary:population_decorations",
|
||||
"sanctuary:population_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.8
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"type": "sanctuary:population_island",
|
||||
"terrain": {
|
||||
"type": "minecraft:squeeze",
|
||||
"input": {
|
||||
"type": "minecraft:interpolated",
|
||||
"cell_size_xz": 8,
|
||||
"cell_size_y": 4,
|
||||
"input": {
|
||||
"type": "minecraft:mul",
|
||||
"left": 0.64,
|
||||
"right": "sanctuary:floating_archipelago_384"
|
||||
}
|
||||
}
|
||||
},
|
||||
"distortion": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:island_edge",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 0.0
|
||||
},
|
||||
"sculpt": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:island_sculpt",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"detail": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:island_detail",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"underside": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:island_underside",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"players": 10,
|
||||
"rift_warp": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:rift_warp",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"rift_detail": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:rift_detail",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"type": "sanctuary:population_island",
|
||||
"terrain": {
|
||||
"type": "minecraft:squeeze",
|
||||
"input": {
|
||||
"type": "minecraft:interpolated",
|
||||
"cell_size_xz": 8,
|
||||
"cell_size_y": 4,
|
||||
"input": {
|
||||
"type": "minecraft:mul",
|
||||
"left": 0.64,
|
||||
"right": "sanctuary:floating_archipelago_384"
|
||||
}
|
||||
}
|
||||
},
|
||||
"distortion": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:island_edge",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 0.0
|
||||
},
|
||||
"sculpt": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:island_sculpt",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"detail": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:island_detail",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"underside": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:island_underside",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"players": 100,
|
||||
"rift_warp": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:rift_warp",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"rift_detail": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:rift_detail",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"type": "sanctuary:population_island",
|
||||
"terrain": {
|
||||
"type": "minecraft:squeeze",
|
||||
"input": {
|
||||
"type": "minecraft:interpolated",
|
||||
"cell_size_xz": 8,
|
||||
"cell_size_y": 4,
|
||||
"input": {
|
||||
"type": "minecraft:mul",
|
||||
"left": 0.64,
|
||||
"right": "sanctuary:floating_archipelago_384"
|
||||
}
|
||||
}
|
||||
},
|
||||
"distortion": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:island_edge",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 0.0
|
||||
},
|
||||
"sculpt": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:island_sculpt",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"detail": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:island_detail",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"underside": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:island_underside",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"players": 20,
|
||||
"rift_warp": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:rift_warp",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"rift_detail": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:rift_detail",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"type": "sanctuary:population_island",
|
||||
"terrain": {
|
||||
"type": "minecraft:squeeze",
|
||||
"input": {
|
||||
"type": "minecraft:interpolated",
|
||||
"cell_size_xz": 8,
|
||||
"cell_size_y": 4,
|
||||
"input": {
|
||||
"type": "minecraft:mul",
|
||||
"left": 0.64,
|
||||
"right": "sanctuary:floating_archipelago_384"
|
||||
}
|
||||
}
|
||||
},
|
||||
"distortion": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:island_edge",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 0.0
|
||||
},
|
||||
"sculpt": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:island_sculpt",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"detail": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:island_detail",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"underside": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:island_underside",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"players": 5,
|
||||
"rift_warp": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:rift_warp",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"rift_detail": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:rift_detail",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"type": "minecraft:geode",
|
||||
"blocks": {
|
||||
"alternate_inner_layer_provider": {
|
||||
"id": "minecraft:budding_amethyst"
|
||||
},
|
||||
"cannot_replace": "#minecraft:features_cannot_replace",
|
||||
"filling_provider": {
|
||||
"id": "minecraft:air"
|
||||
},
|
||||
"inner_layer_provider": {
|
||||
"id": "minecraft:amethyst_block"
|
||||
},
|
||||
"inner_placements": [
|
||||
"minecraft:small_amethyst_bud",
|
||||
"minecraft:medium_amethyst_bud",
|
||||
"minecraft:large_amethyst_bud",
|
||||
"minecraft:amethyst_cluster"
|
||||
],
|
||||
"invalid_blocks": "#minecraft:geode_invalid_blocks",
|
||||
"middle_layer_provider": {
|
||||
"id": "minecraft:calcite"
|
||||
},
|
||||
"outer_layer_provider": {
|
||||
"id": "minecraft:smooth_basalt"
|
||||
}
|
||||
},
|
||||
"crack": {
|
||||
"generate_crack_chance": 1.0
|
||||
},
|
||||
"invalid_blocks_threshold": 1,
|
||||
"layers": {},
|
||||
"outer_wall_distance": 1,
|
||||
"use_alternate_layer0_chance": 0.2,
|
||||
"distribution_points": 2,
|
||||
"point_offset": 1,
|
||||
"min_gen_offset": -5,
|
||||
"max_gen_offset": 7
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"type": "minecraft:sequence",
|
||||
"sequence": [
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:population_strata",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": -0.3,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:blackstone"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:population_strata",
|
||||
"min_threshold": 0.3,
|
||||
"max_threshold": 1000,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:basalt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:deepslate"
|
||||
}
|
||||
]
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"type": "minecraft:sequence",
|
||||
"sequence": [
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 136
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": "sanctuary:population_deep_strata"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 152
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:population_depth_boundary",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": 0.45,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": "sanctuary:population_deep_strata"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 168
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:population_depth_boundary",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": -0.45,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": "sanctuary:population_deep_strata"
|
||||
}
|
||||
},
|
||||
"sanctuary:population_upper_strata"
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:sequence",
|
||||
"sequence": [
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:population_strata",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": -0.36,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:cobblestone"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:population_strata",
|
||||
"min_threshold": 0.36,
|
||||
"max_threshold": 1000,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:packed_mud"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:population_strata",
|
||||
"min_threshold": -0.14,
|
||||
"max_threshold": 0.14,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:mossy_cobblestone"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:tuff"
|
||||
}
|
||||
]
|
||||
}
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
{
|
||||
"type": "minecraft:sequence",
|
||||
"sequence": [
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:biome",
|
||||
"biome_is": "sanctuary:population_sulfur_depths"
|
||||
},
|
||||
"then_run": "sanctuary:population_sulfur_strata"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 136
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": "sanctuary:population_deep_strata"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 152
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:population_depth_boundary",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": 0.45,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": "sanctuary:population_deep_strata"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 168
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:population_depth_boundary",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": -0.45,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": "sanctuary:population_deep_strata"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:biome",
|
||||
"biome_is": "sanctuary:population_lush_caves"
|
||||
},
|
||||
"then_run": "sanctuary:population_humid_strata"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:biome",
|
||||
"biome_is": "sanctuary:population_dark_grove"
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:sequence",
|
||||
"sequence": [
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 192
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": "sanctuary:population_humid_strata"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 208
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:population_depth_boundary",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": 0.45,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": "sanctuary:population_humid_strata"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 224
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:population_depth_boundary",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": -0.45,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": "sanctuary:population_humid_strata"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:biome",
|
||||
"biome_is": "sanctuary:population_bamboo_grove"
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:sequence",
|
||||
"sequence": [
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 192
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": "sanctuary:population_humid_strata"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 208
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:population_depth_boundary",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": 0.45,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": "sanctuary:population_humid_strata"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 224
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:population_depth_boundary",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": -0.45,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": "sanctuary:population_humid_strata"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:biome",
|
||||
"biome_is": "sanctuary:population_rocky_heath"
|
||||
},
|
||||
"then_run": "sanctuary:population_upper_strata"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:population_exposure",
|
||||
"min_threshold": 0.85,
|
||||
"max_threshold": 1000,
|
||||
"is_3d": false
|
||||
},
|
||||
"then_run": "sanctuary:population_upper_strata"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:biome",
|
||||
"biome_is": "sanctuary:population_dry_woodland"
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:population_exposure",
|
||||
"min_threshold": 0.15,
|
||||
"max_threshold": 1000,
|
||||
"is_3d": false
|
||||
},
|
||||
"then_run": "sanctuary:population_upper_strata"
|
||||
}
|
||||
},
|
||||
"sanctuary:starter_island",
|
||||
"sanctuary:population_upper_strata"
|
||||
]
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"type": "minecraft:sequence",
|
||||
"sequence": [
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:population_strata",
|
||||
"min_threshold": -0.15,
|
||||
"max_threshold": 0.55,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:sulfur"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:population_strata",
|
||||
"min_threshold": -0.55,
|
||||
"max_threshold": -0.3,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:cinnabar"
|
||||
}
|
||||
},
|
||||
"sanctuary:population_depth_strata"
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:sequence",
|
||||
"sequence": [
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:population_strata",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": -0.36,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:granite"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:population_strata",
|
||||
"min_threshold": 0.36,
|
||||
"max_threshold": 1000,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:diorite"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:population_strata",
|
||||
"min_threshold": -0.14,
|
||||
"max_threshold": 0.14,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:andesite"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:stone"
|
||||
}
|
||||
]
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"base_amplitude": 1.0,
|
||||
"base_octave": -6,
|
||||
"octave_count": 2
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"base_amplitude": 1.0,
|
||||
"base_octave": -7,
|
||||
"octave_count": 2
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"base_amplitude": 1.0,
|
||||
"base_octave": -6,
|
||||
"octave_count": 2
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"default_block": "minecraft:stone",
|
||||
"default_fluid": "minecraft:air",
|
||||
"disable_mob_generation": false,
|
||||
"legacy_random_source": true,
|
||||
"material_rule": "sanctuary:population_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:population_10",
|
||||
"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:population_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:population_100",
|
||||
"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:population_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:population_20",
|
||||
"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:population_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:population_5",
|
||||
"ridges": 0.0,
|
||||
"temperature": "sanctuary:temperate_variation",
|
||||
"vegetation": "sanctuary:temperate_moisture"
|
||||
},
|
||||
"sea_level": -64,
|
||||
"spawn_target": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"feature": {
|
||||
"type": "sanctuary:population_decorations"
|
||||
},
|
||||
"placement": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"feature": {
|
||||
"type": "sanctuary:population_groves"
|
||||
},
|
||||
"placement": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"feature": {
|
||||
"type": "sanctuary:population_shore_sugar_cane"
|
||||
},
|
||||
"placement": []
|
||||
}
|
||||
+12
-12
@@ -5,19 +5,19 @@
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": {
|
||||
"type": "sanctuary:cavern_island",
|
||||
"oak_forest": "sanctuary:cavern_oak_forest",
|
||||
"birch_forest": "sanctuary:cavern_birch_forest",
|
||||
"clearing": "sanctuary:cavern_clearing",
|
||||
"dry_woodland": "sanctuary:cavern_dry_woodland",
|
||||
"dark_grove": "sanctuary:cavern_dark_grove",
|
||||
"bamboo_grove": "sanctuary:cavern_bamboo_grove",
|
||||
"sulfur_depths": "sanctuary:cavern_sulfur_depths",
|
||||
"rocky_heath": "sanctuary:cavern_rocky_heath",
|
||||
"lush_caves": "sanctuary:cavern_lush_caves",
|
||||
"dripstone_caves": "sanctuary:cavern_dripstone_caves"
|
||||
"type": "sanctuary:population_island",
|
||||
"oak_forest": "sanctuary:population_oak_forest",
|
||||
"birch_forest": "sanctuary:population_birch_forest",
|
||||
"clearing": "sanctuary:population_clearing",
|
||||
"dry_woodland": "sanctuary:population_dry_woodland",
|
||||
"dark_grove": "sanctuary:population_dark_grove",
|
||||
"bamboo_grove": "sanctuary:population_bamboo_grove",
|
||||
"sulfur_depths": "sanctuary:population_sulfur_depths",
|
||||
"rocky_heath": "sanctuary:population_rocky_heath",
|
||||
"lush_caves": "sanctuary:population_lush_caves",
|
||||
"dripstone_caves": "sanctuary:population_dripstone_caves"
|
||||
},
|
||||
"settings": "sanctuary:sanctuary_cavern"
|
||||
"settings": "sanctuary:population_10"
|
||||
}
|
||||
},
|
||||
"minecraft:the_end": {
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"dimensions": {
|
||||
"minecraft:overworld": {
|
||||
"type": "sanctuary:sanctuary_384",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": {
|
||||
"type": "sanctuary:population_island",
|
||||
"oak_forest": "sanctuary:population_oak_forest",
|
||||
"birch_forest": "sanctuary:population_birch_forest",
|
||||
"clearing": "sanctuary:population_clearing",
|
||||
"dry_woodland": "sanctuary:population_dry_woodland",
|
||||
"dark_grove": "sanctuary:population_dark_grove",
|
||||
"bamboo_grove": "sanctuary:population_bamboo_grove",
|
||||
"sulfur_depths": "sanctuary:population_sulfur_depths",
|
||||
"rocky_heath": "sanctuary:population_rocky_heath",
|
||||
"lush_caves": "sanctuary:population_lush_caves",
|
||||
"dripstone_caves": "sanctuary:population_dripstone_caves"
|
||||
},
|
||||
"settings": "sanctuary:population_100"
|
||||
}
|
||||
},
|
||||
"minecraft:the_end": {
|
||||
"type": "minecraft:the_end",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": {
|
||||
"type": "minecraft:the_end"
|
||||
},
|
||||
"settings": "minecraft:end"
|
||||
}
|
||||
},
|
||||
"minecraft:the_nether": {
|
||||
"type": "minecraft:the_nether",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": {
|
||||
"type": "minecraft:multi_noise",
|
||||
"preset": "minecraft:nether"
|
||||
},
|
||||
"settings": "minecraft:nether"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"dimensions": {
|
||||
"minecraft:overworld": {
|
||||
"type": "sanctuary:sanctuary_384",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": {
|
||||
"type": "sanctuary:population_island",
|
||||
"oak_forest": "sanctuary:population_oak_forest",
|
||||
"birch_forest": "sanctuary:population_birch_forest",
|
||||
"clearing": "sanctuary:population_clearing",
|
||||
"dry_woodland": "sanctuary:population_dry_woodland",
|
||||
"dark_grove": "sanctuary:population_dark_grove",
|
||||
"bamboo_grove": "sanctuary:population_bamboo_grove",
|
||||
"sulfur_depths": "sanctuary:population_sulfur_depths",
|
||||
"rocky_heath": "sanctuary:population_rocky_heath",
|
||||
"lush_caves": "sanctuary:population_lush_caves",
|
||||
"dripstone_caves": "sanctuary:population_dripstone_caves"
|
||||
},
|
||||
"settings": "sanctuary:population_20"
|
||||
}
|
||||
},
|
||||
"minecraft:the_end": {
|
||||
"type": "minecraft:the_end",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": {
|
||||
"type": "minecraft:the_end"
|
||||
},
|
||||
"settings": "minecraft:end"
|
||||
}
|
||||
},
|
||||
"minecraft:the_nether": {
|
||||
"type": "minecraft:the_nether",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": {
|
||||
"type": "minecraft:multi_noise",
|
||||
"preset": "minecraft:nether"
|
||||
},
|
||||
"settings": "minecraft:nether"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"dimensions": {
|
||||
"minecraft:overworld": {
|
||||
"type": "sanctuary:sanctuary_384",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": {
|
||||
"type": "sanctuary:population_island",
|
||||
"oak_forest": "sanctuary:population_oak_forest",
|
||||
"birch_forest": "sanctuary:population_birch_forest",
|
||||
"clearing": "sanctuary:population_clearing",
|
||||
"dry_woodland": "sanctuary:population_dry_woodland",
|
||||
"dark_grove": "sanctuary:population_dark_grove",
|
||||
"bamboo_grove": "sanctuary:population_bamboo_grove",
|
||||
"sulfur_depths": "sanctuary:population_sulfur_depths",
|
||||
"rocky_heath": "sanctuary:population_rocky_heath",
|
||||
"lush_caves": "sanctuary:population_lush_caves",
|
||||
"dripstone_caves": "sanctuary:population_dripstone_caves"
|
||||
},
|
||||
"settings": "sanctuary:population_5"
|
||||
}
|
||||
},
|
||||
"minecraft:the_end": {
|
||||
"type": "minecraft:the_end",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": {
|
||||
"type": "minecraft:the_end"
|
||||
},
|
||||
"settings": "minecraft:end"
|
||||
}
|
||||
},
|
||||
"minecraft:the_nether": {
|
||||
"type": "minecraft:the_nether",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": {
|
||||
"type": "minecraft:multi_noise",
|
||||
"preset": "minecraft:nether"
|
||||
},
|
||||
"settings": "minecraft:nether"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
|
||||
/** Capacity and region boundaries are persistence contracts, including negative coordinates. */
|
||||
public final class PopulationCapacitySmoke {
|
||||
public static void main(String[] args) {
|
||||
require(IslandCapacity.values().length == 4, "New small/large and both historical geometries remain supported");
|
||||
for (int players : new int[]{5, 10, 20, 100}) {
|
||||
var capacity = IslandCapacity.fromPlayers(players);
|
||||
double ratio = Math.pow(capacity.radius() / IslandCapacity.FIVE.radius(), 2);
|
||||
require(Math.abs(ratio - players / 5.0) < 1e-10, "Area scales with planned population");
|
||||
var encoded = IslandCapacity.CODEC.encodeStart(JsonOps.INSTANCE, capacity).getOrThrow();
|
||||
require(encoded.getAsInt() == players, "Saved capacity is an explicit player count");
|
||||
require(IslandCapacity.CODEC.parse(JsonOps.INSTANCE, encoded).getOrThrow() == capacity,
|
||||
"All capacities survive a codec round trip");
|
||||
require(capacity.areaExists(0, 0), "The central chunk intersects the envelope");
|
||||
int exterior = Math.floorDiv(capacity.terrainLimit() + 16, 16);
|
||||
require(!capacity.areaExists(exterior, 0) && !capacity.areaExists(-exterior - 1, 0),
|
||||
"Exterior chunks are rejected on both sides of the origin");
|
||||
require(!capacity.areaExists(1_875_000, 1_875_000), "Far-world arithmetic must not wrap");
|
||||
for (int x : new int[]{-1025, -769, -768, -257, -256, -255, -1, 0, 255, 256, 257, 767, 768, 1025}) {
|
||||
for (int z : new int[]{-769, -768, -257, -256, -1, 0, 255, 256, 767, 768}) {
|
||||
var region = capacity.regionAt(x, z);
|
||||
require(x >= region.minX() && x <= region.maxX()
|
||||
&& z >= region.minZ() && z <= region.maxZ(), "Every block has one owner");
|
||||
require(region.maxX() - region.minX() + 1 == 512
|
||||
&& region.maxZ() - region.minZ() + 1 == 512, "Region dimensions stay local");
|
||||
require(capacity.regionAt(region.maxX() + 1, z).x() == region.x() + 1,
|
||||
"Adjacent regions have no gap or overlap");
|
||||
require(capacity.regionAt(region.minX() - 1, z).x() == region.x() - 1,
|
||||
"Negative boundaries use floor division");
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int invalid : new int[]{0, 1, 4, 6, 11, 21, 99, 101, -5}) {
|
||||
require(IslandCapacity.CODEC.parse(JsonOps.INSTANCE, new JsonPrimitive(invalid)).error().isPresent(),
|
||||
"Unsupported saved counts must fail, not silently become a different island");
|
||||
boolean rejected = false;
|
||||
try { IslandCapacity.fromPlayers(invalid); }
|
||||
catch (IllegalArgumentException expected) { rejected = true; }
|
||||
require(rejected, "Unsupported configuration is rejected");
|
||||
}
|
||||
System.out.println("Population capacity smoke passed: small/large, legacy sizes, codec and signed region ownership");
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/** Detects the world-wide stall when a cold region holds the planning lock. */
|
||||
public final class PopulationPlanCacheSmoke {
|
||||
public static void main(String[] args) throws Exception {
|
||||
var cache = new PopulationPlanCache<String, Integer>();
|
||||
cache.getOrCreate("ready", () -> 10);
|
||||
var entered = new CountDownLatch(1);
|
||||
var release = new CountDownLatch(1);
|
||||
try (var workers = Executors.newFixedThreadPool(8)) {
|
||||
Future<Integer> cold = workers.submit(() -> cache.getOrCreate("cold", () -> {
|
||||
entered.countDown();
|
||||
try {
|
||||
if (!release.await(10, TimeUnit.SECONDS)) throw new AssertionError("Cold plan was not released");
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
return 20;
|
||||
}));
|
||||
try {
|
||||
require(entered.await(5, TimeUnit.SECONDS), "Cold plan did not start");
|
||||
Future<Integer> ready = workers.submit(() -> cache.getOrCreate("ready", () -> {
|
||||
throw new AssertionError("Completed plan must not be rebuilt");
|
||||
}));
|
||||
require(ready.get(5, TimeUnit.SECONDS) == 10, "Warm reads must not await unrelated planning");
|
||||
require(cache.size() == 1, "Unfinished plans must not be exposed");
|
||||
} finally {
|
||||
release.countDown();
|
||||
}
|
||||
require(cold.get(5, TimeUnit.SECONDS) == 20, "Cold result lost");
|
||||
|
||||
var builds = new AtomicInteger();
|
||||
var start = new CountDownLatch(1);
|
||||
var results = new ArrayList<Future<Integer>>();
|
||||
for (int i = 0; i < 8; i++) results.add(workers.submit(() -> {
|
||||
start.await();
|
||||
return cache.getOrCreate("shared", builds::incrementAndGet);
|
||||
}));
|
||||
start.countDown();
|
||||
for (var result : results) require(result.get(5, TimeUnit.SECONDS) == 1, "All callers need the same complete plan");
|
||||
require(builds.get() == 1, "Concurrent misses must compute their region once");
|
||||
}
|
||||
|
||||
try {
|
||||
cache.getOrCreate("failed", () -> { throw new IllegalStateException("Expected planner failure"); });
|
||||
throw new AssertionError("Planner failure was swallowed");
|
||||
} catch (IllegalStateException expected) {
|
||||
require(cache.size() == 3, "Failed plan must not be published");
|
||||
}
|
||||
require(cache.getOrCreate("failed", () -> 40) == 40, "A failed computation must remain retryable");
|
||||
System.out.println("Population plan cache smoke passed: warm reads, single publication and failed-plan retry");
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import fr.koka.sanctuary.worldgen.PopulationHydrology.*;
|
||||
|
||||
/** Adversarial fixtures exercise rejection and signed translation without Minecraft or terrain searches. */
|
||||
public final class PopulationRegionPlanSmoke {
|
||||
public static void main(String[] args) throws Exception {
|
||||
water();
|
||||
lava();
|
||||
require(PopulationOwnership.contains(-240, 239, 16), "Hydrology includes its exact admission limits");
|
||||
require(!PopulationOwnership.contains(-241, 0, 16) && !PopulationOwnership.contains(240, 0, 16),
|
||||
"Hydrology leaves both regional margins unused");
|
||||
require(PopulationOwnership.contains(-224, 223, 32), "Lava includes its exact admission limits");
|
||||
require(!PopulationOwnership.contains(-225, 0, 32) && !PopulationOwnership.contains(224, 0, 32),
|
||||
"Lava leaves both regional margins unused");
|
||||
System.out.println("Population region plans smoke passed: atomic admission, dependency removal, signed multi-layer translation");
|
||||
}
|
||||
|
||||
private static void water() throws Exception {
|
||||
Map<Long, Cell> surface = new LinkedHashMap<>();
|
||||
for (Cell cell : List.of(cell(0, 100, 1), cell(10, 100, 2),
|
||||
new Cell(240, 0, -1, 98, 100, ShoreMaterial.GRASS, 2, 3)))
|
||||
surface.put(key(cell.x(), cell.z()), cell);
|
||||
List<Feature> surfaceFeatures = List.of(feature(1, Kind.LAKE, 0, 100), feature(2, Kind.POND, 10, 100));
|
||||
List<Cell> lowerCells = List.of(cell(0, 80, 3), cell(0, 60, 4), cell(30, 80, 5), cell(30, 60, 6));
|
||||
List<Feature> lowerFeatures = List.of(feature(3, Kind.TERRACE, 0, 80), feature(4, Kind.TERRACE, 0, 60),
|
||||
feature(5, Kind.TERRACE, 30, 80), feature(6, Kind.TERRACE, 30, 60));
|
||||
var validSpill = new Spill(101, 3, 4, p(0, 80), p(1, 80), List.of(p(1, 80), p(0, 60)), b(0, 60, 1, 80));
|
||||
var invalidSpill = new Spill(102, 5, 6, p(30, 80), p(31, 80), List.of(p(31, 80), p(30, 60)), b(30, 60, 240, 80));
|
||||
var validSpring = new Spring(201, p(-20, 100), p(-19, 100), List.of(p(-19, 100), p(-19, 0)), b(-20, 0, -19, 100));
|
||||
// This spring is itself in bounds, but its prediction used the rejected pond's cut at x10.
|
||||
var dependentSpring = new Spring(202, p(8, 100), p(9, 100), List.of(p(9, 100), p(10, 98)), b(8, 98, 10, 100));
|
||||
Set<Position> flow = Set.of(validSpring.source(), validSpring.outlet(), dependentSpring.source(), dependentSpring.outlet());
|
||||
var lower = new PopulationTerraces.Result(lowerCells, lowerFeatures,
|
||||
List.of(new Terrace(301, List.of(3L, 4L)), new Terrace(302, List.of(5L, 6L))),
|
||||
List.of(validSpill, invalidSpill), Set.of(validSpill.source(), validSpill.outlet(), invalidSpill.source()));
|
||||
var ctor = Plan.class.getDeclaredConstructor(Map.class, List.class, List.class, Set.class, int.class, long.class,
|
||||
PopulationTerraces.Result.class);
|
||||
ctor.setAccessible(true);
|
||||
var original = ctor.newInstance(surface, surfaceFeatures, List.of(validSpring, dependentSpring), flow, 123, 456L, lower);
|
||||
var admitted = original.admitted();
|
||||
require(admitted.features().stream().map(Feature::id).toList().equals(List.of(1L, 3L, 4L)),
|
||||
"Out-of-bounds pond and both members of an invalid terrace group disappear atomically");
|
||||
require(admitted.cells().stream().allMatch(c -> Set.of(1L, 3L, 4L).contains(c.featureId())),
|
||||
"No water or dry bank belonging to a rejected feature remains");
|
||||
require(admitted.terraces().equals(List.of(new Terrace(301, List.of(3L, 4L))))
|
||||
&& admitted.spills().equals(List.of(validSpill)), "A rejected outlet cannot leave orphan terrace basins");
|
||||
require(admitted.springs().equals(List.of(validSpring)), "A spring relying on a removed cut is rejected too");
|
||||
require(!admitted.allowsSpringFlow(8, 100, 0), "Rejected isolated spring prediction is absent");
|
||||
require(admitted.admitted().cells().equals(admitted.cells()) && admitted.admitted().spills().equals(admitted.spills()),
|
||||
"Admission is idempotent");
|
||||
var moved = admitted.translated(512, -512);
|
||||
require(moved.cellsAt(512, -512).size() == 3 && moved.cellsInChunk(32, -32).size() == 3,
|
||||
"Surface and both lower layers share their translated world column and chunk");
|
||||
require(moved.cellsAt(0, 0).isEmpty() && moved.cellsInChunk(0, 0).isEmpty(), "No stale local indexes survive");
|
||||
require(moved.cellAt(512, 79, -512).featureId() == 3 && moved.cellAt(512, 59, -512).featureId() == 4,
|
||||
"Vertical interval lookups still distinguish both lower pools");
|
||||
require(moved.springsInChunk(30, -32).getFirst().source().equals(new Position(492, 100, -512)),
|
||||
"Spring chunk ownership follows signed world coordinates");
|
||||
require(moved.isSpillOpening(513, 80, -512) && !moved.isSpillOpening(1, 80, 0), "Spill-opening index moves");
|
||||
require(moved.allowsSpringFlow(492, 100, -512), "The flow prediction moves with its source");
|
||||
var restored = moved.translated(-512, 512);
|
||||
require(restored.cells().equals(admitted.cells()) && restored.features().equals(admitted.features())
|
||||
&& restored.springs().equals(admitted.springs()) && restored.spills().equals(admitted.spills())
|
||||
&& restored.terraces().equals(admitted.terraces()), "Inverse translation restores paths, bounds, IDs and stacked cells exactly");
|
||||
require(moved.sampledColumns() == 123 && moved.densitySamples() == 456, "Survey work counts are not geometry");
|
||||
}
|
||||
|
||||
private static void lava() throws Exception {
|
||||
var pocket = List.of(new PopulationLavaDeposit.Cell(0, 100, 0, true), new PopulationLavaDeposit.Cell(0, 101, 0, false));
|
||||
Set<PopulationLavaDeposit.Position> pocketSupport = Set.of(lp(0, 99));
|
||||
var valid = new PopulationLavaFlows.Fall(401, lp(20, 100), lp(21, 100), List.of(lp(21, 100), lp(21, 0)),
|
||||
b(20, 0, 22, 100), List.of(lp(19, 100)));
|
||||
var invalid = new PopulationLavaFlows.Fall(402, lp(210, 100), lp(211, 100), List.of(lp(211, 100), lp(224, 0)),
|
||||
b(210, 0, 224, 100), List.of(lp(209, 100)));
|
||||
var ctor = PopulationLavaDeposit.Plan.class.getDeclaredConstructor(List.class, Set.class,
|
||||
PopulationLavaDeposit.Position.class, List.class, List.class, Set.class);
|
||||
ctor.setAccessible(true);
|
||||
List<PopulationLavaDeposit.Cell> cells = new ArrayList<>(pocket);
|
||||
cells.add(new PopulationLavaDeposit.Cell(20, 100, 0, true));
|
||||
cells.add(new PopulationLavaDeposit.Cell(210, 100, 0, true));
|
||||
Set<PopulationLavaDeposit.Position> supports = new HashSet<>(pocketSupport);
|
||||
supports.addAll(valid.supports()); supports.addAll(invalid.supports());
|
||||
var original = ctor.newInstance(cells, supports, lp(0, 102), List.of(valid, invalid), pocket, pocketSupport);
|
||||
var admitted = original.admitted();
|
||||
require(admitted.falls().equals(List.of(valid)) && admitted.cells().size() == 3 && admitted.lavaCells().size() == 2,
|
||||
"Rejecting a lava fall removes its source while preserving the complete pocket");
|
||||
require(!admitted.supports().contains(lp(209, 100)) && admitted.supports().contains(lp(0, 99)),
|
||||
"A rejected fall leaves no support journal; the pocket keeps its own support");
|
||||
var moved = admitted.translated(-512, 1024);
|
||||
require(moved.cellsInChunk(-32, 64).size() == 2 && moved.supportsInChunk(-32, 64).size() == 1,
|
||||
"Pocket cells and supports use translated negative chunk coordinates");
|
||||
require(moved.fallsInChunk(-31, 64).size() == 1 && moved.access().orElseThrow().equals(
|
||||
new PopulationLavaDeposit.Position(-512, 102, 1024)), "Fall ownership and the player access point move");
|
||||
var restored = moved.translated(512, -1024);
|
||||
require(restored.cells().equals(admitted.cells()) && restored.supports().equals(admitted.supports())
|
||||
&& restored.falls().equals(admitted.falls()) && restored.access().equals(admitted.access()),
|
||||
"Lava inverse translation restores all sources, outlets, paths, bounds and supports");
|
||||
require(admitted.admitted().cells().equals(admitted.cells()), "Lava admission is idempotent");
|
||||
// One invalid pocket cell rejects its entire chamber and access, never the independent valid fall.
|
||||
var badPocket = List.of(new PopulationLavaDeposit.Cell(224, 100, 0, true), new PopulationLavaDeposit.Cell(0, 101, 0, false));
|
||||
var badCombined = new ArrayList<>(badPocket);
|
||||
badCombined.add(new PopulationLavaDeposit.Cell(20, 100, 0, true));
|
||||
var badSupports = new HashSet<>(pocketSupport);
|
||||
badSupports.addAll(valid.supports());
|
||||
var pocketRejected = ctor.newInstance(badCombined, badSupports, lp(0, 102), List.of(valid), badPocket, pocketSupport).admitted();
|
||||
require(pocketRejected.cells().size() == 1 && pocketRejected.access().isEmpty()
|
||||
&& pocketRejected.supports().equals(valid.supports()), "Pocket admission is atomic and leaves no orphan opening or access");
|
||||
}
|
||||
|
||||
private static Cell cell(int x, int waterY, long id) { return new Cell(x, 0, waterY, waterY - 2, waterY, ShoreMaterial.STONE, id, 3); }
|
||||
private static Feature feature(long id, Kind kind, int x, int waterY) {
|
||||
return new Feature(id, kind, waterY, List.of(new Point(x, 0)), 1, new Bounds(x, 0, x, 0));
|
||||
}
|
||||
private static Position p(int x, int y) { return new Position(x, y, 0); }
|
||||
private static PopulationLavaDeposit.Position lp(int x, int y) { return new PopulationLavaDeposit.Position(x, y, 0); }
|
||||
private static FlowBounds b(int minX, int minY, int maxX, int maxY) { return new FlowBounds(minX, minY, 0, maxX, maxY, 0); }
|
||||
private static long key(int x, int z) { return ((long) x << 32) | (z & 0xffffffffL); }
|
||||
private static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); }
|
||||
}
|
||||
+8
-3
@@ -51,13 +51,18 @@ 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.9.mrpack
|
||||
packwiz modrinth export --output ../Sanctuary-0.1.0-alpha.11.mrpack
|
||||
```
|
||||
|
||||
L’exemple vise l’alpha.11 et nécessite ses artefacts vérifiés.
|
||||
Cette version propose Petit/Grand, avec Grand par défaut, et l’optimisation ;
|
||||
les anciennes tailles restent attachées aux sauvegardes déjà créées.
|
||||
L'export contient le JAR Sanctuary local et la référence vérifiée de Fabric API.
|
||||
Il ne publie rien sur Modrinth.
|
||||
L’alpha.9 est publiée et vérifiée. L’instance Sanctuary Beta existante suit
|
||||
le canal packwiz ; cet export reste destiné aux imports ponctuels.
|
||||
L’alpha.11 est publiée sur le canal stable. L’instance Sanctuary Beta existante
|
||||
a été synchronisée deux fois et ses fichiers personnels suivis sont conservés.
|
||||
Les instances déjà raccordées reçoivent cette mise à jour sans nouvelle importation.
|
||||
Cet export reste destiné aux imports ponctuels.
|
||||
|
||||
Utiliser l'URL HTTP affichée de `pack.toml` dans packwiz-installer. Le serveur
|
||||
local doit rester actif pendant l'installation. Ce n'est pas une publication
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "0.1.0-alpha.9"
|
||||
version = "0.1.0-alpha.11"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
Reference in New Issue
Block a user