diff --git a/CHANGELOG.md b/CHANGELOG.md index 99a875f..4b01dd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## 0.1.0-alpha.7 — 2026-09-08 + +- Forêts de chênes plus denses, y compris au cœur et sur les hauteurs de l’île, + avec bouleaux, clairières fleuries et bois plus secs. +- Vrais chênes noirs, champignons, podzol et mycélium sur les corniches intérieures + humides ; mousse plus ponctuelle et rares poches de jungle/bambou. +- Un emplacement facultatif pour un arbre remarquable (cerisier favorisé, + épicéa, jungle, acacia, palétuvier ou chêne pâle), sans terrain ajouté. +- Nappes de pierre, andésite, diorite et granite sur les affleurements ; dépôts + épais de sable, gravier et argile associés aux bassins. Rares matières de + soufre dans les profondeurs, sans grands bassins souterrains. +- Petits filons de charbon, fer et cuivre autorisés à affleurer, avec des essais + sur les roches de surface et les corniches pour les rendre repérables. +- Nouvelle clé `sanctuary:sanctuary_woodland`, avec formes, hauteur, ciel et + plans d’eau conservés. Créer un **nouveau monde Sanctuary** ; les sauvegardes + alpha.6 et antérieures conservent leur génération. + +Validation : dix tests serveur réussis sur chacune des graines `0`, `42` et +`8675309`, arbres et minerais réellement observés, 1 800 ticks de fluides, +et `check build assemblePack` réussi. + ## 0.1.0-alpha.6 — 2026-09-08 - Palette tempérée plus douce, avec des couleurs communes pour l’herbe, le diff --git a/README.md b/README.md index 4f00052..242eacc 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,15 @@ décrits dans la vision ne sont pas encore implémentés. - Mod `sanctuary` indépendant et pack Sanctuary construit avec packwiz. - Preset **Sanctuary** sélectionnable à la création d'un monde, avec une île flottante finie, un terrain naturel et un extérieur vide. -- Île tempérée aux teintes douces : prairies fleuries, bosquets, mousse et - affleurements rocheux, avec des biomes qui varient aussi selon l’altitude. Les +- Île tempérée aux teintes douces : forêts de chênes et de bouleaux, clairières + fleuries et affleurements de pierre, andésite, diorite et granite. Les corniches + intérieures accueillent chênes noirs et champignons ; les essences rares + restent ponctuelles. Les formes de terrain reprennent le générateur 26.2, adapté à l'API 26.3. - Petits filons de charbon, fer, cuivre, or, redstone, lapis et diamant à des - altitudes adaptées au terrain relevé. Les stocks se constatent après génération. + altitudes adaptées au terrain relevé. Charbon, fer et cuivre peuvent aussi + affleurer sur les roches de surface et les corniches. Les stocks se constatent + après génération. - Spawn recherché sur le sol de l'île lors de la création. Les mondes ordinaires et les choix ultérieurs de spawn ne sont pas remplacés. - Nouveaux mondes sur 384 blocs (`Y=0..383`), île relevée de 64 blocs, @@ -48,9 +52,9 @@ le terrain : le spawn cherche un emplacement naturel. Le catalogue TerraMix, les continents déverrouillables, les océans, les ruines et les autres fonctionnalités restent à construire par tickets. -L’alpha.6 utilise de nouveaux paramètres de génération ; créer un **nouveau monde** -pour découvrir ses biomes par altitude et ses eaux. Les sauvegardes alpha.2 -à alpha.5 conservent leur génération. Dappled Forest est désormais réservé aux +L’alpha.7 utilise de nouveaux paramètres de génération ; créer un **nouveau monde** +pour découvrir ses forêts, ses corniches humides et ses affleurements. Les sauvegardes alpha.2 +à alpha.6 conservent leur génération. Dappled Forest est désormais réservé aux futurs continents et reste présent dans les anciennes sauvegardes alpha.5. La validation de cet incrément est suivie dans [Validation](docs/testing.md). Le premier champ de recherche climatique est défini : nord froid, sud chaud, @@ -75,7 +79,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.6 | +| Sanctuary / pack | 0.1.0-alpha.7 | 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 @@ -102,7 +106,7 @@ décrits dans [Validation](docs/testing.md). Résultats : -- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.6.jar` : mod à installer avec +- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.7.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). diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 34b0535..f80fcde 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -11,7 +11,9 @@ Les biomes de départ reprennent les configurations de forêt, plaine, forêt de bouleaux, forêt fleurie et Dappled Forest de Minecraft 26.3-pre-2, adaptées à une île finie. Les variantes alpha.6 réutilisent les végétations vanilla pour les prairies fleuries et les bosquets ; Dappled Forest reste disponible pour -les sauvegardes alpha.5. Les règles des petits minerais sont adaptées à partir des +les sauvegardes alpha.5. L’alpha.7 réemploie les arbres vanilla (dont chêne noir, +cerisier et essences rares), les champignons, le bambou et les blocs de soufre +de cette version, avec des conditions propres à l’île. Les règles des petits minerais sont adaptées à partir des features vanilla de cette même version. Les références de blocs, végétation et minerais sont résolues par Minecraft ; leurs textures et modèles ne sont pas inclus. diff --git a/docs/backlog.md b/docs/backlog.md index 45269e1..c26b035 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -173,6 +173,33 @@ dont Dappled Forest, aux futurs continents. L’île combine prairies fleuries, bosquets et zones rocheuses, avec des teintes cohérentes et une végétation effective sur les corniches inférieures. Cela ne livre pas encore les continents. +### WG-08 — Forêts de survie et minerais visibles — alpha.7 + +**Branche :** `codex/woodland-canopy`. + +**But :** conserver les formes rocheuses appréciées en alpha.6 tout en donnant +à l’île de vraies forêts de surface et des ressources de départ repérables. + +**Critères d’acceptation :** + +- Chênes majoritaires parmi les arbres de surface observés, bouleaux secondaires, + avec des arbres au cœur de l’île et des clairières fleuries. +- Chênes noirs réels et champignons sur des corniches intérieures naturelles ; + jungle/bambou et matières de soufre rares et localisés. +- Un éventuel arbre remarquable, choisi parmi les essences rares, sans ajout + de terrain ni obligation de fournir chaque essence par graine. +- Affleurements de pierre, andésite, diorite et granite ; sable et gravier + associés aux eaux, avec des couches soutenues. +- Charbon, fer et cuivre visibles au contact de l’air sur la roche, avec des + petits filons sans quota corrigé après génération. +- Démonstration dans le moteur, maintien des eaux après 1 800 ticks et + conservation des anciennes générations. Publication packwiz et mise à jour + de l’instance Prism existante en conservant ses données personnelles. + +**Vérification alpha.7 :** les trois graines de référence passent les tests +du moteur, dont les forêts, les minerais exposés et les eaux après 1 800 ticks. +Les observations et leurs limites sont consignées dans [Validation](testing.md). + ## 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. diff --git a/docs/expansion.md b/docs/expansion.md index 4b2d160..443ed08 100644 --- a/docs/expansion.md +++ b/docs/expansion.md @@ -37,6 +37,14 @@ La répartition tient compte de l’altitude, y compris dans les strates inféri La variation locale et l’humidité dépendent de la graine ; elles ne constituent pas encore le champ de recherche directionnel des continents. +L’alpha.7 densifie les forêts de chênes et de bouleaux en surface et donne +aux corniches intérieures humides des chênes noirs et des champignons. +Des champignons vanilla peuvent aussi se rencontrer en surface. Les +clairières fleuries et les affleurements restent présents. Les essences rares +et un éventuel arbre remarquable dépendent du terrain ; chaque graine n’est +pas tenue de fournir tous les bois. Les minerais de départ peuvent affleurer, +avec des petits filons et sans correction des stocks après comptage. + ## Eau de surface et exploration L'île de départ doit pouvoir accueillir naturellement des étangs et lacs de diff --git a/docs/packwiz.md b/docs/packwiz.md index 96ccec5..780cb3e 100644 --- a/docs/packwiz.md +++ b/docs/packwiz.md @@ -19,7 +19,7 @@ Après les vérifications, le commit et le push de la branche du ticket, le scri de publication réalise les étapes de release et de canal ci-dessous : ```sh -python3 scripts/publish_pack.py --notes-file chemin/vers/notes.md --asset build/Sanctuary-0.1.0-alpha.6.mrpack +python3 scripts/publish_pack.py --notes-file chemin/vers/notes.md --asset build/Sanctuary-0.1.0-alpha.7.mrpack ``` Les options sont facultatives. Ce script **publie** sur le Git configuré dans @@ -38,7 +38,7 @@ La procédure complète, également utilisable manuellement : 3. Préparer les métadonnées de la livraison, avec l'URL exacte du futur JAR : ```sh - python3 scripts/pack.py release https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.6/sanctuary-0.1.0-alpha.6.jar + python3 scripts/pack.py release https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.7/sanctuary-0.1.0-alpha.7.jar ``` 4. Pousser le commit source vérifié. Créer une release Gitea correspondant à ce @@ -127,6 +127,10 @@ L’alpha.6 utilise à son tour `sanctuary:sanctuary_layered` pour ses biomes pa altitude et sa nouvelle hydrologie. Créer un nouveau monde Sanctuary ; les sauvegardes alpha.5 conservent notamment leur palette avec Dappled Forest. +L’alpha.7 sélectionne `sanctuary:sanctuary_woodland` pour ses forêts, ses +roches variées et ses minerais affleurants. Les mondes alpha.6 gardent +`sanctuary:sanctuary_layered` ; aucune régénération de chunks n’est effectuée. + 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). diff --git a/docs/testing.md b/docs/testing.md index fbfa6e7..c734a92 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -81,17 +81,17 @@ Les neuf tests Sanctuary vérifient : tandis que les anciens paramètres restent à 256 blocs. Les nuages sont au-dessus de l'île, à Y=352,33. 6. Aucun plancher solide n’apparaît sous l’île, à Y=0..15. Dans les générations - alpha.5 et alpha.6, seules les eaux des cascades déclarées peuvent traverser cette marge. + alpha.5 à alpha.7, seules les eaux des cascades déclarées peuvent traverser cette marge. 7. Le relevé après génération compte exactement tous les états des blocs des chunks demandés, avec des totaux cohérents et une complétude explicite. Voir [Expansion et ressources](expansion.md) pour le mode complet facultatif. 8. Le plan hydrologique se retrouve dans les vrais chunks après décoration : profils épais de sédiments, bassins retenus, raccords entre chunks, canne à - sucre valide, niches rocheuses et descente réelle des cascades. Les cinq - biomes et les minerais sont inspectés sur le terrain. Une réserve de lave + sucre valide, niches rocheuses et descente réelle des cascades. La palette + active et les minerais sont inspectés sur le terrain. Une réserve de lave présente conserve ses 18 sources, ses supports et son accès après simulation. 9. Les anciennes clés gardent leur traitement d’origine. Seuls les nouveaux - paramètres activent les biomes par altitude et les eaux de l’alpha.6. + paramètres activent les forêts et les eaux de l’alpha.7. La recherche de spawn parcourt l'île finie, par anneaux de quatre blocs jusqu'à 288 blocs du centre. Elle examine d'abord la hauteur brute pour éviter de @@ -188,6 +188,66 @@ Pour cet essai visuel en monde de développement créatif, comparer la même vue Y=63 ni assombrissement d'altitude au passage de Y=32. Les brouillards de l'eau, de la météo et des effets d'aveuglement ou d'obscurité doivent rester présents. +## Contrôles et validation alpha.7 — 8 septembre 2026 + +`WoodlandDiagnostics` reprend les preuves hydrologiques et ajoute les forêts, +les champignons sous plafond, les roches exposées et les minerais visibles. +Les arbres sont comptés par pieds de troncs sur le sol naturel, avec au moins +trois bûches verticales et des feuilles réellement observées. Le chêne noir +requiert un pied principal de 2×2 et trois couches de support par pied ; ses +branches retombantes ne comptent pas comme de nouveaux arbres. Podzol et +mycélium sont des sols explicites : le tag `minecraft:dirt` de cette version +ne les contient pas. + +Les essais utilisent des mondes neufs, le vrai preset de production et +Minecraft 26.3-pre-2 / Java 25 / Fabric Loader 0.19.5. Les dix tests requis +passent sur chacune des trois graines. `check build assemblePack` passe en +3 min 41 s sur la graine 0, avec les huit tests purs de forme, hydrologie, +lave, écologie et nouveaux bois. Les exports de densité et le relevé général +facultatifs sont désactivés ; les contrôles de densité restent actifs. + +| Graine | Chunks FULL inspectés | Chênes de surface | Bouleaux de surface | Chênes à rayon ≤128 | Chênes noirs inférieurs | Minerais exposés dehors : charbon / fer / cuivre | +| --- | ---: | ---: | ---: | ---: | ---: | --- | +| `0` | 167 | 292 | 263 | 218 | 30 | 4 / 19 / 5 | +| `42` | 154 | 313 | 125 | 250 | 13 | 8 / 3 / 13 | +| `8675309` | 167 | 336 | 99 | 272 | 36 | 7 / 6 / 9 | + +Ces nombres décrivent un échantillon borné : les eaux et leur voisinage, des +cibles écologiques et l’emplacement de l’arbre remarquable. Ce n’est pas un +inventaire complet de l’île ni un pourcentage représentatif de toutes ses +surfaces. Les sols sont mesurés sous les canopées. Les minerais recensés dans +la dernière colonne ont une face d’air réelle, ouverte vers le ciel ou vers +une paroi dégagée sur douze blocs ; les faces dans de petites cavités sont +comptées séparément. Les trois roches présentent des nappes contiguës exposées. + +La graine 42 démontre un cerisier remarquable en `(-88, 251, -88)`, avec son +tronc, ses feuilles et 21 bûches dans le volume prévu. Le site sélectionné sur +la graine 0 ne produit pas d’arbre rare : l’espace et les conditions après +décoration restent déterminants. Aucun terrain n’est ajouté pour le forcer. +Les autres essences rares et le soufre ne sont pas garantis sur chaque graine. + +Les eaux sont vérifiées après décoration puis pendant 1 800 ticks réels dans +des chunks actifs, avec témoins indépendants du fonctionnement des fluides. +Les bassins et les dix-huit sources de chaque niche de lave sont conservés ; +les cascades restent reliées à leurs sources et dans l’emprise observée. +Les pieds des arbres et les champignons observés gardent leur support. Les +sédiments sont comparés à leur matériau prévu sur toutes leurs couches ; une +roche peut être remplacée seulement par les roches, minerais ou dépôts de +terre effectivement configurés. Les berges de sable et gravier sont comptées +sur les vrais blocs, y compris par profondeur. + +Les anciens générateurs restent isolés : les 77 fichiers historiques suivis +gardent leur SHA-256. La réentrée au post-traitement après consommation des +marqueurs respecte une modification ultérieure de bouche de source sur les +trois graines. Aucun monde de joueur n’est ouvert par ces tests. + +Preuves locales ignorées par Git : `build/alpha7-build.log`, +`build/alpha7-seed42.log`, `build/alpha7-seed8675309.log`, et les fichiers +`woodland-seed--.json`, `-canopy.png`, `-lower-terraces.png` et +profils de sources dans `mods/sanctuary/build/run/gameTest/diagnostics/`. +Les cartes montrent des lectures de blocs ; l’appréciation du paysage dans +le client reste à faire lors du prochain essai. + ## Contrôles alpha.6 `layeredHydrologySmoke` vérifie les bassins sans ruisseau de secours, les diff --git a/docs/vision.md b/docs/vision.md index 47c780e..bc52e9b 100644 --- a/docs/vision.md +++ b/docs/vision.md @@ -66,16 +66,20 @@ remplis d'eau ne sont pas retenus pour cette île. Le terrain doit laisser voir dessous sculpté et dégagé, dans un monde de **384 blocs de hauteur**, avec des nuages relevés et sans disque noir artificiel apparaissant sous un seuil d'altitude. -L’ambiance de départ doit rester douce et tempérée, avec des bosquets, -des prairies fleuries et de larges zones rocheuses. Les biomes très contrastés -de Minecraft 26.3, dont Dappled Forest, sont réservés aux futurs continents. -Les biomes et la végétation varient aussi en hauteur : les corniches inférieures -peuvent porter des bosquets, de la mousse et des fleurs lorsque leur surface -s’y prête. Quelques sources de paroi et ruissellements relient ces étages. -Les plages s’inspirent des nappes de -sable des générations Alpha/Beta : elles peuvent relier des poches d’eau, tandis -que d’autres rives restent rocheuses ou enherbées. Les quelques cascades servent -également d’ascenseurs d’eau pour circuler verticalement. +L’ambiance de départ doit rester douce et tempérée, avec de véritables forêts +et des clairières fleuries, quelques zones plus sèches et de larges masses +rocheuses mêlant pierre, andésite, diorite et granite. Les nappes de sable et +de gravier accompagnent les eaux. Charbon, fer et cuivre doivent aussi pouvoir +affleurer sur la roche exposée pour commencer la survie. + +Les bois dominants sont le chêne, le chêne noir et le bouleau : chênes et +bouleaux en surface, chênes noirs et champignons dans l’intérieur plus humide, +avec moins de mousse uniforme. Épicéa, jungle, acacia, palétuvier et chêne pâle +restent rares mais possibles ; du bambou et de rares poches de soufre peuvent +se rencontrer dans les profondeurs. Un arbre remarquable isolé peut être un +cerisier rose ou une autre de ces essences rares, lorsque le terrain convient. +Les biomes très contrastés de Minecraft 26.3, dont Dappled Forest, restent +réservés aux futurs continents. L’île doit offrir les ressources nécessaires aux premières étapes de Minecraft, en quantités modestes pour pousser à explorer les expansions lors des grands diff --git a/docs/worldgen.md b/docs/worldgen.md index e7473aa..9498e8d 100644 --- a/docs/worldgen.md +++ b/docs/worldgen.md @@ -47,8 +47,9 @@ de test et les sauvegardes personnelles doivent rester distincts. Il n'y a ni plancher de bedrock, ni mer globale, ni reprise du terrain à distance. Le Nether et l'End restent ceux de Minecraft. -- Les nouveaux mondes utilisent une palette tempérée de prairies, de bosquets, - de corniches moussues et de zones rocheuses, répartie aussi en hauteur. +- Les nouveaux mondes utilisent une palette tempérée de forêts de chênes et de + bouleaux, de clairières fleuries et de zones rocheuses. Les strates intérieures + portent des chênes noirs et des champignons sur leurs corniches naturelles. Dappled Forest est réservé aux futurs continents. Leur végétation et leurs animaux proviennent de Minecraft 26.3-pre-2. Les nouveaux identifiants ne reprennent pas les tags de structures vanilla, afin de ne pas placer leurs structures dans le vide. @@ -57,7 +58,7 @@ de test et les sauvegardes personnelles doivent rester distincts. rocheuses. Les arbres et les petits filons ont besoin du terrain existant. Aucun mod de biomes externe n’est nécessaire. -L’alpha.6 associe ces biomes aux plages, aux eaux et aux petits filons décrits +L’alpha.7 associe ces biomes aux plages, aux eaux et aux petits filons décrits ci-dessous. Les ressources de survie ne se limitent pas au bois et à la pierre, mais cet incrément ne garantit pas encore une partie Minecraft complète sur chaque seed. L’accès à chaque progression vanilla et les ressources renouvelables @@ -450,3 +451,66 @@ chunks sauvegardés. La version de génération et les emprises doivent être gelées avant les premières sauvegardes destinées à durer. Ce prototype ne promet pas encore de migration automatique des mondes entre deux versions de terrain. + + +## Forêts, corniches humides et affleurements — génération v7 + +L’alpha.7 crée les nouveaux mondes avec `sanctuary:sanctuary_woodland` et +`sanctuary:woodland_island`. La densité `sanctuary:final_density_384`, les +formes rocheuses, la hauteur et le ciel restent identiques. Les plans des +bassins, sources et petites niches de lave reprennent les algorithmes alpha.6. +Les anciens paramètres, biomes et features sont conservés ; cette livraison +ne modifie pas les mondes existants. + +La palette comporte huit biomes : + +| Identifiant | Rôle | +| --- | --- | +| `sanctuary:woodland_oak_forest` | Forêt de chênes dominante, bouleaux secondaires | +| `sanctuary:woodland_birch_forest` | Forêt de bouleaux complémentaire | +| `sanctuary:woodland_clearing` | Clairière fleurie | +| `sanctuary:woodland_dry_woodland` | Bois sec plus clairsemé | +| `sanctuary:woodland_rocky_heath` | Masses rocheuses exposées | +| `sanctuary:woodland_dark_grove` | Corniches humides à chênes noirs et champignons | +| `sanctuary:woodland_bamboo_grove` | Rare intérieur à jungle et bambou | +| `sanctuary:woodland_sulfur_depths` | Rare poche profonde de soufre sur roche existante | + +Une altitude élevée ne transforme plus automatiquement une forêt en prairie +ou en roche. Les forêts utilisent les véritables arbres vanilla avec leurs +conditions de croissance ; les affleurements sont des patches horizontaux +et des variations de matériau sur le terrain existant. + +`woodland_groves` place de vrais chênes noirs sur des sols naturels de 2×2, +avec trois blocs de support et une place suffisante pour le tronc et la +canopée sous le plafond. Des champignons accompagnent les sols de podzol et +de mycélium, avec quelques touches de mousse. La jungle et le bambou restent +limités aux rares poches qui leur conviennent. + +Un seul emplacement remarquable est recherché par graine, avec une essence +choisie parmi cerisier (favorisé), épicéa, jungle, acacia, palétuvier et chêne +pâle. Il ne produit pas de terrain : si le site ou les conditions de l’arbre +vanilla ne conviennent pas, il ne pousse pas. Ce n’est pas une garantie de +chaque essence sur chaque île. Aucun cœur de Creaking n’est ajouté par l’arbre +pâle utilisé ici. + +Les affleurements mêlent des nappes cohérentes de pierre, andésite, diorite et +granite. Les berges conservent leur excavation limitée et leurs dépôts de +3 à 5 blocs, avec des patches de sable, gravier, argile et roche. Les plantations de `woodland_groves` évitent les sources, leurs exutoires +et les niches de lave. Les décorations vanilla conservent leurs placements ; +le traitement unique après génération peut rouvrir la seule bouche de source +déclarée si une plante l’obstrue. Le soufre +reste une matière rare des profondeurs ; les grands bassins et sources +vanilla du biome Sulfur Caves ne sont pas activés. + +Les anciens petits filons souterrains de charbon, fer et cuivre étaient peu +susceptibles d’atteindre les surfaces hautes ; leur réglage écartait aussi une +partie des blocs voisins de l’air. Les nouvelles features `woodland_ore_*` +utilisent des tailles vanilla limitées (`size=6` pour le charbon, `size=5` +pour fer/cuivre), sans garantir ce nombre de blocs produits par filon, +autorisent cette exposition et ajoutent des essais aléatoires sur les roches +de surface et les corniches rencontrées depuis l’air. Elles remplacent +seulement les roches naturelles reconnues par Minecraft, dont andésite, +diorite et granite. Les autres minerais et les anciens générateurs gardent +leurs réglages ; aucune quantité cible n’est réinjectée après mesure. + +La version et les graines de validation sont consignées dans [Validation](testing.md). diff --git a/gradle.properties b/gradle.properties index b2f246d..3d58e31 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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.6 -pack_version=0.1.0-alpha.6 +mod_version=0.1.0-alpha.7 +pack_version=0.1.0-alpha.7 maven_group=fr.koka.sanctuary diff --git a/mods/sanctuary/build.gradle b/mods/sanctuary/build.gradle index 78cc373..8f8a18e 100644 --- a/mods/sanctuary/build.gradle +++ b/mods/sanctuary/build.gradle @@ -130,3 +130,20 @@ tasks.register('layeredEcologySmoke', JavaExec) { } tasks.named('check') { dependsOn('layeredEcologySmoke') } + +// Alpha.7 keeps previous biome palettes and generators unchanged. +tasks.register('woodlandPaletteSmoke', JavaExec) { + group = 'verification' + description = 'Check surface forest coverage and bounded rare interior biomes.' + dependsOn('testClasses') + classpath = sourceSets.test.runtimeClasspath + mainClass = 'fr.koka.sanctuary.worldgen.WoodlandPaletteSmoke' +} +tasks.register('woodlandGroveSmoke', JavaExec) { + group = 'verification' + description = 'Check natural tree supports and canopy space without creating terrain.' + dependsOn('testClasses') + classpath = sourceSets.test.runtimeClasspath + mainClass = 'fr.koka.sanctuary.worldgen.WoodlandGroveSmoke' +} +tasks.named('check') { dependsOn('woodlandPaletteSmoke', 'woodlandGroveSmoke') } diff --git a/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/SanctuaryWorldGameTests.java b/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/SanctuaryWorldGameTests.java index 9bc9a00..d9356dc 100644 --- a/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/SanctuaryWorldGameTests.java +++ b/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/SanctuaryWorldGameTests.java @@ -3,6 +3,7 @@ package fr.koka.sanctuary.gametest; import fr.koka.sanctuary.worldgen.SanctuarySpawn; import fr.koka.sanctuary.worldgen.NaturalHydrologyRuntime; import fr.koka.sanctuary.worldgen.LayeredHydrologyRuntime; +import fr.koka.sanctuary.worldgen.WoodlandHydrologyRuntime; import net.minecraft.tags.FluidTags; import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator; import fr.koka.sanctuary.worldgen.IslandShape; @@ -30,7 +31,8 @@ public final class SanctuaryWorldGameTests { @GameTest(maxTicks = 3000) public void generatedSurfaceWaterRemainsContained(GameTestHelper helper) throws java.io.IOException { var generator = (NoiseBasedChunkGenerator) helper.getLevel().getChunkSource().getGenerator(); - if (LayeredHydrologyRuntime.enabled(generator)) LayeredHydrologyDiagnostics.start(helper); + if (WoodlandHydrologyRuntime.enabled(generator)) WoodlandDiagnostics.start(helper); + else if (LayeredHydrologyRuntime.enabled(generator)) LayeredHydrologyDiagnostics.start(helper); else if (NaturalHydrologyRuntime.enabled(generator)) NaturalHydrologyDiagnostics.start(helper); else HydrologyDiagnostics.start(helper); } @@ -38,7 +40,8 @@ public final class SanctuaryWorldGameTests { @GameTest(maxTicks = 200) public void hydrologyDoesNotActivateForSavedLegacySettings(GameTestHelper helper) { var generator = (NoiseBasedChunkGenerator) helper.getLevel().getChunkSource().getGenerator(); - if (LayeredHydrologyRuntime.enabled(generator)) LayeredHydrologyDiagnostics.verifyLegacyIsolation(helper); + if (WoodlandHydrologyRuntime.enabled(generator)) WoodlandDiagnostics.verifyLegacyIsolation(helper); + else if (LayeredHydrologyRuntime.enabled(generator)) LayeredHydrologyDiagnostics.verifyLegacyIsolation(helper); else if (NaturalHydrologyRuntime.enabled(generator)) NaturalHydrologyDiagnostics.verifyLegacyIsolation(helper); else HydrologyDiagnostics.verifyLegacyIsolation(helper); helper.succeed(); @@ -117,7 +120,8 @@ public final class SanctuaryWorldGameTests { helper.assertTrue(chunk != null, "The lower-margin test needs a fully generated chunk"); if (chunk.getSection(0).hasOnlyAir()) continue; var generator = (NoiseBasedChunkGenerator) helper.getLevel().getChunkSource().getGenerator(); - helper.assertTrue(NaturalHydrologyRuntime.enabled(generator) || LayeredHydrologyRuntime.enabled(generator), + helper.assertTrue(NaturalHydrologyRuntime.enabled(generator) || LayeredHydrologyRuntime.enabled(generator) + || WoodlandHydrologyRuntime.enabled(generator), "The legacy island must leave its lower margin entirely empty"); for (int x = chunk.getPos().getMinBlockX(); x <= chunk.getPos().getMaxBlockX(); x++) { for (int z = chunk.getPos().getMinBlockZ(); z <= chunk.getPos().getMaxBlockZ(); z++) { @@ -125,7 +129,9 @@ public final class SanctuaryWorldGameTests { BlockPos p = new BlockPos(x, y, z); var state = chunk.getBlockState(p); helper.assertTrue(state.isAir() || state.getFluidState().is(FluidTags.WATER) - && (LayeredHydrologyRuntime.enabled(generator) + && (WoodlandHydrologyRuntime.enabled(generator) + ? WoodlandDiagnostics.isWithinSpringInspectionFootprint(WoodlandHydrologyRuntime.woodlandPlan(helper.getLevel()), x, z) + : LayeredHydrologyRuntime.enabled(generator) ? LayeredHydrologyDiagnostics.isWithinSpringInspectionFootprint(LayeredHydrologyRuntime.layeredPlan(helper.getLevel()), x, z) : NaturalHydrologyDiagnostics.isWithinSpringInspectionFootprint(NaturalHydrologyRuntime.naturalPlan(helper.getLevel()), x, z)), "The lower margin has no artificial floor; water must lie in the finite spring inspection footprint, with provenance checked by the full fluid diagnostic: " + p); diff --git a/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/WoodlandDiagnostics.java b/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/WoodlandDiagnostics.java new file mode 100644 index 0000000..cc87779 --- /dev/null +++ b/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/WoodlandDiagnostics.java @@ -0,0 +1,1773 @@ +package fr.koka.sanctuary.gametest; + +import com.google.gson.GsonBuilder; +import fr.koka.sanctuary.SanctuaryMod; +import fr.koka.sanctuary.worldgen.HydrologyRuntime; +import fr.koka.sanctuary.worldgen.SanctuarySpawn; +import fr.koka.sanctuary.worldgen.LayeredHydrology; +import fr.koka.sanctuary.worldgen.LayeredHydrologyRuntime; +import fr.koka.sanctuary.worldgen.NaturalHydrologyRuntime; +import fr.koka.sanctuary.worldgen.LayeredLavaDeposit; +import fr.koka.sanctuary.worldgen.WoodlandIslandBiomeSource; +import fr.koka.sanctuary.worldgen.WoodlandHydrologyRuntime; +import fr.koka.sanctuary.worldgen.WoodlandMaterials; +import fr.koka.sanctuary.worldgen.WoodlandGrovesFeature; +import fr.koka.sanctuary.worldgen.WoodlandGroveGeometry; +import net.minecraft.core.registries.BuiltInRegistries; +import java.util.ArrayList; +import java.util.ArrayDeque; +import java.util.TreeSet; +import net.minecraft.core.BlockPos; +import net.fabricmc.loader.api.FabricLoader; +import java.time.Instant; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.Registries; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.BlockTags; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.MultifaceBlock; +import net.minecraft.world.level.block.state.BlockState; +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.Heightmap; +import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.densityfunction.DensityFunction; +import net.minecraft.world.level.material.Fluids; + +import javax.imageio.ImageIO; +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** Checks the actual decorated terrain and fluid simulation in a disposable world, far from test fixtures. */ +public final class WoodlandDiagnostics { + private static final List HORIZONTAL = List.of(Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST); + + private WoodlandDiagnostics() {} + + public static void start(GameTestHelper helper) throws IOException { + Context context = new Context(helper); + try { + context.prepare(); + context.verifyNaturalShellAndFinishedWater(); + context.verifyAllObservedWaterIsPlanned(); + context.verifyBiomesAndResources(); + try { + context.verifyWoodlandReferenceExamples(); + } catch (RuntimeException | Error failure) { + context.export("generated", false); + throw failure; + } + context.export("generated", true); + helper.assertTrue(context.caneBlocks > 0, + "Regression seed must provide at least one naturally valid shore sugar cane plant; inspect the exported site map"); + } catch (IOException | RuntimeException | Error failure) { + context.cleanup(); + throw failure; + } + // A loaded FULL chunk and a simulation ticket are not enough in 26.3: the + // fluid scheduler also waits for its ticking future AND entity storage load. + // Start the measured interval only once that exact production predicate passes. + helper.onEachTick(() -> { + try { + context.startWhenFluidChunksReady(); + } catch (RuntimeException | Error failure) { + context.cleanup(); + throw failure; + } + }); + } + + public static void verifyLegacyIsolation(GameTestHelper helper) { + ServerLevel level = helper.getLevel(); + var active = (NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(); + helper.assertTrue(WoodlandHydrologyRuntime.enabled(active), "The new preset must enable alpha.7 hydrology"); + helper.assertFalse(HydrologyRuntime.enabled(active) || NaturalHydrologyRuntime.enabled(active) + || LayeredHydrologyRuntime.enabled(active), "Older hydrology applications must not run with alpha.7"); + helper.assertTrue(active.generatorSettings().is(SanctuarySpawn.WOODLAND_SETTINGS), + "Alpha.7 must use its own settings key, preserving every previous world"); + var registry = 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)) { + var legacy = new NoiseBasedChunkGenerator(active.getBiomeSource(), registry.getOrThrow(key)); + helper.assertFalse(WoodlandHydrologyRuntime.enabled(legacy), "Saved settings must not acquire alpha.7: " + key.identifier()); + helper.assertTrue(HydrologyRuntime.enabled(legacy) == key.equals(SanctuarySpawn.HYDROLOGY_SETTINGS), + "Only saved alpha.4 settings keep alpha.4 hydrology"); + helper.assertTrue(NaturalHydrologyRuntime.enabled(legacy) == key.equals(SanctuarySpawn.NATURAL_SETTINGS), + "Only saved alpha.5 settings keep alpha.5 hydrology"); + helper.assertTrue(LayeredHydrologyRuntime.enabled(legacy) == key.equals(SanctuarySpawn.LAYERED_SETTINGS), + "Only saved alpha.6 settings keep alpha.6 hydrology"); + helper.assertFalse(new WoodlandGrovesFeature().place(level, legacy, RandomSource.create(0), BlockPos.ZERO), + "Woodland decoration must refuse every saved generation key"); + } + helper.assertTrue(active.getBiomeSource() instanceof WoodlandIslandBiomeSource, + "The active preset must use the versioned woodland biome source"); + Set expected = Set.of("sanctuary:woodland_oak_forest", "sanctuary:woodland_birch_forest", + "sanctuary:woodland_clearing", "sanctuary:woodland_dry_woodland", "sanctuary:woodland_rocky_heath", + "sanctuary:woodland_dark_grove", "sanctuary:woodland_bamboo_grove", "sanctuary:woodland_sulfur_depths"); + helper.assertTrue(active.getBiomeSource().possibleBiomes().stream().map(holder -> holder.getRegisteredName()) + .collect(java.util.stream.Collectors.toSet()).equals(expected), + "The woodland palette must have eight variants, with Dappled reserved for future continents"); + } + + /** Coarse finite coverage used by the independent lower-margin smoke check. Actual water + * provenance and absence of an escape through this coverage are checked by the full diagnostic. */ + public static boolean isWithinSpringInspectionFootprint(LayeredHydrology.Plan plan, int x, int z) { + int cx = x >> 4, cz = z >> 4; + return plan.springs().stream().map(LayeredHydrology.Spring::flowBounds).anyMatch(bounds -> { + return cx >= (bounds.minX() >> 4) - 1 && cx <= (bounds.maxX() >> 4) + 1 + && cz >= (bounds.minZ() >> 4) - 1 && cz <= (bounds.maxZ() >> 4) + 1; + }); + } + + private record FlowRoot(long id, String kind, long featureId, BlockPos position) {} + + private record FlowComponent(int id, List sourceIds, int actualWaterBlocks, + int newWaterBlocksSinceInitialSnapshot, LayeredHydrology.FlowBounds actualBounds, + int outsideDensityPredictionVoxelCount, LayeredHydrology.FlowBounds outsideDensityVoxelActualBounds, + int outsideDensityPredictionBoundingBoxCount, LayeredHydrology.FlowBounds outsideDensityBoundingBoxActualBounds, + int horizontalNeighboursOutsideInspection, boolean reachesDimensionFloor) {} + + private record TreeWitness(String log, BlockPos base, List footprint, int verticalTrunkHeight, + BlockPos leafWitness, boolean lowerTerrace, int naturalRoofY, String biome) {} + private record MushroomWitness(String block, BlockPos base, int stalkHeight, int naturalRoofY) {} + private record RockPatch(String block, int exposedBlocks, BlockPos example, LayeredHydrology.FlowBounds bounds) {} + private static final Set REMARKABLE_LOGS = Set.of("minecraft:cherry_log", "minecraft:spruce_log", + "minecraft:jungle_log", "minecraft:acacia_log", "minecraft:mangrove_log", "minecraft:pale_oak_log"); + + private static boolean isTerracePlant(BlockState state) { + return state.is(BlockTags.FLOWERS) || state.is(Blocks.SHORT_GRASS) || state.is(Blocks.MOSS_CARPET) + || state.is(Blocks.AZALEA) || state.is(Blocks.FLOWERING_AZALEA) || state.is(Blocks.FERN) || state.is(Blocks.BUSH) + || state.is(Blocks.BROWN_MUSHROOM) || state.is(Blocks.RED_MUSHROOM); + } + + private static boolean isTerrain(BlockState state) { + return state.is(Blocks.STONE) || state.is(BlockTags.DIRT) || state.is(Blocks.DIRT) || state.is(Blocks.GRASS_BLOCK) || state.is(Blocks.MOSS_BLOCK) + || state.is(Blocks.GRAVEL) || state.is(Blocks.SAND) || state.is(Blocks.CLAY) + || state.is(Blocks.PODZOL) || state.is(Blocks.MYCELIUM) + || state.is(BlockTags.BASE_STONE_OVERWORLD) || isWoodlandOre(state) + || state.is(Blocks.SULFUR) || state.is(Blocks.CINNABAR); + } + + /** Only the seven ore families actually configured in the woodland biomes. */ + private static boolean isWoodlandOre(BlockState state) { + return state.is(Blocks.COAL_ORE) || state.is(Blocks.DEEPSLATE_COAL_ORE) + || state.is(Blocks.IRON_ORE) || state.is(Blocks.DEEPSLATE_IRON_ORE) + || state.is(Blocks.COPPER_ORE) || state.is(Blocks.DEEPSLATE_COPPER_ORE) + || state.is(Blocks.GOLD_ORE) || state.is(Blocks.DEEPSLATE_GOLD_ORE) + || state.is(Blocks.REDSTONE_ORE) || state.is(Blocks.DEEPSLATE_REDSTONE_ORE) + || state.is(Blocks.LAPIS_ORE) || state.is(Blocks.DEEPSLATE_LAPIS_ORE) + || state.is(Blocks.DIAMOND_ORE) || state.is(Blocks.DEEPSLATE_DIAMOND_ORE); + } + + private static final class Context { + final GameTestHelper helper; + final ServerLevel level; + final LayeredHydrology.Plan plan; + final LayeredLavaDeposit.Plan lavaPlan; + final Set expectedLava = new HashSet<>(); + final RandomState random; + final DensityFunction natural; + final Map cells = new HashMap<>(); + final Set declaredWater = new HashSet<>(); + final Set expectedWater = new HashSet<>(); + final Map naturalLogObstructions = new LinkedHashMap<>(); + final Set initialSpringWater = new HashSet<>(); + final Map biomeTargets = new TreeMap<>(); + final Map resourceCounts = new TreeMap<>(); + final Map surfaceBiomes = new TreeMap<>(); + final Map> naturalSurfaceCache = new HashMap<>(); + final List lowerTerraceTargets = new ArrayList<>(); + final Map lowerVegetation = new TreeMap<>(); + final Map lowerPlants = new LinkedHashMap<>(); + final List> thinNaturalLedgePlants = new ArrayList<>(); + final Set lowerSurfaceBiomes = new TreeSet<>(); + final Map surfaceMaterials = new TreeMap<>(); + final Map actualDryShoreTopMaterials = new TreeMap<>(); + final Map> actualDryShoreMaterialsByDepth = new TreeMap<>(); + final Map upperGround = new HashMap<>(); + final Map exposedRocks = new TreeMap<>(); + final Map exposedOreCounts = new TreeMap<>(), outdoorOreCounts = new TreeMap<>(); + final List> exposedOreExamples = new ArrayList<>(); + final List rockPatches = new ArrayList<>(); + final List trees = new ArrayList<>(); + final List mushrooms = new ArrayList<>(); + final Map upperTreeCounts = new TreeMap<>(), lowerTreeCounts = new TreeMap<>(); + final Map remarkableTreeReport = new LinkedHashMap<>(); + final Set ecologyExtraChunks = new TreeSet<>(); + final List centralTerrainTargets = new ArrayList<>(); + final List> incompleteTreeEvidence = new ArrayList<>(); + int interiorOakTrees; + int innerGroundColumns; + int innerCanopyColumns; + int exteriorGroundColumns; + int exteriorCanopyColumns; + int lowerPlantsOnMoss; + int upperFlowers; + int verticalBiomeContrasts; + final Map springDescent = new TreeMap<>(); + final List actualFlowComponents = new ArrayList<>(); + final Map componentBySource = new HashMap<>(); + final List flowRoots = new ArrayList<>(); + int actualWaterBlocks; + int springWaterBlocks; + int maxDryShoreDistance; + final Map chunks = new TreeMap<>(); + final Set newlyForced = new HashSet<>(); + final List witnesses = new ArrayList<>(); + final Set fluidChunks = new HashSet<>(); + boolean witnessPlaced; + boolean simulationStarted; + boolean closed; + long entityLoadWaitMillis; + long simulationStartGameTime; + boolean allWaterChunksTicking; + boolean springMouthsCheckedAfterPostProcessing; + int seams; + int caneBlocks; + + Context(GameTestHelper helper) { + this.helper = helper; + level = helper.getLevel(); + var generator = (NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(); + helper.assertTrue(WoodlandHydrologyRuntime.enabled(generator), "Hydrology test requires the production wet preset"); + plan = WoodlandHydrologyRuntime.woodlandPlan(level); + lavaPlan = WoodlandHydrologyRuntime.lavaPlan(level); + random = level.getChunkSource().randomState(); + natural = generator.generatorSettings().value().noiseRouter().finalDensity(); + } + + void prepare() { + helper.assertTrue(!plan.features().isEmpty(), "Regression seed must exercise real surface water"); + for (var feature : plan.features()) { + SanctuaryMod.LOGGER.info("Hydrology planned site: seed {}, {}, Y{}, start {}, {} water columns, {} path points", + level.getSeed(), feature.kind(), feature.waterY(), feature.path().getFirst(), feature.waterCells(), feature.path().size()); + } + helper.assertTrue(plan.features().stream().allMatch(feature -> feature.kind() == LayeredHydrology.Kind.POND + || feature.kind() == LayeredHydrology.Kind.LAKE), + "The delivered generation may contain only surface ponds and lakes, without imposed short streams"); + for (var spring : plan.springs()) { + SanctuaryMod.LOGGER.info("Natural spring planned: seed {}, source {}, outlet {}, envelope {}", + level.getSeed(), spring.source(), spring.outlet(), spring.flowBounds()); + } + if (level.getSeed() == 0L) helper.assertTrue(!plan.springs().isEmpty(), + "Reference seed 0 must demonstrate a real rock spring, without imposing spring quotas on other seeds"); + Set requested = new HashSet<>(); + for (var cell : plan.cells()) { + helper.assertTrue(cells.put(ChunkPos.pack(cell.x(), cell.z()), cell) == null, + "A plan must own each horizontal column exactly once"); + if (cell.hasWater()) { + for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) { + declaredWater.add(new BlockPos(cell.x(), y, cell.z())); + } + } + int cx = cell.x() >> 4; + int cz = cell.z() >> 4; + for (int dx = -1; dx <= 1; dx++) { + for (int dz = -1; dz <= 1; dz++) requested.add(ChunkPos.pack(cx + dx, cz + dz)); + } + } + for (var spring : plan.springs()) { + var bounds = spring.flowBounds(); + for (int cx = (bounds.minX() >> 4) - 1; cx <= (bounds.maxX() >> 4) + 1; cx++) { + for (int cz = (bounds.minZ() >> 4) - 1; cz <= (bounds.maxZ() >> 4) + 1; cz++) { + requested.add(ChunkPos.pack(cx, cz)); + } + } + } + for (var cell : lavaPlan.lavaCells()) expectedLava.add(new BlockPos(cell.x(), cell.y(), cell.z())); + Set lavaChunks = new HashSet<>(); + for (var cell : lavaPlan.cells()) lavaChunks.add(ChunkPos.pack(cell.x() >> 4, cell.z() >> 4)); + for (var support : lavaPlan.supports()) lavaChunks.add(ChunkPos.pack(support.x() >> 4, support.z() >> 4)); + for (long packed : lavaChunks) { + var p = ChunkPos.unpack(packed); + for (int dx = -1; dx <= 1; dx++) for (int dz = -1; dz <= 1; dz++) requested.add(ChunkPos.pack(p.x() + dx, p.z() + dz)); + } + SanctuaryMod.LOGGER.info("Starter lava planned, seed {}: {} sources; access {}", level.getSeed(), expectedLava.size(), lavaPlan.access()); + findBiomeTargets(requested); + helper.assertTrue(!declaredWater.isEmpty(), "At least one generated water volume must be tested"); + SanctuaryMod.LOGGER.info("Hydrology seed {}: preparing {} site and neighbour chunks for actual water inspection", + level.getSeed(), requested.size()); + List orderedChunks = requested.stream().sorted().toList(); + // Declare the whole finite footprint before loading it, so neighbouring + // chunk promotions cannot temporarily remove another site's support tickets. + for (long packed : orderedChunks) { + ChunkPos pos = ChunkPos.unpack(packed); + if (level.setChunkForced(pos.x(), pos.z(), true)) newlyForced.add(packed); + } + for (long packed : orderedChunks) { + ChunkPos pos = ChunkPos.unpack(packed); + var chunk = level.getChunkSource().getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true); + helper.assertTrue(chunk instanceof LevelChunk, "Hydrology inspection requires a decorated FULL chunk"); + chunks.put(packed, (LevelChunk) chunk); + } + // GameTestServer runs accelerated ticks: 600 ticks can elapse in less + // than a second, so they do not bound the asynchronous entity I/O wait. + // Minecraft's own helper pumps completed entity loads via managedBlock. + // Radius zero waits only for each already inspected, forced FULL chunk. + long entityWaitStarted = System.nanoTime(); + for (long packed : orderedChunks) level.waitForEntities(ChunkPos.unpack(packed), 0); + entityLoadWaitMillis = (System.nanoTime() - entityWaitStarted) / 1_000_000; + helper.assertTrue(orderedChunks.stream().allMatch(level::areEntitiesLoaded), + "Official entity loading must finish for every inspected chunk before fluid readiness is measured"); + SanctuaryMod.LOGGER.info("Hydrology seed {}: official entity loading completed for {} existing chunks in {} ms", + level.getSeed(), orderedChunks.size(), entityLoadWaitMillis); + captureNaturalLogObstructions(); + for (var spring : plan.springs()) flowRoots.add(new FlowRoot(spring.id(), "rock_spring", spring.id(), position(spring.source()))); + BlockPos first = expectedWater.stream().min(Comparator.comparingInt((BlockPos pos) -> pos.getX()) + .thenComparingInt(BlockPos::getZ).thenComparingInt(BlockPos::getY)).orElseThrow(); + Set witnessChunks = new TreeSet<>(); + witnessChunks.add(ChunkPos.pack(first)); + for (BlockPos water : expectedWater) fluidChunks.add(ChunkPos.pack(water)); + for (BlockPos lava : expectedLava) { + fluidChunks.add(ChunkPos.pack(lava)); + witnessChunks.add(ChunkPos.pack(lava)); + } + for (var spring : plan.springs()) { + witnessChunks.add(ChunkPos.pack(position(spring.source()))); + var bounds = spring.flowBounds(); + for (int cx = bounds.minX() >> 4; cx <= bounds.maxX() >> 4; cx++) { + for (int cz = bounds.minZ() >> 4; cz <= bounds.maxZ() >> 4; cz++) fluidChunks.add(ChunkPos.pack(cx, cz)); + } + } + // Decoration may divert water into an already inspected halo chunk. Wait for + // the whole existing finite footprint; this adds no chunk or ticket to the test. + fluidChunks.addAll(chunks.keySet()); + for (long packed : witnessChunks) { + var pos = ChunkPos.unpack(packed); + witnesses.add(new BlockPos(pos.getMinBlockX() + 8, level.getMaxY() - 20, pos.getMinBlockZ() + 8)); + } + logSpringStates("initial FULL chunks"); + for (BlockPos position : expectedWater) { + for (Direction direction : List.of(Direction.EAST, Direction.SOUTH)) { + BlockPos next = position.relative(direction); + if (expectedWater.contains(next) + && (position.getX() >> 4 != next.getX() >> 4 || position.getZ() >> 4 != next.getZ() >> 4)) seams++; + } + } + helper.assertTrue(seams > 0, "Regression seed must exercise a continuous water volume across a chunk seam"); + SanctuaryMod.LOGGER.info("Hydrology seed {}: {} features, {} water blocks, {} FULL chunks including halo, {} seam adjacencies", + level.getSeed(), plan.features().size(), expectedWater.size(), chunks.size(), seams); + } + + void captureNaturalLogObstructions() { + expectedWater.addAll(declaredWater); + for (BlockPos p : declaredWater.stream().sorted(Comparator.comparingInt((BlockPos pos) -> pos.getX()) + .thenComparingInt(BlockPos::getY).thenComparingInt(BlockPos::getZ)).toList()) { + BlockState state = level.getBlockState(p); + if (state.getFluidState().is(FluidTags.WATER) && state.getFluidState().isSource()) continue; + var cell = plan.cellAt(p.getX(), p.getZ()); + boolean horizontalLog = state.is(BlockTags.LOGS) && state.hasProperty(BlockStateProperties.AXIS) + && state.getValue(BlockStateProperties.AXIS) != Direction.Axis.Y; + boolean submerged = declaredWater.contains(p.below()) + && level.getFluidState(p.below()).is(FluidTags.WATER) && level.getFluidState(p.below()).isSource(); + boolean shallowEdge = cell.bedY() == p.getY() - 1 + && level.getBlockState(p.below()).isCollisionShapeFullBlock(level, p.below()); + boolean besideWater = HORIZONTAL.stream().map(p::relative).anyMatch(side -> declaredWater.contains(side) + && level.getFluidState(side).is(FluidTags.WATER) && level.getFluidState(side).isSource()); + helper.assertTrue(horizontalLog && p.getY() == cell.waterY() && (submerged || shallowEdge) && besideWater, + "Only a horizontal surface log beside retained source water, on the bed or with water underneath, may occupy basin water: " + + p + ", actual=" + state); + naturalLogObstructions.put(p.immutable(), state); + expectedWater.remove(p); + } + helper.assertTrue(declaredWater.size() == expectedWater.size() + naturalLogObstructions.size(), + "Every declared water block must be accounted for as retained water or a precise natural log obstruction"); + SanctuaryMod.LOGGER.info("Natural water after decoration, seed {}: {} declared, {} retained sources, {} horizontal surface logs: {}", + level.getSeed(), declaredWater.size(), expectedWater.size(), naturalLogObstructions.size(), naturalLogObstructions); + } + + void verifyNaturalShellAndFinishedWater() { + verifySedimentProfiles(); + verifySpringOrigins(); + verifyStarterLava(); + for (var cell : plan.cells()) { + if (!cell.hasWater()) continue; + helper.assertTrue(cell.waterY() > cell.bedY() && cell.waterY() - cell.bedY() <= LayeredHydrology.MAX_DEPTH, + "A surface basin must keep the declared shallow water depth"); + helper.assertTrue(cell.carveTop() - cell.bedY() <= LayeredHydrology.MAX_CARVE, + "Hydrology must not excavate a deep artificial chamber beneath the surface"); + for (int depth = 0; depth < 3; depth++) { + int y = cell.bedY() - depth; + helper.assertTrue(sample(cell.x(), y, cell.z()) > 0, + "Water must retain three blocks of natural floor, without adding a liner at " + cell.x() + "," + y + "," + cell.z()); + BlockPos floor = new BlockPos(cell.x(), y, cell.z()); + helper.assertTrue(level.getBlockState(floor).isCollisionShapeFullBlock(level, floor), + "Generated water floor must remain solid after decoration and fluid ticks: " + floor); + } + for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) { + BlockPos water = new BlockPos(cell.x(), y, cell.z()); + BlockState obstruction = naturalLogObstructions.get(water); + if (obstruction == null) { + helper.assertTrue(level.getFluidState(water).is(FluidTags.WATER) && level.getFluidState(water).isSource(), + "Every unobstructed planned water cell must remain a retained source: " + water); + } else { + helper.assertTrue(level.getBlockState(water).equals(obstruction), + "A recorded natural surface log must retain exactly its original block state after fluid ticks: " + water); + if (expectedWater.contains(water.below())) { + helper.assertTrue(level.getFluidState(water.below()).is(FluidTags.WATER) + && level.getFluidState(water.below()).isSource(), + "A submerged natural log must retain source water underneath: " + water); + } else { + helper.assertTrue(cell.bedY() == water.getY() - 1 + && level.getBlockState(water.below()).isCollisionShapeFullBlock(level, water.below()), + "A log at the shallow edge must retain its supported natural bed: " + water); + } + helper.assertTrue(HORIZONTAL.stream().map(water::relative).anyMatch(side -> expectedWater.contains(side) + && level.getFluidState(side).is(FluidTags.WATER) && level.getFluidState(side).isSource()), + "Natural surface logs must remain beside actual retained water: " + water); + } + for (Direction direction : HORIZONTAL) { + BlockPos side = water.relative(direction); + // A recorded log occupies the interior of the original basin volume; + // it must not be mistaken for an exterior wall that was originally rock. + if (declaredWater.contains(side)) continue; + helper.assertTrue(sample(side.getX(), side.getY(), side.getZ()) > 0, + "Every external underwater side must be supported by natural terrain: " + side); + helper.assertTrue(level.getBlockState(side).isCollisionShapeFullBlock(level, side), + "A decorated shore must not leave a hole in the three-dimensional water shell: " + side); + } + } + // The original density must have no roof above the explicitly carved surface. + // Leaves and branches from neighbouring trees may shade the water afterwards. + for (int y = cell.carveTop() + 1; y <= level.getMaxY(); y++) { + helper.assertTrue(sample(cell.x(), y, cell.z()) <= 0, + "A surface basin must not be a flooded underground chamber: " + cell.x() + "," + y + "," + cell.z()); + } + } + } + + void verifyAllObservedWaterIsPlanned() { + int[] observed = {0, 0}; + Set actualFlow = new HashSet<>(); + for (LevelChunk chunk : chunks.values()) { + chunk.findBlocks(state -> state.getFluidState().is(FluidTags.WATER) || state.is(Blocks.SUGAR_CANE), (pos, state) -> { + if (state.getFluidState().is(FluidTags.WATER)) { + if (expectedWater.contains(pos)) observed[0]++; + else { + actualFlow.add(pos.immutable()); + if (!allWaterChunksTicking) initialSpringWater.add(pos.immutable()); + } + } + if (state.is(Blocks.SUGAR_CANE)) { + helper.assertTrue(state.canSurvive(level, pos), "Generated shore sugar cane must survive at " + pos); + observed[1]++; + } + }); + } + helper.assertTrue(observed[0] == expectedWater.size(), + "Every closed basin must retain all declared sources except its recorded natural surface logs"); + analyseActualFlow(actualFlow); + for (var spring : plan.springs()) helper.assertTrue(actualFlow.contains(position(spring.source())), + "Every declared spring must remain a real source outside the closed basins: " + spring.source()); + helper.assertTrue(actualFlowComponents.stream().mapToInt(FlowComponent::actualWaterBlocks).sum() == actualFlow.size(), + "The connected components must account for every observed water voxel outside retained basins"); + for (FlowComponent component : actualFlowComponents) { + helper.assertTrue(!component.sourceIds().isEmpty(), + "All water outside retained basin cells must be connected through real six-neighbour water to a declared rock spring: " + component); + helper.assertTrue(component.horizontalNeighboursOutsideInspection() == 0, + "Spring water reached a horizontal boundary of the inspected chunks; its containment is not proven: " + component); + } + actualWaterBlocks = observed[0] + actualFlow.size(); + springWaterBlocks = actualFlow.size(); + caneBlocks = observed[1]; + } + + /** Inspect actual connected water after decoration, without loading further chunks or + * assuming that the earlier bare-rock route can predict canopy-induced deviations. */ + void analyseActualFlow(Set actualFlow) { + actualFlowComponents.clear(); + componentBySource.clear(); + Set visited = new HashSet<>(); + Set graph = new HashSet<>(actualFlow); + List starts = new ArrayList<>(); + for (FlowRoot root : flowRoots) { + starts.add(root.position()); + if (level.getFluidState(root.position()).is(FluidTags.WATER)) graph.add(root.position()); + } + // Keep orphan components in the report too, so an assertion failure remains reviewable. + starts.addAll(actualFlow.stream().sorted(Comparator.comparingInt((BlockPos p) -> p.getX()) + .thenComparingInt(BlockPos::getY).thenComparingInt(BlockPos::getZ)).toList()); + for (BlockPos start : starts) { + if (!graph.contains(start) || !visited.add(start)) continue; + Set componentWater = new HashSet<>(); + ArrayDeque queue = new ArrayDeque<>(); + queue.add(start); + int minX = start.getX(), maxX = minX, minY = start.getY(), maxY = minY, minZ = start.getZ(), maxZ = minZ; + int newWater = 0, externalWater = 0, missingNeighbours = 0; + List outsidePrediction = new ArrayList<>(); + List outsideBounds = new ArrayList<>(); + while (!queue.isEmpty()) { + BlockPos p = queue.removeFirst(); + componentWater.add(p); + minX = Math.min(minX, p.getX()); maxX = Math.max(maxX, p.getX()); + minY = Math.min(minY, p.getY()); maxY = Math.max(maxY, p.getY()); + minZ = Math.min(minZ, p.getZ()); maxZ = Math.max(maxZ, p.getZ()); + if (actualFlow.contains(p)) { + externalWater++; + if (!initialSpringWater.contains(p)) newWater++; + if (!plan.allowsSpringFlow(p.getX(), p.getY(), p.getZ())) outsidePrediction.add(p); + if (!isWithinDensityPredictionBounds(p)) outsideBounds.add(p); + } + for (Direction direction : Direction.values()) { + BlockPos next = p.relative(direction); + if (direction != Direction.UP && direction != Direction.DOWN + && !chunks.containsKey(ChunkPos.pack(next))) missingNeighbours++; + if (graph.contains(next) && visited.add(next)) queue.addLast(next); + } + } + List sources = flowRoots.stream().filter(root -> componentWater.contains(root.position())) + .map(FlowRoot::id).sorted().toList(); + var component = new FlowComponent(actualFlowComponents.size(), sources, externalWater, newWater, + new LayeredHydrology.FlowBounds(minX, minY, minZ, maxX, maxY, maxZ), + outsidePrediction.size(), actualBoundsOf(outsidePrediction), outsideBounds.size(), actualBoundsOf(outsideBounds), + missingNeighbours, minY == level.getMinY()); + actualFlowComponents.add(component); + for (long source : sources) componentBySource.put(source, component); + } + } + + LayeredHydrology.FlowBounds actualBoundsOf(List positions) { + if (positions.isEmpty()) return null; + BlockPos first = positions.getFirst(); + int minX = first.getX(), maxX = minX, minY = first.getY(), maxY = minY, minZ = first.getZ(), maxZ = minZ; + for (BlockPos p : positions) { + minX = Math.min(minX, p.getX()); maxX = Math.max(maxX, p.getX()); + minY = Math.min(minY, p.getY()); maxY = Math.max(maxY, p.getY()); + minZ = Math.min(minZ, p.getZ()); maxZ = Math.max(maxZ, p.getZ()); + } + return new LayeredHydrology.FlowBounds(minX, minY, minZ, maxX, maxY, maxZ); + } + + boolean isWithinDensityPredictionBounds(BlockPos p) { + return plan.springs().stream().map(LayeredHydrology.Spring::flowBounds).anyMatch(b -> { + return p.getX() >= b.minX() && p.getX() <= b.maxX() && p.getY() >= b.minY() && p.getY() <= b.maxY() + && p.getZ() >= b.minZ() && p.getZ() <= b.maxZ(); + }); + } + + void verifySedimentProfiles() { + actualDryShoreTopMaterials.clear(); + actualDryShoreMaterialsByDepth.clear(); + Set depths = new TreeSet<>(); + List wet = plan.cells().stream().filter(LayeredHydrology.Cell::hasWater).toList(); + for (var cell : plan.cells()) { + int depth = cell.sedimentDepth(); + helper.assertTrue(depth >= 3 && depth <= 5, "Sediment deposits must be three to five blocks thick"); + depths.add(depth); + int allowedCut = cell.hasWater() ? LayeredHydrology.MAX_CARVE : 2; + helper.assertTrue(cell.carveTop() - cell.bedY() <= allowedCut, + "Surface treatment must stay shallow rather than cutting a trench: " + cell); + for (int d = 0; d <= depth + 1; d++) { + BlockPos p = new BlockPos(cell.x(), cell.bedY() - d, cell.z()); + helper.assertTrue(sample(p.getX(), p.getY(), p.getZ()) > 0, + "Sediment and its underlying support must replace existing natural terrain: " + p); + BlockState state = level.getBlockState(p); + helper.assertTrue(state.isCollisionShapeFullBlock(level, p), + "Every deposit layer and the unchanged support below it must stay full after gravity ticks: " + p); + if (d >= depth) continue; + BlockState expected = WoodlandMaterials.sedimentBlock(cell, level.getSeed(), d); + // Woodland biomes retain ore_dirt and the vanilla natural-rock + // features, plus exactly the seven metal/mineral families above. + // No other full block (including a log) is an acceptable replacement. + boolean paintedRock = expected.is(BlockTags.BASE_STONE_OVERWORLD); + boolean admissibleRockReplacement = state.is(BlockTags.BASE_STONE_OVERWORLD) + || isWoodlandOre(state) || state.is(Blocks.DIRT); + boolean materialMatches = state.equals(expected) || paintedRock && admissibleRockReplacement + || expected.is(Blocks.GRASS_BLOCK) && state.is(Blocks.DIRT); + helper.assertTrue(materialMatches, "The complete sediment profile must survive decoration: " + p + + " planned=" + expected + " layer=" + d + " actual=" + state); + if (!cell.hasWater()) { + actualDryShoreMaterialsByDepth.computeIfAbsent(d, ignored -> new TreeMap<>()) + .merge(blockId(state), 1, Integer::sum); + if (d == 0) actualDryShoreTopMaterials.merge(blockId(state), 1, Integer::sum); + } + } + if (!cell.hasWater()) { + int distance = wet.stream().mapToInt(w -> Math.abs(w.x() - cell.x()) + Math.abs(w.z() - cell.z())).min().orElse(0); + maxDryShoreDistance = Math.max(maxDryShoreDistance, distance); + } + } + if (level.getSeed() == 0L) { + helper.assertTrue(maxDryShoreDistance >= 4, "Reference terrain must demonstrate shores wider than a one-block rim"); + helper.assertTrue(actualDryShoreTopMaterials.getOrDefault("minecraft:sand", 0) > 0 + && actualDryShoreTopMaterials.getOrDefault("minecraft:gravel", 0) > 0 && actualDryShoreTopMaterials.size() >= 3, + "Reference finished terrain must demonstrate actual sand, gravel and a third shore material; top=" + + actualDryShoreTopMaterials + ", layers=" + actualDryShoreMaterialsByDepth); + helper.assertTrue(depths.size() > 1, "Reference deposits should demonstrate variable thickness"); + } + } + + void verifySpringOrigins() { + for (var spring : plan.springs()) { + BlockPos source = position(spring.source()); + BlockPos outlet = position(spring.outlet()); + helper.assertTrue(chunks.containsKey(ChunkPos.pack(source)) && chunks.containsKey(ChunkPos.pack(outlet)), + "Both spring mouth chunks must already be inspected FULL chunks: source=" + source + ", outlet=" + outlet); + helper.assertTrue(source.distManhattan(outlet) == 1 && source.getY() == outlet.getY(), + "A rock spring must emerge through one horizontal opening"); + helper.assertTrue(sample(source.getX(), source.getY(), source.getZ()) > 0, + "The spring source must replace one existing rock block"); + helper.assertTrue(level.getFluidState(source).is(FluidTags.WATER) && level.getFluidState(source).isSource(), + "The actual rock spring source must exist: " + source); + for (Direction direction : Direction.values()) { + BlockPos side = source.relative(direction); + helper.assertTrue(chunks.containsKey(ChunkPos.pack(side)), + "Every natural niche face must be checked within the existing inspected footprint, including chunk seams: " + side); + if (side.equals(outlet)) { + helper.assertTrue(sample(side.getX(), side.getY(), side.getZ()) <= 0, + "Spring outlet must be naturally open, without tunnelling an artificial outlet: " + outlet); + } else { + helper.assertTrue(sample(side.getX(), side.getY(), side.getZ()) > 0 + && level.getBlockState(side).isCollisionShapeFullBlock(level, side), + "A natural spring needs its rock roof, floor and three solid sides: " + side); + } + } + helper.assertTrue(plan.allowsSpringFlow(source.getX(), source.getY(), source.getZ()), + "Declared spring water must be included in the precise flow attribution envelope"); + } + if (simulationStarted) verifySpringMouthsAfterPostProcessing(); + } + + void verifySpringMouthsAfterPostProcessing() { + for (var spring : plan.springs()) { + BlockPos source = position(spring.source()), outlet = position(spring.outlet()); + helper.assertTrue(level.isPositionTickingWithEntitiesLoaded(ChunkPos.pack(source)) + && level.isPositionTickingWithEntitiesLoaded(ChunkPos.pack(outlet)), + "Spring mouth verification must follow completion of the real ticking futures: source=" + source + ", outlet=" + outlet); + // ChunkMap.prepareTickingChunk calls postProcessGeneration before completing + // its ticking future. A merely FULL chunk may still contain pending decoration. + BlockState state = level.getBlockState(outlet); + helper.assertTrue(state.isAir() || state.is(Blocks.WATER), + "Post-processing must leave the natural spring mouth open; source=" + source + + ", outlet=" + outlet + ", blocking_state=" + state); + if (simulationStarted) helper.assertTrue(state.is(Blocks.WATER) && state.getFluidState().is(FluidTags.WATER), + "After 1800 real fluid ticks the source must feed its adjacent mouth: source=" + source + + ", outlet=" + outlet + ", actual=" + state); + } + springMouthsCheckedAfterPostProcessing = true; + } + + void verifyPostProcessingDoesNotRepairOldMouth() throws IOException { + if (plan.springs().isEmpty()) return; + var spring = plan.springs().getFirst(); + BlockPos outlet = position(spring.outlet()); + LevelChunk chunk = chunks.get(ChunkPos.pack(outlet)); + helper.assertTrue(java.util.Arrays.stream(chunk.getPostProcessing()).allMatch(markers -> markers == null || markers.isEmpty()), + "The re-entry regression requires a fully consumed post-processing marker list: " + chunk.getPos()); + BlockState original = level.getBlockState(outlet); + BlockState obstruction = Blocks.GLOW_LICHEN.defaultBlockState() + .setValue(MultifaceBlock.getFaceProperty(Direction.UP), true); + // No tick or neighbouring update occurs during this temporary obstruction test. + // It runs after the terrain/flow snapshots, and restores the exact state in finally. + try { + level.setBlock(outlet, obstruction, 2); + helper.assertTrue(level.getBlockState(outlet).equals(obstruction), "The temporary obstruction must be placed: " + outlet); + chunk.postProcessGeneration(level); + helper.assertTrue(level.getBlockState(outlet).equals(obstruction), + "Re-entering post-processing without a generation marker must preserve a later edit to the spring mouth: " + outlet + + ", expected=" + obstruction + ", actual=" + level.getBlockState(outlet)); + } finally { + level.setBlock(outlet, original, 2); + } + helper.assertTrue(level.getBlockState(outlet).equals(original), "The temporary mouth test must restore its original state: " + outlet); + Path report = Path.of("diagnostics", "woodland-seed-" + level.getSeed() + "-postprocessing-reentry.json"); + Files.writeString(report, new GsonBuilder().setPrettyPrinting().create().toJson(Map.of( + "seed", level.getSeed(), "spring_id", spring.id(), "outlet", spring.outlet(), "passed", true, + "generation_markers_present", false, "temporary_state", obstruction.toString(), + "restored_state", original.toString(), "scope", "Re-entry after completed generation preserves later edits; test world only")) + "\n"); + SanctuaryMod.LOGGER.info("Spring post-processing re-entry preserved a later edit without markers, seed {}, mouth {}", level.getSeed(), outlet); + } + + List> springMouthDiagnostics() { + List> result = new ArrayList<>(); + for (var spring : plan.springs()) { + BlockPos source = position(spring.source()), outlet = position(spring.outlet()); + long sourceChunk = ChunkPos.pack(source), outletChunk = ChunkPos.pack(outlet); + Map item = new LinkedHashMap<>(); + item.put("spring_id", spring.id()); + item.put("source", spring.source()); + item.put("outlet", spring.outlet()); + item.put("source_state", level.getBlockState(source).toString()); + item.put("outlet_state", level.getBlockState(outlet).toString()); + item.put("outlet_original_density", sample(outlet.getX(), outlet.getY(), outlet.getZ())); + item.put("mouth_crosses_chunk_boundary", sourceChunk != outletChunk); + item.put("niche_touches_chunk_boundary", java.util.Arrays.stream(Direction.values()) + .anyMatch(direction -> ChunkPos.pack(source.relative(direction)) != sourceChunk)); + item.put("source_chunk_scheduler_ready", level.isPositionTickingWithEntitiesLoaded(sourceChunk)); + item.put("outlet_chunk_scheduler_ready", level.isPositionTickingWithEntitiesLoaded(outletChunk)); + result.add(item); + } + return result; + } + + void verifySpringDescent() { + springDescent.clear(); + for (var spring : plan.springs()) { + FlowComponent component = componentBySource.get(spring.id()); + helper.assertTrue(component != null, "The spring needs a connected component of actual water: " + spring.source()); + int drop = spring.source().y() - component.actualBounds().minY(); + int predictedDrop = spring.source().y() - spring.flowPath().stream() + .mapToInt(LayeredHydrology.Position::y).min().orElse(spring.source().y()); + helper.assertTrue(drop >= Math.min(16, predictedDrop) && drop >= 8 && component.newWaterBlocksSinceInitialSnapshot() > 0, + "Real fluid simulation must produce a descending component connected to each source: " + springState(spring) + + ", component=" + component); + springDescent.put(spring.id(), drop); + } + boolean reachesBottomInPlan = plan.springs().stream().anyMatch(spring -> spring.flowPath().stream() + .anyMatch(p -> p.y() == level.getMinY())); + if (level.getSeed() == 0L && reachesBottomInPlan) { + helper.assertTrue(actualFlowComponents.stream().anyMatch(component -> + !component.sourceIds().isEmpty() && component.reachesDimensionFloor()), + "Reference seed must demonstrate a source-connected cascade actually reaching Y0 after 1800 real fluid ticks"); + } + } + + List naturalSurfaces(int x, int z) { + return naturalSurfaceCache.computeIfAbsent(ChunkPos.pack(x, z), ignored -> { + List surfaces = new ArrayList<>(); + boolean above = false, twoAbove = false; + for (int y = level.getMaxY(); y >= 24; y--) { + boolean solid = sample(x, y, z) > 0; + if (solid && !above && !twoAbove) surfaces.add(y); + twoAbove = above; above = solid; + } + return List.copyOf(surfaces); + }); + } + + void findBiomeTargets(Set requested) { + var generator = (NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(); + var resolver = generator.getBiomeSource().createUncachedResolver(random); + Set extraLowerChunks = new HashSet<>(), centralChunks = new HashSet<>(); + int darkTargets = 0; + // Pure density/biome probing loads no chunks. At most 24 ecology chunks are + // added; hydro already supplies its unchanged, finite site-and-halo footprint. + for (int radius = 0; radius <= 208; radius += 16) { + for (int x = -radius; x <= radius; x += 16) { + for (int z = -radius; z <= radius; z += 16) { + if (Math.max(Math.abs(x), Math.abs(z)) != radius || Math.hypot(x, z) > 224) continue; + List surfaces = naturalSurfaces(x + 4, z + 4); + if (surfaces.isEmpty()) continue; + long packed = ChunkPos.pack(x >> 4, z >> 4); + int highest = surfaces.getFirst(); + if (radius <= 64 && centralTerrainTargets.size() < 8 && centralChunks.add(packed) + && addEcologyChunk(requested, packed)) { + centralTerrainTargets.add(new BlockPos(x + 4, highest + 1, z + 4)); + } + for (int y : surfaces) { + String biome = resolver.getNoiseBiome((x + 4) >> 2, (y + 1) >> 2, (z + 4) >> 2).getRegisteredName(); + if (!biomeTargets.containsKey(biome) && addEcologyChunk(requested, packed)) + biomeTargets.put(biome, new BlockPos(x + 4, y + 1, z + 4)); + boolean dark = biome.equals("sanctuary:woodland_dark_grove"); + boolean bamboo = biome.equals("sanctuary:woodland_bamboo_grove"); + if ((dark || bamboo) && highest - y >= 12 && lowerTerraceTargets.size() < 10 && (!dark || darkTargets < 7) + && !extraLowerChunks.contains(packed) && hasNaturalTreeSpace(x + 4, y + 1, z + 4, 12) + && addEcologyChunk(requested, packed)) { + extraLowerChunks.add(packed); + lowerTerraceTargets.add(new BlockPos(x + 4, y + 1, z + 4)); + if (dark) darkTargets++; + } + } + } + } + } + WoodlandGrovesFeature.remarkablePlan(generator, random).ifPresent(tree -> { + remarkableTreeReport.put("planned", true); + remarkableTreeReport.put("base", tree.base()); + remarkableTreeReport.put("feature", tree.featureId()); + remarkableTreeReport.put("radius", tree.radius()); + remarkableTreeReport.put("height", tree.height()); + // The selected optional tree owns a single site; inspect its whole declared + // footprint without choosing a different seed or forcing any plantation. + for (int cx = (tree.base().getX() - tree.radius()) >> 4; cx <= (tree.base().getX() + tree.radius()) >> 4; cx++) + for (int cz = (tree.base().getZ() - tree.radius()) >> 4; cz <= (tree.base().getZ() + tree.radius()) >> 4; cz++) + requested.add(ChunkPos.pack(cx, cz)); + }); + remarkableTreeReport.putIfAbsent("planned", false); + } + + boolean addEcologyChunk(Set requested, long packed) { + if (requested.contains(packed)) return true; + if (ecologyExtraChunks.size() >= 24) return false; + ecologyExtraChunks.add(packed); + requested.add(packed); + return true; + } + + boolean hasNaturalTreeSpace(int x, int y, int z, int height) { + for (int dx = 0; dx <= 1; dx++) for (int dz = 0; dz <= 1; dz++) { + for (int d = 1; d <= 3; d++) if (sample(x + dx, y - d, z + dz) <= 0) return false; + for (int h = 0; h < height; h++) if (sample(x + dx, y + h, z + dz) > 0) return false; + } + return WoodlandGroveGeometry.treeRoom(this::sample, x, y, z, 2, 6, height); + } + + void verifyBiomesAndResources() { + for (var entry : biomeTargets.entrySet()) { + BlockPos p = entry.getValue(); + LevelChunk chunk = chunks.get(ChunkPos.pack(p.getX() >> 4, p.getZ() >> 4)); + helper.assertTrue(chunk.getNoiseBiome(p.getX() >> 2, p.getY() >> 2, p.getZ() >> 2) + .getRegisteredName().equals(entry.getKey()), + "The actual generated biome must match the seeded biome source at " + p); + } + Map counts = new HashMap<>(); + for (LevelChunk chunk : chunks.values()) { + for (var section : chunk.getSections()) section.getStates().count((state, count) -> counts.merge(state, (long) count, Long::sum)); + for (int x = chunk.getPos().getMinBlockX(); x <= chunk.getPos().getMaxBlockX(); x += 4) { + for (int z = chunk.getPos().getMinBlockZ(); z <= chunk.getPos().getMaxBlockZ(); z += 4) { + BlockPos ground = highestActualGround(chunk, x, z); + if (ground == null) continue; + String id = chunk.getNoiseBiome(x >> 2, (ground.getY() + 1) >> 2, z >> 2).getRegisteredName(); + surfaceBiomes.merge(id, 1, Integer::sum); + } + } + } + counts.forEach((state, count) -> { + String id = BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(); + if (id.endsWith("_ore") || state.is(BlockTags.LOGS) || state.is(Blocks.STONE) + || state.is(Blocks.WATER) || state.is(Blocks.LAVA) || state.is(Blocks.SUGAR_CANE)) { + resourceCounts.merge(id, count, Long::sum); + } + }); + verifyLowerVegetationAndPalette(); + verifyWoodlandTreesAndGeology(); + SanctuaryMod.LOGGER.info("Woodland actual site resources, seed {}: {}; biome targets {}", + level.getSeed(), resourceCounts, biomeTargets); + if (level.getSeed() == 0L) { + helper.assertTrue(biomeTargets.size() >= 3, "Reference seed must demonstrate several real layered biomes on natural surfaces"); + for (String ore : List.of("coal", "iron", "copper", "gold", "redstone", "lapis", "diamond")) { + long count = resourceCounts.getOrDefault("minecraft:" + ore + "_ore", 0L) + + resourceCounts.getOrDefault("minecraft:deepslate_" + ore + "_ore", 0L); + helper.assertTrue(count > 0, "Reference inspected site chunks must demonstrate reachable " + ore + + "; this is a seed-specific regression check, never a generation quota"); + } + } + } + + void verifyLowerVegetationAndPalette() { + var generator = (NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(); + Set grassColors = new HashSet<>(), foliageColors = new HashSet<>(), waterColors = new HashSet<>(); + for (var holder : generator.getBiomeSource().possibleBiomes()) { + helper.assertFalse(holder.getRegisteredName().contains("dappled"), "Dappled Forest is reserved for future continents"); + grassColors.add(holder.value().getGrassColor(0, 0)); + foliageColors.add(holder.value().getFoliageColor()); + waterColors.add(holder.value().getWaterColor()); + } + helper.assertTrue(grassColors.size() == 1 && foliageColors.size() == 1 && waterColors.size() == 1, + "The island's layered palette must preserve coherent grass, foliage and water colours"); + for (var target : lowerTerraceTargets) { + var chunk = chunks.get(ChunkPos.pack(target)); + String lower = chunk.getNoiseBiome(target.getX() >> 2, target.getY() >> 2, target.getZ() >> 2).getRegisteredName(); + int top = naturalSurfaces(target.getX(), target.getZ()).getFirst(); + String upper = chunk.getNoiseBiome(target.getX() >> 2, (top + 1) >> 2, target.getZ() >> 2).getRegisteredName(); + if (!lower.equals(upper)) verticalBiomeContrasts++; + } + for (LevelChunk chunk : chunks.values()) { + chunk.findBlocks(WoodlandDiagnostics::isTerracePlant, (p, state) -> { + BlockPos floor = p.below(); + if (!level.getBlockState(floor).isCollisionShapeFullBlock(level, floor)) return; + var surfaces = naturalSurfaces(p.getX(), p.getZ()); + if (!surfaces.isEmpty() && surfaces.getFirst() == floor.getY() && state.is(BlockTags.FLOWERS)) upperFlowers++; + if (surfaces.isEmpty() || surfaces.getFirst() - floor.getY() < 8 || !surfaces.contains(floor.getY())) return; + helper.assertTrue(state.canSurvive(level, p), "A real lower-terrace plant must survive on its natural support: " + p); + List supports = new ArrayList<>(); + boolean threeSupports = true; + for (int d = 0; d < 3; d++) { + float density = sample(floor.getX(), floor.getY() - d, floor.getZ()); + supports.add(density); + threeSupports &= density > 0 && level.getBlockState(floor.below(d)).isCollisionShapeFullBlock(level, floor.below(d)); + } + if (!threeSupports) { + // Vanilla flowers and grass also decorate thin natural ledges. They are + // valid scenery, but cannot prove the new feature's three-support contract. + thinNaturalLedgePlants.add(Map.of("x", p.getX(), "y", p.getY(), "z", p.getZ(), + "plant", state.toString(), "floor", level.getBlockState(floor).toString(), "support_densities", supports)); + return; + } + lowerVegetation.merge(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(), 1L, Long::sum); + lowerPlants.put(p.immutable(), state); + if (level.getBlockState(floor).is(Blocks.MOSS_BLOCK)) lowerPlantsOnMoss++; + lowerSurfaceBiomes.add(chunk.getNoiseBiome(p.getX() >> 2, p.getY() >> 2, p.getZ() >> 2).getRegisteredName()); + }); + for (int x = chunk.getPos().getMinBlockX(); x <= chunk.getPos().getMaxBlockX(); x++) { + for (int z = chunk.getPos().getMinBlockZ(); z <= chunk.getPos().getMaxBlockZ(); z++) { + BlockPos ground = highestActualGround(chunk, x, z); + if (ground == null) continue; + upperGround.put(ChunkPos.pack(x, z), ground); + surfaceMaterials.merge(blockId(chunk.getBlockState(ground)), 1, Integer::sum); + } + } + } + SanctuaryMod.LOGGER.info("Woodland surfaces seed {}: {} lower plants, {} vertical biome contrasts, top materials {}", + level.getSeed(), lowerVegetation, verticalBiomeContrasts, surfaceMaterials); + if (level.getSeed() == 0L) { + helper.assertTrue(!lowerVegetation.isEmpty(), "Reference terrain must show vegetation on a real lower natural terrace"); + helper.assertTrue(verticalBiomeContrasts > 0, "Reference lower terraces must have genuinely vertical biome variation"); + helper.assertTrue(surfaceMaterials.getOrDefault("minecraft:stone", 0) >= 64, + "Reference terrain must demonstrate exposed natural stone areas, not uniform grass everywhere"); + } + } + + String blockId(BlockState state) { return BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(); } + + BlockState inspectedState(BlockPos p) { + LevelChunk chunk = chunks.get(ChunkPos.pack(p)); + return chunk == null ? null : chunk.getBlockState(p); + } + + BlockPos highestActualGround(LevelChunk chunk, int x, int z) { + for (int y = chunk.getHeight(Heightmap.Types.OCEAN_FLOOR, x, z); y >= 24; y--) { + BlockPos p = new BlockPos(x, y, z); + if (isTerrain(chunk.getBlockState(p))) return p; + } + return null; + } + + boolean isVerticalLog(BlockState state) { + return state != null && state.is(BlockTags.LOGS) && state.hasProperty(BlockStateProperties.AXIS) + && state.getValue(BlockStateProperties.AXIS) == Direction.Axis.Y; + } + + int naturalRoofAbove(BlockPos base) { + for (int y = base.getY() + 2; y <= level.getMaxY(); y++) + if (sample(base.getX(), y, base.getZ()) > 0) return y; + return -1; + } + + boolean threeNaturalSupports(BlockPos base) { + for (int d = 1; d <= 3; d++) { + BlockPos p = base.below(d); + BlockState state = inspectedState(p); + if (sample(p.getX(), p.getY(), p.getZ()) <= 0 || state == null + || !state.isCollisionShapeFullBlock(level, p)) return false; + } + return true; + } + + void verifyWoodlandTreesAndGeology() { + Map rootedLogs = new HashMap<>(); + Map> leafColumns = new HashMap<>(); + List mushroomBases = new ArrayList<>(); + Map> exposed = new TreeMap<>(); + for (LevelChunk chunk : chunks.values()) chunk.findBlocks(state -> state.is(BlockTags.LOGS) + || state.is(BlockTags.LEAVES) || state.is(Blocks.MUSHROOM_STEM) + || state.is(Blocks.BROWN_MUSHROOM) || state.is(Blocks.RED_MUSHROOM) + || state.is(Blocks.ANDESITE) || state.is(Blocks.DIORITE) || state.is(Blocks.GRANITE) + || state.is(Blocks.COAL_ORE) || state.is(Blocks.IRON_ORE) || state.is(Blocks.COPPER_ORE) + || state.is(Blocks.DEEPSLATE_COAL_ORE) || state.is(Blocks.DEEPSLATE_IRON_ORE) + || state.is(Blocks.DEEPSLATE_COPPER_ORE), (p, state) -> { + if (state.is(BlockTags.LEAVES)) { + leafColumns.computeIfAbsent(ChunkPos.pack(p.getX(), p.getZ()), ignored -> new ArrayList<>()).add(p.immutable()); + } else if (isVerticalLog(state)) { + BlockState floor = inspectedState(p.below()); + if (floor == null || !isTerrain(floor) || !floor.isCollisionShapeFullBlock(level, p.below())) return; + // Actual feet on natural terrain, not horizontal fallen logs or branches + // above a lower trunk. A leaf must also be observed before counting a tree. + if (sample(p.getX(), p.getY() - 1, p.getZ()) <= 0 || sample(p.getX(), p.getY(), p.getZ()) > 0) return; + int vertical = 0; + while (vertical < 48) { + BlockState next = inspectedState(p.above(vertical)); + if (!isVerticalLog(next) || !next.is(state.getBlock())) break; + vertical++; + } + if (vertical >= 3) rootedLogs.put(p.immutable(), state); + } else if (state.is(Blocks.MUSHROOM_STEM) || state.is(Blocks.BROWN_MUSHROOM) || state.is(Blocks.RED_MUSHROOM)) { + BlockState floor = inspectedState(p.below()); + if (floor != null && isTerrain(floor) && floor.isCollisionShapeFullBlock(level, p.below())) mushroomBases.add(p.immutable()); + } else if (blockId(state).endsWith("_ore")) { + recordExposedOre(p, state); + } else if (state.is(Blocks.ANDESITE) || state.is(Blocks.DIORITE) || state.is(Blocks.GRANITE)) { + // Surface-rule deposits can be on vertical walls as well as the upper + // ground. Count only rock faces adjoining natural air, never buried ore. + boolean face = false; + for (Direction direction : Direction.values()) { + BlockPos side = p.relative(direction); + BlockState neighbour = inspectedState(side); + if (neighbour != null && (neighbour.isAir() || neighbour.is(BlockTags.LEAVES) + || neighbour.getCollisionShape(level, side).isEmpty()) + && sample(side.getX(), side.getY(), side.getZ()) <= 0) { face = true; break; } + } + if (face) exposed.computeIfAbsent(blockId(state), ignored -> new HashSet<>()).add(p.immutable()); + } + }); + Set ungrouped = new HashSet<>(rootedLogs.keySet()); + for (BlockPos first : rootedLogs.keySet().stream().sorted(Comparator.comparingInt((BlockPos p) -> p.getX()) + .thenComparingInt(BlockPos::getZ).thenComparingInt(BlockPos::getY)).toList()) { + if (!ungrouped.remove(first)) continue; + List footprint = new ArrayList<>(); + ArrayDeque queue = new ArrayDeque<>(); queue.add(first); + BlockState trunk = rootedLogs.get(first); + while (!queue.isEmpty()) { + BlockPos p = queue.removeFirst(); footprint.add(p); + for (Direction direction : HORIZONTAL) { + BlockPos next = p.relative(direction); + if (ungrouped.contains(next) && rootedLogs.get(next).is(trunk.getBlock())) { + ungrouped.remove(next); queue.addLast(next); + } + } + } + if (trunk.is(Blocks.DARK_OAK_LOG)) { + Set feet = new HashSet<>(footprint); + BlockPos principal = footprint.stream().filter(p -> feet.contains(p.east()) && feet.contains(p.south()) + && feet.contains(p.east().south())) + .min(Comparator.comparingInt((BlockPos p) -> p.getX()).thenComparingInt(BlockPos::getZ)).orElse(null); + if (principal == null) { + // DarkOakTrunkPlacer in 26.3-pre.2 adds separate 2–4-log + // branches downwards from the canopy outside its 2x2 trunk. + // On sloping ledges these can touch natural ground; they do + // not prove another tree. Partial inspected groups do not either. + incompleteTreeEvidence.add(Map.of("base", first, "log", blockId(trunk), "footprint", List.copyOf(footprint), + "reason", "No complete 2x2 main-trunk footprint; possible vanilla hanging branch or partial inspection")); + continue; + } + List mainFeet = List.of(principal, principal.east(), principal.south(), principal.east().south()); + if (footprint.size() > 4) incompleteTreeEvidence.add(Map.of("base", principal, "log", blockId(trunk), + "footprint", footprint.stream().filter(p -> !mainFeet.contains(p)).toList(), + "reason", "Additional grounded branch columns adjacent to the complete 2x2 main trunk")); + first = principal; + footprint = mainFeet; + } + int height = 0; + while (height < 48) { + BlockState next = inspectedState(first.above(height)); + if (!isVerticalLog(next) || !next.is(trunk.getBlock())) break; + height++; + } + String logId = blockId(trunk), leafId = logId.replace("_log", "_leaves"); + BlockPos leaf = null; + searchLeaves: for (int dx = -6; dx <= 6; dx++) for (int dz = -6; dz <= 6; dz++) { + for (BlockPos p : leafColumns.getOrDefault(ChunkPos.pack(first.getX() + dx, first.getZ() + dz), List.of())) { + if (p.getY() >= first.getY() + 2 && p.getY() <= first.getY() + height + 8 + && blockId(inspectedState(p)).equals(leafId)) { leaf = p; break searchLeaves; } + } + } + if (leaf == null) { + incompleteTreeEvidence.add(Map.of("base", first, "log", logId, "reason", "No matching canopy leaf observed inside the finite footprint")); + continue; + } + var surfaces = naturalSurfaces(first.getX(), first.getZ()); + boolean lower = !surfaces.isEmpty() && surfaces.getFirst() - (first.getY() - 1) >= 8; + int roof = lower ? naturalRoofAbove(first) : -1; + String biome = chunks.get(ChunkPos.pack(first)).getNoiseBiome(first.getX() >> 2, first.getY() >> 2, first.getZ() >> 2).getRegisteredName(); + TreeWitness tree = new TreeWitness(logId, first, List.copyOf(footprint), height, leaf, lower, roof, biome); + trees.add(tree); + if (lower) { + // Vanilla may grow oak on a thin ledge too. Only the new sheltered + // dark-oak grove serves as evidence for the stronger floor contract. + if (trunk.is(Blocks.DARK_OAK_LOG)) { + helper.assertTrue(roof > first.getY() + 3 && footprint.size() == 4, + "A sheltered dark oak needs a natural roof and a real thick trunk: " + tree); + for (BlockPos p : footprint) helper.assertTrue(threeNaturalSupports(p), + "Every lower dark-oak foot must have three original and actual solid supports: " + p + ", tree=" + tree); + } + lowerTreeCounts.merge(logId, 1, Integer::sum); + } else { + upperTreeCounts.merge(logId, 1, Integer::sum); + if (trunk.is(Blocks.OAK_LOG) && Math.hypot(first.getX(), first.getZ()) <= 128) interiorOakTrees++; + } + } + for (BlockPos base : mushroomBases) { + var surfaces = naturalSurfaces(base.getX(), base.getZ()); + if (surfaces.isEmpty() || surfaces.getFirst() - (base.getY() - 1) < 8) continue; + int roof = naturalRoofAbove(base); + if (roof < base.getY() + 2 || !threeNaturalSupports(base)) continue; + BlockState state = inspectedState(base); + int height = 1; + if (state.is(Blocks.MUSHROOM_STEM)) { + while (height < 24 && inspectedState(base.above(height)).is(Blocks.MUSHROOM_STEM)) height++; + boolean cap = false; + for (int dx = -3; dx <= 3; dx++) for (int dz = -3; dz <= 3; dz++) { + BlockState top = inspectedState(base.offset(dx, height, dz)); + cap |= top != null && (top.is(Blocks.BROWN_MUSHROOM_BLOCK) || top.is(Blocks.RED_MUSHROOM_BLOCK)); + } + helper.assertTrue(cap, "A real lower huge mushroom requires its cap, not only a stem: " + base); + } else helper.assertTrue(state.canSurvive(level, base), "A real lower mushroom must survive under its natural roof: " + base); + mushrooms.add(new MushroomWitness(blockId(state), base, height, roof)); + } + for (var entry : exposed.entrySet()) { + exposedRocks.put(entry.getKey(), entry.getValue().size()); + Set remaining = new HashSet<>(entry.getValue()); + while (!remaining.isEmpty()) { + BlockPos first = remaining.iterator().next(); remaining.remove(first); + List patch = new ArrayList<>(); ArrayDeque queue = new ArrayDeque<>(); queue.add(first); + while (!queue.isEmpty()) { + BlockPos p = queue.removeFirst(); patch.add(p); + for (Direction direction : Direction.values()) { + BlockPos next = p.relative(direction); + if (remaining.remove(next)) queue.addLast(next); + } + } + rockPatches.add(new RockPatch(entry.getKey(), patch.size(), first, actualBoundsOf(patch))); + } + } + rockPatches.sort(Comparator.comparingInt(RockPatch::exposedBlocks).reversed()); + for (var entry : upperGround.entrySet()) { + BlockPos floor = entry.getValue(); + boolean inner = Math.hypot(floor.getX(), floor.getZ()) <= 128; + boolean canopy = leafColumns.getOrDefault(entry.getKey(), List.of()).stream().anyMatch(p -> p.getY() > floor.getY()); + if (inner) { innerGroundColumns++; if (canopy) innerCanopyColumns++; } + else { exteriorGroundColumns++; if (canopy) exteriorCanopyColumns++; } + } + verifyRemarkableTree(); + SanctuaryMod.LOGGER.info("Woodland observations seed {}: upper tree roots {}, interior oak {}, lower tree roots {}, roofed mushrooms {}, exposed rocks {}", + level.getSeed(), upperTreeCounts, interiorOakTrees, lowerTreeCounts, mushrooms.size(), exposedRocks); + } + + void verifyWoodlandReferenceExamples() { + if (level.getSeed() == 0L) { + int total = upperTreeCounts.values().stream().mapToInt(Integer::intValue).sum(); + helper.assertTrue(total > 0 && upperTreeCounts.getOrDefault("minecraft:oak_log", 0) * 2 > total, + "Reference seed must show an actual oak majority among sampled upper tree root groups: " + upperTreeCounts); + helper.assertTrue(interiorOakTrees > 0, "Reference seed must show rooted oaks inside radius 128, not only on the perimeter"); + helper.assertTrue(lowerTreeCounts.getOrDefault("minecraft:dark_oak_log", 0) > 0, + "Reference inspected lower terraces must demonstrate a real supported dark oak under natural rock"); + helper.assertTrue(!mushrooms.isEmpty(), "Reference inspected lower terraces must demonstrate a real supported mushroom under natural rock"); + for (String mineral : List.of("coal", "iron", "copper")) helper.assertTrue(outdoorOreCounts.getOrDefault(mineral, 0) > 0, + "Reference inspection must demonstrate naturally exposed outdoor " + mineral + " ore, not buried resources: " + exposedOreCounts); + for (String rock : List.of("minecraft:andesite", "minecraft:diorite", "minecraft:granite")) + helper.assertTrue(rockPatches.stream().anyMatch(patch -> patch.block().equals(rock) && patch.exposedBlocks() >= 8), + "Reference exposed geology must show a connected patch of at least eight visible " + rock + " blocks; actual=" + exposedRocks); + } + } + + void recordExposedOre(BlockPos p, BlockState state) { + String mineral = blockId(state).replace("minecraft:", "").replace("deepslate_", "").replace("_ore", ""); + List> faces = new ArrayList<>(); + boolean outdoor = false; + for (Direction direction : Direction.values()) { + BlockPos side = p.relative(direction); + BlockState neighbour = inspectedState(side); + if (neighbour == null || !neighbour.isAir()) continue; + var surfaces = naturalSurfaces(side.getX(), side.getZ()); + boolean sky = surfaces.isEmpty() || surfaces.getFirst() < side.getY(); + boolean openWall = !sky && direction.getAxis().isHorizontal(); + if (openWall) for (int step = 1; step <= 12; step++) { + BlockPos ray = p.relative(direction, step); + BlockState hit = inspectedState(ray); + if (hit == null || sample(ray.getX(), ray.getY(), ray.getZ()) > 0 + || hit.isCollisionShapeFullBlock(level, ray)) { openWall = false; break; } + } + String kind = sky ? "open_sky" : openWall ? "open_wall_12_blocks" : "sheltered_cavity"; + outdoor |= sky || openWall; + faces.add(Map.of("direction", direction.getName(), "air", side, "kind", kind)); + } + if (faces.isEmpty()) return; + exposedOreCounts.merge(mineral, 1, Integer::sum); + if (outdoor) outdoorOreCounts.merge(mineral, 1, Integer::sum); + exposedOreExamples.add(Map.of("position", p.immutable(), "block", blockId(state), "outdoor", outdoor, "air_faces", faces)); + } + + void verifyRemarkableTree() { + var selected = WoodlandGrovesFeature.remarkablePlan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(), random); + List remarkable = trees.stream().filter(tree -> !tree.lowerTerrace() && REMARKABLE_LOGS.contains(tree.log())).toList(); + remarkableTreeReport.put("observed_root_groups", remarkable); + String selectedSpecies = selected.map(tree -> tree.featureId().contains("jungle") ? "minecraft:jungle_log" + : tree.featureId().contains("pale_oak") ? "minecraft:pale_oak_log" + : tree.featureId().replace("minecraft:", "").replace("_bonemeal", "") + "_log").orElse(""); + if (!selectedSpecies.isEmpty() && !selectedSpecies.contains(":")) selectedSpecies = "minecraft:" + selectedSpecies; + int observedSelectedLogs = 0; + if (selected.isPresent()) { + var site = selected.orElseThrow(); + for (int x = site.base().getX() - site.radius(); x <= site.base().getX() + site.radius(); x++) + for (int z = site.base().getZ() - site.radius(); z <= site.base().getZ() + site.radius(); z++) + for (int y = site.base().getY(); y <= site.base().getY() + site.height(); y++) { + BlockState state = inspectedState(new BlockPos(x, y, z)); + helper.assertTrue(state != null, "The optional remarkable-tree footprint must be inspected in full"); + if (blockId(state).equals(selectedSpecies)) observedSelectedLogs++; + } + } + remarkableTreeReport.put("observed_selected_log_blocks_in_planned_volume", observedSelectedLogs); + remarkableTreeReport.put("planted", observedSelectedLogs > 0); + remarkableTreeReport.put("expected_log", selectedSpecies); + remarkableTreeReport.put("scope", "One optional planned site, inspected in full; no requirement for each species or for successful placement"); + helper.assertTrue(remarkable.size() <= 1, "At most one remarkable tree root group may be observed: " + remarkable); + for (TreeWitness tree : remarkable) { + helper.assertTrue(selected.isPresent(), "An observed remarkable tree needs a declared seed-selected site: " + tree); + var plan = selected.orElseThrow(); + helper.assertTrue(Math.abs(tree.base().getX() - plan.base().getX()) <= plan.radius() + && Math.abs(tree.base().getZ() - plan.base().getZ()) <= plan.radius() + && tree.base().getY() >= plan.base().getY() - 1 && tree.base().getY() <= plan.base().getY() + plan.height(), + "The optional remarkable tree must belong to its single planned site: " + tree + ", plan=" + plan); + } + } + + void verifyWoodlandSurvives() { + for (TreeWitness tree : trees) { + for (BlockPos p : tree.footprint()) { + BlockState state = inspectedState(p), floor = inspectedState(p.below()); + helper.assertTrue(isVerticalLog(state) && blockId(state).equals(tree.log()) + && floor != null && floor.isCollisionShapeFullBlock(level, p.below()), + "Observed tree feet must retain stable support after 1800 actual fluid ticks: " + tree + ", foot=" + p); + } + } + for (MushroomWitness mushroom : mushrooms) { + BlockState state = inspectedState(mushroom.base()); + helper.assertTrue(state != null && blockId(state).equals(mushroom.block()) && threeNaturalSupports(mushroom.base()) + && (state.is(Blocks.MUSHROOM_STEM) || state.canSurvive(level, mushroom.base())), + "Observed lower mushrooms must remain on their natural support after 1800 ticks: " + mushroom); + } + } + + void verifyLowerPlantsStillSurvive() { + for (BlockPos p : lowerPlants.keySet()) { + BlockState current = level.getBlockState(p); + helper.assertTrue(isTerracePlant(current) && current.canSurvive(level, p) + && level.getBlockState(p.below()).isCollisionShapeFullBlock(level, p.below()), + "Lower-terrace vegetation must remain on stable support after the real fluid simulation: " + p); + } + } + + static BlockPos position(LayeredHydrology.Position p) { return new BlockPos(p.x(), p.y(), p.z()); } + + void verifyStarterLava() { + if (!expectedLava.isEmpty()) { + helper.assertTrue(expectedLava.size() == 18, "A selected starter niche contains eighteen source blocks, not a large lava lake"); + helper.assertTrue(lavaPlan.access().isPresent(), "Selected lava needs a reachable dry approach"); + } + for (BlockPos p : expectedLava) { + helper.assertTrue(level.getFluidState(p).is(FluidTags.LAVA) && level.getFluidState(p).isSource(), + "The small lava deposit must retain every planned source after real ticks: " + p); + for (Direction direction : Direction.values()) { + if (direction == Direction.UP) continue; + BlockPos side = p.relative(direction); + if (expectedLava.contains(side)) continue; + helper.assertTrue(sample(side.getX(), side.getY(), side.getZ()) > 0 + && level.getBlockState(side).isCollisionShapeFullBlock(level, side), + "Lava needs a complete natural floor and side shell, including chunk boundaries: " + side); + } + } + for (var support : lavaPlan.supports()) { + BlockPos p = new BlockPos(support.x(), support.y(), support.z()); + BlockState state = level.getBlockState(p); + helper.assertTrue(sample(p.getX(), p.getY(), p.getZ()) > 0 + && state.isCollisionShapeFullBlock(level, p) && !state.ignitedByLava(), + "Lava supports must remain natural, full and nonflammable: " + p); + } + lavaPlan.access().ifPresent(access -> { + BlockPos p = new BlockPos(access.x(), access.y(), access.z()); + helper.assertTrue(level.getBlockState(p).isAir() && level.getBlockState(p.above()).isAir() + && level.getBlockState(p.below()).isCollisionShapeFullBlock(level, p.below()), + "The lava access requires a stable floor and two dry free blocks: " + p); + }); + int[] observed = {0}; + for (LevelChunk chunk : chunks.values()) chunk.findBlocks(state -> state.getFluidState().is(FluidTags.LAVA) + || state.is(Blocks.FIRE) || state.is(Blocks.SOUL_FIRE), (p, state) -> { + helper.assertFalse(state.is(Blocks.FIRE) || state.is(Blocks.SOUL_FIRE), + "Generated lava must not ignite surrounding decoration: " + p); + helper.assertTrue(expectedLava.contains(p), "No uncontrolled lava may leave the declared niche: " + p); + observed[0]++; + }); + helper.assertTrue(observed[0] == expectedLava.size(), "Observed lava must exactly match the small declared niche"); + } + + void startWhenFluidChunksReady() { + if (closed || simulationStarted || helper.getTick() < 20) return; + List pending = fluidChunks.stream().filter(packed -> !level.isPositionTickingWithEntitiesLoaded(packed)).sorted().toList(); + if (!pending.isEmpty()) { + if (helper.getTick() % 200 == 0) { + SanctuaryMod.LOGGER.info("Hydrology awaiting actual fluid scheduler readiness, seed {}, tick {}, {} pending chunks: {}", + level.getSeed(), helper.getTick(), pending.size(), pending.stream().limit(8).map(packed -> { + var pos = ChunkPos.unpack(packed); + return pos + " tickets=" + level.shouldTickBlocksAt(packed) + " entities=" + level.areEntitiesLoaded(packed) + + " tickingFuture=" + level.getChunkSource().isPositionTicking(packed); + }).toList()); + } + helper.assertTrue(helper.getTick() < 600, + "Hydrology could not start real fluid simulation: " + pending.size() + " chunks did not become scheduler-ready"); + return; + } + verifySpringMouthsAfterPostProcessing(); + logSpringStates("post-processing complete before measured fluid simulation"); + simulationStarted = true; + simulationStartGameTime = level.getGameTime(); + startFluidWitness(); + helper.runAfterDelay(30, () -> { + if (closed) return; + try { + verifyFluidWitnessAndRemoveIt(); + } catch (RuntimeException | Error failure) { + cleanup(); + throw failure; + } + }); + helper.runAfterDelay(1800, () -> { + if (closed) return; + try { + logSpringStates("after 1800 fluid ticks"); + // Export before assertions so a failed source still leaves its actual profile. + export("after-1800-ticks", false); + for (long packed : fluidChunks) helper.assertTrue(level.isPositionTickingWithEntitiesLoaded(packed), + "Fluid chunk must remain scheduler-ready throughout the simulation: " + ChunkPos.unpack(packed)); + verifyNaturalShellAndFinishedWater(); + verifyAllObservedWaterIsPlanned(); + verifySpringDescent(); + verifyLowerPlantsStillSurvive(); + verifyWoodlandSurvives(); + export("after-1800-ticks", true); + verifyPostProcessingDoesNotRepairOldMouth(); + SanctuaryMod.LOGGER.info("Natural hydrology seed {}: {} basin sources retained through {} world ticks, {} seams checked", + level.getSeed(), expectedWater.size(), level.getGameTime() - simulationStartGameTime, seams); + helper.succeed(); + } catch (IOException exception) { + throw new IllegalStateException("Could not write natural hydrology diagnostics", exception); + } finally { + cleanup(); + } + }); + } + + void startFluidWitness() { + for (long packed : fluidChunks) helper.assertTrue(level.isPositionTickingWithEntitiesLoaded(packed), + "Every fluid chunk must satisfy the exact production scheduler predicate: " + ChunkPos.unpack(packed)); + for (BlockPos witness : witnesses) { + for (int dx = -1; dx <= 1; dx++) { + for (int dz = -1; dz <= 1; dz++) { + for (int dy = -4; dy <= 0; dy++) helper.assertTrue(level.getBlockState(witness.offset(dx, dy, dz)).isAir(), + "A fluid witness must occupy empty development test space"); + } + } + } + witnessPlaced = true; + for (BlockPos witness : witnesses) { + for (int dx = -1; dx <= 1; dx++) { + for (int dz = -1; dz <= 1; dz++) { + for (int dy = -4; dy <= 0; dy++) { + if (dx != 0 || dz != 0 || dy == -4) level.setBlock(witness.offset(dx, dy, dz), Blocks.STONE.defaultBlockState(), 3); + } + } + } + level.setBlock(witness, Blocks.WATER.defaultBlockState(), 3); + helper.assertTrue(level.getFluidState(witness.below()).isEmpty(), "Witness begins with an empty cell below its source"); + level.scheduleTick(witness, Fluids.WATER, 1); + } + for (BlockPos water : expectedWater) level.scheduleTick(water, Fluids.WATER, 1); + for (FlowRoot root : flowRoots) level.scheduleTick(root.position(), Fluids.WATER, 1); + for (BlockPos lava : expectedLava) level.scheduleTick(lava, Fluids.LAVA, 1); + allWaterChunksTicking = true; + SanctuaryMod.LOGGER.info("Natural hydrology seed {}: {} actual scheduler-ready chunks, {} independent fluid witnesses, start tick {}", + level.getSeed(), fluidChunks.size(), witnesses.size(), simulationStartGameTime); + } + + void verifyFluidWitnessAndRemoveIt() { + try { + for (BlockPos witness : witnesses) { + helper.assertTrue(level.getFluidState(witness.below()).is(FluidTags.WATER), + "A real fluid witness must flow in each spring and lava chunk: " + witness + + ", schedulerReady=" + level.isPositionTickingWithEntitiesLoaded(ChunkPos.pack(witness))); + SanctuaryMod.LOGGER.info("Natural hydrology seed {}: independent fluid witness passed at {}", level.getSeed(), witness); + } + } finally { + removeWitness(); + } + } + + void removeWitness() { + if (!witnessPlaced) return; + for (BlockPos witness : witnesses) { + // Remove contained fluid before opening the walls of this disposable test fixture. + for (int dy = 0; dy >= -3; dy--) level.setBlock(witness.offset(0, dy, 0), Blocks.AIR.defaultBlockState(), 3); + for (int dx = -1; dx <= 1; dx++) for (int dz = -1; dz <= 1; dz++) { + for (int dy = -4; dy <= 0; dy++) level.setBlock(witness.offset(dx, dy, dz), Blocks.AIR.defaultBlockState(), 3); + } + } + witnessPlaced = false; + } + + void logSpringStates(String phase) { + for (var spring : plan.springs()) SanctuaryMod.LOGGER.info("Spring state {}, seed {}: {}", phase, level.getSeed(), springState(spring)); + } + + String springState(LayeredHydrology.Spring spring) { + BlockPos source = position(spring.source()), outlet = position(spring.outlet()); + List column = new ArrayList<>(); + for (int d = 0; d <= 10; d++) column.add("Y" + (outlet.getY() - d) + "=" + level.getBlockState(outlet.below(d))); + long packed = ChunkPos.pack(source); + return "source=" + source + "=" + level.getBlockState(source) + ", outlet=" + outlet + ", below=" + column + + ", tickets=" + level.shouldTickBlocksAt(packed) + ", entities=" + level.areEntitiesLoaded(packed) + + ", tickingFuture=" + level.getChunkSource().isPositionTicking(packed) + + ", schedulerReady=" + level.isPositionTickingWithEntitiesLoaded(packed); + } + + void cleanup() { + if (closed) return; + closed = true; + removeWitness(); + for (long packed : newlyForced) { + ChunkPos chunk = ChunkPos.unpack(packed); + level.setChunkForced(chunk.x(), chunk.z(), false); + } + } + + float sample(int x, int y, int z) { + return random.sampleBlockValueUncached(natural, x, y, z); + } + + void export(String phase, boolean snapshotChecksPassed) throws IOException { + Path directory = Path.of("diagnostics"); + Files.createDirectories(directory); + String stem = "woodland-seed-" + level.getSeed() + "-" + phase; + Map report = new LinkedHashMap<>(); + report.put("schema_version", 1); + report.put("created_at_utc", Instant.now().toString()); + report.put("generator_noise_settings", ((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator()) + .generatorSettings().getRegisteredName()); + report.put("mod_version", FabricLoader.getInstance().getModContainer("sanctuary").orElseThrow() + .getMetadata().getVersion().getFriendlyString()); + report.put("seed", level.getSeed()); + report.put("phase", phase); + report.put("snapshot_checks_passed", snapshotChecksPassed); + int[] visibleBlocks = {0, 0, 0, 0, 0, 0}; + Set actualFlow = new HashSet<>(); + for (LevelChunk chunk : chunks.values()) chunk.findBlocks( + state -> !state.getFluidState().isEmpty() || state.is(Blocks.SUGAR_CANE), (p, state) -> { + if (state.getFluidState().is(FluidTags.WATER)) { + visibleBlocks[0]++; + if (!expectedWater.contains(p)) { + visibleBlocks[1]++; + actualFlow.add(p.immutable()); + } else visibleBlocks[5]++; + } else if (state.getFluidState().is(FluidTags.LAVA)) { + visibleBlocks[2]++; + if (state.getFluidState().isSource()) visibleBlocks[4]++; + } + if (state.is(Blocks.SUGAR_CANE)) visibleBlocks[3]++; + }); + analyseActualFlow(actualFlow); + report.put("actual_flow_components", actualFlowComponents); + report.put("flow_provenance_rule", "Actual water outside retained basins is six-neighbour connected to declared rock springs, with no horizontal neighbour outside the finite inspection"); + report.put("flow_prediction_role", "Bare-rock density prediction is a comparison, not a post-decoration safety envelope; trees may divert real water"); + report.put("flow_component_mergers", actualFlowComponents.stream().filter(component -> component.sourceIds().size() > 1).toList()); + report.put("actual_water_blocks", visibleBlocks[0]); + report.put("actual_lava_blocks", visibleBlocks[2]); + report.put("actual_lava_source_blocks", visibleBlocks[4]); + report.put("planned_closed_basin_water_blocks", declaredWater.size()); + report.put("expected_retained_closed_basin_water_blocks", expectedWater.size()); + report.put("natural_log_obstructions", naturalLogObstructions.entrySet().stream().map(entry -> Map.of( + "x", entry.getKey().getX(), "y", entry.getKey().getY(), "z", entry.getKey().getZ(), + "block", BuiltInRegistries.BLOCK.getKey(entry.getValue().getBlock()).toString(), + "state", entry.getValue().toString())).toList()); + report.put("actual_closed_basin_water_blocks", visibleBlocks[5]); + report.put("actual_water_blocks_outside_closed_basin", visibleBlocks[1]); + report.put("planned_starter_lava_sources", expectedLava.size()); + report.put("starter_lava_cells", lavaPlan.cells()); + report.put("starter_lava_natural_supports", lavaPlan.supports()); + report.put("starter_lava_access", lavaPlan.access().orElse(null)); + report.put("springs", plan.springs()); + report.put("spring_mouth_diagnostics", springMouthDiagnostics()); + report.put("spring_mouths_checked_after_post_processing", springMouthsCheckedAfterPostProcessing); + report.put("flow_roots", flowRoots.stream().map(root -> Map.of("id", root.id(), "kind", root.kind(), + "feature_id", root.featureId(), "x", root.position().getX(), "y", root.position().getY(), "z", root.position().getZ())).toList()); + report.put("actual_spring_descent_blocks", springDescent); + report.put("spring_descent_basis", "Lowest actual water in the source-connected component; merged sources share their explicitly reported component"); + report.put("maximum_dry_shore_manhattan_distance_to_water", maxDryShoreDistance); + report.put("actual_dry_shore_top_materials", actualDryShoreTopMaterials); + report.put("actual_dry_shore_materials_by_depth_below_top", actualDryShoreMaterialsByDepth); + report.put("sediment_material_verification", "Every actual layer is compared with WoodlandMaterials; only painted natural rock permits configured rock/ore or ore_dirt replacements, never arbitrary full blocks"); + report.put("actual_resources_in_inspected_chunks", resourceCounts); + report.put("actual_lower_terrace_vegetation", lowerVegetation); + report.put("lower_vegetation_scope", "Observed plants on lower natural floors with three original and actual solid supports; vanilla plants on thinner ledges are reported separately"); + report.put("thin_natural_ledge_plants", thinNaturalLedgePlants); + report.put("initial_lower_plants_on_moss_observed_not_required", lowerPlantsOnMoss); + report.put("initial_upper_surface_flowers", upperFlowers); + report.put("lower_vegetation_examples", lowerPlants.entrySet().stream().limit(64).map(entry -> Map.of( + "x", entry.getKey().getX(), "y", entry.getKey().getY(), "z", entry.getKey().getZ(), + "initial_state", entry.getValue().toString(), "current_state", level.getBlockState(entry.getKey()).toString(), + "floor_state", level.getBlockState(entry.getKey().below()).toString())).toList()); + report.put("actual_lower_terrace_biomes", lowerSurfaceBiomes); + report.put("verified_vertical_biome_contrasts", verticalBiomeContrasts); + report.put("actual_upper_surface_material_columns", surfaceMaterials); + report.put("upper_ground_measurement", "Highest actual terrain block per inspected column, excluding leaves, logs and plants; canopy height is not ground height"); + report.put("actual_upper_tree_root_groups", upperTreeCounts); + report.put("actual_lower_tree_root_groups", lowerTreeCounts); + report.put("tree_root_evidence", trees); + report.put("tree_root_measurement", "Vertical trunks at least three logs tall, natural ground at the foot, matching observed leaves; adjacent same-species feet at the same Y form one group including 2x2 trunks"); + report.put("incomplete_tree_or_grounded_branch_evidence", incompleteTreeEvidence); + report.put("interior_oak_root_groups_radius_128", interiorOakTrees); + report.put("canopy_projection", Map.of("interior_ground_columns", innerGroundColumns, + "interior_columns_with_leaves_above_ground", innerCanopyColumns, "exterior_ground_columns", exteriorGroundColumns, + "exterior_columns_with_leaves_above_ground", exteriorCanopyColumns, + "scope", "Projection over inspected terrain columns only; separate from trunk groups and not an island-wide percentage")); + report.put("actual_roofed_mushrooms", mushrooms); + report.put("remarkable_tree", remarkableTreeReport); + report.put("actual_exposed_rock_blocks", exposedRocks); + report.put("actual_exposed_rock_connected_patches", rockPatches); + report.put("rock_patch_scope", "Six-neighbour connected exposed blocks; their actual neighbouring space is open and the original density there is empty"); + report.put("actual_ore_blocks_with_air_face", exposedOreCounts); + report.put("actual_outdoor_ore_blocks_with_air_face", outdoorOreCounts); + report.put("actual_exposed_ore_locations", exposedOreExamples); + report.put("ore_exposure_definition", "Actual AIR on at least one face. Outdoor means above the raw highest surface in that air column, or a 12-block natural horizontal opening inspected in full; other air faces are reported as sheltered cavities"); + report.put("ecology_added_chunks", ecologyExtraChunks.stream().map(ChunkPos::unpack).toList()); + report.put("central_terrain_targets", centralTerrainTargets); + report.put("ecology_sampling", "Hydrology footprint plus at most 24 targeted ecology chunks and the one optional remarkable-tree volume; selected biome/ledge targets bias this sample, never an island-wide forest inventory"); + report.put("lower_terrace_targets", lowerTerraceTargets.stream().map(p -> Map.of("x", p.getX(), "y", p.getY(), "z", p.getZ())).toList()); + report.put("actual_biomes_sampled_on_surface_every_four_blocks", surfaceBiomes); + report.put("verified_real_biome_locations", biomeTargets.entrySet().stream().map(entry -> Map.of( + "biome", entry.getKey(), "x", entry.getValue().getX(), "y", entry.getValue().getY(), "z", entry.getValue().getZ())).toList()); + report.put("resource_scope", "Exact palette counts in the listed FULL chunks before tick simulation; not an island-wide total"); + report.put("actual_cane_blocks", visibleBlocks[3]); + report.put("checked_chunk_seam_adjacencies", seams); + report.put("simulation_started_after_all_fluid_chunks_were_scheduler_ready", allWaterChunksTicking); + report.put("official_entity_load_wait_ms", entityLoadWaitMillis); + report.put("entity_load_scope", "All inspection tickets declared before FULL loading; waitForEntities with radius zero on those same chunks before accelerated tick simulation"); + report.put("all_fluid_chunks_currently_scheduler_ready", fluidChunks.stream().allMatch(level::isPositionTickingWithEntitiesLoaded)); + report.put("simulation_world_ticks_elapsed", simulationStarted ? level.getGameTime() - simulationStartGameTime : 0); + report.put("independent_fluid_witnesses", witnesses.stream().map(p -> Map.of("x", p.getX(), "y", p.getY(), "z", p.getZ())).toList()); + report.put("inspected_full_chunks", chunks.values().stream().map(chunk -> + Map.of("x", chunk.getPos().x(), "z", chunk.getPos().z())).toList()); + report.put("features", plan.features()); + report.put("shore_diagnostics_by_feature", shoreDiagnostics()); + report.put("planned_columns", plan.cells()); + report.put("scope", "actual blocks in completed site chunks and a one-chunk halo; uninspected map cells remain blank"); + Files.writeString(directory.resolve(stem + ".json"), new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n"); + exportMap(directory.resolve(stem + ".png"), phase); + exportLowerMap(directory.resolve(stem + "-lower-terraces.png"), phase); + exportCanopyMap(directory.resolve(stem + "-canopy.png"), phase); + for (var spring : plan.springs()) exportSpringProfile(directory.resolve(stem + "-spring-" + spring.id() + ".png"), spring, phase); + } + + void exportLowerMap(Path output, String phase) throws IOException { + Map plantsByChunk = new HashMap<>(); + for (BlockPos p : lowerPlants.keySet()) plantsByChunk.merge(ChunkPos.pack(p), 1, Integer::sum); + List selected = plantsByChunk.entrySet().stream() + .sorted(Map.Entry.comparingByValue().reversed().thenComparing(Map.Entry.comparingByKey())) + .limit(6).map(Map.Entry::getKey).toList(); + if (selected.isEmpty()) selected = lowerTerraceTargets.stream().map(ChunkPos::pack).distinct().limit(6).toList(); + int tile = 208, scale = 12, rows = Math.max(1, (selected.size() + 2) / 3); + BufferedImage image = new BufferedImage(760, rows * 242 + 100, BufferedImage.TYPE_INT_RGB); + Graphics2D g = image.createGraphics(); + g.setColor(new Color(0xF0F0EC)); g.fillRect(0, 0, image.getWidth(), image.getHeight()); + g.setColor(new Color(0x17222C)); g.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16)); + g.drawString("Actual lower terraces | seed " + level.getSeed() + " | " + phase, 20, 25); + g.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); + g.drawString("Six inspected chunks with most observed lower plants. Highest valid lower ledge per column.", 20, 46); + g.drawString("Moss dark green; grass muted green; flowers pink; plants bright green; stone grey; no lower ledge dark blue.", 20, 64); + g.drawString("Upper floors are removed from this diagnostic view only. No new chunks loaded; not a Minecraft render.", 20, 82); + for (int i = 0; i < selected.size(); i++) { + LevelChunk chunk = chunks.get(selected.get(i)); + int left = 20 + (i % 3) * 242, top = 105 + (i / 3) * 242; + g.setColor(new Color(0x17222C)); g.drawString("Chunk " + chunk.getPos(), left, top); + for (int lx = 0; lx < 16; lx++) for (int lz = 0; lz < 16; lz++) { + int x = chunk.getPos().getMinBlockX() + lx, z = chunk.getPos().getMinBlockZ() + lz; + int color = 0x142738; + var surfaces = naturalSurfaces(x, z); + for (int y : surfaces) { + if (surfaces.getFirst() - y < 8) continue; + BlockPos floor = new BlockPos(x, y, z); + BlockState ground = chunk.getBlockState(floor), plant = chunk.getBlockState(floor.above()); + if (!ground.isCollisionShapeFullBlock(level, floor) || !isTerrain(ground)) continue; + color = blockColor(ground, y); + if (isTerracePlant(plant)) color = plant.is(BlockTags.FLOWERS) ? 0xE49AC5 : 0xA7CF70; + break; + } + g.setColor(new Color(color)); g.fillRect(left + lx * scale, top + 10 + lz * scale, scale, scale); + } + g.setColor(new Color(0x17222C)); g.drawRect(left, top + 10, tile - 16, tile - 16); + } + g.dispose(); ImageIO.write(image, "png", output.toFile()); + } + + int blockColor(BlockState state, int y) { + if (state.getFluidState().is(FluidTags.WATER)) return 0x207BD4; + if (state.getFluidState().is(FluidTags.LAVA)) return 0xED691E; + if (state.is(Blocks.SAND)) return 0xD5B668; + if (state.is(Blocks.GRAVEL)) return 0x9B8878; + if (state.is(Blocks.CLAY)) return 0x6D9FAD; + if (state.is(Blocks.MOSS_BLOCK)) return 0x58734C; + if (state.is(Blocks.GRASS_BLOCK)) return 0x879D73; + if (state.is(Blocks.PODZOL)) return 0x86633E; + if (state.is(Blocks.MYCELIUM)) return 0x958095; + if (state.is(Blocks.ANDESITE)) return 0x87999A; + if (state.is(Blocks.DIORITE)) return 0xDBD9CC; + if (state.is(Blocks.GRANITE)) return 0xBD8880; + if (state.is(Blocks.SULFUR)) return 0xD6C464; + if (state.isAir()) return 0x142738; + int value = 40 + Math.clamp(y, 0, level.getHeight()) * 180 / level.getHeight(); + return value << 16 | value << 8 | value; + } + + void exportCanopyMap(Path output, String phase) throws IOException { + int minX = chunks.values().stream().mapToInt(chunk -> chunk.getPos().getMinBlockX()).min().orElseThrow(); + int maxX = chunks.values().stream().mapToInt(chunk -> chunk.getPos().getMaxBlockX()).max().orElseThrow(); + int minZ = chunks.values().stream().mapToInt(chunk -> chunk.getPos().getMinBlockZ()).min().orElseThrow(); + int maxZ = chunks.values().stream().mapToInt(chunk -> chunk.getPos().getMaxBlockZ()).max().orElseThrow(); + int scale = 2, top = 125, left = 20; + BufferedImage image = new BufferedImage(Math.max(1060, (maxX - minX + 1) * scale + 40), + (maxZ - minZ + 1) * scale + top + 20, BufferedImage.TYPE_INT_RGB); + Graphics2D g = image.createGraphics(); + g.setColor(new Color(0xF0F0EC)); g.fillRect(0, 0, image.getWidth(), image.getHeight()); + g.setColor(new Color(0x17222C)); g.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16)); + g.drawString("Actual canopy and rooted trees | seed " + level.getSeed() + " | " + phase, 20, 26); + g.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); + g.drawString("Upper roots " + upperTreeCounts + "; interior oaks " + interiorOakTrees + ". Ground is measured below leaves.", 20, 48); + g.drawString("Leaves green; oak feet gold; other upper feet white; lower dark-oak feet purple; rare tree magenta. Circle: radius 128.", 20, 68); + g.drawString("Only inspected chunks, including selected ecology targets. Dark brown: uninspected; dark blue: void. Not a Minecraft render.", 20, 88); + g.drawString("Andesite blue-grey; diorite pale; granite pink; podzol brown; mycelium mauve. No new chunk is loaded for this view.", 20, 108); + for (int x = minX; x <= maxX; x++) for (int z = minZ; z <= maxZ; z++) { + LevelChunk chunk = chunks.get(ChunkPos.pack(x >> 4, z >> 4)); + int color = 0x3A3030; + if (chunk != null) { + color = 0x142738; + for (int y = chunk.getHeight(Heightmap.Types.WORLD_SURFACE, x, z); y >= level.getMinY(); y--) { + BlockState state = chunk.getBlockState(new BlockPos(x, y, z)); + if (state.isAir() || isTerracePlant(state)) continue; + if (state.is(BlockTags.LEAVES)) { color = state.is(Blocks.DARK_OAK_LEAVES) ? 0x345947 : 0x5B8B51; break; } + if (state.is(BlockTags.LOGS)) { color = 0x73573C; break; } + if (state.getFluidState().isEmpty() && !isTerrain(state)) continue; + color = blockColor(state, y); break; + } + } + g.setColor(new Color(color)); g.fillRect(left + (x - minX) * scale, top + (z - minZ) * scale, scale, scale); + } + g.setColor(new Color(255, 255, 255, 110)); + g.drawOval(left + (-128 - minX) * scale, top + (-128 - minZ) * scale, 256 * scale, 256 * scale); + for (TreeWitness tree : trees) { + int color = tree.lowerTerrace() ? 0xB496E9 : tree.log().equals("minecraft:oak_log") ? 0xFFD76B + : REMARKABLE_LOGS.contains(tree.log()) ? 0xEE72CB : 0xF8F4E7; + int x = left + (tree.base().getX() - minX) * scale, z = top + (tree.base().getZ() - minZ) * scale; + g.setColor(new Color(0x17222C)); g.fillOval(x - 3, z - 3, 7, 7); + g.setColor(new Color(color)); g.fillOval(x - 2, z - 2, 5, 5); + } + g.dispose(); ImageIO.write(image, "png", output.toFile()); + } + + Map> shoreDiagnostics() { + Map> result = new LinkedHashMap<>(); + for (var feature : plan.features()) { + int dryBanks = 0; + int banksAtWaterLevel = 0; + int baseCanSurvive = 0; + int unobstructedBases = 0; + int viableUnobstructedBases = 0; + int actualCane = 0; + for (var cell : plan.cells()) { + if (cell.featureId() != feature.id() || cell.hasWater()) continue; + dryBanks++; + if (cell.bedY() == feature.waterY()) banksAtWaterLevel++; + BlockPos base = new BlockPos(cell.x(), cell.bedY() + 1, cell.z()); + boolean viable = Blocks.SUGAR_CANE.defaultBlockState().canSurvive(level, base); + boolean clear = level.getBlockState(base).isAir() || level.getBlockState(base).is(Blocks.SUGAR_CANE); + if (viable) baseCanSurvive++; + if (clear) unobstructedBases++; + if (viable && clear) viableUnobstructedBases++; + for (int height = 0; height < 4; height++) { + if (level.getBlockState(base.above(height)).is(Blocks.SUGAR_CANE)) actualCane++; + else break; + } + } + result.put(feature.id(), Map.of("kind", feature.kind().name(), "water_y", feature.waterY(), + "dry_bank_columns", dryBanks, "bank_surface_at_water_level", banksAtWaterLevel, + "bases_where_cane_can_survive", baseCanSurvive, "unobstructed_bases", unobstructedBases, + "viable_unobstructed_bases", viableUnobstructedBases, + "actual_cane_blocks_on_planned_banks", actualCane)); + } + return result; + } + + void exportSpringProfile(Path output, LayeredHydrology.Spring spring, String phase) throws IOException { + int dx = spring.outlet().x() - spring.source().x(); + int dz = spring.outlet().z() - spring.source().z(); + int first = -8, last = 32, top = Math.min(level.getMaxY(), spring.source().y() + 12); + int scale = 3, width = (last - first + 1) * scale, height = (top - level.getMinY() + 1) * scale; + BufferedImage image = new BufferedImage(Math.max(720, width + 40), height + 105, BufferedImage.TYPE_INT_RGB); + Graphics2D graphics = image.createGraphics(); + graphics.setColor(new Color(0xF0F0EC)); graphics.fillRect(0, 0, image.getWidth(), image.getHeight()); + graphics.setColor(new Color(0x17222C)); graphics.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 15)); + graphics.drawString("Actual spring profile | seed " + level.getSeed() + " | " + phase, 20, 24); + graphics.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); + graphics.drawString("Source " + spring.source() + "; plane follows its horizontal outlet.", 20, 44); + graphics.drawString("Water blue; rock grey; air dark blue; uninspected brown. Not a Minecraft render.", 20, 62); + graphics.drawString("Y=" + top + ".." + level.getMinY() + ". Sideways flow outside this plane is listed in the JSON.", 20, 80); + for (int step = first; step <= last; step++) { + for (int y = level.getMinY(); y <= top; y++) { + int x = spring.source().x() + dx * step, z = spring.source().z() + dz * step; + LevelChunk chunk = chunks.get(ChunkPos.pack(x >> 4, z >> 4)); + int color = 0x3A3030; + if (chunk != null) { + BlockState state = chunk.getBlockState(new BlockPos(x, y, z)); + color = state.isAir() ? 0x142738 : state.getFluidState().is(FluidTags.WATER) ? 0x207BD4 + : state.is(Blocks.SAND) ? 0xD5B668 : state.is(Blocks.GRAVEL) ? 0x9B8878 + : state.is(Blocks.CLAY) ? 0x6D9FAD : 0xA2A2A2; + } + graphics.setColor(new Color(color)); + graphics.fillRect(20 + (step - first) * scale, 95 + (top - y) * scale, scale, scale); + } + } + graphics.dispose(); ImageIO.write(image, "png", output.toFile()); + } + + void exportMap(Path output, String phase) throws IOException { + int minX = chunks.values().stream().mapToInt(chunk -> chunk.getPos().getMinBlockX()).min().orElseThrow(); + int maxX = chunks.values().stream().mapToInt(chunk -> chunk.getPos().getMaxBlockX()).max().orElseThrow(); + int minZ = chunks.values().stream().mapToInt(chunk -> chunk.getPos().getMinBlockZ()).min().orElseThrow(); + int maxZ = chunks.values().stream().mapToInt(chunk -> chunk.getPos().getMaxBlockZ()).max().orElseThrow(); + int scale = Math.max(1, Math.min(3, 1000 / Math.max(maxX - minX + 1, maxZ - minZ + 1))); + int width = (maxX - minX + 1) * scale; + int height = (maxZ - minZ + 1) * scale; + BufferedImage image = new BufferedImage(Math.max(width + 40, 920), height + 110, BufferedImage.TYPE_INT_RGB); + Graphics2D graphics = image.createGraphics(); + graphics.setColor(new Color(0xF0F0EC)); + graphics.fillRect(0, 0, image.getWidth(), image.getHeight()); + graphics.setColor(new Color(0x17222C)); + graphics.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 17)); + graphics.drawString("Actual FULL chunks | seed " + level.getSeed() + " | " + phase, 20, 24); + graphics.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); + graphics.drawString("Water blue; sand gold; gravel brown; moss/grass muted green; flowers pink; stone grey. Canopy skipped.", 20, 45); + graphics.drawString("Not a Minecraft render. X=" + minX + ".." + maxX + ", Z=" + minZ + ".." + maxZ + + ". Dark blue = void; dark brown = not inspected; faint lines = chunk seams.", 20, 63); + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (int x = minX; x <= maxX; x++) { + for (int z = minZ; z <= maxZ; z++) { + LevelChunk chunk = chunks.get(ChunkPos.pack(x >> 4, z >> 4)); + int color = 0x3A3030; + if (chunk != null) { + color = 0x142738; + for (int y = chunk.getHeight(Heightmap.Types.WORLD_SURFACE, x, z); y >= level.getMinY(); y--) { + position.set(x, y, z); + BlockState state = chunk.getBlockState(position); + if (state.getFluidState().is(FluidTags.WATER)) { color = 0x207BD4; break; } + if (state.getFluidState().is(FluidTags.LAVA)) { color = 0xED691E; break; } + if (state.is(Blocks.SUGAR_CANE)) { color = 0x7AFF38; break; } + if (state.is(Blocks.SAND)) { color = 0xD5B668; break; } + if (state.is(Blocks.CLAY)) { color = 0x6D9FAD; break; } + if (state.is(Blocks.GRAVEL)) { color = 0x9B8878; break; } + if (state.is(BlockTags.FLOWERS)) { color = 0xE49AC5; break; } + if (state.is(Blocks.MOSS_BLOCK) || state.is(Blocks.GRASS_BLOCK)) { color = blockColor(state, y); break; } + if (state.isAir() || state.is(BlockTags.LEAVES) || state.is(BlockTags.LOGS) + || state.getCollisionShape(level, position).isEmpty()) continue; + int value = 40 + Math.clamp(y, 0, level.getHeight()) * 180 / level.getHeight(); + color = value << 16 | value << 8 | value; + break; + } + } + graphics.setColor(new Color(color)); + graphics.fillRect(20 + (x - minX) * scale, 80 + (z - minZ) * scale, scale, scale); + } + } + graphics.setColor(new Color(255, 255, 255, 35)); + for (int x = minX; x <= maxX; x += 16) graphics.drawLine(20 + (x - minX) * scale, 80, 20 + (x - minX) * scale, 80 + height); + for (int z = minZ; z <= maxZ; z += 16) graphics.drawLine(20, 80 + (z - minZ) * scale, 20 + width, 80 + (z - minZ) * scale); + graphics.dispose(); + ImageIO.write(image, "png", output.toFile()); + } + } +} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/SanctuaryMod.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/SanctuaryMod.java index aec4cf4..47e8dee 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/SanctuaryMod.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/SanctuaryMod.java @@ -1,6 +1,9 @@ package fr.koka.sanctuary; import fr.koka.sanctuary.worldgen.MainIslandDensity; +import fr.koka.sanctuary.worldgen.WoodlandIslandBiomeSource; +import fr.koka.sanctuary.worldgen.WoodlandShoreSugarCaneFeature; +import fr.koka.sanctuary.worldgen.WoodlandGrovesFeature; import fr.koka.sanctuary.worldgen.NaturalShoreSugarCaneFeature; import fr.koka.sanctuary.worldgen.TemperateIslandBiomeSource; import fr.koka.sanctuary.worldgen.LayeredIslandBiomeSource; @@ -31,6 +34,9 @@ public final class SanctuaryMod implements ModInitializer { Registry.register(BuiltInRegistries.FEATURE_TYPE, id("layered_shore_sugar_cane"), LayeredShoreSugarCaneFeature.CODEC); Registry.register(BuiltInRegistries.FEATURE_TYPE, id("lower_terrace_vegetation"), LowerTerraceVegetationFeature.CODEC); Registry.register(BuiltInRegistries.BIOME_SOURCE, id("layered_island"), LayeredIslandBiomeSource.CODEC); + Registry.register(BuiltInRegistries.BIOME_SOURCE, id("woodland_island"), WoodlandIslandBiomeSource.CODEC); + Registry.register(BuiltInRegistries.FEATURE_TYPE, id("woodland_shore_sugar_cane"), WoodlandShoreSugarCaneFeature.CODEC); + Registry.register(BuiltInRegistries.FEATURE_TYPE, id("woodland_groves"), WoodlandGrovesFeature.CODEC); LOGGER.info("Sanctuary initialized: the Sanctuary world preset is available."); } } diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/SurfaceHydrologyMixin.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/SurfaceHydrologyMixin.java index 0c3ddf4..3d8f884 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/SurfaceHydrologyMixin.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/SurfaceHydrologyMixin.java @@ -3,6 +3,7 @@ package fr.koka.sanctuary.mixin; import fr.koka.sanctuary.worldgen.HydrologyRuntime; import fr.koka.sanctuary.worldgen.NaturalHydrologyRuntime; import fr.koka.sanctuary.worldgen.LayeredHydrologyRuntime; +import fr.koka.sanctuary.worldgen.WoodlandHydrologyRuntime; import java.util.Set; import java.util.concurrent.CompletableFuture; import net.minecraft.core.Holder; @@ -27,7 +28,12 @@ public abstract class SurfaceHydrologyMixin { StructureManager structures, BiomeManager biomeManager, WorldGenRegion region, Set> biomes, CallbackInfoReturnable> callback) { var generator = (NoiseBasedChunkGenerator) (Object) this; - if (LayeredHydrologyRuntime.enabled(generator)) { + if (WoodlandHydrologyRuntime.enabled(generator)) { + callback.setReturnValue(callback.getReturnValue().thenApply(generated -> { + WoodlandHydrologyRuntime.apply(generator, randomState, generated); + return generated; + })); + } else if (LayeredHydrologyRuntime.enabled(generator)) { callback.setReturnValue(callback.getReturnValue().thenApply(generated -> { LayeredHydrologyRuntime.apply(generator, randomState, generated); return generated; diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/WoodlandSpringOutletMixin.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/WoodlandSpringOutletMixin.java new file mode 100644 index 0000000..8a88c3c --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/WoodlandSpringOutletMixin.java @@ -0,0 +1,29 @@ +package fr.koka.sanctuary.mixin; + +import fr.koka.sanctuary.worldgen.WoodlandSpringOutlets; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.chunk.LevelChunk; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** Finish only pending generation markers; completed saved chunks are never repaired on reload. */ +@Mixin(LevelChunk.class) +public abstract class WoodlandSpringOutletMixin { + @Unique + private WoodlandSpringOutlets.Pending sanctuary$pendingWoodlandOutlets = WoodlandSpringOutlets.Pending.EMPTY; + + @Inject(method = "postProcessGeneration", at = @At("HEAD")) + private void sanctuary$captureWoodlandOutlets(ServerLevel level, CallbackInfo callback) { + sanctuary$pendingWoodlandOutlets = WoodlandSpringOutlets.capturePending(level, (LevelChunk) (Object) this); + } + + @Inject(method = "postProcessGeneration", at = @At("TAIL")) + private void sanctuary$finishWoodlandOutlets(ServerLevel level, CallbackInfo callback) { + var pending = sanctuary$pendingWoodlandOutlets; + sanctuary$pendingWoodlandOutlets = WoodlandSpringOutlets.Pending.EMPTY; + WoodlandSpringOutlets.finish(level, (LevelChunk) (Object) this, pending); + } +} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/SanctuarySpawn.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/SanctuarySpawn.java index e614009..6d58ac8 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/SanctuarySpawn.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/SanctuarySpawn.java @@ -27,6 +27,8 @@ public final class SanctuarySpawn { public static final ResourceKey LAYERED_SETTINGS = ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("sanctuary_layered")); + public static final ResourceKey WOODLAND_SETTINGS = + ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("sanctuary_woodland")); private SanctuarySpawn() {} @@ -36,7 +38,8 @@ public final class SanctuarySpawn { && (generator.generatorSettings().is(SETTINGS) || generator.generatorSettings().is(RAISED_SETTINGS) || generator.generatorSettings().is(HYDROLOGY_SETTINGS) || generator.generatorSettings().is(NATURAL_SETTINGS) - || generator.generatorSettings().is(LAYERED_SETTINGS)); + || generator.generatorSettings().is(LAYERED_SETTINGS) + || generator.generatorSettings().is(WOODLAND_SETTINGS)); } /** Called only during first creation; saved spawns and /setworldspawn survive subsequent loads. */ diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/WoodlandBiomePalette.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/WoodlandBiomePalette.java new file mode 100644 index 0000000..53bb583 --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/WoodlandBiomePalette.java @@ -0,0 +1,29 @@ +package fr.koka.sanctuary.worldgen; + +/** Alpha.7 keeps forest at every surface altitude; depth changes the sheltered interior palette. */ +public final class WoodlandBiomePalette { + public enum Kind { + OAK_FOREST, BIRCH_FOREST, CLEARING, DRY_WOODLAND, ROCKY_HEATH, + DARK_GROVE, BAMBOO_GROVE, SULFUR_DEPTHS + } + + private WoodlandBiomePalette() {} + + 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; + if (elevation < 112 && variation > 0.62F && moisture < -0.25F) return Kind.SULFUR_DEPTHS; + // Preserve the strongest horizontal rock patches of alpha.6, with a smaller footprint. + // Height alone never converts an otherwise forested mountain into exposed stone. + if (variation > 0.48F) return Kind.ROCKY_HEATH; + 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; + } +} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/WoodlandGroveGeometry.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/WoodlandGroveGeometry.java new file mode 100644 index 0000000..b9cef97 --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/WoodlandGroveGeometry.java @@ -0,0 +1,46 @@ +package fr.koka.sanctuary.worldgen; + +/** Natural supports and canopy space; independent of Minecraft blocks and decoration order. */ +public final class WoodlandGroveGeometry { + @FunctionalInterface public interface Sampler { float sample(int x, int y, int z); } + + private WoodlandGroveGeometry() {} + + public static int highestSurface(Sampler sampler, int x, int z) { + return highestSurfaceBelow(sampler, x, z, 383); + } + + /** The caller supplies a proven upper bound, including any removed natural surface. */ + public static int highestSurfaceBelow(Sampler sampler, int x, int z, int ceiling) { + for (int y = Math.min(383, ceiling); y >= 24; y--) if (sampler.sample(x, y, z) > 0) return y; + return -1; + } + + public static boolean naturalFootprint(Sampler sampler, int x, int bedY, int z, int width) { + if (bedY < 32 || bedY > 350) return false; + for (int dx = 0; dx < width; dx++) for (int dz = 0; dz < width; dz++) { + for (int dy = -2; dy <= 0; dy++) if (sampler.sample(x + dx, bedY + dy, z + dz) <= 0) return false; + if (sampler.sample(x + dx, bedY + 1, z + dz) > 0) return false; + } + return true; + } + + public static boolean treeRoom(Sampler sampler, int x, int baseY, int z, int width, int radius, int height) { + if (baseY + height >= 384) return false; + for (int y = baseY; y <= baseY + height; y++) { + int from = y < baseY + 3 ? 0 : -radius; + int to = y < baseY + 3 ? width - 1 : radius; + for (int dx = from; dx <= to; dx++) for (int dz = from; dz <= to; dz++) { + if (sampler.sample(x + dx, y, z + dz) > 0) return false; + } + } + return true; + } + + public static long seed(long worldSeed, int x, int y, int z, long salt) { + long value = worldSeed ^ ((long) x << 32) ^ (z & 0xffffffffL) ^ y * 0x9e3779b97f4a7c15L ^ salt; + value = (value ^ value >>> 30) * 0xbf58476d1ce4e5b9L; + value = (value ^ value >>> 27) * 0x94d049bb133111ebL; + return value ^ value >>> 31; + } +} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/WoodlandGrovesFeature.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/WoodlandGrovesFeature.java new file mode 100644 index 0000000..6bc84c6 --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/WoodlandGrovesFeature.java @@ -0,0 +1,236 @@ +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; + +/** Alpha.7: real vanilla trees 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 WoodlandGrovesFeature implements Feature { + public static final MapCodec CODEC = MapCodec.unit(WoodlandGrovesFeature::new); + private static final Map> 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 key, int width, int radius, int height) {} + private record Candidate(int x, int z, long rank) {} + 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 codec() { return CODEC; } + + public static Optional remarkablePlan(ServerLevel level) { + if (!(level.getChunkSource().getGenerator() instanceof NoiseBasedChunkGenerator noise)) return Optional.empty(); + return remarkablePlan(noise, level.getChunkSource().randomState()); + } + + public static Optional remarkablePlan(NoiseBasedChunkGenerator noise, RandomState state) { + if (!WoodlandHydrologyRuntime.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); + var water = WoodlandHydrologyRuntime.plan(noise, state); + var lava = WoodlandHydrologyRuntime.lavaPlan(noise, state); + Species species = SPECIES[Math.floorMod(WoodlandGroveGeometry.seed(state.seed(), 0, 0, 0, 0x70EEL), SPECIES.length)]; + var candidates = new ArrayList(); + 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(water, lava, 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) || !WoodlandHydrologyRuntime.enabled(noise)) return false; + int minX = (origin.getX() >> 4) * 16, minZ = (origin.getZ() >> 4) * 16; + if (Math.abs((long) minX) > 272 || Math.abs((long) minZ) > 272) return false; + var state = level.getLevel().getChunkSource().randomState(); + var density = noise.generatorSettings().value().noiseRouter().finalDensity(); + WoodlandGroveGeometry.Sampler sampler = (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z); + var water = WoodlandHydrologyRuntime.plan(noise, state); + var lava = WoodlandHydrologyRuntime.lavaPlan(noise, state); + 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, water, lava, 0x723BL); + break; + } + } + } + // 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, water, 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.getBlockState(base.below())) || !level.getBlockState(base).isAir()) continue; + var biome = level.getBiome(base); + boolean dark = biome.is(WoodlandIslandBiomeSource.DARK_GROVE); + boolean bamboo = biome.is(WoodlandIslandBiomeSource.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), water, lava, 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), water, lava, 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(water, lava, 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; + } + } + // Ground cover uses coherent patches; mushrooms can survive on podzol/mycelium in shade. + for (int x = minX; x < minX + 16; x++) for (int z = minZ; z < minZ + 16; z++) { + int highest = localTop(level, sampler, water, minX, minZ, x, z, heights), floors = 0; + for (int bed = highest - 8; bed >= 32; bed--) { + BlockPos floor = new BlockPos(x, bed, z), plant = floor.above(); + if (!ground(level.getBlockState(floor)) || !level.getBlockState(plant).isAir()) continue; + var biome = level.getBiome(plant); + if (!biome.is(WoodlandIslandBiomeSource.DARK_GROVE) && !biome.is(WoodlandIslandBiomeSource.BAMBOO_GROVE)) continue; + if (!WoodlandGroveGeometry.naturalFootprint(sampler, x, bed, z, 1) || blocked(water, lava, plant, 1, 2)) continue; + long patch = WoodlandGroveGeometry.seed(state.seed(), Math.floorDiv(x, 7), 0, Math.floorDiv(z, 7), 0x742DL); + BlockState soil = Math.floorMod(patch, 20) == 0 ? Blocks.MOSS_BLOCK.defaultBlockState() + : Math.floorMod(patch, 3) == 0 ? Blocks.MYCELIUM.defaultBlockState() : Blocks.PODZOL.defaultBlockState(); + setBlock(level, floor, soil); + long detail = WoodlandGroveGeometry.seed(state.seed(), x, bed, z, 0x743EL); + if (Math.floorMod(detail, 5) == 0) { + BlockState mushroom = (Math.floorMod(detail, 2) == 0 ? Blocks.BROWN_MUSHROOM : Blocks.RED_MUSHROOM).defaultBlockState(); + if (mushroom.canSurvive(level, plant)) setBlock(level, plant, mushroom); + } + changed = true; + if (++floors >= 3) break; + } + } + return changed; + } + + private boolean tree(WorldGenLevel level, ChunkGenerator generator, WoodlandGroveGeometry.Sampler sampler, long seed, + BlockPos base, Species species, LayeredHydrology.Plan water, LayeredLavaDeposit.Plan lava, long salt) { + if (blocked(water, lava, 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.getBlockState(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(BlockState state) { + 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); + } + + private static int localTop(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler, LayeredHydrology.Plan water, + 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]; + // This feature runs first in the vegetation step. Ores preserve the surface; hydrology + // is the only preceding operation that removes it and records the original carveTop. + int ceiling = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z) - 1; + var modified = water.cellAt(x, z); + if (modified != null) ceiling = Math.max(ceiling, modified.carveTop()); + return heights[index] = WoodlandGroveGeometry.highestSurfaceBelow(sampler, x, z, ceiling); + } + + private static int rootDepth(Species species) { return species.key().equals(TreeFeatures.MANGROVE) ? 16 : 4; } + + private static boolean blocked(LayeredHydrology.Plan water, LayeredLavaDeposit.Plan lava, BlockPos base, int radius, int height) { + return blocked(water, lava, base, radius, height, 4); + } + + private static boolean blocked(LayeredHydrology.Plan water, LayeredLavaDeposit.Plan lava, BlockPos base, + int radius, int height, int rootDepth) { + 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++) { + var cell = water.cellAt(x, z); + if (cell != null && 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 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; + } +} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/WoodlandHydrologyRuntime.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/WoodlandHydrologyRuntime.java new file mode 100644 index 0000000..b1587b4 --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/WoodlandHydrologyRuntime.java @@ -0,0 +1,142 @@ +package fr.koka.sanctuary.worldgen; + +import fr.koka.sanctuary.SanctuaryMod; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Map; +import java.util.WeakHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator; +import net.minecraft.world.level.levelgen.RandomState; + +/** Alpha.7 only: immutable plans cached by world state, applied to fresh terrain before decoration. */ +public final class WoodlandHydrologyRuntime { + private static final Map PLANS = + Collections.synchronizedMap(new WeakHashMap<>()); + private static final Map LAVA_PLANS = + Collections.synchronizedMap(new WeakHashMap<>()); + + private WoodlandHydrologyRuntime() {} + + public static boolean enabled(NoiseBasedChunkGenerator generator) { + return generator.generatorSettings().is(SanctuarySpawn.WOODLAND_SETTINGS); + } + + public static LayeredHydrology.Plan woodlandPlan(ServerLevel level) { + return plan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(), + level.getChunkSource().randomState()); + } + + public static LayeredHydrology.Plan plan(NoiseBasedChunkGenerator generator, RandomState randomState) { + if (!enabled(generator)) throw new IllegalArgumentException("Woodland hydrology is not enabled for this generator"); + return PLANS.computeIfAbsent(randomState, state -> { + long start = System.nanoTime(); + var density = generator.generatorSettings().value().noiseRouter().finalDensity(); + var plan = LayeredHydrology.create(state.seed(), + (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z)); + SanctuaryMod.LOGGER.info("Sanctuary woodland hydrology: seed {}, {} features, {} columns, {} springs, {} ms", + state.seed(), plan.features().size(), plan.cells().size(), plan.springs().size(), + (System.nanoTime() - start) / 1_000_000); + return plan; + }); + } + + public static LayeredLavaDeposit.Plan lavaPlan(ServerLevel level) { + return lavaPlan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(), + level.getChunkSource().randomState()); + } + + public static LayeredLavaDeposit.Plan lavaPlan(NoiseBasedChunkGenerator generator, RandomState randomState) { + // Acquire the water plan before the lava cache lock; the water planner never acquires it. + var water = plan(generator, randomState); + return LAVA_PLANS.computeIfAbsent(randomState, state -> { + long start = System.nanoTime(); + var density = generator.generatorSettings().value().noiseRouter().finalDensity(); + var lava = LayeredLavaDeposit.create(state.seed(), + (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z), water); + SanctuaryMod.LOGGER.info("Sanctuary starter lava: seed {}, {} sources, access {}, {} ms", + state.seed(), lava.lavaCells().size(), lava.access(), (System.nanoTime() - start) / 1_000_000); + return lava; + }); + } + + public static void apply(NoiseBasedChunkGenerator generator, RandomState randomState, ChunkAccess chunk) { + if (!enabled(generator)) return; + var chunkPos = chunk.getPos(); + if (Math.abs((long) chunkPos.x() * 16) > IslandShape.TERRAIN_LIMIT + 16L + || Math.abs((long) chunkPos.z() * 16) > IslandShape.TERRAIN_LIMIT + 16L) return; + var plan = plan(generator, randomState); + var cells = plan.cellsInChunk(chunkPos.x(), chunkPos.z()); + var springs = plan.springsInChunk(chunkPos.x(), chunkPos.z()); + var outlets = plan.springs().stream().map(LayeredHydrology.Spring::outlet) + .filter(outlet -> (outlet.x() >> 4) == chunkPos.x() && (outlet.z() >> 4) == chunkPos.z()).toList(); + var lava = lavaPlan(generator, randomState); + var lavaCells = lava.cellsInChunk(chunkPos.x(), chunkPos.z()); + var lavaSupports = lava.supportsInChunk(chunkPos.x(), chunkPos.z()); + if (cells.isEmpty() && springs.isEmpty() && outlets.isEmpty() && lavaCells.isEmpty() && lavaSupports.isEmpty()) return; + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + // Validate all replacements and two intact support layers before modifying this chunk. + for (var cell : cells) { + for (int y = cell.bedY() - cell.sedimentDepth() - 1; y <= cell.bedY(); y++) { + requireSolid(chunk, pos.set(cell.x(), y, cell.z()), randomState.seed()); + } + } + for (var spring : springs) { + var source = spring.source(); + requireSolid(chunk, pos.set(source.x(), source.y(), source.z()), randomState.seed()); + } + for (var support : lavaSupports) { + requireSolid(chunk, pos.set(support.x(), support.y(), support.z()), randomState.seed()); + if (chunk.getBlockState(pos).ignitedByLava()) { + throw new IllegalStateException("Lava pocket requires nonflammable natural support at " + pos); + } + } + for (var cell : lavaCells) { + requireSolid(chunk, pos.set(cell.x(), cell.y(), cell.z()), randomState.seed()); + } + for (var cell : cells) { + for (int depth = 0; depth < cell.sedimentDepth(); depth++) { + pos.set(cell.x(), cell.bedY() - depth, cell.z()); + BlockState material = WoodlandMaterials.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)); + } + + private static void requireSolid(ChunkAccess chunk, BlockPos pos, long seed) { + if (!chunk.getBlockState(pos).isCollisionShapeFullBlock(chunk, pos)) { + throw new IllegalStateException("Woodland hydrology differs from terrain at " + pos + " for seed " + seed); + } + } +} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/WoodlandIslandBiomeSource.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/WoodlandIslandBiomeSource.java new file mode 100644 index 0000000..0690985 --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/WoodlandIslandBiomeSource.java @@ -0,0 +1,89 @@ +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.7 woodland climate, separate from all saved earlier biome-source codecs. */ +public final class WoodlandIslandBiomeSource extends BiomeSource { + public static final ResourceKey OAK_FOREST = key("woodland_oak_forest"); + public static final ResourceKey BIRCH_FOREST = key("woodland_birch_forest"); + public static final ResourceKey CLEARING = key("woodland_clearing"); + public static final ResourceKey DRY_WOODLAND = key("woodland_dry_woodland"); + public static final ResourceKey ROCKY_HEATH = key("woodland_rocky_heath"); + public static final ResourceKey DARK_GROVE = key("woodland_dark_grove"); + public static final ResourceKey BAMBOO_GROVE = key("woodland_bamboo_grove"); + public static final ResourceKey SULFUR_DEPTHS = key("woodland_sulfur_depths"); + + public static final MapCodec 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) + ).apply(instance, WoodlandIslandBiomeSource::new)); + + private final Holder oakForest; + private final Holder birchForest; + private final Holder clearing; + private final Holder dryWoodland; + private final Holder rockyHeath; + private final Holder darkGrove; + private final Holder bambooGrove; + private final Holder sulfurDepths; + + public WoodlandIslandBiomeSource(Holder oakForest, Holder birchForest, Holder clearing, + Holder dryWoodland, Holder rockyHeath, Holder darkGrove, + Holder bambooGrove, Holder sulfurDepths) { + this.oakForest = oakForest; + this.birchForest = birchForest; + this.clearing = clearing; + this.dryWoodland = dryWoodland; + this.rockyHeath = rockyHeath; + this.darkGrove = darkGrove; + this.bambooGrove = bambooGrove; + this.sulfurDepths = sulfurDepths; + } + + @Override + protected MapCodec codec() { return CODEC; } + + @Override + protected Stream> collectPossibleBiomes() { + return Stream.of(oakForest, birchForest, clearing, dryWoodland, rockyHeath, darkGrove, bambooGrove, sulfurDepths); + } + + @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 (WoodlandBiomePalette.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; + }; + }; + } + + private static ResourceKey key(String path) { + return ResourceKey.create(Registries.BIOME, SanctuaryMod.id(path)); + } +} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/WoodlandMaterials.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/WoodlandMaterials.java new file mode 100644 index 0000000..b2e02d9 --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/WoodlandMaterials.java @@ -0,0 +1,43 @@ +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.7 shore deposits: broad continuous patches through the existing sediment thickness. */ +public final class WoodlandMaterials { + private WoodlandMaterials() {} + + public static BlockState sedimentBlock(LayeredHydrology.Cell cell, long seed, int depth) { + if (cell.material() == LayeredHydrology.ShoreMaterial.GRASS) { + return (depth == 0 ? Blocks.GRASS_BLOCK : Blocks.DIRT).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; } +} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/WoodlandShoreSugarCaneFeature.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/WoodlandShoreSugarCaneFeature.java new file mode 100644 index 0000000..1036d4f --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/WoodlandShoreSugarCaneFeature.java @@ -0,0 +1,41 @@ +package fr.koka.sanctuary.worldgen; + +import com.mojang.serialization.MapCodec; +import net.minecraft.core.BlockPos; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator; +import net.minecraft.world.level.levelgen.feature.Feature; + +/** A few plants on existing suitable shores, after the forest has been decorated. */ +public final class WoodlandShoreSugarCaneFeature implements Feature { + public static final MapCodec CODEC = MapCodec.unit(WoodlandShoreSugarCaneFeature::new); + + @Override + public MapCodec codec() { + return CODEC; + } + + @Override + public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource random, BlockPos origin) { + if (!(generator instanceof NoiseBasedChunkGenerator noise) || !WoodlandHydrologyRuntime.enabled(noise)) return false; + var plan = WoodlandHydrologyRuntime.plan(noise, level.getLevel().getChunkSource().randomState()); + var cane = Blocks.SUGAR_CANE.defaultBlockState(); + boolean placed = false; + for (var cell : plan.cellsInChunk(origin.getX() >> 4, origin.getZ() >> 4)) { + if (cell.hasWater() || random.nextInt(7) != 0) continue; + BlockPos base = new BlockPos(cell.x(), cell.bedY() + 1, cell.z()); + if (!level.getBlockState(base).isAir() || !cane.canSurvive(level, base)) continue; + int height = 1 + random.nextInt(3); + for (int y = 0; y < height; y++) { + BlockPos pos = base.above(y); + if (!level.getBlockState(pos).isAir() || !cane.canSurvive(level, pos)) break; + setBlock(level, pos, cane); + placed = true; + } + } + return placed; + } +} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/WoodlandSpringOutlets.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/WoodlandSpringOutlets.java new file mode 100644 index 0000000..9c7fb00 --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/WoodlandSpringOutlets.java @@ -0,0 +1,94 @@ +package fr.koka.sanctuary.worldgen; + +import java.util.ArrayList; +import java.util.List; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.BlockTags; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.VegetationBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator; +import net.minecraft.world.level.material.Fluids; + +/** One-time alpha.7 outlet finishing, using vanilla's persisted and consumed generation queue. */ +public final class WoodlandSpringOutlets { + public record Pending(List sources, List 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 WoodlandSpringOutlets() {} + + /** 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) + || !WoodlandHydrologyRuntime.enabled(noise)) return Pending.EMPTY; + boolean queued = false; + for (var section : chunk.getPostProcessing()) if (section != null && !section.isEmpty()) { + queued = true; + break; + } + if (!queued) return Pending.EMPTY; + var plan = WoodlandHydrologyRuntime.plan(noise, level.getChunkSource().randomState()); + List sources = new ArrayList<>(), outlets = new ArrayList<>(); + for (var spring : plan.springs()) { + if (markedHere(chunk, spring.source())) sources.add(spring.source()); + if (markedHere(chunk, spring.outlet())) outlets.add(spring.outlet()); + } + return new Pending(sources, outlets); + } + + /** All neighbouring FEATURES have finished before a chunk reaches this stage. */ + public static void finish(ServerLevel level, LevelChunk chunk, Pending pending) { + if (pending.isEmpty() || !(level.getChunkSource().getGenerator() instanceof NoiseBasedChunkGenerator noise) + || !WoodlandHydrologyRuntime.enabled(noise)) return; + var random = level.getChunkSource().randomState(); + var density = noise.generatorSettings().value().noiseRouter().finalDensity(); + for (var outlet : pending.outlets()) { + if (!owns(chunk, outlet)) throw new IllegalArgumentException("An outlet belongs to another chunk: " + outlet); + if (random.sampleBlockValueUncached(density, outlet.x(), outlet.y(), outlet.z()) > 0) { + throw new IllegalStateException("A spring outlet must already be natural air: " + outlet); + } + BlockPos pos = blockPos(outlet); + BlockState current = chunk.getBlockState(pos); + if (removableDecoration(current)) { + // Only this declared air cell is reopened. Neighbour notification wakes the + // adjacent source even if its own chunk was post-processed first. + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + } + } + for (var source : pending.sources()) { + if (!owns(chunk, source)) throw new IllegalArgumentException("A source belongs to another chunk: " + source); + BlockPos pos = blockPos(source); + BlockState current = chunk.getBlockState(pos); + if (current.is(Blocks.WATER) && current.getFluidState().isSource()) { + level.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(level)); + } + } + } + + private static boolean markedHere(LevelChunk chunk, LayeredHydrology.Position position) { + if (!owns(chunk, position)) return false; + var queue = chunk.getPostProcessing()[chunk.getSectionIndex(position.y())]; + return queue != null && queue.contains(ProtoChunk.packOffsetCoordinates(blockPos(position))); + } + + private static boolean owns(LevelChunk chunk, LayeredHydrology.Position position) { + return (position.x() >> 4) == chunk.getPos().x() && (position.z() >> 4) == chunk.getPos().z(); + } + + private static BlockPos blockPos(LayeredHydrology.Position position) { + return new BlockPos(position.x(), position.y(), position.z()); + } + + private static boolean removableDecoration(BlockState state) { + if (state.hasBlockEntity()) return false; + return state.is(Blocks.GLOW_LICHEN) || state.is(Blocks.VINE) || state.is(Blocks.MOSS_CARPET) + || state.getBlock() instanceof VegetationBlock || state.is(BlockTags.LEAVES) || state.is(BlockTags.LOGS); + } +} diff --git a/mods/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json b/mods/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json index bd7cebf..cfa1c2c 100644 --- a/mods/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json +++ b/mods/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json @@ -11,5 +11,13 @@ "biome.sanctuary.layered_flower_meadow": "Flowered Highland Meadow", "biome.sanctuary.layered_lower_grove": "Ledge Woodland", "biome.sanctuary.layered_mossy_terraces": "Mossy Ledges", - "biome.sanctuary.layered_rocky_terraces": "Rocky Terraces" + "biome.sanctuary.layered_rocky_terraces": "Rocky Terraces", + "biome.sanctuary.woodland_oak_forest": "Sanctuary Oak Woodland", + "biome.sanctuary.woodland_birch_forest": "Sanctuary Birch Woodland", + "biome.sanctuary.woodland_clearing": "Flowered Clearing", + "biome.sanctuary.woodland_dry_woodland": "Dry Woodland", + "biome.sanctuary.woodland_rocky_heath": "Rocky Heath", + "biome.sanctuary.woodland_dark_grove": "Dark Undergrowth", + "biome.sanctuary.woodland_bamboo_grove": "Bamboo Grove", + "biome.sanctuary.woodland_sulfur_depths": "Sulfur Depths" } diff --git a/mods/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json b/mods/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json index 66ede61..4367aba 100644 --- a/mods/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json +++ b/mods/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json @@ -11,5 +11,13 @@ "biome.sanctuary.layered_flower_meadow": "Prairie fleurie des hauteurs", "biome.sanctuary.layered_lower_grove": "Sous-bois des corniches", "biome.sanctuary.layered_mossy_terraces": "Corniches moussues", - "biome.sanctuary.layered_rocky_terraces": "Terrasses rocheuses" + "biome.sanctuary.layered_rocky_terraces": "Terrasses rocheuses", + "biome.sanctuary.woodland_oak_forest": "Chênaie de Sanctuary", + "biome.sanctuary.woodland_birch_forest": "Bétulaie de Sanctuary", + "biome.sanctuary.woodland_clearing": "Clairière fleurie", + "biome.sanctuary.woodland_dry_woodland": "Bois sec", + "biome.sanctuary.woodland_rocky_heath": "Lande rocheuse", + "biome.sanctuary.woodland_dark_grove": "Sous-bois sombre", + "biome.sanctuary.woodland_bamboo_grove": "Bosquet de bambous", + "biome.sanctuary.woodland_sulfur_depths": "Profondeurs soufrées" } diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/woodland_bamboo_grove.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/woodland_bamboo_grove.json new file mode 100644 index 0000000..349d00a --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/woodland_bamboo_grove.json @@ -0,0 +1,173 @@ +{ + "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": [ + [], + [], + [], + [], + [], + [], + [ + "minecraft:ore_dirt", + "minecraft:ore_granite_upper", + "minecraft:ore_granite_lower", + "minecraft:ore_diorite_upper", + "minecraft:ore_diorite_lower", + "minecraft:ore_andesite_upper", + "minecraft:ore_andesite_lower", + "minecraft:ore_tuff", + "sanctuary:woodland_ore_coal", + "sanctuary:woodland_ore_iron", + "sanctuary:woodland_ore_copper", + "sanctuary:island_ore_gold", + "sanctuary:island_ore_redstone", + "sanctuary:island_ore_lapis", + "sanctuary:island_ore_diamond", + "sanctuary:woodland_ore_coal_surface", + "sanctuary:woodland_ore_iron_surface", + "sanctuary:woodland_ore_copper_surface", + "sanctuary:woodland_ore_coal_ledges", + "sanctuary:woodland_ore_iron_ledges", + "sanctuary:woodland_ore_copper_ledges" + ], + [], + [], + [ + "sanctuary:woodland_groves", + "minecraft:glow_lichen", + "minecraft:brown_mushroom_normal", + "minecraft:red_mushroom_normal", + "sanctuary:woodland_shore_sugar_cane" + ], + [ + "minecraft:freeze_top_layer" + ] + ], + "has_precipitation": true, + "temperature": 0.8 +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/woodland_birch_forest.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/woodland_birch_forest.json new file mode 100644 index 0000000..b89c9a4 --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/woodland_birch_forest.json @@ -0,0 +1,168 @@ +{ + "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": [ + [], + [], + [], + [], + [], + [], + [ + "minecraft:ore_dirt", + "minecraft:ore_granite_upper", + "minecraft:ore_granite_lower", + "minecraft:ore_diorite_upper", + "minecraft:ore_diorite_lower", + "minecraft:ore_andesite_upper", + "minecraft:ore_andesite_lower", + "minecraft:ore_tuff", + "sanctuary:woodland_ore_coal", + "sanctuary:woodland_ore_iron", + "sanctuary:woodland_ore_copper", + "sanctuary:island_ore_gold", + "sanctuary:island_ore_redstone", + "sanctuary:island_ore_lapis", + "sanctuary:island_ore_diamond", + "sanctuary:woodland_ore_coal_surface", + "sanctuary:woodland_ore_iron_surface", + "sanctuary:woodland_ore_copper_surface", + "sanctuary:woodland_ore_coal_ledges", + "sanctuary:woodland_ore_iron_ledges", + "sanctuary:woodland_ore_copper_ledges" + ], + [], + [], + [ + "sanctuary:woodland_groves", + "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:woodland_shore_sugar_cane" + ], + [ + "minecraft:freeze_top_layer" + ] + ], + "has_precipitation": true, + "temperature": 0.7 +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/woodland_clearing.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/woodland_clearing.json new file mode 100644 index 0000000..ce33906 --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/woodland_clearing.json @@ -0,0 +1,170 @@ +{ + "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": [ + [], + [], + [], + [], + [], + [], + [ + "minecraft:ore_dirt", + "minecraft:ore_granite_upper", + "minecraft:ore_granite_lower", + "minecraft:ore_diorite_upper", + "minecraft:ore_diorite_lower", + "minecraft:ore_andesite_upper", + "minecraft:ore_andesite_lower", + "minecraft:ore_tuff", + "sanctuary:woodland_ore_coal", + "sanctuary:woodland_ore_iron", + "sanctuary:woodland_ore_copper", + "sanctuary:island_ore_gold", + "sanctuary:island_ore_redstone", + "sanctuary:island_ore_lapis", + "sanctuary:island_ore_diamond", + "sanctuary:woodland_ore_coal_surface", + "sanctuary:woodland_ore_iron_surface", + "sanctuary:woodland_ore_copper_surface", + "sanctuary:woodland_ore_coal_ledges", + "sanctuary:woodland_ore_iron_ledges", + "sanctuary:woodland_ore_copper_ledges" + ], + [], + [], + [ + "sanctuary:woodland_groves", + "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:woodland_shore_sugar_cane" + ], + [ + "minecraft:freeze_top_layer" + ] + ], + "has_precipitation": true, + "temperature": 0.7 +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/woodland_dark_grove.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/woodland_dark_grove.json new file mode 100644 index 0000000..3a01e3e --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/woodland_dark_grove.json @@ -0,0 +1,159 @@ +{ + "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": [ + [], + [], + [], + [], + [], + [], + [ + "minecraft:ore_dirt", + "minecraft:ore_granite_upper", + "minecraft:ore_granite_lower", + "minecraft:ore_diorite_upper", + "minecraft:ore_diorite_lower", + "minecraft:ore_andesite_upper", + "minecraft:ore_andesite_lower", + "minecraft:ore_tuff", + "sanctuary:woodland_ore_coal", + "sanctuary:woodland_ore_iron", + "sanctuary:woodland_ore_copper", + "sanctuary:island_ore_gold", + "sanctuary:island_ore_redstone", + "sanctuary:island_ore_lapis", + "sanctuary:island_ore_diamond", + "sanctuary:woodland_ore_coal_surface", + "sanctuary:woodland_ore_iron_surface", + "sanctuary:woodland_ore_copper_surface", + "sanctuary:woodland_ore_coal_ledges", + "sanctuary:woodland_ore_iron_ledges", + "sanctuary:woodland_ore_copper_ledges" + ], + [], + [], + [ + "sanctuary:woodland_groves", + "minecraft:glow_lichen", + "minecraft:brown_mushroom_normal", + "minecraft:red_mushroom_normal", + "sanctuary:woodland_shore_sugar_cane" + ], + [ + "minecraft:freeze_top_layer" + ] + ], + "has_precipitation": true, + "temperature": 0.7 +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/woodland_dry_woodland.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/woodland_dry_woodland.json new file mode 100644 index 0000000..4e29f13 --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/woodland_dry_woodland.json @@ -0,0 +1,169 @@ +{ + "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": [ + [], + [], + [], + [], + [], + [], + [ + "minecraft:ore_dirt", + "minecraft:ore_granite_upper", + "minecraft:ore_granite_lower", + "minecraft:ore_diorite_upper", + "minecraft:ore_diorite_lower", + "minecraft:ore_andesite_upper", + "minecraft:ore_andesite_lower", + "minecraft:ore_tuff", + "sanctuary:woodland_ore_coal", + "sanctuary:woodland_ore_iron", + "sanctuary:woodland_ore_copper", + "sanctuary:island_ore_gold", + "sanctuary:island_ore_redstone", + "sanctuary:island_ore_lapis", + "sanctuary:island_ore_diamond", + "sanctuary:woodland_ore_coal_surface", + "sanctuary:woodland_ore_iron_surface", + "sanctuary:woodland_ore_copper_surface", + "sanctuary:woodland_ore_coal_ledges", + "sanctuary:woodland_ore_iron_ledges", + "sanctuary:woodland_ore_copper_ledges" + ], + [], + [], + [ + "sanctuary:woodland_groves", + "minecraft:glow_lichen", + "minecraft:forest_flowers", + "sanctuary:woodland_sparse_trees", + "minecraft:patch_bush", + "minecraft:flower_default", + "minecraft:patch_grass_forest", + "minecraft:brown_mushroom_normal", + "minecraft:red_mushroom_normal", + "sanctuary:woodland_shore_sugar_cane" + ], + [ + "minecraft:freeze_top_layer" + ] + ], + "has_precipitation": true, + "temperature": 0.75 +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/woodland_oak_forest.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/woodland_oak_forest.json new file mode 100644 index 0000000..06c70e6 --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/woodland_oak_forest.json @@ -0,0 +1,172 @@ +{ + "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": [ + [], + [], + [], + [], + [], + [], + [ + "minecraft:ore_dirt", + "minecraft:ore_granite_upper", + "minecraft:ore_granite_lower", + "minecraft:ore_diorite_upper", + "minecraft:ore_diorite_lower", + "minecraft:ore_andesite_upper", + "minecraft:ore_andesite_lower", + "minecraft:ore_tuff", + "sanctuary:woodland_ore_coal", + "sanctuary:woodland_ore_iron", + "sanctuary:woodland_ore_copper", + "sanctuary:island_ore_gold", + "sanctuary:island_ore_redstone", + "sanctuary:island_ore_lapis", + "sanctuary:island_ore_diamond", + "sanctuary:woodland_ore_coal_surface", + "sanctuary:woodland_ore_iron_surface", + "sanctuary:woodland_ore_copper_surface", + "sanctuary:woodland_ore_coal_ledges", + "sanctuary:woodland_ore_iron_ledges", + "sanctuary:woodland_ore_copper_ledges" + ], + [], + [], + [ + "sanctuary:woodland_groves", + "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:woodland_shore_sugar_cane" + ], + [ + "minecraft:freeze_top_layer" + ] + ], + "has_precipitation": true, + "temperature": 0.7 +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/woodland_rocky_heath.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/woodland_rocky_heath.json new file mode 100644 index 0000000..9e41269 --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/woodland_rocky_heath.json @@ -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.65, + "effects": { + "water_color": "#3f76e4", + "grass_color": "#88b96b", + "foliage_color": "#79aa61", + "dry_foliage_color": "#b9a766" + }, + "features": [ + [], + [], + [], + [], + [], + [], + [ + "minecraft:ore_dirt", + "minecraft:ore_granite_upper", + "minecraft:ore_granite_lower", + "minecraft:ore_diorite_upper", + "minecraft:ore_diorite_lower", + "minecraft:ore_andesite_upper", + "minecraft:ore_andesite_lower", + "minecraft:ore_tuff", + "sanctuary:woodland_ore_coal", + "sanctuary:woodland_ore_iron", + "sanctuary:woodland_ore_copper", + "sanctuary:island_ore_gold", + "sanctuary:island_ore_redstone", + "sanctuary:island_ore_lapis", + "sanctuary:island_ore_diamond", + "sanctuary:woodland_ore_coal_surface", + "sanctuary:woodland_ore_iron_surface", + "sanctuary:woodland_ore_copper_surface", + "sanctuary:woodland_ore_coal_ledges", + "sanctuary:woodland_ore_iron_ledges", + "sanctuary:woodland_ore_copper_ledges" + ], + [], + [], + [ + "sanctuary:woodland_groves", + "minecraft:glow_lichen", + "sanctuary:woodland_shore_sugar_cane" + ], + [ + "minecraft:freeze_top_layer" + ] + ], + "has_precipitation": true, + "temperature": 0.7 +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/woodland_sulfur_depths.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/woodland_sulfur_depths.json new file mode 100644 index 0000000..71c6e57 --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/woodland_sulfur_depths.json @@ -0,0 +1,162 @@ +{ + "attributes": { + "minecraft:audio/background_music": { + "default": { + "max_delay": 24000, + "min_delay": 12000, + "sound": "minecraft:music.overworld.forest" + } + }, + "minecraft:gameplay/natural_mob_spawns": { + "argument": { + "spawn_costs": {}, + "spawns_by_category": { + "ambient": [ + { + "type": "minecraft:bat", + "count": 8, + "weight": 10 + } + ], + "creature": [ + { + "type": "minecraft:sheep", + "count": 4, + "weight": 12 + }, + { + "type": "minecraft:pig", + "count": 4, + "weight": 10 + }, + { + "type": "minecraft:chicken", + "count": 4, + "weight": 10 + }, + { + "type": "minecraft:cow", + "count": 4, + "weight": 8 + }, + { + "type": "minecraft:wolf", + "count": 4, + "weight": 5 + } + ], + "monster": [ + { + "type": "minecraft:spider", + "count": 4, + "weight": 100 + }, + { + "type": "minecraft:zombie", + "count": 4, + "weight": 95 + }, + { + "type": "minecraft:zombie_villager", + "count": 1, + "weight": 5 + }, + { + "type": "minecraft:skeleton", + "count": 4, + "weight": 100 + }, + { + "type": "minecraft:creeper", + "count": 4, + "weight": 100 + }, + { + "type": "minecraft:slime", + "count": 4, + "weight": 100 + }, + { + "type": "minecraft:enderman", + "count": { + "type": "minecraft:uniform", + "max_inclusive": 4, + "min_inclusive": 1 + }, + "weight": 10 + }, + { + "type": "minecraft:witch", + "count": 1, + "weight": 5 + } + ], + "underground_water_creature": [ + { + "type": "minecraft:glow_squid", + "count": { + "type": "minecraft:uniform", + "max_inclusive": 6, + "min_inclusive": 4 + }, + "weight": 10 + } + ] + } + }, + "modifier": "overlay" + }, + "minecraft:visual/sky_color": "#79a6ff" + }, + "carvers": [], + "downfall": 0.5, + "effects": { + "water_color": "#3f76e4", + "grass_color": "#88b96b", + "foliage_color": "#79aa61", + "dry_foliage_color": "#b9a766" + }, + "features": [ + [], + [], + [], + [], + [], + [], + [ + "minecraft:ore_dirt", + "minecraft:ore_granite_upper", + "minecraft:ore_granite_lower", + "minecraft:ore_diorite_upper", + "minecraft:ore_diorite_lower", + "minecraft:ore_andesite_upper", + "minecraft:ore_andesite_lower", + "minecraft:ore_tuff", + "sanctuary:woodland_ore_coal", + "sanctuary:woodland_ore_iron", + "sanctuary:woodland_ore_copper", + "sanctuary:island_ore_gold", + "sanctuary:island_ore_redstone", + "sanctuary:island_ore_lapis", + "sanctuary:island_ore_diamond", + "sanctuary:woodland_ore_coal_surface", + "sanctuary:woodland_ore_iron_surface", + "sanctuary:woodland_ore_copper_surface", + "sanctuary:woodland_ore_coal_ledges", + "sanctuary:woodland_ore_iron_ledges", + "sanctuary:woodland_ore_copper_ledges" + ], + [], + [], + [ + "sanctuary:woodland_groves", + "minecraft:glow_lichen", + "sanctuary:woodland_shore_sugar_cane" + ], + [ + "minecraft:freeze_top_layer" + ] + ], + "has_precipitation": true, + "temperature": 0.8 +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/feature/woodland_ore_coal.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/feature/woodland_ore_coal.json new file mode 100644 index 0000000..19db242 --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/feature/woodland_ore_coal.json @@ -0,0 +1,83 @@ +{ + "type": "minecraft:ore", + "discard_chance_on_air_exposure": 0.0, + "size": 6, + "targets": [ + { + "state": "minecraft:coal_ore", + "target": { + "predicate_type": "minecraft:any_of", + "rules": [ + { + "predicate_type": "minecraft:all_of", + "rules": [ + { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:height_specific_ore_replaceables" + }, + { + "max_inclusive": 2031, + "min_inclusive": 0, + "predicate_type": "minecraft:height_match" + } + ] + }, + { + "predicate_type": "minecraft:all_of", + "rules": [ + { + "predicate_type": "minecraft:not", + "rule": { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:height_specific_ore_replaceables" + } + }, + { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:stone_ore_replaceables" + } + ] + } + ] + } + }, + { + "state": "minecraft:deepslate_coal_ore", + "target": { + "predicate_type": "minecraft:any_of", + "rules": [ + { + "predicate_type": "minecraft:all_of", + "rules": [ + { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:height_specific_ore_replaceables" + }, + { + "max_inclusive": 8, + "min_inclusive": -2032, + "predicate_type": "minecraft:height_match" + } + ] + }, + { + "predicate_type": "minecraft:all_of", + "rules": [ + { + "predicate_type": "minecraft:not", + "rule": { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:height_specific_ore_replaceables" + } + }, + { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:deepslate_ore_replaceables" + } + ] + } + ] + } + } + ] +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/feature/woodland_ore_copper.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/feature/woodland_ore_copper.json new file mode 100644 index 0000000..58803b9 --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/feature/woodland_ore_copper.json @@ -0,0 +1,83 @@ +{ + "type": "minecraft:ore", + "discard_chance_on_air_exposure": 0.0, + "size": 5, + "targets": [ + { + "state": "minecraft:copper_ore", + "target": { + "predicate_type": "minecraft:any_of", + "rules": [ + { + "predicate_type": "minecraft:all_of", + "rules": [ + { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:height_specific_ore_replaceables" + }, + { + "max_inclusive": 2031, + "min_inclusive": 0, + "predicate_type": "minecraft:height_match" + } + ] + }, + { + "predicate_type": "minecraft:all_of", + "rules": [ + { + "predicate_type": "minecraft:not", + "rule": { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:height_specific_ore_replaceables" + } + }, + { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:stone_ore_replaceables" + } + ] + } + ] + } + }, + { + "state": "minecraft:deepslate_copper_ore", + "target": { + "predicate_type": "minecraft:any_of", + "rules": [ + { + "predicate_type": "minecraft:all_of", + "rules": [ + { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:height_specific_ore_replaceables" + }, + { + "max_inclusive": 8, + "min_inclusive": -2032, + "predicate_type": "minecraft:height_match" + } + ] + }, + { + "predicate_type": "minecraft:all_of", + "rules": [ + { + "predicate_type": "minecraft:not", + "rule": { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:height_specific_ore_replaceables" + } + }, + { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:deepslate_ore_replaceables" + } + ] + } + ] + } + } + ] +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/feature/woodland_ore_iron.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/feature/woodland_ore_iron.json new file mode 100644 index 0000000..6b35295 --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/feature/woodland_ore_iron.json @@ -0,0 +1,83 @@ +{ + "type": "minecraft:ore", + "discard_chance_on_air_exposure": 0.0, + "size": 5, + "targets": [ + { + "state": "minecraft:iron_ore", + "target": { + "predicate_type": "minecraft:any_of", + "rules": [ + { + "predicate_type": "minecraft:all_of", + "rules": [ + { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:height_specific_ore_replaceables" + }, + { + "max_inclusive": 2031, + "min_inclusive": 0, + "predicate_type": "minecraft:height_match" + } + ] + }, + { + "predicate_type": "minecraft:all_of", + "rules": [ + { + "predicate_type": "minecraft:not", + "rule": { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:height_specific_ore_replaceables" + } + }, + { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:stone_ore_replaceables" + } + ] + } + ] + } + }, + { + "state": "minecraft:deepslate_iron_ore", + "target": { + "predicate_type": "minecraft:any_of", + "rules": [ + { + "predicate_type": "minecraft:all_of", + "rules": [ + { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:height_specific_ore_replaceables" + }, + { + "max_inclusive": 8, + "min_inclusive": -2032, + "predicate_type": "minecraft:height_match" + } + ] + }, + { + "predicate_type": "minecraft:all_of", + "rules": [ + { + "predicate_type": "minecraft:not", + "rule": { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:height_specific_ore_replaceables" + } + }, + { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:deepslate_ore_replaceables" + } + ] + } + ] + } + } + ] +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/material_rule/woodland_island.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/material_rule/woodland_island.json new file mode 100644 index 0000000..d1be08e --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/material_rule/woodland_island.json @@ -0,0 +1,65 @@ +{ + "type": "minecraft:sequence", + "sequence": [ + { + "type": "minecraft:condition", + "if_true": { + "type": "minecraft:biome", + "biome_is": "sanctuary:woodland_sulfur_depths" + }, + "then_run": { + "type": "minecraft:condition", + "if_true": { + "type": "minecraft:noise_threshold", + "noise": "sanctuary:woodland_strata", + "min_threshold": 0.0, + "max_threshold": 1000, + "is_3d": true + }, + "then_run": { + "type": "minecraft:block", + "result_state": "minecraft:sulfur" + } + } + }, + { + "type": "minecraft:condition", + "if_true": { + "type": "minecraft:biome", + "biome_is": "sanctuary:woodland_rocky_heath" + }, + "then_run": "sanctuary:woodland_strata" + }, + { + "type": "minecraft:condition", + "if_true": { + "type": "minecraft:noise_threshold", + "noise": "sanctuary:woodland_exposure", + "min_threshold": 0.85, + "max_threshold": 1000, + "is_3d": false + }, + "then_run": "sanctuary:woodland_strata" + }, + { + "type": "minecraft:condition", + "if_true": { + "type": "minecraft:biome", + "biome_is": "sanctuary:woodland_dry_woodland" + }, + "then_run": { + "type": "minecraft:condition", + "if_true": { + "type": "minecraft:noise_threshold", + "noise": "sanctuary:woodland_exposure", + "min_threshold": 0.15, + "max_threshold": 1000, + "is_3d": false + }, + "then_run": "sanctuary:woodland_strata" + } + }, + "sanctuary:starter_island", + "sanctuary:woodland_strata" + ] +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/material_rule/woodland_strata.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/material_rule/woodland_strata.json new file mode 100644 index 0000000..a22d03a --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/material_rule/woodland_strata.json @@ -0,0 +1,51 @@ +{ + "type": "minecraft:sequence", + "sequence": [ + { + "type": "minecraft:condition", + "if_true": { + "type": "minecraft:noise_threshold", + "noise": "sanctuary:woodland_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:woodland_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:woodland_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" + } + ] +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/noise/woodland_exposure.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/noise/woodland_exposure.json new file mode 100644 index 0000000..f4e40d6 --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/noise/woodland_exposure.json @@ -0,0 +1,5 @@ +{ + "base_amplitude": 1.0, + "base_octave": -7, + "octave_count": 2 +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/noise/woodland_strata.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/noise/woodland_strata.json new file mode 100644 index 0000000..ed46579 --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/noise/woodland_strata.json @@ -0,0 +1,5 @@ +{ + "base_amplitude": 1.0, + "base_octave": -6, + "octave_count": 2 +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/noise_settings/sanctuary_woodland.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/noise_settings/sanctuary_woodland.json new file mode 100644 index 0000000..d6c5ebd --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/noise_settings/sanctuary_woodland.json @@ -0,0 +1,23 @@ +{ + "default_block": "minecraft:stone", + "default_fluid": "minecraft:air", + "disable_mob_generation": false, + "legacy_random_source": true, + "material_rule": "sanctuary:woodland_island", + "noise": { + "height": 384, + "min_y": 0 + }, + "noise_router": { + "chunk_surface_level": 0.0, + "continents": 0.0, + "depth": 0.0, + "erosion": 0.0, + "final_density": "sanctuary:final_density_384", + "ridges": 0.0, + "temperature": "sanctuary:temperate_variation", + "vegetation": "sanctuary:temperate_moisture" + }, + "sea_level": -64, + "spawn_target": [] +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_groves.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_groves.json new file mode 100644 index 0000000..2c44307 --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_groves.json @@ -0,0 +1,6 @@ +{ + "feature": { + "type": "sanctuary:woodland_groves" + }, + "placement": [] +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_coal.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_coal.json new file mode 100644 index 0000000..d59c2ec --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_coal.json @@ -0,0 +1,27 @@ +{ + "feature": "sanctuary:woodland_ore_coal", + "placement": [ + { + "type": "minecraft:count", + "count": 2 + }, + { + "type": "minecraft:in_square" + }, + { + "type": "minecraft:height_range", + "height": { + "type": "minecraft:trapezoid", + "min_inclusive": { + "absolute": 96 + }, + "max_inclusive": { + "absolute": 288 + } + } + }, + { + "type": "minecraft:biome" + } + ] +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_coal_ledges.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_coal_ledges.json new file mode 100644 index 0000000..291d585 --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_coal_ledges.json @@ -0,0 +1,53 @@ +{ + "feature": "sanctuary:woodland_ore_coal", + "placement": [ + { + "type": "minecraft:rarity_filter", + "chance": 3 + }, + { + "type": "minecraft:in_square" + }, + { + "type": "minecraft:height_range", + "height": { + "type": "minecraft:uniform", + "min_inclusive": { + "absolute": 48 + }, + "max_inclusive": { + "absolute": 272 + } + } + }, + { + "type": "minecraft:block_predicate_filter", + "predicate": { + "type": "minecraft:matching_block_tag", + "tag": "minecraft:air" + } + }, + { + "type": "minecraft:environment_scan", + "direction_of_search": "down", + "max_steps": 32, + "target_condition": { + "type": "minecraft:matching_block_tag", + "tag": "minecraft:stone_ore_replaceables" + }, + "allowed_search_condition": { + "type": "minecraft:matching_block_tag", + "tag": "minecraft:air" + } + }, + { + "type": "minecraft:offset", + "x": 0, + "y": 1, + "z": 0 + }, + { + "type": "minecraft:biome" + } + ] +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_coal_surface.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_coal_surface.json new file mode 100644 index 0000000..c2ae430 --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_coal_surface.json @@ -0,0 +1,31 @@ +{ + "feature": "sanctuary:woodland_ore_coal", + "placement": [ + { + "type": "minecraft:rarity_filter", + "chance": 3 + }, + { + "type": "minecraft:in_square" + }, + { + "type": "minecraft:heightmap", + "heightmap": "OCEAN_FLOOR_WG" + }, + { + "type": "minecraft:block_predicate_filter", + "predicate": { + "type": "minecraft:matching_block_tag", + "tag": "minecraft:stone_ore_replaceables", + "offset": [ + 0, + -1, + 0 + ] + } + }, + { + "type": "minecraft:biome" + } + ] +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_copper.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_copper.json new file mode 100644 index 0000000..6d9aa06 --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_copper.json @@ -0,0 +1,27 @@ +{ + "feature": "sanctuary:woodland_ore_copper", + "placement": [ + { + "type": "minecraft:count", + "count": 1 + }, + { + "type": "minecraft:in_square" + }, + { + "type": "minecraft:height_range", + "height": { + "type": "minecraft:trapezoid", + "min_inclusive": { + "absolute": 96 + }, + "max_inclusive": { + "absolute": 288 + } + } + }, + { + "type": "minecraft:biome" + } + ] +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_copper_ledges.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_copper_ledges.json new file mode 100644 index 0000000..44de768 --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_copper_ledges.json @@ -0,0 +1,53 @@ +{ + "feature": "sanctuary:woodland_ore_copper", + "placement": [ + { + "type": "minecraft:rarity_filter", + "chance": 4 + }, + { + "type": "minecraft:in_square" + }, + { + "type": "minecraft:height_range", + "height": { + "type": "minecraft:uniform", + "min_inclusive": { + "absolute": 48 + }, + "max_inclusive": { + "absolute": 272 + } + } + }, + { + "type": "minecraft:block_predicate_filter", + "predicate": { + "type": "minecraft:matching_block_tag", + "tag": "minecraft:air" + } + }, + { + "type": "minecraft:environment_scan", + "direction_of_search": "down", + "max_steps": 32, + "target_condition": { + "type": "minecraft:matching_block_tag", + "tag": "minecraft:stone_ore_replaceables" + }, + "allowed_search_condition": { + "type": "minecraft:matching_block_tag", + "tag": "minecraft:air" + } + }, + { + "type": "minecraft:offset", + "x": 0, + "y": 1, + "z": 0 + }, + { + "type": "minecraft:biome" + } + ] +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_copper_surface.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_copper_surface.json new file mode 100644 index 0000000..73df78f --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_copper_surface.json @@ -0,0 +1,31 @@ +{ + "feature": "sanctuary:woodland_ore_copper", + "placement": [ + { + "type": "minecraft:rarity_filter", + "chance": 4 + }, + { + "type": "minecraft:in_square" + }, + { + "type": "minecraft:heightmap", + "heightmap": "OCEAN_FLOOR_WG" + }, + { + "type": "minecraft:block_predicate_filter", + "predicate": { + "type": "minecraft:matching_block_tag", + "tag": "minecraft:stone_ore_replaceables", + "offset": [ + 0, + -1, + 0 + ] + } + }, + { + "type": "minecraft:biome" + } + ] +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_iron.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_iron.json new file mode 100644 index 0000000..7ec18ce --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_iron.json @@ -0,0 +1,27 @@ +{ + "feature": "sanctuary:woodland_ore_iron", + "placement": [ + { + "type": "minecraft:count", + "count": 2 + }, + { + "type": "minecraft:in_square" + }, + { + "type": "minecraft:height_range", + "height": { + "type": "minecraft:trapezoid", + "min_inclusive": { + "absolute": 88 + }, + "max_inclusive": { + "absolute": 288 + } + } + }, + { + "type": "minecraft:biome" + } + ] +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_iron_ledges.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_iron_ledges.json new file mode 100644 index 0000000..ce03fa5 --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_iron_ledges.json @@ -0,0 +1,53 @@ +{ + "feature": "sanctuary:woodland_ore_iron", + "placement": [ + { + "type": "minecraft:rarity_filter", + "chance": 4 + }, + { + "type": "minecraft:in_square" + }, + { + "type": "minecraft:height_range", + "height": { + "type": "minecraft:uniform", + "min_inclusive": { + "absolute": 48 + }, + "max_inclusive": { + "absolute": 272 + } + } + }, + { + "type": "minecraft:block_predicate_filter", + "predicate": { + "type": "minecraft:matching_block_tag", + "tag": "minecraft:air" + } + }, + { + "type": "minecraft:environment_scan", + "direction_of_search": "down", + "max_steps": 32, + "target_condition": { + "type": "minecraft:matching_block_tag", + "tag": "minecraft:stone_ore_replaceables" + }, + "allowed_search_condition": { + "type": "minecraft:matching_block_tag", + "tag": "minecraft:air" + } + }, + { + "type": "minecraft:offset", + "x": 0, + "y": 1, + "z": 0 + }, + { + "type": "minecraft:biome" + } + ] +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_iron_surface.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_iron_surface.json new file mode 100644 index 0000000..a1842db --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_ore_iron_surface.json @@ -0,0 +1,31 @@ +{ + "feature": "sanctuary:woodland_ore_iron", + "placement": [ + { + "type": "minecraft:rarity_filter", + "chance": 4 + }, + { + "type": "minecraft:in_square" + }, + { + "type": "minecraft:heightmap", + "heightmap": "OCEAN_FLOOR_WG" + }, + { + "type": "minecraft:block_predicate_filter", + "predicate": { + "type": "minecraft:matching_block_tag", + "tag": "minecraft:stone_ore_replaceables", + "offset": [ + 0, + -1, + 0 + ] + } + }, + { + "type": "minecraft:biome" + } + ] +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_shore_sugar_cane.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_shore_sugar_cane.json new file mode 100644 index 0000000..0baa9db --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_shore_sugar_cane.json @@ -0,0 +1,6 @@ +{ + "feature": { + "type": "sanctuary:woodland_shore_sugar_cane" + }, + "placement": [] +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_sparse_trees.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_sparse_trees.json new file mode 100644 index 0000000..316979f --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/woodland_sparse_trees.json @@ -0,0 +1,23 @@ +{ + "feature": "minecraft:trees_birch_and_oak_leaf_litter", + "placement": [ + { + "type": "minecraft:count", + "count": 2 + }, + { + "type": "minecraft:in_square" + }, + { + "type": "minecraft:surface_water_depth_filter", + "max_water_depth": 0 + }, + { + "type": "minecraft:heightmap", + "heightmap": "OCEAN_FLOOR" + }, + { + "type": "minecraft:biome" + } + ] +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/world_preset/sanctuary.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/world_preset/sanctuary.json index f8833be..32b612e 100644 --- a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/world_preset/sanctuary.json +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/world_preset/sanctuary.json @@ -5,14 +5,17 @@ "generator": { "type": "minecraft:noise", "biome_source": { - "type": "sanctuary:layered_island", - "meadow": "sanctuary:layered_meadow", - "flower_meadow": "sanctuary:layered_flower_meadow", - "lower_grove": "sanctuary:layered_lower_grove", - "mossy_terraces": "sanctuary:layered_mossy_terraces", - "rocky_terraces": "sanctuary:layered_rocky_terraces" + "type": "sanctuary:woodland_island", + "oak_forest": "sanctuary:woodland_oak_forest", + "birch_forest": "sanctuary:woodland_birch_forest", + "clearing": "sanctuary:woodland_clearing", + "dry_woodland": "sanctuary:woodland_dry_woodland", + "dark_grove": "sanctuary:woodland_dark_grove", + "bamboo_grove": "sanctuary:woodland_bamboo_grove", + "sulfur_depths": "sanctuary:woodland_sulfur_depths", + "rocky_heath": "sanctuary:woodland_rocky_heath" }, - "settings": "sanctuary:sanctuary_layered" + "settings": "sanctuary:sanctuary_woodland" } }, "minecraft:the_end": { diff --git a/mods/sanctuary/src/main/resources/sanctuary.mixins.json b/mods/sanctuary/src/main/resources/sanctuary.mixins.json index 8053e7c..b014df9 100644 --- a/mods/sanctuary/src/main/resources/sanctuary.mixins.json +++ b/mods/sanctuary/src/main/resources/sanctuary.mixins.json @@ -5,7 +5,8 @@ "mixins": [ "InitialSpawnMixin", "SurfaceHydrologyMixin", - "LayeredSpringOutletMixin" + "LayeredSpringOutletMixin", + "WoodlandSpringOutletMixin" ], "client": [ "client.SanctuarySkyMixin", diff --git a/mods/sanctuary/src/test/java/fr/koka/sanctuary/worldgen/WoodlandGroveSmoke.java b/mods/sanctuary/src/test/java/fr/koka/sanctuary/worldgen/WoodlandGroveSmoke.java new file mode 100644 index 0000000..d044908 --- /dev/null +++ b/mods/sanctuary/src/test/java/fr/koka/sanctuary/worldgen/WoodlandGroveSmoke.java @@ -0,0 +1,19 @@ +package fr.koka.sanctuary.worldgen; + +public final class WoodlandGroveSmoke { + public static void main(String[] args) { + WoodlandGroveGeometry.Sampler cave = (x,y,z) -> y >= 80 && y <= 100 || y >= 120 && y <= 130 ? 1 : -1; + require(WoodlandGroveGeometry.highestSurface(cave, 0, 0) == 130, "Highest roof must stay distinct from the lower floor"); + require(WoodlandGroveGeometry.naturalFootprint(cave, 0, 100, 0, 2), "A true 2x2 dark oak foot should fit"); + require(WoodlandGroveGeometry.treeRoom(cave, 0, 101, 0, 2, 6, 12), "Dark oak needs an actual free crown below the roof"); + require(!WoodlandGroveGeometry.treeRoom(cave, 0, 101, 0, 2, 6, 20), "Do not clip a canopy through its ceiling"); + WoodlandGroveGeometry.Sampler thin = (x,y,z) -> y == 100 ? 1 : -1; + require(!WoodlandGroveGeometry.naturalFootprint(thin, 0, 100, 0, 2), "A floating one-block floor must not support a tree"); + WoodlandGroveGeometry.Sampler pierced = (x,y,z) -> x == 1 && z == 1 && y <= 100 ? -1 : cave.sample(x,y,z); + require(!WoodlandGroveGeometry.naturalFootprint(pierced, 0, 100, 0, 2), "Every one of the four trunk supports must exist"); + require(WoodlandGroveGeometry.seed(42, 3, 4, 5, 9) == WoodlandGroveGeometry.seed(42, 3, 4, 5, 9), "Placement must replay"); + require(WoodlandGroveGeometry.seed(42, 3, 4, 5, 9) != WoodlandGroveGeometry.seed(43, 3, 4, 5, 9), "World seed must affect placement"); + System.out.println("WoodlandGroveSmoke: natural multi-block feet and real canopy clearance passed."); + } + private static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); } +} diff --git a/mods/sanctuary/src/test/java/fr/koka/sanctuary/worldgen/WoodlandPaletteSmoke.java b/mods/sanctuary/src/test/java/fr/koka/sanctuary/worldgen/WoodlandPaletteSmoke.java new file mode 100644 index 0000000..73f19d5 --- /dev/null +++ b/mods/sanctuary/src/test/java/fr/koka/sanctuary/worldgen/WoodlandPaletteSmoke.java @@ -0,0 +1,40 @@ +package fr.koka.sanctuary.worldgen; + +import java.util.EnumSet; + +/** Regression: a higher hill must not replace ordinary oak/birch climate with exposed stone. */ +public final class WoodlandPaletteSmoke { + public static void main(String[] args) { + for (int y = 240; y < 384; y++) { + require(WoodlandBiomePalette.select(y, 0.2F, 0.2F) == WoodlandBiomePalette.Kind.OAK_FOREST, + "Raising a temperate forest must retain its oak cover"); + require(WoodlandBiomePalette.select(y, -0.45F, 0.1F) == WoodlandBiomePalette.Kind.BIRCH_FOREST, + "A high birch woodland must not become a meadow or bare stone because of altitude"); + } + require(WoodlandBiomePalette.select(144, 0.2F, 0.2F) == WoodlandBiomePalette.Kind.DARK_GROVE, + "The corresponding sheltered lower layer must have a distinct humid woodland"); + require(WoodlandBiomePalette.select(144, -0.6F, 0.7F) == WoodlandBiomePalette.Kind.BAMBOO_GROVE, + "A warm humid lower pocket must support the rare bamboo palette"); + require(WoodlandBiomePalette.select(80, 0.75F, -0.5F) == WoodlandBiomePalette.Kind.SULFUR_DEPTHS, + "The deep geological pocket must remain reachable"); + for (int y = 144; y < 384; y++) require( + WoodlandBiomePalette.select(y, 0.75F, -0.5F) != WoodlandBiomePalette.Kind.SULFUR_DEPTHS, + "Sulfur pockets must not cover ordinary upper surfaces"); + for (int y = 144; y < 384; y += 8) require( + WoodlandBiomePalette.select(y, 0.6F, 0.2F) == WoodlandBiomePalette.Kind.ROCKY_HEATH, + "The strongest horizontal rock patches must remain recognizable across layers"); + EnumSet reachable = EnumSet.noneOf(WoodlandBiomePalette.Kind.class); + for (int y = 32; y < 384; y += 8) for (int variation = -10; variation <= 10; variation++) { + for (int moisture = -10; moisture <= 10; moisture++) { + reachable.add(WoodlandBiomePalette.select(y, variation / 10.0F, moisture / 10.0F)); + } + } + require(reachable.equals(EnumSet.allOf(WoodlandBiomePalette.Kind.class)), + "All eight declared palettes must be reachable within the dimension"); + System.out.println("WoodlandPaletteSmoke: persistent upper forests, sheltered groves, bounded sulfur depths and horizontal rock patches passed."); + } + + private static void require(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } +} diff --git a/packwiz/README.md b/packwiz/README.md index e9ec3bf..3768f88 100644 --- a/packwiz/README.md +++ b/packwiz/README.md @@ -51,7 +51,7 @@ Pour produire un fichier importable par un lanceur compatible Modrinth : ```sh cd build/packwiz # depuis la racine du dépôt -packwiz modrinth export --output ../Sanctuary-0.1.0-alpha.6.mrpack +packwiz modrinth export --output ../Sanctuary-0.1.0-alpha.7.mrpack ``` L'export contient le JAR Sanctuary local et la référence vérifiée de Fabric API. diff --git a/packwiz/pack.toml b/packwiz/pack.toml index 103bea5..9ce72c5 100644 --- a/packwiz/pack.toml +++ b/packwiz/pack.toml @@ -1,6 +1,6 @@ name = "Sanctuary" author = "KOKA99CAB" -version = "0.1.0-alpha.6" +version = "0.1.0-alpha.7" pack-format = "packwiz:1.1.0" [index]