From 392ade0b7e82d1646d33723c2ec8c1f875fcd353 Mon Sep 17 00:00:00 2001 From: koka Date: Sat, 12 Sep 2026 00:36:17 +0200 Subject: [PATCH] Release alpha27 from alpha24 terrain with cave swamps and restored vanilla ships --- .gitignore | 1 + CHANGELOG.md | 10 + README.md | 26 +- docs/backlog.md | 12 + docs/generation-alpha27.md | 64 +++++ docs/packwiz.md | 19 ++ docs/testing-alpha27.md | 97 +++++++ gradle.properties | 4 +- mods/sanctuary/build.gradle | 10 + .../Aerial24FlowAdmissionGameTests.java | 2 +- .../gametest/Aerial24WorldGameTests.java | 16 +- .../gametest/Swamp27WorldGameTests.java | 247 ++++++++++++++++++ .../src/gametest/resources/fabric.mod.json | 2 +- .../java/fr/koka/sanctuary/SanctuaryMod.java | 1 + .../worldgen/SanctuaryBiomeSource.java | 20 +- .../sanctuary/worldgen/Swamp27Decoration.java | 138 ++++++++++ .../sanctuary/worldgen/Swamp27Volumes.java | 37 +++ .../worldgen/aerial/AerialGeometry24.java | 13 +- .../worldgen/aerial/AerialPlan24.java | 10 +- .../worldgen/aerial/AerialPlanner24.java | 23 +- .../worldgen/aerial/AerialRenderer24.java | 107 +------- .../worldgen/aerial/AerialStructure24.java | 2 +- .../worldgen/aerial/AerialStructures24.java | 9 +- .../worldgen/aerial/NativeMerchantShip27.java | 81 ++++++ .../assets/sanctuary/lang/en_us.json | 6 +- .../assets/sanctuary/lang/fr_fr.json | 6 +- .../loot_table/chests/aerial24/balloon.json | 127 --------- .../worldgen/biome/swamp27_caves.json | 172 ++++++++++++ .../placed_feature/swamp27_decoration.json | 6 + .../worldgen/structure/aerial_balloon.json | 10 - .../worldgen/world_preset/sanctuary.json | 1 + .../worldgen/Swamp27VolumesSmoke.java | 34 +++ .../worldgen/aerial/Aerial24Smoke.java | 35 ++- .../aerial/AerialPersistence24Smoke.java | 13 +- packwiz/pack.toml | 2 +- 35 files changed, 1065 insertions(+), 298 deletions(-) create mode 100644 docs/generation-alpha27.md create mode 100644 docs/testing-alpha27.md create mode 100644 mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Swamp27WorldGameTests.java create mode 100644 mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/Swamp27Decoration.java create mode 100644 mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/Swamp27Volumes.java create mode 100644 mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/NativeMerchantShip27.java delete mode 100644 mods/sanctuary/src/main/resources/data/sanctuary/loot_table/chests/aerial24/balloon.json create mode 100644 mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/swamp27_caves.json create mode 100644 mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/swamp27_decoration.json delete mode 100644 mods/sanctuary/src/main/resources/data/sanctuary/worldgen/structure/aerial_balloon.json create mode 100644 mods/sanctuary/src/test/java/fr/koka/sanctuary/worldgen/Swamp27VolumesSmoke.java diff --git a/.gitignore b/.gitignore index a90e1ea..7168e57 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ screenshots/ .env .env.* !.env.example +__pycache__/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bd0d5f..76304a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 0.1.0-alpha.27 — 2026-09-12 + +Retour à la base alpha.24 après rejet visuel de l’alpha.25. Le relief et les +souterrains de la 24 sont conservés ; la montgolfière est retirée. Première +passe limitée aux marais souterrains et à des épaves vanilla redressées +et restaurées pour les navires marchands. Les nouvelles +montagnes, cavités et garanties océaniques propres à la 25 ne sont pas reprises. +Alpha.26 sautée ; essai sur nouveau monde, sans migration. Voir le +[contrat alpha.27](docs/generation-alpha27.md) et les [vérifications](docs/testing-alpha27.md). + ## 0.1.0-alpha.20 — 2026-09-10 - Passe architecturale sur les six bâtiments urbains, les quatorze anciennes diff --git a/README.md b/README.md index 97829c4..fa530e1 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,29 @@ infrastructures qui permettront progressivement d'ouvrir de nouveaux continents. Ce dépôt démarre la nouvelle base Fabric. La [vision complète](docs/vision.md) conserve les intentions ; le [backlog](docs/backlog.md) suit les tickets. Les systèmes d’économie, de progression collective et les dimensions -décrits dans la vision ne sont pas encore implémentés. **L’alpha.24 est -publiée** : cinq lieux suspendus accompagnent l’île naturelle et ses secrets. -Voir [les essais](docs/testing-alpha24.md) et [la distribution](docs/packwiz.md). +décrits dans la vision ne sont pas encore implémentés. **L’alpha.27 repart de +l’alpha.24**, après le rejet du rendu de l’alpha.25. Cette première passe conserve +le relief et les souterrains de la 24, retire la montgolfière et travaille les +marais souterrains et la restauration de vrais modèles d’épaves vanilla. Voir le +[chantier et ses limites](docs/generation-alpha27.md). + +## Alpha.27 — retour au terrain de la 24 + +La base est le commit `e7d1f29`, sans les massifs coniques, chambres creusées +et couches de planification de l’alpha.25. Les changements de relief seront +repris séparément après un essai visuel. L’alpha.26 est sautée à la demande +du créateur. + +Les deux navires et les deux îlots restent présents, avec un seul îlot à +cascade. Les marais occupent des volumes de biomes dans les cavités existantes, +sans produire une nouvelle géométrie de grotte. Ce retour ne prétend pas +remplacer le creusement Sanctuary par l’ensemble du moteur de grottes vanilla. + +Créer un **nouveau monde Sanctuary** ; tailles 512 / 724 / 1 024 et Moyen par +défaut conservés. Le codec de terrain reste `sanctuary:island_v24` dans ce +prototype alpha.27 ; la reprise des anciennes sauvegardes n’est pas un objectif. +La distribution et les vérifications effectuées sont détaillées dans +[les essais alpha.27](docs/testing-alpha27.md) et [packwiz](docs/packwiz.md). ## Alpha.24 — les lieux suspendus diff --git a/docs/backlog.md b/docs/backlog.md index 953f961..bd7660a 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -954,3 +954,15 @@ herbes et fleurs. [Validation](testing-alpha24.md) réussie sur Mac : 14 scénar Release alpha.24 publiée ; canal packwiz et instance Prism existante synchronisés deux fois. Les 925 fichiers personnels et réglages suivis sont conservés. Aucun essai Windows natif revendiqué. + +### WG-29 — Reprendre la génération par petits incréments — alpha.27 + +Branche `codex/reprise-sanctuary-alpha27`, directement depuis `e7d1f29` (alpha.24). +La 25 est rejetée pour son rendu artificiel : les validations fonctionnelles ne +validaient pas l’expérience visuelle attendue. Alpha.26 sautée. + +Première passe : retrouver le terrain 24, enlever la montgolfière, proposer des +marais volumétriques dans les cavités existantes et affiner les coques. Voir le +[contrat](generation-alpha27.md). Les étagements, l’érosion, les nouvelles grottes +et les petites sorties d’eau seront traités un à un, avec retour en jeu entre +les changements ; ils ne sont pas déclarés implémentés dans cette passe. diff --git a/docs/generation-alpha27.md b/docs/generation-alpha27.md new file mode 100644 index 0000000..382c627 --- /dev/null +++ b/docs/generation-alpha27.md @@ -0,0 +1,64 @@ +# Alpha.27 — reprendre la 24 et regarder le résultat + +Le retour du 12 septembre 2026 rejette l'alpha.25 : cônes creux, enveloppes +rocheuses trop minces, cavités génériques et île trop perforée. Les tests +fonctionnels passaient mais ne démontraient pas un rendu naturel. La branche +`codex/reprise-sanctuary-alpha27` repart directement de `e7d1f29`, la base 24 +publiée ; elle ne désactive pas simplement quelques options de la 25. + +## Cette première passe + +- Retrouver le relief, les cavités et l'hydrologie de la 24. Aucun ajout de + montagne, volume de cavité, lac ou plateforme pour faire tenir un biome. +- Retirer la montgolfière, son placement et ses réservations ; conserver les + deux navires marchands et les deux îlots minéraux, dont un seul à cascade. +- Reprendre directement `minecraft:shipwreck/rightsideup_full`, une épave + vanilla complète déjà à l’endroit, puis restaurer les trous, les mâts et + l’accès. Conserver ses blocs, dalles, escaliers et sa cabine, au lieu de + dessiner une nouvelle coque approximative. Garder les marchands et un + coffre de cargaison par navire. +- Répartir des secteurs de marais en trois dimensions dans les souterrains + existants, remplaçant localement les autres biomes de grottes. Le sol et les + végétaux changent, pas la densité rocheuse. Chêne et végétation humide plutôt + que bosquets de chêne noir dans ces secteurs. + +Minecraft reste en **26.3-pre-2**. Les tailles restent **512 / 724 / 1 024**, Moyen +par défaut, avec le bouton natif Personnaliser. Les options existantes des +structures gardent leur rôle. Les marais sont naturels, indépendants de ces +options. Le Deep Dark n'est pas ajouté. + +Le prototype alpha.27 conserve le codec `sanctuary:island_v24` et ses ressources +de terrain. Le créateur teste des mondes jetables et a levé l'objectif de +compatibilité ; on ne convertit ni ne régénère ses sauvegardes. **Nouveau monde +nécessaire.** Les archives de la 25 restent sur `codex/terrain-sanctuary-alpha25`, +commit de livraison `df51e17` et documentation `1bf3a3b`. + +## Ce qui reste à travailler après l'essai + +Les plateaux étagés, l'érosion, les galeries proches des bruits de grottes +vanilla et les sorties d'eau libres ne sont pas réintroduits en bloc. Ils +seront examinés séparément, à partir de ce que l'on voit en jeu. Des points +d'eau peuvent retomber sur l'île ou dans le vide ; leur intérêt ne dépend pas +d'une arrivée dans un bassin calculé. + +Mangroves, grenouilles, slimes et lucioles restent les références de l'écologie +des marais. La liste réellement présente et les restrictions natives sont +consignées dans [les vérifications](testing-alpha27.md), sans promettre une +population observée à partir d'une simple entrée dans un biome. + +Les fonctions propres à la 25 — nouveau village souterrain, cabane du mineur, +cadre du sanctuaire et garantie spécifique du monument — sont écartées avec +sa base. La 24 conserve ses structures et expansions ; leur présence n'est +pas présentée comme une reprise de tous les contrats de la 25. + +## Méthode retenue + +Une passe limitée, un pack jouable, puis un retour sur son apparence. Vérifier +le build et les nouveaux points de comportement ne remplace pas ce retour. +Pas de matrice exhaustive ni de benchmark supplémentaire avant cet essai. +Les prochaines optimisations devront conserver les formes jugées bonnes, +au lieu de fabriquer des espaces faciles à certifier. + +Le benchmark archivé de la 25 mesurait 169,816 s contre 60,932 s pour un parcours +de 369 chunks (générations 24/25 dans les mêmes sources 25). Il documente un coût +sur ce témoin Mac ; il ne sauve pas le rendu refusé et ne mesure pas cette 27. diff --git a/docs/packwiz.md b/docs/packwiz.md index 6428977..6b65185 100644 --- a/docs/packwiz.md +++ b/docs/packwiz.md @@ -13,6 +13,25 @@ JAR Sanctuary sont des pièces jointes de releases Gitea. Aucun JAR de mod ou dépendance téléchargée n'entre dans l'historique Git. Le canal Beta est distinct de l'ancien pack 26.2. +## Alpha.27 prête à publier — 12 septembre 2026 + +La reprise repart directement de l’alpha.24 : relief et cavités conservés, +marais souterrains localisés, montgolfière retirée et navires fondés sur le +véritable template vanilla `minecraft:shipwreck/rightsideup_full`, restauré. +Voir [le périmètre](generation-alpha27.md) et [les essais](testing-alpha27.md). +Minecraft 26.3-pre-2 et les dépendances de la 24 sont conservés. + +`check build` puis `assemblePack` passent sur Mac. Le JAR, le MRpack et le ZIP +d’amorçage sont vérifiés localement. La publication et la synchronisation de +l’instance existante sont les prochaines étapes ; elles ne sont pas encore +attestées par ce commit. + +SHA-256 JAR : `56144b291407650e148aa3769596165a5e87398ea4a847cf67352ce2a2c625bf`. +SHA-256 MRpack : `e13be90c6454bad3cdd3418fc54b827488700876f6e4ddb42e4a05f57acfecb6`. + +Créer un **nouveau monde Sanctuary** pour cet essai. Les sauvegardes et réglages +personnels sont conservés. Aucun essai Windows natif n’est revendiqué. + ## Alpha.24 publiée — 11 septembre 2026 `check build assemblePack` et les essais natifs passent. Le JAR et diff --git a/docs/testing-alpha27.md b/docs/testing-alpha27.md new file mode 100644 index 0000000..f23edcd --- /dev/null +++ b/docs/testing-alpha27.md @@ -0,0 +1,97 @@ +# Alpha.27 — vérifications de reprise + +Base de travail : `e7d1f29` (alpha.24), branche +`codex/reprise-sanctuary-alpha27`. Mod et pack : `0.1.0-alpha.27`, Minecraft +`26.3-pre-2`. Nouveau monde Sanctuary, Moyen 724 par défaut. + +Les fichiers de densité et les ressources de relief de la 24 sont conservés. +Les deux changements localisés concernent les biomes de marais et les lieux +aériens. Voir le [périmètre exact](generation-alpha27.md). + +## Résultat local du 12 septembre 2026 + +`./gradlew check build` réussit en **3 min 28 s**. Le serveur de développement +termine ses **19 tests obligatoires**, dont les témoins marais et aériens, sans +échec. Le journal faisant foi est `build/alpha27-check-build-final.log` ; les +premiers journaux de préparation ne décrivent pas ce résultat final. + +Commande exacte : + +```sh +./gradlew check build -PsanctuaryDensityMaps=false -PsanctuaryAerialPreview -PsanctuaryAerial24Tests=true +./gradlew assemblePack -x :sanctuary:check +``` + +La première option désactive les images de densité facultatives. Le contrôle +`check` vient de réussir et n’est pas répété pendant l’assemblage du pack. +Les 56 fichiers de densité, relief et hydrologie recensés sont identiques à +la base `e7d1f29` (`build/alpha27-evidence/terrain-base.json`). + +Les deux reçus natifs ci-dessous proviennent du même processus `48406`, dans +un **nouveau monde Moyen, diamètre 724, graine 0**, avec les structures et +l'option expérimentale activées. Ils ont été écrits à 00:32:38 et 00:32:47, +pendant le dernier passage réussi. Le générateur reste `sanctuary:island_v24` +dans cette reprise ; la version du mod est bien alpha.27. + +## Marais souterrain réel + +`Swamp27WorldGameTests` réutilise le plan hydrologique central déjà en cache, +puis inspecte au maximum huit chunks FULL demandés explicitement. Aucune +région supplémentaire n'est planifiée pour sélectionner les eaux témoins ; +aucun bloc ni point d'eau n'est posé par le test. + +Le reçu `mods/sanctuary/build/run/gameTest/diagnostics/swamp27/native.json` +confirme : + +- **1 444 sols de cavités sous voûte**, dont 1 315 couverts d'herbe, mousse ou boue ; +- **355 blocs d'eau** dans le biome de marais ; +- **494 plantes** : 107 buissons à lucioles, 292 herbes courtes et 95 lianes ; +- des eaux souterraines existantes à Y118, 120, 129 et 130 ; 289 colonnes d'eau + recoupent le champ du marais dans la région inspectée. + +Repère pour visiter ce témoin dans un nouveau monde de mêmes paramètres : + +```text +/tp @s -6.5 130 -92.5 +``` + +`diagnostics/swamp27/blocks.json` exporte une fenêtre des **blocs FULL réels** +et une coupe destinée à l'aperçu. Le monde reste intact. Cette projection ne +reproduit pas l'éclairage, les shaders ou le rendu exact du client Minecraft. + +## Quatre lieux aériens + +Le reçu `mods/sanctuary/build/run/gameTest/diagnostics/aerial24/create.json` +porte `passed: true` et confirme **deux bateaux, deux îlots, deux marchands +réels, quatre coffres et une source de cascade**. Les deux marchands sont +observés après chargement effectif des entités. Les 56 chunks du témoin +aérien incluent les structures et leurs points de rivage. + +Les coques lisent le vrai template Minecraft `shipwreck/rightsideup_full`, +palette 0 : 643 des 659 blocs physiques d’origine sont conservés lors de la +restauration, 16 remplacés et 76 ajoutés avant le mobilier marchand. Les +escaliers, dalles et la cabine native restent présents. Les aperçus de blocs +simplifiés ont été inspectés ; ils ne sont pas des captures du client. + +Le test contrôle les blocs posés, les accès, les coffres enterrés des îlots, +les starts natifs et leur sérialisation. Il ouvre les quatre coffres, +déplace les deux marchands et retire quelques blocs dans le monde jetable, +puis sauvegarde. Les quatre sites restent enregistrés comme générés ; le +journal d'expansion est inchangé. + +Les tests purs couvrent également **neuf combinaisons** de graines 0/42/2026 +et diamètres 512/724/1024 : déterminisme des plans, orientations, limites et +réservations. Ce sont des calculs de plans et de géométrie, **pas neuf mondes +natifs intégralement générés**. + +## Limites + +Cette passe ne valide pas une nouvelle matrice de rechargements, toutes les +graines ni toute l'île. Elle ne garantit pas des grenouilles ou des chênes +dans chaque cavité. Le premier témoin sec a servi à corriger la sélection +du test ; les chiffres ci-dessus proviennent du témoin humide final. + +Aucun essai Windows natif, mesure de FPS ou validation visuelle par le joueur +n'est revendiqué. La durée du build comprend les vérifications et ne mesure +pas le temps de chargement d'une partie. La publication des artefacts et la +synchronisation Prism font l'objet de reçus de livraison distincts. diff --git a/gradle.properties b/gradle.properties index 7532532..3044c72 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.24 -pack_version=0.1.0-alpha.24 +mod_version=0.1.0-alpha.27 +pack_version=0.1.0-alpha.27 maven_group=fr.koka.sanctuary diff --git a/mods/sanctuary/build.gradle b/mods/sanctuary/build.gradle index e91af78..620fe98 100644 --- a/mods/sanctuary/build.gradle +++ b/mods/sanctuary/build.gradle @@ -603,6 +603,8 @@ tasks.register('aerial24Smoke', JavaExec) { dependsOn('testClasses') classpath = sourceSets.test.runtimeClasspath mainClass = 'fr.koka.sanctuary.worldgen.aerial.Aerial24Smoke' + if (providers.gradleProperty('sanctuaryAerialPreview').isPresent()) + systemProperty('sanctuary.aerial.preview', rootProject.layout.buildDirectory.file('alpha27-preview/native-ships.json').get().asFile.absolutePath) } tasks.named('check') { dependsOn('aerial24Smoke') } @@ -613,3 +615,11 @@ tasks.register('aerialPersistence24Smoke', JavaExec) { mainClass = 'fr.koka.sanctuary.worldgen.aerial.AerialPersistence24Smoke' } tasks.named('check') { dependsOn('aerialPersistence24Smoke') } + +tasks.register('swamp27VolumesSmoke', JavaExec) { + group = 'verification' + dependsOn('testClasses') + classpath = sourceSets.test.runtimeClasspath + mainClass = 'fr.koka.sanctuary.worldgen.Swamp27VolumesSmoke' +} +tasks.named('check') { dependsOn('swamp27VolumesSmoke') } diff --git a/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Aerial24FlowAdmissionGameTests.java b/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Aerial24FlowAdmissionGameTests.java index 68bcba2..6d29738 100644 --- a/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Aerial24FlowAdmissionGameTests.java +++ b/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Aerial24FlowAdmissionGameTests.java @@ -33,7 +33,7 @@ public final class Aerial24FlowAdmissionGameTests { && level.structureManager().shouldGenerateStructures(), "Fresh generation24 with aerial sites and native structures enabled"); var plan = generator.aerialPlan24(); var session = Objects.requireNonNull(ExpansionRuntime.session(level)); - helper.assertTrue(session.islands().size() == 1 && plan.sites().size() == 5, "Use an untouched disposable world for admission witnesses"); + helper.assertTrue(session.islands().size() == 1 && plan.sites().size() == 4, "Use an untouched disposable world for admission witnesses"); var evidence = StructureCensusService.snapshot(level.getServer()).evidence(); String dimension = level.dimension().identifier().toString(); for (var site : plan.sites()) for (var chunk : site.claimedChunks()) diff --git a/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Aerial24WorldGameTests.java b/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Aerial24WorldGameTests.java index 928fb1b..067f070 100644 --- a/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Aerial24WorldGameTests.java +++ b/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Aerial24WorldGameTests.java @@ -47,7 +47,7 @@ public final class Aerial24WorldGameTests { private static final com.google.gson.Gson JSON = new GsonBuilder().setPrettyPrinting().create(); @GameTest(maxTicks = 24000) - public void fiveSitesPersistAfterUse(GameTestHelper helper) throws Exception { + public void fourSitesPersistAfterUse(GameTestHelper helper) throws Exception { var report = new LinkedHashMap(); try { begin(helper, report); } catch (Exception failure) { failure(report, failure); throw failure; } @@ -69,10 +69,10 @@ public final class Aerial24WorldGameTests { helper.assertTrue(StructureCensusService.aerialSites(level).isEmpty(), "Disabled aerial sites are not presented as planned"); report.put("passed", true); write(Path.of("diagnostics/aerial24/off.json"), report); helper.succeed(); return; } - helper.assertTrue(plan.sites().size() == 5, "All five initial aerial sites have a finite saved plan"); + helper.assertTrue(plan.sites().size() == 4, "All four initial aerial sites have a finite saved plan"); helper.assertTrue(plan.sites().stream().filter(AerialPlan24.Site::merchant).count() == 2, "Two merchant ships"); helper.assertTrue(plan.sites().stream().filter(AerialPlan24.Site::waterfall).count() == 1, "One waterfall site"); - if (generator.initialDiameter() == 724 && level.getSeed() == 0) + if (generator.initialDiameter() == 724 && (level.getSeed() == 0 || level.getSeed() == 42)) AerialRenderer24.writeDiagnostic(Path.of("diagnostics/aerial24/blocks.json"), plan.sites()); report.put("geometry", JSON.toJsonTree(plan.sites())); report.put("claim_count", plan.claimsChunks().size()); report.put("census_before", StructureCensusService.aerialSites(level)); @@ -148,8 +148,8 @@ public final class Aerial24WorldGameTests { report.put("native_tags", tags); report.put("full_chunks", chunks.size()); report.put("blocks_after", countSites(level, plan)); var census = StructureCensusService.aerialSites(level); - helper.assertTrue(census.size() == 5 && census.stream().allMatch(site -> site.status() == StructureCensusService.AerialStatus.GENERATED), - "Five historical generated sites remain indexed after loot, mining and demolition"); + helper.assertTrue(census.size() == 4 && census.stream().allMatch(site -> site.status() == StructureCensusService.AerialStatus.GENERATED), + "Four historical generated sites remain indexed after loot, mining and demolition"); report.put("census_after", census); helper.assertTrue(equalFiles(journalBefore, files(world.resolve("data").resolve(generator.journalDirectory()))), "Aerial tests never reserve or activate an expansion"); report.put("expansion_journal_unchanged", true); @@ -253,7 +253,7 @@ public final class Aerial24WorldGameTests { helper.assertTrue(state.isAir() || site.waterfall() && !state.getFluidState().isEmpty(), "No manufactured structure leaks outside its authored footprint"); } } - helper.assertTrue(chestCount == 5 && waterSources == 1, "Five physical chests and exactly one planned source"); + helper.assertTrue(chestCount == 4 && waterSources == 1, "Four physical chests and exactly one planned source"); report.put("chests", chestCount); report.put("waterfall_sources", waterSources); report.put("blocks_before", countSites(level, plan)); report.put("vegetation", vegetation); return expected; @@ -305,8 +305,8 @@ public final class Aerial24WorldGameTests { for (var entry : ores) { level.setBlock(entry.getKey(), Blocks.AIR.defaultBlockState(), 2); removed.add(coordinates(entry.getKey())); } } } - var balloon = plan.sites().stream().filter(site -> site.kind() == AerialPlan24.Kind.BALLOON).findFirst().orElseThrow(); - var demolition = AerialGeometry24.entry(balloon).below(); + var ship = plan.sites().stream().filter(AerialPlan24.Site::merchant).findFirst().orElseThrow(); + var demolition = AerialGeometry24.entry(ship).below(); helper.assertTrue(level.getBlockState(demolition).isSolid(), "The demolition witness removes an existing floor"); level.setBlock(demolition, Blocks.AIR.defaultBlockState(), 2); removed.add(coordinates(demolition)); var extracted = new TreeMap>(); int i = 0; diff --git a/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Swamp27WorldGameTests.java b/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Swamp27WorldGameTests.java new file mode 100644 index 0000000..8138763 --- /dev/null +++ b/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Swamp27WorldGameTests.java @@ -0,0 +1,247 @@ +package fr.koka.sanctuary.gametest; + +import com.google.gson.GsonBuilder; +import fr.koka.sanctuary.SanctuaryMod; +import fr.koka.sanctuary.worldgen.PopulationGenerationContext; +import fr.koka.sanctuary.worldgen.PopulationHydrology; +import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime; +import fr.koka.sanctuary.worldgen.IslandCapacity; +import fr.koka.sanctuary.worldgen.SanctuaryBiomeSource; +import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator; +import fr.koka.sanctuary.worldgen.Swamp27Volumes; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import net.fabricmc.fabric.api.gametest.v1.GameTest; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.NoiseColumn; +import net.minecraft.world.level.biome.BiomeResolver; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.status.ChunkStatus; + +/** One bounded native observation. Never places blocks, entities, or synthetic water. */ +public final class Swamp27WorldGameTests { + private static final com.google.gson.Gson JSON = new GsonBuilder().setPrettyPrinting().create(); + private static final Path OUT = Path.of("diagnostics/swamp27"); + private static final int MAX_COLUMNS = 256; + private static final int MAX_FULL_CHUNKS = 8; + + @GameTest(maxTicks = 2400) + public void nativeUndergroundSwamp(GameTestHelper helper) throws Exception { + var report = new LinkedHashMap(); + long started = System.nanoTime(); + try { + ServerLevel level = helper.getLevel(); + var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator(); + report.put("seed", level.getSeed()); report.put("diameter", generator.initialDiameter()); + report.put("generator", generator.rootGeneration()); report.put("process_id", ProcessHandle.current().pid()); + report.put("generate_structures", level.structureManager().shouldGenerateStructures()); + report.put("experimental", generator.experimentalStructures()); + helper.assertTrue(generator.rootGeneration() == 24, "Alpha27 uses the restored24 terrain generator"); + var binding = Objects.requireNonNull(PopulationGenerationContext.of(level)); + var resolver = generator.source().createResolver(binding.random().createClimateSampler(net.minecraft.world.level.levelgen.densityfunction.SamplerContext.builder().enableCaches().build())); + var candidates = waterCandidates(helper, binding, resolver, report); + int columns = 0; + // Deterministic rings visit the middle of distinct chunks, with a strict sampler budget. + int radius = Math.min(8, generator.initialDiameter() / 48); + search: for (int ring = 0; ring <= radius; ring++) + for (int cx = -ring; cx <= ring; cx++) for (int cz = -ring; cz <= ring; cz++) { + if (Math.max(Math.abs(cx), Math.abs(cz)) != ring) continue; + if (columns++ >= MAX_COLUMNS) break search; + int x = cx * 16 + 8, z = cz * 16 + 8; + NoiseColumn column = binding.generator().getBaseColumn(x, z, level, binding.random()); + for (int y = Swamp27Volumes.MAX_Y - 2; y >= Swamp27Volumes.MIN_Y; y--) { + if (!column.getBlock(y).isAir() || !column.getBlock(y + 1).isAir() + || !natural(column.getBlock(y - 1))) continue; + if (!resolver.getNoiseBiome(x >> 2, y >> 2, z >> 2).is(SanctuaryBiomeSource.SWAMP27)) continue; + int roof = roof(column, y); + if (roof < 0) continue; + candidates.putIfAbsent(new ChunkPos(cx, cz), new BlockPos(x, y, z)); + break; + } + if (candidates.size() == MAX_FULL_CHUNKS) break search; + } + report.put("sampled_columns", Math.min(columns, MAX_COLUMNS)); + report.put("candidate_positions", candidates.values().stream().map(Swamp27WorldGameTests::xyz).toList()); + helper.assertTrue(!candidates.isEmpty(), "The bounded biome/terrain scan finds a real cave floor below rock"); + var pending = candidates.keySet().iterator(); + var loaded = new ArrayList(); + Runnable[] step = new Runnable[1]; + step[0] = () -> { + try { + if (pending.hasNext()) { + var chunk = pending.next(); report.put("stage", "FULL " + chunk); + level.getChunk(chunk.x(), chunk.z(), ChunkStatus.FULL, true); loaded.add(chunk); + helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return; + } + report.put("stage", "inspect actual cave floors and vegetation"); + inspect(helper, level, loaded, report); + report.put("explicit_full_chunks", loaded.size()); + report.put("full_chunk_limit", MAX_FULL_CHUNKS); + report.put("elapsed_seconds", (System.nanoTime() - started) / 1_000_000_000.0); + report.put("passed", true); write(OUT.resolve("native.json"), report); helper.succeed(); + } catch (Exception error) { + failure(report, error); throw new IllegalStateException("Swamp27 native witness failed", error); + } + }; + helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); + } catch (Exception error) { failure(report, error); throw error; } + } + + private static LinkedHashMap waterCandidates(GameTestHelper helper, + PopulationGenerationContext.Binding binding, BiomeResolver resolver, Map report) { + int before = PopulationHydrologyRuntime.cachedRegionCount(binding.random()); + var plan = PopulationHydrologyRuntime.regionPlan(binding.generator(), binding.random(), new IslandCapacity.Region(0, 0)).water(); + int after = PopulationHydrologyRuntime.cachedRegionCount(binding.random()); + helper.assertTrue(before == after, "The water witness reuses the already cached central region"); + var undergroundIds = new HashSet(); + plan.features().stream().filter(feature -> feature.kind() == PopulationHydrology.Kind.TERRACE) + .forEach(feature -> undergroundIds.add(feature.id())); + var allY = new TreeMap(); var lowerY = new TreeMap(); + var byChunk = new HashMap(); var positions = new HashMap(); + int intersections = 0, lowerIntersections = 0, waterTopIntersections = 0; + for (var cell : plan.cells()) { + if (!cell.hasWater()) continue; + allY.merge(cell.waterY(), 1, Integer::sum); + boolean lower = undergroundIds.contains(cell.featureId()); + if (lower) lowerY.merge(cell.waterY(), 1, Integer::sum); + boolean inWater = resolver.getNoiseBiome(cell.x() >> 2, cell.waterY() >> 2, cell.z() >> 2).is(SanctuaryBiomeSource.SWAMP27); + boolean aboveWater = resolver.getNoiseBiome(cell.x() >> 2, (cell.waterY() + 1) >> 2, cell.z() >> 2).is(SanctuaryBiomeSource.SWAMP27); + if (inWater) { intersections++; if (lower) lowerIntersections++; } + if (aboveWater) waterTopIntersections++; + if (!lower || !inWater && !aboveWater) continue; + var chunk = new ChunkPos(cell.x() >> 4, cell.z() >> 4); + byChunk.merge(chunk, 1, Integer::sum); + positions.putIfAbsent(chunk, new BlockPos(cell.x(), cell.waterY() + 1, cell.z())); + } + var selected = new LinkedHashMap(); + byChunk.keySet().stream().sorted(Comparator.comparingInt(chunk -> byChunk.get(chunk)).reversed() + .thenComparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).limit(4) + .forEach(chunk -> selected.put(chunk, positions.get(chunk))); + var evidence = new LinkedHashMap(); + evidence.put("region", List.of(0, 0)); evidence.put("new_regions_planned", after - before); + evidence.put("all_water_columns_by_surface_y", allY); + evidence.put("underground_terrace_water_columns_by_surface_y", lowerY); + evidence.put("swamp_band", List.of(Swamp27Volumes.MIN_Y, Swamp27Volumes.MAX_Y - 1)); + evidence.put("water_surface_biome_intersections", intersections); + evidence.put("underground_water_surface_biome_intersections", lowerIntersections); + evidence.put("above_water_biome_intersections", waterTopIntersections); + evidence.put("prioritized_chunks", selected.keySet().stream().map(chunk -> List.of(chunk.x(), chunk.z())).toList()); + evidence.put("scope", "Existing central region only; zeros are observations, not a required water quota"); + report.put("cached_water_selection", evidence); + return selected; + } + + private static void inspect(GameTestHelper helper, ServerLevel level, List chunks, + Map report) throws Exception { + int floors = 0, covered = 0, plants = 0, water = 0; + BlockPos witness = null; int bestScore = -1, witnessRoof = -1; + var counts = new TreeMap(); + var byChunk = new ArrayList>(); + for (var chunk : chunks) { + int chunkFloors = 0, chunkPlants = 0, chunkWater = 0; + for (int x = chunk.x() * 16; x < chunk.x() * 16 + 16; x++) + for (int z = chunk.z() * 16; z < chunk.z() * 16 + 16; z++) + for (int y = Swamp27Volumes.MIN_Y; y < Swamp27Volumes.MAX_Y; y++) { + var at = new BlockPos(x, y, z); + if (!level.getBiome(at).is(SanctuaryBiomeSource.SWAMP27)) continue; + var state = level.getBlockState(at); + if (state.getFluidState().is(FluidTags.WATER)) { water++; chunkWater++; } + if (plant(state)) { + // A feature must obey native placement; old vanilla vines elsewhere are irrelevant. + if (!state.is(Blocks.VINE)) helper.assertTrue(state.canSurvive(level, at), "Native swamp plant retains its support: " + at + " " + state); + plants++; chunkPlants++; counts.merge(id(state), 1, Integer::sum); + } + if ((!state.isAir() && !plant(state)) || !state.getFluidState().isEmpty()) continue; + var ground = level.getBlockState(at.below()); + if (!natural(ground)) continue; + int ceiling = roof(level, at); + if (ceiling < 0 || !level.getBlockState(at.above()).getCollisionShape(level, at.above()).isEmpty()) continue; + floors++; chunkFloors++; + boolean soil = ground.is(Blocks.MOSS_BLOCK) || ground.is(Blocks.GRASS_BLOCK) || ground.is(Blocks.MUD); + if (soil) covered++; + int score = (waterNearby(level, at.below(), chunk) ? 40 : 0) + + (plant(state) ? 20 : 0) + (soil ? 5 : 0) - Math.max(0, ceiling - y - 24); + // Keep the export within this already loaded chunk, without another request. + if (score > bestScore && (x & 15) >= 3 && (x & 15) <= 12 && (z & 15) >= 3 && (z & 15) <= 12) { + witness = at; bestScore = score; witnessRoof = ceiling; + } + } + byChunk.add(Map.of("chunk", List.of(chunk.x(), chunk.z()), "cave_floors", chunkFloors, + "plants", chunkPlants, "water_blocks", chunkWater)); + } + report.put("native_cave_floors", floors); report.put("covered_floors", covered); + report.put("plants", plants); report.put("plant_counts", counts); report.put("water_blocks", water); + report.put("chunks", byChunk); + helper.assertTrue(floors > 0 && covered > 0, "FULL contains the new swamp biome over actual grass/moss/mud cave floors with a rock ceiling"); + helper.assertTrue(plants > 0, "The bounded FULL witness contains naturally generated swamp vegetation"); + helper.assertTrue(witness != null, "A visitable cave witness exists inside the loaded footprint"); + report.put("visit", xyz(witness)); report.put("ceiling_y", witnessRoof); + report.put("teleport", "/tp @s " + (witness.getX() + .5) + " " + witness.getY() + " " + (witness.getZ() + .5)); + export(level, witness, witnessRoof, report); + } + + private static void export(ServerLevel level, BlockPos witness, int ceiling, Map report) throws Exception { + int minX = (witness.getX() >> 4) * 16, minZ = (witness.getZ() >> 4) * 16; + int minY = Math.max(0, witness.getY() - 3), maxY = Math.min(ceiling + 1, witness.getY() + 32); + var blocks = new ArrayList>(); var cutaway = new ArrayList>(); + for (int x = minX; x < minX + 16; x++) for (int z = minZ; z < minZ + 16; z++) for (int y = minY; y <= maxY; y++) { + BlockState state = level.getBlockState(new BlockPos(x, y, z)); + if (state.isAir()) continue; + List block = List.of(x - minX, y - minY, z - minZ, id(state)); blocks.add(block); + // A documented display cut only; the actual world and the full export stay intact. + if (y < witness.getY() || z >= witness.getZ() && y < ceiling) cutaway.add(block); + } + var origin = List.of(minX, minY, minZ); var size = List.of(16, maxY - minY + 1, 16); + write(OUT.resolve("blocks.json"), List.of( + Map.of("id", "swamp27_native_full", "size", size, "origin", origin, "blocks", blocks), + Map.of("id", "swamp27_native_cutaway", "size", size, "origin", origin, "blocks", cutaway))); + report.put("export", Map.of("path", "diagnostics/swamp27/blocks.json", "origin", origin, "size", size, + "blocks", blocks.size(), "cutaway_blocks", cutaway.size(), "source", "actual FULL blocks; no test-authored terrain")); + } + + private static int roof(NoiseColumn column, int y) { + for (int top = y + 3; top <= Math.min(280, y + 96); top++) if (natural(column.getBlock(top))) return top; + return -1; + } + private static boolean waterNearby(ServerLevel level, BlockPos floor, ChunkPos chunk) { + for (int dx = -2; dx <= 2; dx++) for (int dz = -2; dz <= 2; dz++) { + var at = floor.offset(dx, 0, dz); + if (at.getX() >> 4 != chunk.x() || at.getZ() >> 4 != chunk.z()) continue; + if (level.getFluidState(at).is(FluidTags.WATER)) return true; + } + return false; + } + private static int roof(ServerLevel level, BlockPos at) { + for (int y = at.getY() + 3; y <= Math.min(280, at.getY() + 96); y++) + if (natural(level.getBlockState(at.atY(y)))) return y; + return -1; + } + private static boolean plant(BlockState state) { + return state.is(Blocks.SHORT_GRASS) || state.is(Blocks.FIREFLY_BUSH) || state.is(Blocks.SUGAR_CANE) + || state.is(Blocks.LILY_PAD) || state.is(Blocks.VINE); + } + private static boolean natural(BlockState state) { + return state.is(Blocks.STONE) || state.is(Blocks.DEEPSLATE) || state.is(Blocks.TUFF) || state.is(Blocks.CALCITE) + || state.is(Blocks.ANDESITE) || state.is(Blocks.GRANITE) || state.is(Blocks.DIORITE) || state.is(Blocks.DIRT) + || state.is(Blocks.COARSE_DIRT) || state.is(Blocks.GRASS_BLOCK) || state.is(Blocks.ROOTED_DIRT) + || state.is(Blocks.PODZOL) || state.is(Blocks.MYCELIUM) || state.is(Blocks.MOSS_BLOCK) + || state.is(Blocks.MUD) || state.is(Blocks.PACKED_MUD) || state.is(Blocks.CLAY); + } + private static List xyz(BlockPos at) { return List.of(at.getX(), at.getY(), at.getZ()); } + private static String id(BlockState state) { return BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(); } + private static void write(Path path, Object value) throws Exception { + Files.createDirectories(path.getParent()); Files.writeString(path, JSON.toJson(value) + "\n"); + } + private static void failure(Map report, Exception error) { + report.put("passed", false); report.put("error", error.toString()); + try { write(OUT.resolve("failure.json"), report); } catch (Exception io) { error.addSuppressed(io); } + SanctuaryMod.LOGGER.error("Swamp27 native witness failed", error); + } +} diff --git a/mods/sanctuary/src/gametest/resources/fabric.mod.json b/mods/sanctuary/src/gametest/resources/fabric.mod.json index a76d261..ea1ea4b 100644 --- a/mods/sanctuary/src/gametest/resources/fabric.mod.json +++ b/mods/sanctuary/src/gametest/resources/fabric.mod.json @@ -6,7 +6,7 @@ "environment": "*", "license": "GPL-3.0-or-later", "entrypoints": { - "fabric-gametest": ["${gametest_entrypoint}", "fr.koka.sanctuary.gametest.BlockKnowledgeGameTests", "fr.koka.sanctuary.gametest.CensusGameTestsTerrain", "fr.koka.sanctuary.gametest.CensusStockGameTests", "fr.koka.sanctuary.gametest.MaterialActivityGameTests"], + "fabric-gametest": ["${gametest_entrypoint}", "fr.koka.sanctuary.gametest.Swamp27WorldGameTests", "fr.koka.sanctuary.gametest.BlockKnowledgeGameTests", "fr.koka.sanctuary.gametest.CensusGameTestsTerrain", "fr.koka.sanctuary.gametest.CensusStockGameTests", "fr.koka.sanctuary.gametest.MaterialActivityGameTests"], "fabric-client-gametest": ["fr.koka.sanctuary.gametest.SanctuaryClientRenderTests"] }, "mixins": ["sanctuary-gametest.mixins.json"], 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 5792f28..990fc7f 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/SanctuaryMod.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/SanctuaryMod.java @@ -86,6 +86,7 @@ public final class SanctuaryMod implements ModInitializer { fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.register(); Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("population_island"), PopulationIslandDensity.CODEC); Registry.register(BuiltInRegistries.BIOME_SOURCE, id("population_island"), PopulationIslandBiomeSource.CODEC); + Registry.register(BuiltInRegistries.FEATURE_TYPE, id("swamp27_decoration"), fr.koka.sanctuary.worldgen.Swamp27Decoration.CODEC); Registry.register(BuiltInRegistries.FEATURE_TYPE, id("population_groves"), PopulationGrovesFeature.CODEC); Registry.register(BuiltInRegistries.FEATURE_TYPE, id("population_decorations"), PopulationDecorationsFeature.CODEC); Registry.register(BuiltInRegistries.FEATURE_TYPE, id("population_shore_sugar_cane"), PopulationShoreSugarCaneFeature.CODEC); diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/SanctuaryBiomeSource.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/SanctuaryBiomeSource.java index e67372c..649c839 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/SanctuaryBiomeSource.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/SanctuaryBiomeSource.java @@ -16,13 +16,15 @@ import net.minecraft.world.level.biome.Climate; /** Alpha.13 keeps Population's origin woodlands and the laboratory's native cave pockets. * Child islands retain the laboratory's complete climate palette. */ public final class SanctuaryBiomeSource extends ExpansionBiomeSource { + public static final ResourceKey SWAMP27 = ResourceKey.create(net.minecraft.core.registries.Registries.BIOME, + fr.koka.sanctuary.SanctuaryMod.id("swamp27_caves")); public static final List REQUIRED_BIOMES = Stream.concat( ExpansionBiomeSource.REQUIRED_BIOMES.stream(), Stream.of( PopulationIslandBiomeSource.OAK_FOREST, PopulationIslandBiomeSource.BIRCH_FOREST, PopulationIslandBiomeSource.CLEARING, PopulationIslandBiomeSource.DRY_WOODLAND, PopulationIslandBiomeSource.ROCKY_HEATH, PopulationIslandBiomeSource.DARK_GROVE, PopulationIslandBiomeSource.BAMBOO_GROVE, PopulationIslandBiomeSource.SULFUR_DEPTHS, - PopulationIslandBiomeSource.LUSH_CAVES, PopulationIslandBiomeSource.DRIPSTONE_CAVES + PopulationIslandBiomeSource.LUSH_CAVES, PopulationIslandBiomeSource.DRIPSTONE_CAVES, SWAMP27 ).map(key -> key.identifier().toString())).toList(); public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(i -> i.group( @@ -71,6 +73,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource { private final boolean cities; private final int generation; private final Holder cityBiome; + private final Holder swamp27; private volatile java.util.Map cityPlans = java.util.Map.of(); public SanctuaryBiomeSource(List> biomes) { @@ -81,6 +84,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource { this.generation = generation; this.cities = ExpansionIsland.spatialGeneration(generation); allBiomes = List.copyOf(biomes); + swamp27 = generation == 24 ? required(SWAMP27) : null; if ((generation == 23 || generation == 24)) for (String name : fr.koka.sanctuary.expansion.ExpansionOcean23.BIOMES) required(ResourceKey.create(net.minecraft.core.registries.Registries.BIOME, net.minecraft.resources.Identifier.withDefaultNamespace(name))); @@ -122,6 +126,20 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource { // Reuse the historical selector itself: its noise, thresholds, order and child // climate choices stay exact rather than being approximated by another climate field. Holder laboratory = super.resolve(snapshot, x, y, z); + if (generation == 24 && y >= Swamp27Volumes.MIN_Y && y < Swamp27Volumes.MAX_Y) { + for (ExpansionIsland island : snapshot) { + if (!island.origin() || !island.contains(x, z)) continue; + Holder lower = woodlands.getNoiseBiome(quartX, quartY, quartZ); + boolean cave = laboratory.is(Biomes.LUSH_CAVES) || laboratory.is(Biomes.DRIPSTONE_CAVES) + || laboratory.is(Biomes.SULFUR_CAVES) || lower.is(PopulationIslandBiomeSource.LUSH_CAVES) + || lower.is(PopulationIslandBiomeSource.DRIPSTONE_CAVES) + || lower.is(PopulationIslandBiomeSource.SULFUR_DEPTHS) + || lower.is(PopulationIslandBiomeSource.DARK_GROVE); + if (cave && Swamp27Volumes.contains(island.seed(), island.diameter(), + x - island.centerX(), y, z - island.centerZ())) return swamp27; + break; + } + } if (y < 0 || y >= 384 || laboratory.is(Biomes.LUSH_CAVES) || laboratory.is(Biomes.DRIPSTONE_CAVES) || laboratory.is(Biomes.SULFUR_CAVES)) return laboratory; for (ExpansionIsland island : snapshot) { diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/Swamp27Decoration.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/Swamp27Decoration.java new file mode 100644 index 0000000..087002a --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/Swamp27Decoration.java @@ -0,0 +1,138 @@ +package fr.koka.sanctuary.worldgen; + +import com.mojang.serialization.MapCodec; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.Registries; +import net.minecraft.data.worldgen.features.TreeFeatures; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityTypes; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.VineBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.feature.Feature; + +/** Decorates existing floors, water and ceilings only. Writes never leave the current chunk. */ +public final class Swamp27Decoration implements Feature { + public static final MapCodec CODEC = MapCodec.unit(Swamp27Decoration::new); + @Override public MapCodec codec() { return CODEC; } + + @Override public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource random, BlockPos origin) { + if (!(level.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator host) + || host.rootGeneration() != 24) return false; + var binding = PopulationGenerationContext.of(generator, level.getLevel()); + if (binding == null) return false; + int minX = (origin.getX() >> 4) * 16, minZ = (origin.getZ() >> 4) * 16; + long seed = binding.random().seed(); + boolean changed = false, frogPlaced = false; + int trees = 0; + boolean frogOpportunity = Math.floorMod(WoodlandGroveGeometry.seed(seed, minX, 0, minZ, 0xF207L), 8) == 0; + for (int x = minX; x < minX + 16; x++) for (int z = minZ; z < minZ + 16; z++) { + int floors = 0; + for (int y = Swamp27Volumes.MAX_Y - 1; y >= Swamp27Volumes.MIN_Y; y--) { + BlockPos at = new BlockPos(x, y, z); + if (!level.getBlockState(at).isAir() || !level.getBiome(at).is(SanctuaryBiomeSource.SWAMP27)) continue; + BlockPos floor = at.below(); + BlockState ground = level.getBlockState(floor); + boolean water = ground.getFluidState().is(FluidTags.WATER) && ground.getFluidState().isSource(); + if (!water && !natural(ground)) continue; + if (!roof(level, at) || lavaNearby(level, at)) continue; + long local = WoodlandGroveGeometry.seed(seed, x, y, z, 0xB07527L); + RandomSource detail = RandomSource.create(local); + if (water) { + if (detail.nextInt(9) == 0) changed |= plant(level, at, Blocks.LILY_PAD.defaultBlockState()); + if (++floors >= 3) break; + continue; + } + // Replace a single already solid natural floor, never a support in air or an ore. + boolean wet = waterNearby(level, floor, 2); + long patch = WoodlandGroveGeometry.seed(seed, Math.floorDiv(x, 6), y / 5, Math.floorDiv(z, 6), 0x501127L); + BlockState soil = (wet ? Blocks.MUD : Math.floorMod(patch, 5) < 2 + ? Blocks.MOSS_BLOCK : Blocks.GRASS_BLOCK).defaultBlockState(); + changed |= level.setBlock(floor, soil, 2); + if (trees < 2 && (x & 15) >= 4 && (x & 15) <= 11 && (z & 15) >= 4 && (z & 15) <= 11 + && Math.floorMod(local, 23) == 0 && at.getY() + 9 < Swamp27Volumes.MAX_Y) { + // Native placement decides whether the trunk and crown fit an irregular room. + // A radius-four margin keeps every write inside this chunk; existing hydrology + // and structure protection still guard each native write. + if (level.setBlock(floor, Blocks.GRASS_BLOCK.defaultBlockState(), 2) + && level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(TreeFeatures.SWAMP_OAK) + .value().place(level, generator, RandomSource.create(local), at)) { + trees++; + changed = true; + } + } + if (!level.getBlockState(at).isAir()) continue; + if (wet && detail.nextInt(5) == 0) changed |= plant(level, at, Blocks.SUGAR_CANE.defaultBlockState()); + else if (detail.nextInt(13) == 0) changed |= plant(level, at, Blocks.FIREFLY_BUSH.defaultBlockState()); + else if (detail.nextInt(4) == 0) changed |= plant(level, at, Blocks.SHORT_GRASS.defaultBlockState()); + + if (!frogPlaced && frogOpportunity && wet && level.getBlockState(at).isAir() + && level.getBlockState(at.above()).isAir() + && !level.getBlockState(floor).getCollisionShape(level, floor).isEmpty()) { + var frog = EntityTypes.FROG.create(level.getLevel(), EntitySpawnReason.CHUNK_GENERATION); + if (frog != null) { + frog.snapTo(x + .5, y, z + .5, detail.nextFloat() * 360, 0); + frog.finalizeSpawn(level, level.getCurrentDifficultyAt(at), EntitySpawnReason.CHUNK_GENERATION, null); + frog.setPersistenceRequired(); + level.addFreshEntityWithPassengers(frog); + frogPlaced = true; + changed = true; + } + } + if (++floors >= 3) break; + } + } + // Short vines cling to actual walls; every segment must still have its own support. + for (int attempt = 0; attempt < 40; attempt++) { + RandomSource vines = RandomSource.create(WoodlandGroveGeometry.seed(seed, minX, attempt, minZ, 0x710E27L)); + BlockPos at = new BlockPos(minX + 1 + vines.nextInt(14), + Swamp27Volumes.MIN_Y + vines.nextInt(Swamp27Volumes.MAX_Y - Swamp27Volumes.MIN_Y - 4), + minZ + 1 + vines.nextInt(14)); + if (!level.getBiome(at).is(SanctuaryBiomeSource.SWAMP27) || !roof(level, at)) continue; + for (Direction face : Direction.Plane.HORIZONTAL) { + if (!natural(level.getBlockState(at.relative(face)))) continue; + BlockState vine = Blocks.VINE.defaultBlockState().setValue(VineBlock.getPropertyForFace(face), true); + for (int n = 0; n < 4 && at.getY() - n >= Swamp27Volumes.MIN_Y; n++) { + BlockPos segment = at.below(n); + if (!level.getBlockState(segment).isAir() + || !level.getBiome(segment).is(SanctuaryBiomeSource.SWAMP27) + || !natural(level.getBlockState(segment.relative(face))) || lavaNearby(level, segment)) break; + changed |= plant(level, segment, vine); + } + break; + } + } + return changed; + } + + private static boolean plant(WorldGenLevel level, BlockPos at, BlockState plant) { + return level.getBlockState(at).isAir() && plant.canSurvive(level, at) && level.setBlock(at, plant, 2); + } + private static boolean roof(WorldGenLevel level, BlockPos at) { + for (int y = at.getY() + 3; y <= Math.min(280, at.getY() + 96); y++) + if (natural(level.getBlockState(new BlockPos(at.getX(), y, at.getZ())))) return true; + return false; + } + private static boolean waterNearby(WorldGenLevel level, BlockPos floor, int radius) { + for (int dx = -radius; dx <= radius; dx++) for (int dz = -radius; dz <= radius; dz++) + if (level.getFluidState(floor.offset(dx, 0, dz)).is(FluidTags.WATER)) return true; + return false; + } + private static boolean lavaNearby(WorldGenLevel level, BlockPos at) { + for (Direction face : Direction.values()) if (level.getFluidState(at.relative(face)).is(FluidTags.LAVA)) return true; + return false; + } + static boolean natural(BlockState state) { + return state.is(Blocks.STONE) || state.is(Blocks.DEEPSLATE) || state.is(Blocks.TUFF) + || state.is(Blocks.CALCITE) || state.is(Blocks.ANDESITE) || state.is(Blocks.GRANITE) + || state.is(Blocks.DIORITE) || state.is(Blocks.DIRT) || state.is(Blocks.COARSE_DIRT) + || state.is(Blocks.GRASS_BLOCK) || state.is(Blocks.ROOTED_DIRT) || state.is(Blocks.PODZOL) + || state.is(Blocks.MYCELIUM) || state.is(Blocks.MOSS_BLOCK) || state.is(Blocks.MUD) + || state.is(Blocks.PACKED_MUD) || state.is(Blocks.CLAY); + } +} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/Swamp27Volumes.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/Swamp27Volumes.java new file mode 100644 index 0000000..54c7b3f --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/Swamp27Volumes.java @@ -0,0 +1,37 @@ +package fr.koka.sanctuary.worldgen; + +/** A biome field only: never a density, aquifer, heightmap or terrain mutation. */ +public final class Swamp27Volumes { + public static final int MIN_Y = 72; + public static final int MAX_Y = 188; + private Swamp27Volumes() {} + + public static boolean contains(long seed, int diameter, int x, int y, int z) { + if (y < MIN_Y || y >= MAX_Y || diameter <= 0) return false; + double radius = diameter * .46; + if ((double) x * x + (double) z * z > radius * radius) return false; + double verticalEdge = Math.min(1, Math.min((y - MIN_Y) / 16.0, (MAX_Y - y) / 16.0)); + // Broad, connected sectors change with height; finer noise softens their borders. + double broad = noise(seed, x / 94.0, y / 48.0, z / 94.0); + double detail = noise(seed ^ 0x7757A27L, x / 37.0, y / 31.0, z / 37.0); + return broad + detail * .22 > .04 + (1 - verticalEdge) * .7; + } + + private static double noise(long seed, double x, double y, double z) { + int ix = (int) Math.floor(x), iy = (int) Math.floor(y), iz = (int) Math.floor(z); + double u = fade(x - ix), v = fade(y - iy), w = fade(z - iz); + double low = lerp(w, lerp(u, point(seed, ix, iy, iz), point(seed, ix + 1, iy, iz)), + lerp(u, point(seed, ix, iy, iz + 1), point(seed, ix + 1, iy, iz + 1))); + double high = lerp(w, lerp(u, point(seed, ix, iy + 1, iz), point(seed, ix + 1, iy + 1, iz)), + lerp(u, point(seed, ix, iy + 1, iz + 1), point(seed, ix + 1, iy + 1, iz + 1))); + return lerp(v, low, high); + } + private static double point(long seed, int x, int y, int z) { + long h = seed ^ x * 0x632BE59BD9B4E019L ^ y * 0x9E3779B97F4A7C15L ^ z * 0x94D049BB133111EBL; + h = (h ^ h >>> 30) * 0xBF58476D1CE4E5B9L; + h = (h ^ h >>> 27) * 0x94D049BB133111EBL; + return ((h ^ h >>> 31) >>> 11) * 0x1.0p-52 - 1; + } + private static double fade(double t) { return t * t * (3 - 2 * t); } + private static double lerp(double t, double a, double b) { return a + (b - a) * t; } +} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialGeometry24.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialGeometry24.java index a42d4eb..590df8b 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialGeometry24.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialGeometry24.java @@ -14,8 +14,7 @@ public final class AerialGeometry24 { } public static BlockPos chest(AerialPlan24.Site s) { return switch (s.kind()) { - case BALLOON -> world(s, s.width() / 2 - 2, 2, s.depth() / 2 + 2); - case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> world(s, 7, 6, 22); + case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> world(s, 7, 4, 24); case MINERAL_ISLET -> { int x = s.width() / 2 + 4, z = s.depth() / 2 + 2; yield world(s, x, terrainTop(s, x, z) - 3, z); @@ -24,27 +23,25 @@ public final class AerialGeometry24 { } public static BlockPos occupant(AerialPlan24.Site s) { if (!s.merchant()) throw new IllegalArgumentException("Only merchant ships have occupants"); - return world(s, 5, 6, 20); + return world(s, 5, 4, 22); } public static UUID occupantUuid(AerialPlan24.Site s) { return UUID.nameUUIDFromBytes(("sanctuary:aerial24:" + s.id() + ":" + s.seed() + ":merchant") .getBytes(StandardCharsets.UTF_8)); } - public static BlockPos workstation(AerialPlan24.Site s) { return world(s, 7, 6, 19); } + public static BlockPos workstation(AerialPlan24.Site s) { return world(s, 3, 4, 23); } public static BlockPos waterfallSource(AerialPlan24.Site s) { if (!s.waterfall()) throw new IllegalArgumentException("This site has no waterfall"); return world(s, s.width() / 2, rockHeight(s) - 3, 3); } public static BlockPos entry(AerialPlan24.Site s) { return switch (s.kind()) { - case BALLOON -> world(s, s.width() / 2, 2, s.depth() / 2 - 3); - case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> world(s, 7, 6, 12); + case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> world(s, 3, 5, 13); case MINERAL_ISLET -> world(s, s.width() / 2, terrainTop(s, s.width() / 2, s.depth() / 2) + 1, s.depth() / 2); }; } public static List interiorChecks(AerialPlan24.Site s) { - if (s.merchant()) return List.of(entry(s), world(s, 5, 6, 16), occupant(s), world(s, 5, 6, 23)); - if (s.kind() == AerialPlan24.Kind.BALLOON) return List.of(entry(s), world(s, s.width() / 2, 2, s.depth() / 2)); + if (s.merchant()) return List.of(entry(s), occupant(s), world(s, 5, 4, 21), world(s, 3, 4, 24)); return List.of(entry(s)); } public static int terrainTop(AerialPlan24.Site s, int x, int z) { diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialPlan24.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialPlan24.java index e9507fb..4168ece 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialPlan24.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialPlan24.java @@ -19,17 +19,16 @@ public final class AerialPlan24 { private final List sites; private final Set claims; - public enum Kind { BALLOON, MERCHANT_PROVISIONS, MERCHANT_TOOLS, MINERAL_ISLET } + public enum Kind { MERCHANT_PROVISIONS, MERCHANT_TOOLS, MINERAL_ISLET } public AerialPlan24(List sites) { this.sites = List.copyOf(sites); - if (!sites.isEmpty() && (sites.size() != 5 - || sites.stream().filter(s -> s.kind() == Kind.BALLOON).count() != 1 + if (!sites.isEmpty() && (sites.size() != 4 || sites.stream().filter(s -> s.kind() == Kind.MERCHANT_PROVISIONS).count() != 1 || sites.stream().filter(s -> s.kind() == Kind.MERCHANT_TOOLS).count() != 1 || sites.stream().filter(s -> s.kind() == Kind.MINERAL_ISLET).count() != 2 || sites.stream().filter(Site::waterfall).count() != 1)) - throw new IllegalArgumentException("An aerial plan contains all five sites and exactly one waterfall, or is disabled"); + throw new IllegalArgumentException("An aerial plan contains all four sites and exactly one waterfall, or is disabled"); var ids = new HashSet(); var columns = new HashSet(); for (Site site : sites) { @@ -70,8 +69,7 @@ public final class AerialPlan24 { || waterfall && kind != Kind.MINERAL_ISLET) throw new IllegalArgumentException("Invalid aerial site geometry"); if (kind == Kind.MINERAL_ISLET && (width < 40 || depth < 40 || height < 20 + VEGETATION_HEADROOM) - || kind == Kind.BALLOON && height != 28 - || (kind == Kind.MERCHANT_PROVISIONS || kind == Kind.MERCHANT_TOOLS) && (width != 11 || depth != 27 || height != 18)) + || (kind == Kind.MERCHANT_PROVISIONS || kind == Kind.MERCHANT_TOOLS) && (width != 11 || depth != 28 || height != 18)) throw new IllegalArgumentException("Aerial site differs from its version24 gabarit"); int worldWidth = facing % 2 == 0 ? width : depth; int worldDepth = facing % 2 == 0 ? depth : width; diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialPlanner24.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialPlanner24.java index ca794d0..2f5f841 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialPlanner24.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialPlanner24.java @@ -40,19 +40,21 @@ public final class AerialPlanner24 { var probe = new Probe(terrain, radius); var sites = new ArrayList(); var claimed = new HashSet(); - Kind[] kinds = {Kind.BALLOON, Kind.MERCHANT_PROVISIONS, Kind.MERCHANT_TOOLS, Kind.MINERAL_ISLET, Kind.MINERAL_ISLET}; - String[] ids = {"balloon", "merchant_provisions", "merchant_tools", "mineral_islet_water", "mineral_islet_dry"}; + Kind[] kinds = {Kind.MERCHANT_PROVISIONS, Kind.MERCHANT_TOOLS, Kind.MINERAL_ISLET, Kind.MINERAL_ISLET}; + String[] ids = {"merchant_provisions", "merchant_tools", "mineral_islet_water", "mineral_islet_dry"}; double initialAngle = unit(mix(seed ^ 0xA24AE21AL)) * Math.PI * 2; for (int ordinal = 0; ordinal < kinds.length; ordinal++) { long siteSeed = ExpansionIsland.deriveSeed(seed ^ 0xA24AE21AL, ids[ordinal]); - int width = kinds[ordinal] == Kind.BALLOON ? 15 : ordinal < 3 ? 11 : 40 + 4 * (int) Math.floorMod(siteSeed, 7); - int depth = kinds[ordinal] == Kind.BALLOON ? 19 : ordinal < 3 ? 27 : width; - int bodyHeight = kinds[ordinal] == Kind.BALLOON ? 28 : ordinal < 3 ? 18 : 20 + (int) Math.floorMod(mix(siteSeed), 16); - int height = bodyHeight + (ordinal >= 3 ? AerialPlan24.VEGETATION_HEADROOM : 0); + boolean merchant = ordinal < 2; + int width = merchant ? 11 : 40 + 4 * (int) Math.floorMod(siteSeed, 7); + int depth = merchant ? 28 : width; + int bodyHeight = merchant ? 18 : 20 + (int) Math.floorMod(mix(siteSeed), 16); + int height = bodyHeight + (merchant ? 0 : AerialPlan24.VEGETATION_HEADROOM); Site accepted = null; for (int attempt = 0; attempt < MAX_ATTEMPTS_PER_SITE; attempt++) { long draw = mix(siteSeed + attempt * 0x9e3779b97f4a7c15L); - double angle = initialAngle + ordinal * (Math.PI * 2 / 5) + (unit(draw) - .5) * .9 + // Keep the four remaining bearings and seeded shapes; the former first sector is simply free. + double angle = initialAngle + (ordinal + 1) * (Math.PI * 2 / 5) + (unit(draw) - .5) * .9 + (attempt / 16) * .41; Shore shore = shore(probe, angle, radius); if (shore == null) continue; @@ -62,19 +64,18 @@ public final class AerialPlanner24 { double distance = radius + 32 + Math.hypot(worldWidth, worldDepth) / 2 + 64 + (attempt % 4) * 20; int cx = (int) Math.round(Math.cos(angle) * distance), cz = (int) Math.round(Math.sin(angle) * distance); if (Math.hypot(cx - shore.x, cz - shore.z) > 500) continue; - int bottom = ordinal == 0 ? Math.clamp(shore.y + 24, 286, 321) - : ordinal < 3 ? Math.clamp(shore.y - 4 + (int) Math.floorMod(draw, 13), 180, 300) + int bottom = merchant ? Math.clamp(shore.y - 4 + (int) Math.floorMod(draw, 13), 180, 300) : Math.clamp(shore.y + 20 - bodyHeight / 2, 210, 320 - bodyHeight); bottom = Math.min(bottom, 350 - height); var site = new Site(ids[ordinal], kinds[ordinal], cx - worldWidth / 2, bottom, cz - worldDepth / 2, - width, height, depth, facing, siteSeed, shore.x, shore.y, shore.z, ordinal == 3); + width, height, depth, facing, siteSeed, shore.x, shore.y, shore.z, ordinal == 2); if (site.claimedChunks().stream().anyMatch(c -> claimed.contains(c.pack()))) continue; boolean rootOverlap = site.claimedChunks().stream().anyMatch(c -> { int nearestX = Math.clamp(0, c.getMinBlockX(), c.getMaxBlockX()); int nearestZ = Math.clamp(0, c.getMinBlockZ(), c.getMaxBlockZ()); return Math.hypot(nearestX, nearestZ) <= radius + 32; }); - if (rootOverlap || !visible(probe, shore, cx, bottom + (ordinal == 0 ? 18 : bodyHeight - 4), cz)) continue; + if (rootOverlap || !visible(probe, shore, cx, bottom + bodyHeight - 4, cz)) continue; accepted = site; break; } if (accepted == null) throw new IllegalStateException("aerial_sites_unavailable: could not place " + ids[ordinal] + " within the bounded search"); diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialRenderer24.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialRenderer24.java index 0826a50..7770f21 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialRenderer24.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialRenderer24.java @@ -41,7 +41,12 @@ public final class AerialRenderer24 { BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(), state.toString()); }).toList(); result.add(Map.of("id", site.id(), "kind", site.kind().name(), "min", List.of(site.minX(), site.minY(), site.minZ()), - "size", List.of(site.worldWidth(), site.height(), site.worldDepth()), "seed", site.seed(), "facing", site.facing(), "blocks", list)); + "size", List.of(site.worldWidth(), site.height(), site.worldDepth()), "seed", site.seed(), "facing", site.facing(), "blocks", list, + "native_template", site.merchant() ? Map.of("id", NativeMerchantShip27.TEMPLATE, "palette", NativeMerchantShip27.PALETTE, + "native_blocks", NativeMerchantShip27.restored().nativeBlocks(), "preserved_blocks", NativeMerchantShip27.restored().preservedBlocks(), + "added_blocks", NativeMerchantShip27.restored().addedBlocks(), "replaced_blocks", NativeMerchantShip27.restored().replacedBlocks(), + "drained_blocks", NativeMerchantShip27.restored().drainedBlocks(), + "restoration", "Native hull and stern cabin; repaired side planks/windows, mast and sail; merchant bed/workstation/light added afterwards") : Map.of())); } if (path.toAbsolutePath().getParent() != null) Files.createDirectories(path.toAbsolutePath().getParent()); Files.writeString(path, new com.google.gson.GsonBuilder().create().toJson(result) + "\n"); @@ -73,7 +78,6 @@ public final class AerialRenderer24 { var destination = connected == null ? clipped : connected; var b = new Builder(s, destination, connected == null ? clip : s.bounds()); switch (s.kind()) { - case BALLOON -> b.balloon(); case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> b.ship(); case MINERAL_ISLET -> b.islet(); } @@ -114,99 +118,16 @@ public final class AerialRenderer24 { set(x, y, z, state.setValue(BlockStateProperties.BED_PART, BedPart.FOOT)); set(x, y, z + 1, state.setValue(BlockStateProperties.BED_PART, BedPart.HEAD)); } - void balloon() { - int cx = s.width() / 2, cz = s.depth() / 2; - // The envelope is a rounded shell. Continuous spars attach the four real suspensions. - Block accent = (s.seed() & 1) == 0 ? Blocks.WOOL.yellow() : Blocks.WOOL.lightBlue(); - for (int y = 9; y <= 27; y++) for (int x = 0; x < s.width(); x++) for (int z = 0; z < s.depth(); z++) { - double q = Math.pow((x - cx) / 7.1, 2) + Math.pow((z - cz) / 9.1, 2) + Math.pow((y - 18) / 9.2, 2); - if (q <= 1.02 && q >= .73) set(x, y, z, Math.abs(x - cx) <= 1 || y == 19 ? accent : y % 6 == 0 ? Blocks.WOOL.lightGray() : Blocks.WOOL.white()); - } - box(cx - 3, 0, cz - 4, cx + 3, 1, cz + 4, Blocks.OAK_PLANKS); - for (int x = cx - 3; x <= cx + 3; x++) for (int z = cz - 4; z <= cz + 4; z++) { - if (Math.abs(x - cx) == 3 || Math.abs(z - cz) == 4) { - set(x, 2, z, Blocks.OAK_PLANKS); slab(x, 3, z); - } - } - // One entry faces a usable standing deck, with a removable railing for boarding. - set(cx, 2, cz - 4, Blocks.OAK_FENCE_GATE); set(cx, 3, cz - 4, Blocks.AIR); - for (int x : new int[]{cx - 3, cx + 3}) for (int z : new int[]{cz - 4, cz + 4}) { - box(x, 3, z, x, 5, z, Blocks.STRIPPED_OAK_LOG); - box(x, 6, z, x, 12, z, Blocks.IRON_CHAIN); - } - for (int z : new int[]{cz - 4, cz + 4}) box(cx - 4, 12, z, cx + 4, 12, z, Blocks.STRIPPED_OAK_LOG); - for (int x : new int[]{cx - 3, cx + 3}) box(x, 12, cz - 5, x, 12, cz + 5, Blocks.STRIPPED_OAK_LOG); - box(cx - 3, 6, cz, cx + 3, 6, cz, Blocks.STRIPPED_OAK_LOG); - box(cx - 3, 5, cz - 4, cx - 3, 6, cz + 4, Blocks.STRIPPED_OAK_LOG); - box(cx + 3, 5, cz - 4, cx + 3, 6, cz + 4, Blocks.STRIPPED_OAK_LOG); - set(cx, 5, cz, Blocks.COPPER_BULB.waxed().unaffected().defaultBlockState().setValue(BlockStateProperties.LIT, true)); - lantern(cx - 2, 5, cz); lantern(cx + 2, 5, cz); - set(cx + 2, 2, cz + 2, Blocks.CRAFTING_TABLE); - set(cx - 2, 2, cz - 2, Blocks.BARREL); - set(cx + 2, 2, cz - 2, Blocks.HAY_BLOCK); - } - int halfWidth(int y, int z) { - int end = Math.min(z, s.depth() - 1 - z); - return Math.max(0, Math.min(5, Math.min(1 + end, 1 + y))); - } void ship() { - int cx = 5; - for (int z = 0; z < s.depth(); z++) for (int y = 0; y <= 5; y++) { - int half = halfWidth(y, z); - for (int x = cx - half; x <= cx + half; x++) { - boolean hull = y == 0 || y == 5 || Math.abs(x - cx) == half || z < 2 || z >= 25; - set(x, y, z, hull ? (y == 0 ? Blocks.STRIPPED_OAK_LOG : Blocks.OAK_PLANKS) : Blocks.AIR); - } + // The complete upright vanilla ship, offset one block in X for its saved envelope. + for (var entry : NativeMerchantShip27.restored().blocks().entrySet()) { + var at = entry.getKey(); set(at.getX() + 1, at.getY(), at.getZ(), entry.getValue()); } - // Curved gunwales remain closed; the wide centre stays clear for boarding small boats. - for (int z = 1; z <= 25; z++) { - int half = halfWidth(5, z); - for (int side : new int[]{-1, 1}) { - int x = cx + side * half; - set(x, 6, z, Blocks.OAK_FENCE); // Connection properties are completed below. - if (z % 5 == 0) set(x, 6, z, Blocks.STRIPPED_OAK_LOG); - } - } - for (int x = 3; x <= 7; x++) { set(x, 6, 1, Blocks.OAK_FENCE); set(x, 6, 25, Blocks.OAK_FENCE); } - // Roof, walls, threshold and cabin circulation share one continuous frame. - box(2, 6, 17, 8, 9, 24, Blocks.OAK_PLANKS); - box(3, 6, 18, 7, 9, 23, Blocks.AIR); - box(2, 10, 17, 8, 10, 24, Blocks.OAK_PLANKS); - for (int z = 17; z <= 24; z++) { - stair(1, 10, z, Direction.EAST, Half.BOTTOM); stair(9, 10, z, Direction.WEST, Half.BOTTOM); - } - for (int x : new int[]{2, 8}) for (int z : new int[]{17, 24}) box(x, 6, z, x, 10, z, Blocks.STRIPPED_OAK_LOG); - for (int z = 19; z <= 21; z++) for (int y = 7; y <= 8; y++) { set(2, y, z, Blocks.GLASS); set(8, y, z, Blocks.GLASS); } - for (int x = 4; x <= 6; x++) set(x, 8, 24, Blocks.GLASS); - door(5, 6, 17, Direction.SOUTH); - bed(3, 6, 22); - set(7, 6, 19, s.kind() == AerialPlan24.Kind.MERCHANT_PROVISIONS ? Blocks.COMPOSTER : Blocks.SMITHING_TABLE); - set(3, 6, 19, Blocks.CRAFTING_TABLE); set(3, 6, 20, Blocks.BARREL); - lantern(5, 9, 20); lantern(5, 9, 23); - // Cale reached by an actual supported stair run; no disconnected decorative floor. - for (int step = 0; step < 4; step++) { - int z = 10 + step, y = 4 - step; - box(5, 1, z, 5, y, z, Blocks.OAK_PLANKS); - box(5, y + 1, z, 5, 6, z, Blocks.AIR); - stair(5, y, z, Direction.NORTH, Half.BOTTOM); - } - box(3, 1, 7, 7, 1, 20, Blocks.OAK_PLANKS); - set(3, 2, 17, Blocks.BARREL); set(7, 2, 17, Blocks.BARREL); - lantern(3, 4, 8); lantern(7, 4, 8); - Block cargo = s.kind() == AerialPlan24.Kind.MERCHANT_PROVISIONS ? Blocks.HAY_BLOCK : Blocks.IRON_BLOCK; - box(2, 6, 5, 3, 7, 7, cargo); box(7, 6, 5, 8, 6, 7, Blocks.BARREL); - box(5, 6, 4, 5, 16, 4, Blocks.STRIPPED_OAK_LOG); - for (int x = 1; x <= 9; x++) - set(x, 16, 4, Blocks.STRIPPED_OAK_LOG.defaultBlockState().setValue(BlockStateProperties.AXIS, Direction.Axis.X)); - for (int x = 2; x <= 8; x++) - set(x, 8, 4, Blocks.STRIPPED_OAK_LOG.defaultBlockState().setValue(BlockStateProperties.AXIS, Direction.Axis.X)); - set(5, 17, 4, Blocks.LANTERN); - // Seven by seven sail around the continuous mast: yard above, boom below, clear deck beneath. - for (int y = 9; y <= 15; y++) for (int x = 2; x <= 8; x++) - if (x != cx) set(x, y, 4, y == 11 ? Blocks.WOOL.lightGray() : Blocks.WOOL.white()); - // Stable handrails are explicit, rather than depending on neighbouring chunk updates. - for (int z = 2; z <= 24; z++) for (int x : new int[]{0, 10}) if (halfWidth(5, z) == 5 && z % 5 != 0 && !(x == 0 && z == 12)) - set(x, 6, z, Blocks.OAK_FENCE.defaultBlockState().setValue(BlockStateProperties.NORTH, true).setValue(BlockStateProperties.SOUTH, true)); + // Reuse the native stern cabin, with its original floor, roof, staircase and treasure-cache position. + set(3, 4, 23, s.kind() == AerialPlan24.Kind.MERCHANT_PROVISIONS ? Blocks.COMPOSTER : Blocks.SMITHING_TABLE); + bed(3, 4, 21); + set(7, 4, 21, Blocks.CRAFTING_TABLE); + lantern(5, 6, 22); } void islet() { for (int x = 0; x < s.width(); x++) for (int z = 0; z < s.depth(); z++) { diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialStructure24.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialStructure24.java index 6a53022..cebd0d0 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialStructure24.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialStructure24.java @@ -17,7 +17,7 @@ public final class AerialStructure24 extends Structure { private final String family; public AerialStructure24(StructureSettings settings, String family) { super(settings); - if (!Set.of("balloon", "merchant_ship", "mineral_islet").contains(family)) throw new IllegalArgumentException("Unknown aerial family"); + if (!Set.of("merchant_ship", "mineral_islet").contains(family)) throw new IllegalArgumentException("Unknown aerial family"); this.family = family; } public String family() { return family; } diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialStructures24.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialStructures24.java index 5df3aba..53a074d 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialStructures24.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialStructures24.java @@ -22,12 +22,11 @@ import net.minecraft.world.level.levelgen.structure.StructureType; import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceType; import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; -/** Native starts for five immutable initial sites. Their void chunks need no terrain island owner. */ +/** Native starts for four immutable initial sites. Their void chunks need no terrain island owner. */ public final class AerialStructures24 { - public static final ResourceKey BALLOON = key("aerial_balloon"); public static final ResourceKey MERCHANT_SHIP = key("aerial_merchant_ship"); public static final ResourceKey MINERAL_ISLET = key("aerial_mineral_islet"); - public static final List> KEYS = List.of(BALLOON, MERCHANT_SHIP, MINERAL_ISLET); + public static final List> KEYS = List.of(MERCHANT_SHIP, MINERAL_ISLET); public static final StructureType TYPE = Registry.register(BuiltInRegistries.STRUCTURE_TYPE, SanctuaryMod.id("aerial_v24"), () -> AerialStructure24.CODEC); public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE, @@ -37,10 +36,10 @@ public final class AerialStructures24 { private static ResourceKey key(String id) { return ResourceKey.create(Registries.STRUCTURE, SanctuaryMod.id(id)); } public static void register() {} public static String family(AerialPlan24.Site site) { - return switch (site.kind()) { case BALLOON -> "balloon"; case MINERAL_ISLET -> "mineral_islet"; default -> "merchant_ship"; }; + return switch (site.kind()) { case MINERAL_ISLET -> "mineral_islet"; default -> "merchant_ship"; }; } public static ResourceKey key(AerialPlan24.Site site) { - return switch (site.kind()) { case BALLOON -> BALLOON; case MINERAL_ISLET -> MINERAL_ISLET; default -> MERCHANT_SHIP; }; + return switch (site.kind()) { case MINERAL_ISLET -> MINERAL_ISLET; default -> MERCHANT_SHIP; }; } public static String structureId(AerialPlan24.Site site) { return key(site).identifier().toString(); } public static boolean isPlacing() { return PLACING.get() != null; } diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/NativeMerchantShip27.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/NativeMerchantShip27.java new file mode 100644 index 0000000..3494645 --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/NativeMerchantShip27.java @@ -0,0 +1,81 @@ +package fr.koka.sanctuary.worldgen.aerial; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.nbt.NbtAccounter; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.NbtOps; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; + +/** The actual Mojang upright ship, with local repairs; the vanilla hull, cabin, stairs and slabs remain the source. */ +public final class NativeMerchantShip27 { + public static final String TEMPLATE = "minecraft:shipwreck/rightsideup_full"; + public static final int WIDTH = 9, DEPTH = 28, NATIVE_HEIGHT = 9, PALETTE = 0; + private NativeMerchantShip27() {} + public record Restored(Map blocks, int nativeBlocks, int preservedBlocks, + int addedBlocks, int replacedBlocks, int drainedBlocks) {} + private static final class Loaded { private static final Restored VALUE = load(); } + public static Restored restored() { return Loaded.VALUE; } + private static Restored load() { + String resource = "/data/minecraft/structure/shipwreck/rightsideup_full.nbt"; + try (var input = NativeMerchantShip27.class.getResourceAsStream(resource)) { + if (input == null) throw new IOException("Missing vanilla ship template " + resource); + var tag = NbtIo.readCompressed(input, NbtAccounter.defaultQuota()); + var size = tag.getListOrEmpty("size"); + if (size.getIntOr(0, -1) != WIDTH || size.getIntOr(1, -1) != NATIVE_HEIGHT || size.getIntOr(2, -1) != DEPTH) + throw new IOException("Unexpected vanilla ship dimensions"); + var palettes = tag.getListOrEmpty("palettes"); + var palette = palettes.getListOrEmpty(PALETTE); + var states = new java.util.ArrayList(); + for (var value : palette) states.add(BlockState.CODEC.parse(NbtOps.INSTANCE, value).getOrThrow()); + var original = new LinkedHashMap(); + for (var value : tag.getListOrEmpty("blocks")) { + var block = value.asCompound().orElseThrow(); var pos = block.getListOrEmpty("pos"); + var at = new BlockPos(pos.getIntOr(0, -1), pos.getIntOr(1, -1), pos.getIntOr(2, -1)); + var state = states.get(block.getIntOr("state", -1)); + if (!state.is(Blocks.STRUCTURE_BLOCK)) original.put(at, state); + } + var result = new LinkedHashMap<>(original); + int drained = (int) original.values().stream().filter(state -> state.hasProperty(BlockStateProperties.WATERLOGGED) + && state.getValue(BlockStateProperties.WATERLOGGED)).count(); + result.replaceAll((at, state) -> state.hasProperty(BlockStateProperties.WATERLOGGED) + ? state.setValue(BlockStateProperties.WATERLOGGED, false) : state); + // Keep only the treasure chest as the site's loot chest; other native caches become empty barrels. + for (var entry : original.entrySet()) if (entry.getValue().is(Blocks.CHEST)) + result.put(entry.getKey(), Blocks.BARREL.defaultBlockState()); + result.put(new BlockPos(6, 4, 24), Blocks.CHEST.defaultBlockState()); + // Repair missing side planks by continuing their actual neighbouring native row. + for (int z : new int[]{12, 15, 18}) for (int x : new int[]{1, 2, 6, 7}) { + var at = new BlockPos(x, 3, z); + var neighbour = new BlockPos(x, 3, z - 1); + if (!result.containsKey(at) && original.containsKey(neighbour)) result.put(at, original.get(neighbour)); + } + // Restore the existing stern cabin's two damaged window strips, leaving its native stepped roof intact. + for (int x : new int[]{1, 7}) for (int z = 21; z <= 24; z++) + for (int y = 5; y <= 6; y++) result.put(new BlockPos(x, y, z), Blocks.GLASS.defaultBlockState()); + for (int x : new int[]{0, 8}) result.put(new BlockPos(x, 6, 22), Blocks.OAK_PLANKS.defaultBlockState()); + // The original main mast starts at (4,2,11); extend it rather than inventing another hull. + for (int y = 6; y <= 16; y++) result.put(new BlockPos(4, y, 11), Blocks.OAK_LOG.defaultBlockState()); + var yard = Blocks.OAK_LOG.defaultBlockState().setValue(BlockStateProperties.AXIS, Direction.Axis.X); + for (int x = 0; x < WIDTH; x++) result.put(new BlockPos(x, 16, 11), yard); + for (int y = 10; y <= 15; y++) for (int x = 1; x <= 7; x++) if (x != 4) + result.put(new BlockPos(x, y, 11), (y == 12 ? Blocks.WOOL.lightGray() : Blocks.WOOL.white()).defaultBlockState()); + // Complete the short rear cargo hoist using its native stump, clear of the cabin entrance. + for (int y = 6; y <= 8; y++) result.put(new BlockPos(4, y, 16), Blocks.OAK_LOG.defaultBlockState()); + for (int x = 3; x <= 5; x++) result.put(new BlockPos(x, 8, 16), yard); + int preserved = 0, added = 0, replaced = 0; + for (var entry : result.entrySet()) { + var old = original.get(entry.getKey()); + if (old == null) added++; else if (old.equals(entry.getValue())) preserved++; else replaced++; + } + return new Restored(Map.copyOf(result), original.size(), preserved, added, replaced, drained); + } catch (IOException | RuntimeException error) { + throw new IllegalStateException("Cannot restore required vanilla ship " + TEMPLATE, error); + } + } +} 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 4076f34..e85950b 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 @@ -226,7 +226,7 @@ "sanctuary.structures.census.status.incompatible": "Incompatible habitat", "sanctuary.structures.census.status.other_dimension": "Reserved for another dimension", "sanctuary.mineral_sanctuary.entry": "Mineral sanctuary: %s %s %s", - "sanctuary.world_options.aerial24_hint": "Secrets, abandoned routes and five aerial sites. Vanilla structures remain independent.", + "sanctuary.world_options.aerial24_hint": "Secrets, abandoned routes, two ships and two floating islets. Vanilla structures remain independent.", "sanctuary.structures.census.aerial.none": "No aerial sites reserved in this world.", "sanctuary.structures.census.aerial.unknown_site": "Unknown aerial site: %s", "sanctuary.structures.census.aerial.site": "%s · %s · %s · starts %s/%s · features %s/%s · full chunks %s/%s", @@ -235,7 +235,7 @@ "sanctuary.structures.census.aerial.status.generated": "generated", "sanctuary.structures.census.aerial.status.missing": "start missing after verification", "sanctuary.structures.census.aerial.status.unknown": "unknown", - "structure.sanctuary.aerial_balloon": "Hot air balloon", "structure.sanctuary.aerial_merchant_ship": "Merchant ship", - "structure.sanctuary.aerial_mineral_islet": "Mineral islet" + "structure.sanctuary.aerial_mineral_islet": "Mineral islet", + "biome.sanctuary.swamp27_caves": "Underground swamp" } 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 c542c75..8f10ea1 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 @@ -226,7 +226,7 @@ "sanctuary.structures.census.status.incompatible": "Milieu incompatible", "sanctuary.structures.census.status.other_dimension": "Réservée à une autre dimension", "sanctuary.mineral_sanctuary.entry": "Sanctuaire minéral : %s %s %s", - "sanctuary.world_options.aerial24_hint": "Secrets, voies abandonnées et cinq lieux suspendus. Les structures vanilla restent indépendantes.", + "sanctuary.world_options.aerial24_hint": "Secrets, voies abandonnées, deux navires et deux îlots suspendus. Les structures vanilla restent indépendantes.", "sanctuary.structures.census.aerial.none": "Aucun lieu suspendu réservé dans ce monde.", "sanctuary.structures.census.aerial.unknown_site": "Lieu suspendu inconnu : %s", "sanctuary.structures.census.aerial.site": "%s · %s · %s · départs %s/%s · décoration %s/%s · chunks complets %s/%s", @@ -235,7 +235,7 @@ "sanctuary.structures.census.aerial.status.generated": "généré", "sanctuary.structures.census.aerial.status.missing": "départ absent après vérification", "sanctuary.structures.census.aerial.status.unknown": "inconnu", - "structure.sanctuary.aerial_balloon": "Montgolfière", "structure.sanctuary.aerial_merchant_ship": "Bateau marchand", - "structure.sanctuary.aerial_mineral_islet": "Îlot minéral" + "structure.sanctuary.aerial_mineral_islet": "Îlot minéral", + "biome.sanctuary.swamp27_caves": "Marais souterrain" } diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/loot_table/chests/aerial24/balloon.json b/mods/sanctuary/src/main/resources/data/sanctuary/loot_table/chests/aerial24/balloon.json deleted file mode 100644 index e58fdf8..0000000 --- a/mods/sanctuary/src/main/resources/data/sanctuary/loot_table/chests/aerial24/balloon.json +++ /dev/null @@ -1,127 +0,0 @@ -{ - "type": "minecraft:chest", - "pools": [ - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "minecraft:spyglass" - } - ] - }, - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "minecraft:compass" - } - ] - }, - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "minecraft:clock" - } - ] - }, - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "minecraft:gold_ingot", - "functions": [ - { - "function": "minecraft:set_count", - "count": { - "type": "minecraft:uniform", - "min": 8, - "max": 16 - } - } - ] - } - ] - }, - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "minecraft:diamond", - "functions": [ - { - "function": "minecraft:set_count", - "count": { - "type": "minecraft:uniform", - "min": 2, - "max": 4 - } - } - ] - } - ] - }, - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "minecraft:lead", - "functions": [ - { - "function": "minecraft:set_count", - "count": { - "type": "minecraft:uniform", - "min": 4, - "max": 8 - } - } - ] - } - ] - }, - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "minecraft:bread", - "functions": [ - { - "function": "minecraft:set_count", - "count": { - "type": "minecraft:uniform", - "min": 12, - "max": 24 - } - } - ] - } - ] - }, - { - "rolls": 1, - "entries": [ - { - "type": "minecraft:item", - "name": "minecraft:emerald", - "functions": [ - { - "function": "minecraft:set_count", - "count": { - "type": "minecraft:uniform", - "min": 8, - "max": 16 - } - } - ] - } - ] - } - ] -} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/swamp27_caves.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/swamp27_caves.json new file mode 100644 index 0000000..4d46ae9 --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/swamp27_caves.json @@ -0,0 +1,172 @@ +{ + "attributes": { + "minecraft:audio/background_music": { + "default": { + "max_delay": 24000, + "min_delay": 12000, + "sound": "minecraft:music.overworld.swamp" + } + }, + "minecraft:gameplay/increased_fire_burnout": true, + "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:frog", + "count": { + "type": "minecraft:uniform", + "max_inclusive": 5, + "min_inclusive": 2 + }, + "weight": 10 + } + ], + "monster": [ + { + "type": "minecraft:spider", + "count": 4, + "weight": 100 + }, + { + "type": "minecraft:zombie", + "count": 4, + "weight": 95 + }, + { + "type": "minecraft:zombie_villager", + "count": 1, + "weight": 5 + }, + { + "type": "minecraft:skeleton", + "count": 4, + "weight": 70 + }, + { + "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 + }, + { + "type": "minecraft:slime", + "count": 1, + "weight": 1 + }, + { + "type": "minecraft:bogged", + "count": 4, + "weight": 30 + } + ], + "underground_water_creature": [ + { + "type": "minecraft:glow_squid", + "count": { + "type": "minecraft:uniform", + "max_inclusive": 6, + "min_inclusive": 4 + }, + "weight": 10 + } + ] + } + }, + "modifier": "overlay" + }, + "minecraft:visual/sky_color": "#78a7ff", + "minecraft:visual/water_fog_color": "#232317", + "minecraft:visual/water_fog_end_distance": { + "argument": 0.85, + "modifier": "multiply" + } + }, + "carvers": [], + "downfall": 0.9, + "effects": { + "dry_foliage_color": "#7b5334", + "foliage_color": "#6a7039", + "grass_color_modifier": "swamp", + "water_color": "#617b64" + }, + "features": [ + [], + [], + [], + [], + [], + [], + [ + "sanctuary:rift_ore_coal", + "sanctuary:rift_ore_iron", + "sanctuary:rift_ore_copper", + "sanctuary:rift_ore_gold", + "sanctuary:rift_ore_redstone", + "sanctuary:rift_ore_lapis", + "sanctuary:rift_ore_diamond", + "sanctuary:rift_ore_emerald", + "sanctuary:rift_ore_coal_surface", + "sanctuary:rift_ore_iron_surface", + "sanctuary:rift_ore_copper_surface", + "sanctuary:rift_ore_coal_ledges", + "sanctuary:rift_ore_iron_ledges", + "sanctuary:rift_ore_copper_ledges" + ], + [], + [], + [ + "minecraft:glow_lichen", + "sanctuary:swamp27_decoration" + ], + [] + ], + "has_precipitation": true, + "temperature": 0.8 +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/swamp27_decoration.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/swamp27_decoration.json new file mode 100644 index 0000000..4080e58 --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/placed_feature/swamp27_decoration.json @@ -0,0 +1,6 @@ +{ + "feature": { + "type": "sanctuary:swamp27_decoration" + }, + "placement": [] +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/structure/aerial_balloon.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/structure/aerial_balloon.json deleted file mode 100644 index 413f497..0000000 --- a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/structure/aerial_balloon.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "sanctuary:aerial_v24", - "family": "balloon", - "biomes": [ - "minecraft:the_void" - ], - "step": "top_layer_modification", - "terrain_adaptation": "none", - "spawn_overrides": {} -} 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 5bbc350..1b4a52b 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 @@ -51,6 +51,7 @@ "sanctuary:population_lush_caves", "sanctuary:population_dripstone_caves", "sanctuary:lost_city", + "sanctuary:swamp27_caves", "minecraft:ocean", "minecraft:deep_ocean", "minecraft:cold_ocean", diff --git a/mods/sanctuary/src/test/java/fr/koka/sanctuary/worldgen/Swamp27VolumesSmoke.java b/mods/sanctuary/src/test/java/fr/koka/sanctuary/worldgen/Swamp27VolumesSmoke.java new file mode 100644 index 0000000..5b0cf88 --- /dev/null +++ b/mods/sanctuary/src/test/java/fr/koka/sanctuary/worldgen/Swamp27VolumesSmoke.java @@ -0,0 +1,34 @@ +package fr.koka.sanctuary.worldgen; + +/** Small pure selector test; it does not claim that biome volumes contain generated air. */ +public final class Swamp27VolumesSmoke { + public static void main(String[] args) { + for (int diameter : new int[] {512, 724, 1024}) for (long seed : new long[] {0, 42, 2026}) { + int selected = 0, total = 0, stackedChanges = 0; + for (int x = -diameter / 3; x <= diameter / 3; x += 16) + for (int z = -diameter / 3; z <= diameter / 3; z += 16) { + boolean below = Swamp27Volumes.contains(seed, diameter, x, 100, z); + if (below != Swamp27Volumes.contains(seed, diameter, x, 164, z)) stackedChanges++; + for (int y = 88; y <= 172; y += 12) { + boolean first = Swamp27Volumes.contains(seed, diameter, x, y, z); + // Other probes must not alter the same result. + Swamp27Volumes.contains(seed ^ 87, diameter, -x, 256, -z); + require(first == Swamp27Volumes.contains(seed, diameter, x, y, z), "mutable selector"); + if (first) selected++; + total++; + } + for (int y : new int[] {-64, 0, 71, 188, 200, 256, 383, 384}) + require(!Swamp27Volumes.contains(seed, diameter, x, y, z), "surface/out-of-height selected"); + } + require(selected > 0 && selected < total, "sector must coexist with other caves"); + require(stackedChanges > 0, "biome field became a full-height column"); + require(!Swamp27Volumes.contains(seed, diameter, diameter, 120, 0), "outside island selected"); + System.out.println("swamp27 " + diameter + "/" + seed + " selected=" + selected + + "/" + total + " vertical_changes=" + stackedChanges); + } + require(!Swamp27Volumes.contains(0, 0, 0, 120, 0), "invalid diameter"); + } + private static void require(boolean value, String message) { + if (!value) throw new AssertionError(message); + } +} diff --git a/mods/sanctuary/src/test/java/fr/koka/sanctuary/worldgen/aerial/Aerial24Smoke.java b/mods/sanctuary/src/test/java/fr/koka/sanctuary/worldgen/aerial/Aerial24Smoke.java index 8ff6979..db0e1ce 100644 --- a/mods/sanctuary/src/test/java/fr/koka/sanctuary/worldgen/aerial/Aerial24Smoke.java +++ b/mods/sanctuary/src/test/java/fr/koka/sanctuary/worldgen/aerial/Aerial24Smoke.java @@ -19,7 +19,7 @@ import net.minecraft.world.level.storage.loot.LootTable; /** Pure finite planning witnesses: nine size/seed combinations, rotation and pre-visit claims. */ public final class Aerial24Smoke { - public static void main(String[] args) { + public static void main(String[] args) throws java.io.IOException { SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); BlockState[] column = new BlockState[384]; for (int y = 0; y < column.length; y++) column[y] = (y >= 80 && y <= 240 ? Blocks.STONE : Blocks.AIR).defaultBlockState(); @@ -27,10 +27,13 @@ public final class Aerial24Smoke { int[] queries = {0}; var plan = AerialPlanner24.plan(seed, size, (x, z) -> { queries[0]++; return new NoiseColumn(0, column); }); var replay = AerialPlanner24.plan(seed, size, (x, z) -> new NoiseColumn(0, column)); + String preview = System.getProperty("sanctuary.aerial.preview"); + if (preview != null && size == 724 && seed == 42) + AerialRenderer24.writeDiagnostic(java.nio.file.Path.of(preview), plan.sites().stream().filter(AerialPlan24.Site::merchant).toList()); require(plan.sites().equals(replay.sites()) && plan.claimsChunks().equals(replay.claimsChunks()), "Plan does not depend on time or loaded chunks"); require(queries[0] <= AerialPlanner24.MAX_COLUMNS, "Finite terrain query budget"); - require(plan.sites().size() == 5 && plan.sites().stream().filter(AerialPlan24.Site::merchant).count() == 2, - "Five complete sites, including two distinct merchant ships"); + require(plan.sites().size() == 4 && plan.sites().stream().filter(AerialPlan24.Site::merchant).count() == 2, + "Four complete sites, including two distinct merchant ships"); require(plan.sites().stream().filter(AerialPlan24.Site::waterfall).count() == 1, "Exactly one waterfall site"); var allClaims = new HashSet(); var root = ExpansionIsland.origin(size, seed, 24); @@ -68,7 +71,7 @@ public final class Aerial24Smoke { require(AerialPiece24.decode(AerialPiece24.encode(site)).equals(site), "Saved site descriptor round-trips without changing its seed or rotation"); if (seed == 0) verifyRender(site); } - require(allClaims.equals(plan.claimsChunks()), "Combined claims contain only the five finite site masks"); + require(allClaims.equals(plan.claimsChunks()), "Combined claims contain only the four finite site masks"); var far = new ExpansionIsland("far", "sanctuary", "east", "temperate", 64, 4096, 4096, 1, "natural", "reserved", 24); require(!plan.conflicts(far), "Aerial sites do not reserve an infinite ring around Sanctuary"); require(!plan.conflicts(root), "Root ownership is not treated as a future expansion"); @@ -78,11 +81,11 @@ public final class Aerial24Smoke { catch (UnsupportedOperationException expected) { } } require(AerialPlan24.EMPTY.isEmpty() && AerialPlan24.EMPTY.claimsChunks().isEmpty(), "Disabled sites reserve no space"); - try { new AerialPlan24(List.of(new AerialPlan24.Site("balloon", AerialPlan24.Kind.BALLOON, - 500, 290, 0, 15, 28, 19, 0, 0, 250, 240, 0, false))); throw new AssertionError("Partial plan accepted"); } + try { new AerialPlan24(List.of(new AerialPlan24.Site("merchant_provisions", AerialPlan24.Kind.MERCHANT_PROVISIONS, + 500, 290, 0, 11, 18, 28, 0, 0, 250, 240, 0, false))); throw new AssertionError("Partial plan accepted"); } catch (IllegalArgumentException expected) { } verifyReservedShoreAndView(column); - System.out.println("Aerial24Smoke: nine deterministic plans, five sites, four rotations, finite non-overlapping claims and pre-visit conflicts passed."); + System.out.println("Aerial24Smoke: nine deterministic plans, four sites, four rotations, finite non-overlapping claims and pre-visit conflicts passed."); } private static void verifyReservedShoreAndView(BlockState[] column) { var original = AerialPlanner24.plan(0, 724, (x, z) -> new NoiseColumn(0, column)).sites().getFirst(); @@ -126,6 +129,24 @@ public final class Aerial24Smoke { require(complete.equals(clipped) && clippedChests[0] == 1, "Chunk clipping reconstructs the complete authored structure without duplicate chests"); long sources = complete.values().stream().filter(state -> state.is(Blocks.WATER)).count(); require(sources == (site.waterfall() ? 1 : 0), "Renderer writes one source, never a fabricated full waterfall column"); + if (site.merchant()) { + require(complete.values().stream().allMatch(state -> state.getFluidState().isEmpty()), + "Restored ships contain no fluid, including inside stairs, slabs, fences or trapdoors"); + var nativeShip = NativeMerchantShip27.restored(); + require(nativeShip.nativeBlocks() == 659, "The exact vanilla template contributes 662 authored blocks minus its three data markers"); + require(nativeShip.preservedBlocks() + nativeShip.replacedBlocks() == nativeShip.nativeBlocks(), "No native hull position disappears during restoration"); + require(nativeShip.blocks().values().stream().anyMatch(state -> state.is(Blocks.OAK_STAIRS)) + && nativeShip.blocks().values().stream().anyMatch(state -> state.is(Blocks.SPRUCE_SLAB)), + "The ship retains its native shaped stairs and slabs"); + require(complete.values().stream().noneMatch(state -> state.is(Blocks.STRUCTURE_BLOCK)), "No native data marker is placed in the restored ship"); + for (var at : AerialGeometry24.interiorChecks(site)) { + require(complete.getOrDefault(at, Blocks.AIR.defaultBlockState()).isAir() + && complete.getOrDefault(at.above(), Blocks.AIR.defaultBlockState()).isAir(), + "Boarding, cabin circulation and merchant standing spaces remain open"); + require(complete.getOrDefault(at.below(), Blocks.AIR.defaultBlockState()).isSolid(), + "Each boarding and interior witness retains its real floor"); + } + } if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET) { var chest = AerialGeometry24.chest(site); for (int dy = 1; dy <= 3; dy++) require(complete.get(chest.above(dy)) != null && complete.get(chest.above(dy)).isSolid(), "Mineral chest is buried under three intact layers"); diff --git a/mods/sanctuary/src/test/java/fr/koka/sanctuary/worldgen/aerial/AerialPersistence24Smoke.java b/mods/sanctuary/src/test/java/fr/koka/sanctuary/worldgen/aerial/AerialPersistence24Smoke.java index 95ffe43..c5f775e 100644 --- a/mods/sanctuary/src/test/java/fr/koka/sanctuary/worldgen/aerial/AerialPersistence24Smoke.java +++ b/mods/sanctuary/src/test/java/fr/koka/sanctuary/worldgen/aerial/AerialPersistence24Smoke.java @@ -48,7 +48,7 @@ public final class AerialPersistence24Smoke { require(!site.ownsChunk((site.maxX() >> 4) + 2, (site.maxZ() >> 4) + 2), "Unused corners of the old broad margin stay free"); } require(!Files.exists(directory), "Manifest creation must not prematurely initialize the journal"); - require(draws.get() == 1 && stored.plan().sites().size() == 5, "One complete initial plan is recorded"); + require(draws.get() == 1 && stored.plan().sites().size() == 4, "One complete initial plan is recorded"); var journal = new ExpansionJournal(directory, SEED, 724, 24, stored.token()); var root = ExpansionIsland.origin(724, SEED, 24); Path journalFile = directory.resolve(ExpansionJournal.FILE_NAME); @@ -65,7 +65,7 @@ public final class AerialPersistence24Smoke { journal.commit(List.of(root)); require(Arrays.equals(pristineJournal, Files.readAllBytes(journalFile)), "A no-op commit preserves bytes"); - var collision = child("collision", 672, 0); + var collision = child("collision", "south", 0, 672); require(!collision.conflicts(root), "Collision witness is outside initial island reservations"); require(stored.plan().conflicts(collision), "A never-generated satellite reserves its chunks"); reject(() -> journal.commit(List.of(root, collision)), "Reservation over an unvisited aerial site"); @@ -135,15 +135,14 @@ public final class AerialPersistence24Smoke { } } - private static ExpansionIsland child(String id, int x, int z) { - return new ExpansionIsland(id, "sanctuary", "east", "temperate", 64, x, z, + private static ExpansionIsland child(String id, String direction, int x, int z) { + return new ExpansionIsland(id, "sanctuary", direction, "temperate", 64, x, z, ExpansionIsland.deriveSeed(SEED, id), "natural", "reserved", 24); } private static AerialPlan24 fixture() { return new AerialPlan24(List.of( - new Site("balloon", Kind.BALLOON, 660, 290, 0, 15, 28, 19, 0, 1, 390, 240, 0, false), - new Site("merchant_provisions", Kind.MERCHANT_PROVISIONS, 0, 235, 660, 11, 18, 27, 0, 2, 0, 240, 390, false), - new Site("merchant_tools", Kind.MERCHANT_TOOLS, -740, 242, 0, 11, 18, 27, 0, 3, -390, 240, 0, false), + new Site("merchant_provisions", Kind.MERCHANT_PROVISIONS, 0, 235, 660, 11, 18, 28, 0, 2, 0, 240, 390, false), + new Site("merchant_tools", Kind.MERCHANT_TOOLS, -740, 242, 0, 11, 18, 28, 0, 3, -390, 240, 0, false), new Site("mineral_islet_water", Kind.MINERAL_ISLET, -450, 255, -650, 48, 36, 48, 0, 4, -200, 240, -330, true), new Site("mineral_islet_dry", Kind.MINERAL_ISLET, 460, 260, -600, 56, 40, 56, 0, 5, 250, 240, -300, false))); } diff --git a/packwiz/pack.toml b/packwiz/pack.toml index bfa39dc..f05eb08 100644 --- a/packwiz/pack.toml +++ b/packwiz/pack.toml @@ -1,6 +1,6 @@ name = "Sanctuary" author = "KOKA99CAB" -version = "0.1.0-alpha.24" +version = "0.1.0-alpha.27" pack-format = "packwiz:1.1.0" [index]