Release alpha27 from alpha24 terrain with cave swamps and restored vanilla ships
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-12 00:36:17 +02:00
parent e7d1f298b9
commit 392ade0b7e
35 changed files with 1065 additions and 298 deletions
+1
View File
@@ -15,3 +15,4 @@ screenshots/
.env .env
.env.* .env.*
!.env.example !.env.example
__pycache__/
+10
View File
@@ -1,5 +1,15 @@
# Changelog # Changelog
## 0.1.0-alpha.27 — 2026-09-12
Retour à la base alpha.24 après rejet visuel de lalpha.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 ## 0.1.0-alpha.20 — 2026-09-10
- Passe architecturale sur les six bâtiments urbains, les quatorze anciennes - Passe architecturale sur les six bâtiments urbains, les quatorze anciennes
+23 -3
View File
@@ -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) 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 conserve les intentions ; le [backlog](docs/backlog.md) suit les
tickets. Les systèmes d’économie, de progression collective et les dimensions tickets. Les systèmes d’économie, de progression collective et les dimensions
décrits dans la vision ne sont pas encore implémentés. **Lalpha.24 est décrits dans la vision ne sont pas encore implémentés. **Lalpha.27 repart de
publiée** : cinq lieux suspendus accompagnent l’île naturelle et ses secrets. lalpha.24**, après le rejet du rendu de lalpha.25. Cette première passe conserve
Voir [les essais](docs/testing-alpha24.md) et [la distribution](docs/packwiz.md). 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 lalpha.25. Les changements de relief seront
repris séparément après un essai visuel. Lalpha.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 lensemble 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 nest 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 ## Alpha.24 — les lieux suspendus
+12
View File
@@ -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 Release alpha.24 publiée ; canal packwiz et instance Prism existante
synchronisés deux fois. Les 925 fichiers personnels et réglages suivis sont synchronisés deux fois. Les 925 fichiers personnels et réglages suivis sont
conservés. Aucun essai Windows natif revendiqué. 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 lexpé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 deau 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.
+64
View File
@@ -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à à lendroit, puis restaurer les trous, les mâts et
laccè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.
+19
View File
@@ -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 dépendance téléchargée n'entre dans l'historique Git. Le canal Beta est distinct
de l'ancien pack 26.2. de l'ancien pack 26.2.
## Alpha.27 prête à publier — 12 septembre 2026
La reprise repart directement de lalpha.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
damorçage sont vérifiés localement. La publication et la synchronisation de
linstance 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 nest revendiqué.
## Alpha.24 publiée — 11 septembre 2026 ## Alpha.24 publiée — 11 septembre 2026
`check build assemblePack` et les essais natifs passent. Le JAR et `check build assemblePack` et les essais natifs passent. Le JAR et
+97
View File
@@ -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 nest pas répété pendant lassemblage 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 dorigine 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.
+2 -2
View File
@@ -8,6 +8,6 @@ loader_version=0.19.5
loom_version=1.17.20 loom_version=1.17.20
fabric_api_version=0.160.0+26.3 fabric_api_version=0.160.0+26.3
mod_version=0.1.0-alpha.24 mod_version=0.1.0-alpha.27
pack_version=0.1.0-alpha.24 pack_version=0.1.0-alpha.27
maven_group=fr.koka.sanctuary maven_group=fr.koka.sanctuary
+10
View File
@@ -603,6 +603,8 @@ tasks.register('aerial24Smoke', JavaExec) {
dependsOn('testClasses') dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.aerial.Aerial24Smoke' 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') } tasks.named('check') { dependsOn('aerial24Smoke') }
@@ -613,3 +615,11 @@ tasks.register('aerialPersistence24Smoke', JavaExec) {
mainClass = 'fr.koka.sanctuary.worldgen.aerial.AerialPersistence24Smoke' mainClass = 'fr.koka.sanctuary.worldgen.aerial.AerialPersistence24Smoke'
} }
tasks.named('check') { dependsOn('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') }
@@ -33,7 +33,7 @@ public final class Aerial24FlowAdmissionGameTests {
&& level.structureManager().shouldGenerateStructures(), "Fresh generation24 with aerial sites and native structures enabled"); && level.structureManager().shouldGenerateStructures(), "Fresh generation24 with aerial sites and native structures enabled");
var plan = generator.aerialPlan24(); var plan = generator.aerialPlan24();
var session = Objects.requireNonNull(ExpansionRuntime.session(level)); 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(); var evidence = StructureCensusService.snapshot(level.getServer()).evidence();
String dimension = level.dimension().identifier().toString(); String dimension = level.dimension().identifier().toString();
for (var site : plan.sites()) for (var chunk : site.claimedChunks()) for (var site : plan.sites()) for (var chunk : site.claimedChunks())
@@ -47,7 +47,7 @@ public final class Aerial24WorldGameTests {
private static final com.google.gson.Gson JSON = new GsonBuilder().setPrettyPrinting().create(); private static final com.google.gson.Gson JSON = new GsonBuilder().setPrettyPrinting().create();
@GameTest(maxTicks = 24000) @GameTest(maxTicks = 24000)
public void fiveSitesPersistAfterUse(GameTestHelper helper) throws Exception { public void fourSitesPersistAfterUse(GameTestHelper helper) throws Exception {
var report = new LinkedHashMap<String, Object>(); var report = new LinkedHashMap<String, Object>();
try { begin(helper, report); } try { begin(helper, report); }
catch (Exception failure) { failure(report, failure); throw failure; } 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"); 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; 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::merchant).count() == 2, "Two merchant ships");
helper.assertTrue(plan.sites().stream().filter(AerialPlan24.Site::waterfall).count() == 1, "One waterfall site"); 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()); 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("geometry", JSON.toJsonTree(plan.sites())); report.put("claim_count", plan.claimsChunks().size());
report.put("census_before", StructureCensusService.aerialSites(level)); 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("native_tags", tags); report.put("full_chunks", chunks.size());
report.put("blocks_after", countSites(level, plan)); report.put("blocks_after", countSites(level, plan));
var census = StructureCensusService.aerialSites(level); var census = StructureCensusService.aerialSites(level);
helper.assertTrue(census.size() == 5 && census.stream().allMatch(site -> site.status() == StructureCensusService.AerialStatus.GENERATED), helper.assertTrue(census.size() == 4 && census.stream().allMatch(site -> site.status() == StructureCensusService.AerialStatus.GENERATED),
"Five historical generated sites remain indexed after loot, mining and demolition"); "Four historical generated sites remain indexed after loot, mining and demolition");
report.put("census_after", census); report.put("census_after", census);
helper.assertTrue(equalFiles(journalBefore, files(world.resolve("data").resolve(generator.journalDirectory()))), "Aerial tests never reserve or activate an expansion"); 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); 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(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("chests", chestCount); report.put("waterfall_sources", waterSources); report.put("blocks_before", countSites(level, plan));
report.put("vegetation", vegetation); report.put("vegetation", vegetation);
return expected; 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())); } 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 ship = plan.sites().stream().filter(AerialPlan24.Site::merchant).findFirst().orElseThrow();
var demolition = AerialGeometry24.entry(balloon).below(); var demolition = AerialGeometry24.entry(ship).below();
helper.assertTrue(level.getBlockState(demolition).isSolid(), "The demolition witness removes an existing floor"); helper.assertTrue(level.getBlockState(demolition).isSolid(), "The demolition witness removes an existing floor");
level.setBlock(demolition, Blocks.AIR.defaultBlockState(), 2); removed.add(coordinates(demolition)); level.setBlock(demolition, Blocks.AIR.defaultBlockState(), 2); removed.add(coordinates(demolition));
var extracted = new TreeMap<String, List<Double>>(); int i = 0; var extracted = new TreeMap<String, List<Double>>(); int i = 0;
@@ -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<String, Object>();
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<ChunkPos>();
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<ChunkPos, BlockPos> waterCandidates(GameTestHelper helper,
PopulationGenerationContext.Binding binding, BiomeResolver resolver, Map<String, Object> 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<Long>();
plan.features().stream().filter(feature -> feature.kind() == PopulationHydrology.Kind.TERRACE)
.forEach(feature -> undergroundIds.add(feature.id()));
var allY = new TreeMap<Integer, Integer>(); var lowerY = new TreeMap<Integer, Integer>();
var byChunk = new HashMap<ChunkPos, Integer>(); var positions = new HashMap<ChunkPos, BlockPos>();
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<ChunkPos, BlockPos>();
byChunk.keySet().stream().sorted(Comparator.<ChunkPos>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<String, Object>();
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<ChunkPos> chunks,
Map<String, Object> report) throws Exception {
int floors = 0, covered = 0, plants = 0, water = 0;
BlockPos witness = null; int bestScore = -1, witnessRoof = -1;
var counts = new TreeMap<String, Integer>();
var byChunk = new ArrayList<Map<String, Object>>();
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<String, Object> 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<List<Object>>(); var cutaway = new ArrayList<List<Object>>();
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<Object> 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<Integer> 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<String, Object> 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);
}
}
@@ -6,7 +6,7 @@
"environment": "*", "environment": "*",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"entrypoints": { "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"] "fabric-client-gametest": ["fr.koka.sanctuary.gametest.SanctuaryClientRenderTests"]
}, },
"mixins": ["sanctuary-gametest.mixins.json"], "mixins": ["sanctuary-gametest.mixins.json"],
@@ -86,6 +86,7 @@ public final class SanctuaryMod implements ModInitializer {
fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.register(); fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.register();
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("population_island"), PopulationIslandDensity.CODEC); Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("population_island"), PopulationIslandDensity.CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("population_island"), PopulationIslandBiomeSource.CODEC); Registry.register(BuiltInRegistries.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_groves"), PopulationGrovesFeature.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("population_decorations"), PopulationDecorationsFeature.CODEC); Registry.register(BuiltInRegistries.FEATURE_TYPE, id("population_decorations"), PopulationDecorationsFeature.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("population_shore_sugar_cane"), PopulationShoreSugarCaneFeature.CODEC); Registry.register(BuiltInRegistries.FEATURE_TYPE, id("population_shore_sugar_cane"), PopulationShoreSugarCaneFeature.CODEC);
@@ -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. /** Alpha.13 keeps Population's origin woodlands and the laboratory's native cave pockets.
* Child islands retain the laboratory's complete climate palette. */ * Child islands retain the laboratory's complete climate palette. */
public final class SanctuaryBiomeSource extends ExpansionBiomeSource { public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
public static final ResourceKey<Biome> SWAMP27 = ResourceKey.create(net.minecraft.core.registries.Registries.BIOME,
fr.koka.sanctuary.SanctuaryMod.id("swamp27_caves"));
public static final List<String> REQUIRED_BIOMES = Stream.concat( public static final List<String> REQUIRED_BIOMES = Stream.concat(
ExpansionBiomeSource.REQUIRED_BIOMES.stream(), Stream.of( ExpansionBiomeSource.REQUIRED_BIOMES.stream(), Stream.of(
PopulationIslandBiomeSource.OAK_FOREST, PopulationIslandBiomeSource.BIRCH_FOREST, PopulationIslandBiomeSource.OAK_FOREST, PopulationIslandBiomeSource.BIRCH_FOREST,
PopulationIslandBiomeSource.CLEARING, PopulationIslandBiomeSource.DRY_WOODLAND, PopulationIslandBiomeSource.CLEARING, PopulationIslandBiomeSource.DRY_WOODLAND,
PopulationIslandBiomeSource.ROCKY_HEATH, PopulationIslandBiomeSource.DARK_GROVE, PopulationIslandBiomeSource.ROCKY_HEATH, PopulationIslandBiomeSource.DARK_GROVE,
PopulationIslandBiomeSource.BAMBOO_GROVE, PopulationIslandBiomeSource.SULFUR_DEPTHS, 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(); ).map(key -> key.identifier().toString())).toList();
public static final MapCodec<SanctuaryBiomeSource> CODEC = RecordCodecBuilder.mapCodec(i -> i.group( public static final MapCodec<SanctuaryBiomeSource> CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
@@ -71,6 +73,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
private final boolean cities; private final boolean cities;
private final int generation; private final int generation;
private final Holder<Biome> cityBiome; private final Holder<Biome> cityBiome;
private final Holder<Biome> swamp27;
private volatile java.util.Map<String, fr.koka.sanctuary.worldgen.city.LostCityPlan> cityPlans = java.util.Map.of(); private volatile java.util.Map<String, fr.koka.sanctuary.worldgen.city.LostCityPlan> cityPlans = java.util.Map.of();
public SanctuaryBiomeSource(List<Holder<Biome>> biomes) { public SanctuaryBiomeSource(List<Holder<Biome>> biomes) {
@@ -81,6 +84,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
this.generation = generation; this.generation = generation;
this.cities = ExpansionIsland.spatialGeneration(generation); this.cities = ExpansionIsland.spatialGeneration(generation);
allBiomes = List.copyOf(biomes); allBiomes = List.copyOf(biomes);
swamp27 = generation == 24 ? required(SWAMP27) : null;
if ((generation == 23 || generation == 24)) for (String name : fr.koka.sanctuary.expansion.ExpansionOcean23.BIOMES) if ((generation == 23 || generation == 24)) for (String name : fr.koka.sanctuary.expansion.ExpansionOcean23.BIOMES)
required(ResourceKey.create(net.minecraft.core.registries.Registries.BIOME, required(ResourceKey.create(net.minecraft.core.registries.Registries.BIOME,
net.minecraft.resources.Identifier.withDefaultNamespace(name))); 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 // Reuse the historical selector itself: its noise, thresholds, order and child
// climate choices stay exact rather than being approximated by another climate field. // climate choices stay exact rather than being approximated by another climate field.
Holder<Biome> laboratory = super.resolve(snapshot, x, y, z); Holder<Biome> 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<Biome> 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) if (y < 0 || y >= 384 || laboratory.is(Biomes.LUSH_CAVES)
|| laboratory.is(Biomes.DRIPSTONE_CAVES) || laboratory.is(Biomes.SULFUR_CAVES)) return laboratory; || laboratory.is(Biomes.DRIPSTONE_CAVES) || laboratory.is(Biomes.SULFUR_CAVES)) return laboratory;
for (ExpansionIsland island : snapshot) { for (ExpansionIsland island : snapshot) {
@@ -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<Swamp27Decoration> CODEC = MapCodec.unit(Swamp27Decoration::new);
@Override public MapCodec<Swamp27Decoration> 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);
}
}
@@ -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; }
}
@@ -14,8 +14,7 @@ public final class AerialGeometry24 {
} }
public static BlockPos chest(AerialPlan24.Site s) { public static BlockPos chest(AerialPlan24.Site s) {
return switch (s.kind()) { 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, 4, 24);
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> world(s, 7, 6, 22);
case MINERAL_ISLET -> { case MINERAL_ISLET -> {
int x = s.width() / 2 + 4, z = s.depth() / 2 + 2; int x = s.width() / 2 + 4, z = s.depth() / 2 + 2;
yield world(s, x, terrainTop(s, x, z) - 3, z); 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) { public static BlockPos occupant(AerialPlan24.Site s) {
if (!s.merchant()) throw new IllegalArgumentException("Only merchant ships have occupants"); 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) { public static UUID occupantUuid(AerialPlan24.Site s) {
return UUID.nameUUIDFromBytes(("sanctuary:aerial24:" + s.id() + ":" + s.seed() + ":merchant") return UUID.nameUUIDFromBytes(("sanctuary:aerial24:" + s.id() + ":" + s.seed() + ":merchant")
.getBytes(StandardCharsets.UTF_8)); .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) { public static BlockPos waterfallSource(AerialPlan24.Site s) {
if (!s.waterfall()) throw new IllegalArgumentException("This site has no waterfall"); if (!s.waterfall()) throw new IllegalArgumentException("This site has no waterfall");
return world(s, s.width() / 2, rockHeight(s) - 3, 3); return world(s, s.width() / 2, rockHeight(s) - 3, 3);
} }
public static BlockPos entry(AerialPlan24.Site s) { public static BlockPos entry(AerialPlan24.Site s) {
return switch (s.kind()) { return switch (s.kind()) {
case BALLOON -> world(s, s.width() / 2, 2, s.depth() / 2 - 3); case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> world(s, 3, 5, 13);
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> world(s, 7, 6, 12);
case MINERAL_ISLET -> world(s, s.width() / 2, terrainTop(s, s.width() / 2, s.depth() / 2) + 1, s.depth() / 2); case MINERAL_ISLET -> world(s, s.width() / 2, terrainTop(s, s.width() / 2, s.depth() / 2) + 1, s.depth() / 2);
}; };
} }
public static List<BlockPos> interiorChecks(AerialPlan24.Site s) { public static List<BlockPos> 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.merchant()) return List.of(entry(s), occupant(s), world(s, 5, 4, 21), world(s, 3, 4, 24));
if (s.kind() == AerialPlan24.Kind.BALLOON) return List.of(entry(s), world(s, s.width() / 2, 2, s.depth() / 2));
return List.of(entry(s)); return List.of(entry(s));
} }
public static int terrainTop(AerialPlan24.Site s, int x, int z) { public static int terrainTop(AerialPlan24.Site s, int x, int z) {
@@ -19,17 +19,16 @@ public final class AerialPlan24 {
private final List<Site> sites; private final List<Site> sites;
private final Set<Long> claims; private final Set<Long> claims;
public enum Kind { BALLOON, MERCHANT_PROVISIONS, MERCHANT_TOOLS, MINERAL_ISLET } public enum Kind { MERCHANT_PROVISIONS, MERCHANT_TOOLS, MINERAL_ISLET }
public AerialPlan24(List<Site> sites) { public AerialPlan24(List<Site> sites) {
this.sites = List.copyOf(sites); this.sites = List.copyOf(sites);
if (!sites.isEmpty() && (sites.size() != 5 if (!sites.isEmpty() && (sites.size() != 4
|| sites.stream().filter(s -> s.kind() == Kind.BALLOON).count() != 1
|| sites.stream().filter(s -> s.kind() == Kind.MERCHANT_PROVISIONS).count() != 1 || 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.MERCHANT_TOOLS).count() != 1
|| sites.stream().filter(s -> s.kind() == Kind.MINERAL_ISLET).count() != 2 || sites.stream().filter(s -> s.kind() == Kind.MINERAL_ISLET).count() != 2
|| sites.stream().filter(Site::waterfall).count() != 1)) || 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<String>(); var ids = new HashSet<String>();
var columns = new HashSet<Long>(); var columns = new HashSet<Long>();
for (Site site : sites) { for (Site site : sites) {
@@ -70,8 +69,7 @@ public final class AerialPlan24 {
|| waterfall && kind != Kind.MINERAL_ISLET) || waterfall && kind != Kind.MINERAL_ISLET)
throw new IllegalArgumentException("Invalid aerial site geometry"); throw new IllegalArgumentException("Invalid aerial site geometry");
if (kind == Kind.MINERAL_ISLET && (width < 40 || depth < 40 || height < 20 + VEGETATION_HEADROOM) 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 != 28 || height != 18))
|| (kind == Kind.MERCHANT_PROVISIONS || kind == Kind.MERCHANT_TOOLS) && (width != 11 || depth != 27 || height != 18))
throw new IllegalArgumentException("Aerial site differs from its version24 gabarit"); throw new IllegalArgumentException("Aerial site differs from its version24 gabarit");
int worldWidth = facing % 2 == 0 ? width : depth; int worldWidth = facing % 2 == 0 ? width : depth;
int worldDepth = facing % 2 == 0 ? depth : width; int worldDepth = facing % 2 == 0 ? depth : width;
@@ -40,19 +40,21 @@ public final class AerialPlanner24 {
var probe = new Probe(terrain, radius); var probe = new Probe(terrain, radius);
var sites = new ArrayList<Site>(); var sites = new ArrayList<Site>();
var claimed = new HashSet<Long>(); var claimed = new HashSet<Long>();
Kind[] kinds = {Kind.BALLOON, Kind.MERCHANT_PROVISIONS, Kind.MERCHANT_TOOLS, Kind.MINERAL_ISLET, Kind.MINERAL_ISLET}; Kind[] kinds = {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"}; String[] ids = {"merchant_provisions", "merchant_tools", "mineral_islet_water", "mineral_islet_dry"};
double initialAngle = unit(mix(seed ^ 0xA24AE21AL)) * Math.PI * 2; double initialAngle = unit(mix(seed ^ 0xA24AE21AL)) * Math.PI * 2;
for (int ordinal = 0; ordinal < kinds.length; ordinal++) { for (int ordinal = 0; ordinal < kinds.length; ordinal++) {
long siteSeed = ExpansionIsland.deriveSeed(seed ^ 0xA24AE21AL, ids[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); boolean merchant = ordinal < 2;
int depth = kinds[ordinal] == Kind.BALLOON ? 19 : ordinal < 3 ? 27 : width; int width = merchant ? 11 : 40 + 4 * (int) Math.floorMod(siteSeed, 7);
int bodyHeight = kinds[ordinal] == Kind.BALLOON ? 28 : ordinal < 3 ? 18 : 20 + (int) Math.floorMod(mix(siteSeed), 16); int depth = merchant ? 28 : width;
int height = bodyHeight + (ordinal >= 3 ? AerialPlan24.VEGETATION_HEADROOM : 0); int bodyHeight = merchant ? 18 : 20 + (int) Math.floorMod(mix(siteSeed), 16);
int height = bodyHeight + (merchant ? 0 : AerialPlan24.VEGETATION_HEADROOM);
Site accepted = null; Site accepted = null;
for (int attempt = 0; attempt < MAX_ATTEMPTS_PER_SITE; attempt++) { for (int attempt = 0; attempt < MAX_ATTEMPTS_PER_SITE; attempt++) {
long draw = mix(siteSeed + attempt * 0x9e3779b97f4a7c15L); 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; + (attempt / 16) * .41;
Shore shore = shore(probe, angle, radius); Shore shore = shore(probe, angle, radius);
if (shore == null) continue; 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; 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); 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; if (Math.hypot(cx - shore.x, cz - shore.z) > 500) continue;
int bottom = ordinal == 0 ? Math.clamp(shore.y + 24, 286, 321) int bottom = merchant ? Math.clamp(shore.y - 4 + (int) Math.floorMod(draw, 13), 180, 300)
: ordinal < 3 ? Math.clamp(shore.y - 4 + (int) Math.floorMod(draw, 13), 180, 300)
: Math.clamp(shore.y + 20 - bodyHeight / 2, 210, 320 - bodyHeight); : Math.clamp(shore.y + 20 - bodyHeight / 2, 210, 320 - bodyHeight);
bottom = Math.min(bottom, 350 - height); bottom = Math.min(bottom, 350 - height);
var site = new Site(ids[ordinal], kinds[ordinal], cx - worldWidth / 2, bottom, cz - worldDepth / 2, 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; if (site.claimedChunks().stream().anyMatch(c -> claimed.contains(c.pack()))) continue;
boolean rootOverlap = site.claimedChunks().stream().anyMatch(c -> { boolean rootOverlap = site.claimedChunks().stream().anyMatch(c -> {
int nearestX = Math.clamp(0, c.getMinBlockX(), c.getMaxBlockX()); int nearestX = Math.clamp(0, c.getMinBlockX(), c.getMaxBlockX());
int nearestZ = Math.clamp(0, c.getMinBlockZ(), c.getMaxBlockZ()); int nearestZ = Math.clamp(0, c.getMinBlockZ(), c.getMaxBlockZ());
return Math.hypot(nearestX, nearestZ) <= radius + 32; 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; accepted = site; break;
} }
if (accepted == null) throw new IllegalStateException("aerial_sites_unavailable: could not place " + ids[ordinal] + " within the bounded search"); if (accepted == null) throw new IllegalStateException("aerial_sites_unavailable: could not place " + ids[ordinal] + " within the bounded search");
@@ -41,7 +41,12 @@ public final class AerialRenderer24 {
BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(), state.toString()); BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(), state.toString());
}).toList(); }).toList();
result.add(Map.of("id", site.id(), "kind", site.kind().name(), "min", List.of(site.minX(), site.minY(), site.minZ()), 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()); if (path.toAbsolutePath().getParent() != null) Files.createDirectories(path.toAbsolutePath().getParent());
Files.writeString(path, new com.google.gson.GsonBuilder().create().toJson(result) + "\n"); 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 destination = connected == null ? clipped : connected;
var b = new Builder(s, destination, connected == null ? clip : s.bounds()); var b = new Builder(s, destination, connected == null ? clip : s.bounds());
switch (s.kind()) { switch (s.kind()) {
case BALLOON -> b.balloon();
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> b.ship(); case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> b.ship();
case MINERAL_ISLET -> b.islet(); 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, state.setValue(BlockStateProperties.BED_PART, BedPart.FOOT));
set(x, y, z + 1, state.setValue(BlockStateProperties.BED_PART, BedPart.HEAD)); 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() { void ship() {
int cx = 5; // The complete upright vanilla ship, offset one block in X for its saved envelope.
for (int z = 0; z < s.depth(); z++) for (int y = 0; y <= 5; y++) { for (var entry : NativeMerchantShip27.restored().blocks().entrySet()) {
int half = halfWidth(y, z); var at = entry.getKey(); set(at.getX() + 1, at.getY(), at.getZ(), entry.getValue());
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);
}
} }
// Curved gunwales remain closed; the wide centre stays clear for boarding small boats. // Reuse the native stern cabin, with its original floor, roof, staircase and treasure-cache position.
for (int z = 1; z <= 25; z++) { set(3, 4, 23, s.kind() == AerialPlan24.Kind.MERCHANT_PROVISIONS ? Blocks.COMPOSTER : Blocks.SMITHING_TABLE);
int half = halfWidth(5, z); bed(3, 4, 21);
for (int side : new int[]{-1, 1}) { set(7, 4, 21, Blocks.CRAFTING_TABLE);
int x = cx + side * half; lantern(5, 6, 22);
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));
} }
void islet() { void islet() {
for (int x = 0; x < s.width(); x++) for (int z = 0; z < s.depth(); z++) { for (int x = 0; x < s.width(); x++) for (int z = 0; z < s.depth(); z++) {
@@ -17,7 +17,7 @@ public final class AerialStructure24 extends Structure {
private final String family; private final String family;
public AerialStructure24(StructureSettings settings, String family) { public AerialStructure24(StructureSettings settings, String family) {
super(settings); 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; this.family = family;
} }
public String family() { return family; } public String family() { return family; }
@@ -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.pieces.StructurePieceType;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; 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 final class AerialStructures24 {
public static final ResourceKey<Structure> BALLOON = key("aerial_balloon");
public static final ResourceKey<Structure> MERCHANT_SHIP = key("aerial_merchant_ship"); public static final ResourceKey<Structure> MERCHANT_SHIP = key("aerial_merchant_ship");
public static final ResourceKey<Structure> MINERAL_ISLET = key("aerial_mineral_islet"); public static final ResourceKey<Structure> MINERAL_ISLET = key("aerial_mineral_islet");
public static final List<ResourceKey<Structure>> KEYS = List.of(BALLOON, MERCHANT_SHIP, MINERAL_ISLET); public static final List<ResourceKey<Structure>> KEYS = List.of(MERCHANT_SHIP, MINERAL_ISLET);
public static final StructureType<AerialStructure24> TYPE = Registry.register(BuiltInRegistries.STRUCTURE_TYPE, public static final StructureType<AerialStructure24> TYPE = Registry.register(BuiltInRegistries.STRUCTURE_TYPE,
SanctuaryMod.id("aerial_v24"), () -> AerialStructure24.CODEC); SanctuaryMod.id("aerial_v24"), () -> AerialStructure24.CODEC);
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE, public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
@@ -37,10 +36,10 @@ public final class AerialStructures24 {
private static ResourceKey<Structure> key(String id) { return ResourceKey.create(Registries.STRUCTURE, SanctuaryMod.id(id)); } private static ResourceKey<Structure> key(String id) { return ResourceKey.create(Registries.STRUCTURE, SanctuaryMod.id(id)); }
public static void register() {} public static void register() {}
public static String family(AerialPlan24.Site site) { 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<Structure> key(AerialPlan24.Site site) { public static ResourceKey<Structure> 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 String structureId(AerialPlan24.Site site) { return key(site).identifier().toString(); }
public static boolean isPlacing() { return PLACING.get() != null; } public static boolean isPlacing() { return PLACING.get() != null; }
@@ -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<BlockPos, BlockState> 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<BlockState>();
for (var value : palette) states.add(BlockState.CODEC.parse(NbtOps.INSTANCE, value).getOrThrow());
var original = new LinkedHashMap<BlockPos, BlockState>();
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);
}
}
}
@@ -226,7 +226,7 @@
"sanctuary.structures.census.status.incompatible": "Incompatible habitat", "sanctuary.structures.census.status.incompatible": "Incompatible habitat",
"sanctuary.structures.census.status.other_dimension": "Reserved for another dimension", "sanctuary.structures.census.status.other_dimension": "Reserved for another dimension",
"sanctuary.mineral_sanctuary.entry": "Mineral sanctuary: %s %s %s", "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.none": "No aerial sites reserved in this world.",
"sanctuary.structures.census.aerial.unknown_site": "Unknown aerial site: %s", "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", "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.generated": "generated",
"sanctuary.structures.census.aerial.status.missing": "start missing after verification", "sanctuary.structures.census.aerial.status.missing": "start missing after verification",
"sanctuary.structures.census.aerial.status.unknown": "unknown", "sanctuary.structures.census.aerial.status.unknown": "unknown",
"structure.sanctuary.aerial_balloon": "Hot air balloon",
"structure.sanctuary.aerial_merchant_ship": "Merchant ship", "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"
} }
@@ -226,7 +226,7 @@
"sanctuary.structures.census.status.incompatible": "Milieu incompatible", "sanctuary.structures.census.status.incompatible": "Milieu incompatible",
"sanctuary.structures.census.status.other_dimension": "Réservée à une autre dimension", "sanctuary.structures.census.status.other_dimension": "Réservée à une autre dimension",
"sanctuary.mineral_sanctuary.entry": "Sanctuaire minéral : %s %s %s", "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.none": "Aucun lieu suspendu réservé dans ce monde.",
"sanctuary.structures.census.aerial.unknown_site": "Lieu suspendu inconnu : %s", "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", "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.generated": "généré",
"sanctuary.structures.census.aerial.status.missing": "départ absent après vérification", "sanctuary.structures.census.aerial.status.missing": "départ absent après vérification",
"sanctuary.structures.census.aerial.status.unknown": "inconnu", "sanctuary.structures.census.aerial.status.unknown": "inconnu",
"structure.sanctuary.aerial_balloon": "Montgolfière",
"structure.sanctuary.aerial_merchant_ship": "Bateau marchand", "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"
} }
@@ -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
}
}
]
}
]
}
]
}
@@ -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
}
@@ -0,0 +1,6 @@
{
"feature": {
"type": "sanctuary:swamp27_decoration"
},
"placement": []
}
@@ -1,10 +0,0 @@
{
"type": "sanctuary:aerial_v24",
"family": "balloon",
"biomes": [
"minecraft:the_void"
],
"step": "top_layer_modification",
"terrain_adaptation": "none",
"spawn_overrides": {}
}
@@ -51,6 +51,7 @@
"sanctuary:population_lush_caves", "sanctuary:population_lush_caves",
"sanctuary:population_dripstone_caves", "sanctuary:population_dripstone_caves",
"sanctuary:lost_city", "sanctuary:lost_city",
"sanctuary:swamp27_caves",
"minecraft:ocean", "minecraft:ocean",
"minecraft:deep_ocean", "minecraft:deep_ocean",
"minecraft:cold_ocean", "minecraft:cold_ocean",
@@ -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);
}
}
@@ -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. */ /** Pure finite planning witnesses: nine size/seed combinations, rotation and pre-visit claims. */
public final class Aerial24Smoke { public final class Aerial24Smoke {
public static void main(String[] args) { public static void main(String[] args) throws java.io.IOException {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); SharedConstants.tryDetectVersion(); Bootstrap.bootStrap();
BlockState[] column = new BlockState[384]; BlockState[] column = new BlockState[384];
for (int y = 0; y < column.length; y++) column[y] = (y >= 80 && y <= 240 ? Blocks.STONE : Blocks.AIR).defaultBlockState(); 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}; int[] queries = {0};
var plan = AerialPlanner24.plan(seed, size, (x, z) -> { queries[0]++; return new NoiseColumn(0, column); }); 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)); 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(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(queries[0] <= AerialPlanner24.MAX_COLUMNS, "Finite terrain query budget");
require(plan.sites().size() == 5 && plan.sites().stream().filter(AerialPlan24.Site::merchant).count() == 2, require(plan.sites().size() == 4 && plan.sites().stream().filter(AerialPlan24.Site::merchant).count() == 2,
"Five complete sites, including two distinct merchant ships"); "Four complete sites, including two distinct merchant ships");
require(plan.sites().stream().filter(AerialPlan24.Site::waterfall).count() == 1, "Exactly one waterfall site"); require(plan.sites().stream().filter(AerialPlan24.Site::waterfall).count() == 1, "Exactly one waterfall site");
var allClaims = new HashSet<Long>(); var allClaims = new HashSet<Long>();
var root = ExpansionIsland.origin(size, seed, 24); 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"); require(AerialPiece24.decode(AerialPiece24.encode(site)).equals(site), "Saved site descriptor round-trips without changing its seed or rotation");
if (seed == 0) verifyRender(site); 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); 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(far), "Aerial sites do not reserve an infinite ring around Sanctuary");
require(!plan.conflicts(root), "Root ownership is not treated as a future expansion"); require(!plan.conflicts(root), "Root ownership is not treated as a future expansion");
@@ -78,11 +81,11 @@ public final class Aerial24Smoke {
catch (UnsupportedOperationException expected) { } catch (UnsupportedOperationException expected) { }
} }
require(AerialPlan24.EMPTY.isEmpty() && AerialPlan24.EMPTY.claimsChunks().isEmpty(), "Disabled sites reserve no space"); 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, try { new AerialPlan24(List.of(new AerialPlan24.Site("merchant_provisions", AerialPlan24.Kind.MERCHANT_PROVISIONS,
500, 290, 0, 15, 28, 19, 0, 0, 250, 240, 0, false))); throw new AssertionError("Partial plan accepted"); } 500, 290, 0, 11, 18, 28, 0, 0, 250, 240, 0, false))); throw new AssertionError("Partial plan accepted"); }
catch (IllegalArgumentException expected) { } catch (IllegalArgumentException expected) { }
verifyReservedShoreAndView(column); 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) { private static void verifyReservedShoreAndView(BlockState[] column) {
var original = AerialPlanner24.plan(0, 724, (x, z) -> new NoiseColumn(0, column)).sites().getFirst(); 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"); 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(); 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"); 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) { if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET) {
var chest = AerialGeometry24.chest(site); 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"); 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");
@@ -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(!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(!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 journal = new ExpansionJournal(directory, SEED, 724, 24, stored.token());
var root = ExpansionIsland.origin(724, SEED, 24); var root = ExpansionIsland.origin(724, SEED, 24);
Path journalFile = directory.resolve(ExpansionJournal.FILE_NAME); Path journalFile = directory.resolve(ExpansionJournal.FILE_NAME);
@@ -65,7 +65,7 @@ public final class AerialPersistence24Smoke {
journal.commit(List.of(root)); journal.commit(List.of(root));
require(Arrays.equals(pristineJournal, Files.readAllBytes(journalFile)), "A no-op commit preserves bytes"); 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(!collision.conflicts(root), "Collision witness is outside initial island reservations");
require(stored.plan().conflicts(collision), "A never-generated satellite reserves its chunks"); 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"); 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) { private static ExpansionIsland child(String id, String direction, int x, int z) {
return new ExpansionIsland(id, "sanctuary", "east", "temperate", 64, x, z, return new ExpansionIsland(id, "sanctuary", direction, "temperate", 64, x, z,
ExpansionIsland.deriveSeed(SEED, id), "natural", "reserved", 24); ExpansionIsland.deriveSeed(SEED, id), "natural", "reserved", 24);
} }
private static AerialPlan24 fixture() { private static AerialPlan24 fixture() {
return new AerialPlan24(List.of( 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, 28, 0, 2, 0, 240, 390, 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, 28, 0, 3, -390, 240, 0, false),
new Site("merchant_tools", Kind.MERCHANT_TOOLS, -740, 242, 0, 11, 18, 27, 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_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))); new Site("mineral_islet_dry", Kind.MINERAL_ISLET, 460, 260, -600, 56, 40, 56, 0, 5, 250, 240, -300, false)));
} }
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary" name = "Sanctuary"
author = "KOKA99CAB" author = "KOKA99CAB"
version = "0.1.0-alpha.24" version = "0.1.0-alpha.27"
pack-format = "packwiz:1.1.0" pack-format = "packwiz:1.1.0"
[index] [index]