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
+10
View File
@@ -603,6 +603,8 @@ tasks.register('aerial24Smoke', JavaExec) {
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.aerial.Aerial24Smoke'
if (providers.gradleProperty('sanctuaryAerialPreview').isPresent())
systemProperty('sanctuary.aerial.preview', rootProject.layout.buildDirectory.file('alpha27-preview/native-ships.json').get().asFile.absolutePath)
}
tasks.named('check') { dependsOn('aerial24Smoke') }
@@ -613,3 +615,11 @@ tasks.register('aerialPersistence24Smoke', JavaExec) {
mainClass = 'fr.koka.sanctuary.worldgen.aerial.AerialPersistence24Smoke'
}
tasks.named('check') { dependsOn('aerialPersistence24Smoke') }
tasks.register('swamp27VolumesSmoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.Swamp27VolumesSmoke'
}
tasks.named('check') { dependsOn('swamp27VolumesSmoke') }
@@ -33,7 +33,7 @@ public final class Aerial24FlowAdmissionGameTests {
&& level.structureManager().shouldGenerateStructures(), "Fresh generation24 with aerial sites and native structures enabled");
var plan = generator.aerialPlan24();
var session = Objects.requireNonNull(ExpansionRuntime.session(level));
helper.assertTrue(session.islands().size() == 1 && plan.sites().size() == 5, "Use an untouched disposable world for admission witnesses");
helper.assertTrue(session.islands().size() == 1 && plan.sites().size() == 4, "Use an untouched disposable world for admission witnesses");
var evidence = StructureCensusService.snapshot(level.getServer()).evidence();
String dimension = level.dimension().identifier().toString();
for (var site : plan.sites()) for (var chunk : site.claimedChunks())
@@ -47,7 +47,7 @@ public final class Aerial24WorldGameTests {
private static final com.google.gson.Gson JSON = new GsonBuilder().setPrettyPrinting().create();
@GameTest(maxTicks = 24000)
public void fiveSitesPersistAfterUse(GameTestHelper helper) throws Exception {
public void fourSitesPersistAfterUse(GameTestHelper helper) throws Exception {
var report = new LinkedHashMap<String, Object>();
try { begin(helper, report); }
catch (Exception failure) { failure(report, failure); throw failure; }
@@ -69,10 +69,10 @@ public final class Aerial24WorldGameTests {
helper.assertTrue(StructureCensusService.aerialSites(level).isEmpty(), "Disabled aerial sites are not presented as planned");
report.put("passed", true); write(Path.of("diagnostics/aerial24/off.json"), report); helper.succeed(); return;
}
helper.assertTrue(plan.sites().size() == 5, "All five initial aerial sites have a finite saved plan");
helper.assertTrue(plan.sites().size() == 4, "All four initial aerial sites have a finite saved plan");
helper.assertTrue(plan.sites().stream().filter(AerialPlan24.Site::merchant).count() == 2, "Two merchant ships");
helper.assertTrue(plan.sites().stream().filter(AerialPlan24.Site::waterfall).count() == 1, "One waterfall site");
if (generator.initialDiameter() == 724 && level.getSeed() == 0)
if (generator.initialDiameter() == 724 && (level.getSeed() == 0 || level.getSeed() == 42))
AerialRenderer24.writeDiagnostic(Path.of("diagnostics/aerial24/blocks.json"), plan.sites());
report.put("geometry", JSON.toJsonTree(plan.sites())); report.put("claim_count", plan.claimsChunks().size());
report.put("census_before", StructureCensusService.aerialSites(level));
@@ -148,8 +148,8 @@ public final class Aerial24WorldGameTests {
report.put("native_tags", tags); report.put("full_chunks", chunks.size());
report.put("blocks_after", countSites(level, plan));
var census = StructureCensusService.aerialSites(level);
helper.assertTrue(census.size() == 5 && census.stream().allMatch(site -> site.status() == StructureCensusService.AerialStatus.GENERATED),
"Five historical generated sites remain indexed after loot, mining and demolition");
helper.assertTrue(census.size() == 4 && census.stream().allMatch(site -> site.status() == StructureCensusService.AerialStatus.GENERATED),
"Four historical generated sites remain indexed after loot, mining and demolition");
report.put("census_after", census);
helper.assertTrue(equalFiles(journalBefore, files(world.resolve("data").resolve(generator.journalDirectory()))), "Aerial tests never reserve or activate an expansion");
report.put("expansion_journal_unchanged", true);
@@ -253,7 +253,7 @@ public final class Aerial24WorldGameTests {
helper.assertTrue(state.isAir() || site.waterfall() && !state.getFluidState().isEmpty(), "No manufactured structure leaks outside its authored footprint");
}
}
helper.assertTrue(chestCount == 5 && waterSources == 1, "Five physical chests and exactly one planned source");
helper.assertTrue(chestCount == 4 && waterSources == 1, "Four physical chests and exactly one planned source");
report.put("chests", chestCount); report.put("waterfall_sources", waterSources); report.put("blocks_before", countSites(level, plan));
report.put("vegetation", vegetation);
return expected;
@@ -305,8 +305,8 @@ public final class Aerial24WorldGameTests {
for (var entry : ores) { level.setBlock(entry.getKey(), Blocks.AIR.defaultBlockState(), 2); removed.add(coordinates(entry.getKey())); }
}
}
var balloon = plan.sites().stream().filter(site -> site.kind() == AerialPlan24.Kind.BALLOON).findFirst().orElseThrow();
var demolition = AerialGeometry24.entry(balloon).below();
var ship = plan.sites().stream().filter(AerialPlan24.Site::merchant).findFirst().orElseThrow();
var demolition = AerialGeometry24.entry(ship).below();
helper.assertTrue(level.getBlockState(demolition).isSolid(), "The demolition witness removes an existing floor");
level.setBlock(demolition, Blocks.AIR.defaultBlockState(), 2); removed.add(coordinates(demolition));
var extracted = new TreeMap<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": "*",
"license": "GPL-3.0-or-later",
"entrypoints": {
"fabric-gametest": ["${gametest_entrypoint}", "fr.koka.sanctuary.gametest.BlockKnowledgeGameTests", "fr.koka.sanctuary.gametest.CensusGameTestsTerrain", "fr.koka.sanctuary.gametest.CensusStockGameTests", "fr.koka.sanctuary.gametest.MaterialActivityGameTests"],
"fabric-gametest": ["${gametest_entrypoint}", "fr.koka.sanctuary.gametest.Swamp27WorldGameTests", "fr.koka.sanctuary.gametest.BlockKnowledgeGameTests", "fr.koka.sanctuary.gametest.CensusGameTestsTerrain", "fr.koka.sanctuary.gametest.CensusStockGameTests", "fr.koka.sanctuary.gametest.MaterialActivityGameTests"],
"fabric-client-gametest": ["fr.koka.sanctuary.gametest.SanctuaryClientRenderTests"]
},
"mixins": ["sanctuary-gametest.mixins.json"],
@@ -86,6 +86,7 @@ public final class SanctuaryMod implements ModInitializer {
fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.register();
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("population_island"), PopulationIslandDensity.CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("population_island"), PopulationIslandBiomeSource.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("swamp27_decoration"), fr.koka.sanctuary.worldgen.Swamp27Decoration.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("population_groves"), PopulationGrovesFeature.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("population_decorations"), PopulationDecorationsFeature.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("population_shore_sugar_cane"), PopulationShoreSugarCaneFeature.CODEC);
@@ -16,13 +16,15 @@ import net.minecraft.world.level.biome.Climate;
/** Alpha.13 keeps Population's origin woodlands and the laboratory's native cave pockets.
* Child islands retain the laboratory's complete climate palette. */
public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
public static final ResourceKey<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(
ExpansionBiomeSource.REQUIRED_BIOMES.stream(), Stream.of(
PopulationIslandBiomeSource.OAK_FOREST, PopulationIslandBiomeSource.BIRCH_FOREST,
PopulationIslandBiomeSource.CLEARING, PopulationIslandBiomeSource.DRY_WOODLAND,
PopulationIslandBiomeSource.ROCKY_HEATH, PopulationIslandBiomeSource.DARK_GROVE,
PopulationIslandBiomeSource.BAMBOO_GROVE, PopulationIslandBiomeSource.SULFUR_DEPTHS,
PopulationIslandBiomeSource.LUSH_CAVES, PopulationIslandBiomeSource.DRIPSTONE_CAVES
PopulationIslandBiomeSource.LUSH_CAVES, PopulationIslandBiomeSource.DRIPSTONE_CAVES, SWAMP27
).map(key -> key.identifier().toString())).toList();
public static final MapCodec<SanctuaryBiomeSource> CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
@@ -71,6 +73,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
private final boolean cities;
private final int generation;
private final Holder<Biome> cityBiome;
private final Holder<Biome> swamp27;
private volatile java.util.Map<String, fr.koka.sanctuary.worldgen.city.LostCityPlan> cityPlans = java.util.Map.of();
public SanctuaryBiomeSource(List<Holder<Biome>> biomes) {
@@ -81,6 +84,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
this.generation = generation;
this.cities = ExpansionIsland.spatialGeneration(generation);
allBiomes = List.copyOf(biomes);
swamp27 = generation == 24 ? required(SWAMP27) : null;
if ((generation == 23 || generation == 24)) for (String name : fr.koka.sanctuary.expansion.ExpansionOcean23.BIOMES)
required(ResourceKey.create(net.minecraft.core.registries.Registries.BIOME,
net.minecraft.resources.Identifier.withDefaultNamespace(name)));
@@ -122,6 +126,20 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
// Reuse the historical selector itself: its noise, thresholds, order and child
// climate choices stay exact rather than being approximated by another climate field.
Holder<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)
|| laboratory.is(Biomes.DRIPSTONE_CAVES) || laboratory.is(Biomes.SULFUR_CAVES)) return laboratory;
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) {
return switch (s.kind()) {
case BALLOON -> world(s, s.width() / 2 - 2, 2, s.depth() / 2 + 2);
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> world(s, 7, 6, 22);
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> world(s, 7, 4, 24);
case MINERAL_ISLET -> {
int x = s.width() / 2 + 4, z = s.depth() / 2 + 2;
yield world(s, x, terrainTop(s, x, z) - 3, z);
@@ -24,27 +23,25 @@ public final class AerialGeometry24 {
}
public static BlockPos occupant(AerialPlan24.Site s) {
if (!s.merchant()) throw new IllegalArgumentException("Only merchant ships have occupants");
return world(s, 5, 6, 20);
return world(s, 5, 4, 22);
}
public static UUID occupantUuid(AerialPlan24.Site s) {
return UUID.nameUUIDFromBytes(("sanctuary:aerial24:" + s.id() + ":" + s.seed() + ":merchant")
.getBytes(StandardCharsets.UTF_8));
}
public static BlockPos workstation(AerialPlan24.Site s) { return world(s, 7, 6, 19); }
public static BlockPos workstation(AerialPlan24.Site s) { return world(s, 3, 4, 23); }
public static BlockPos waterfallSource(AerialPlan24.Site s) {
if (!s.waterfall()) throw new IllegalArgumentException("This site has no waterfall");
return world(s, s.width() / 2, rockHeight(s) - 3, 3);
}
public static BlockPos entry(AerialPlan24.Site s) {
return switch (s.kind()) {
case BALLOON -> world(s, s.width() / 2, 2, s.depth() / 2 - 3);
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> world(s, 7, 6, 12);
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> world(s, 3, 5, 13);
case MINERAL_ISLET -> world(s, s.width() / 2, terrainTop(s, s.width() / 2, s.depth() / 2) + 1, s.depth() / 2);
};
}
public static List<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.kind() == AerialPlan24.Kind.BALLOON) return List.of(entry(s), world(s, s.width() / 2, 2, s.depth() / 2));
if (s.merchant()) return List.of(entry(s), occupant(s), world(s, 5, 4, 21), world(s, 3, 4, 24));
return List.of(entry(s));
}
public static int terrainTop(AerialPlan24.Site s, int x, int z) {
@@ -19,17 +19,16 @@ public final class AerialPlan24 {
private final List<Site> sites;
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) {
this.sites = List.copyOf(sites);
if (!sites.isEmpty() && (sites.size() != 5
|| sites.stream().filter(s -> s.kind() == Kind.BALLOON).count() != 1
if (!sites.isEmpty() && (sites.size() != 4
|| sites.stream().filter(s -> s.kind() == Kind.MERCHANT_PROVISIONS).count() != 1
|| sites.stream().filter(s -> s.kind() == Kind.MERCHANT_TOOLS).count() != 1
|| sites.stream().filter(s -> s.kind() == Kind.MINERAL_ISLET).count() != 2
|| sites.stream().filter(Site::waterfall).count() != 1))
throw new IllegalArgumentException("An aerial plan contains all five sites and exactly one waterfall, or is disabled");
throw new IllegalArgumentException("An aerial plan contains all four sites and exactly one waterfall, or is disabled");
var ids = new HashSet<String>();
var columns = new HashSet<Long>();
for (Site site : sites) {
@@ -70,8 +69,7 @@ public final class AerialPlan24 {
|| waterfall && kind != Kind.MINERAL_ISLET)
throw new IllegalArgumentException("Invalid aerial site geometry");
if (kind == Kind.MINERAL_ISLET && (width < 40 || depth < 40 || height < 20 + VEGETATION_HEADROOM)
|| kind == Kind.BALLOON && height != 28
|| (kind == Kind.MERCHANT_PROVISIONS || kind == Kind.MERCHANT_TOOLS) && (width != 11 || depth != 27 || height != 18))
|| (kind == Kind.MERCHANT_PROVISIONS || kind == Kind.MERCHANT_TOOLS) && (width != 11 || depth != 28 || height != 18))
throw new IllegalArgumentException("Aerial site differs from its version24 gabarit");
int worldWidth = facing % 2 == 0 ? width : depth;
int worldDepth = facing % 2 == 0 ? depth : width;
@@ -40,19 +40,21 @@ public final class AerialPlanner24 {
var probe = new Probe(terrain, radius);
var sites = new ArrayList<Site>();
var claimed = new HashSet<Long>();
Kind[] kinds = {Kind.BALLOON, Kind.MERCHANT_PROVISIONS, Kind.MERCHANT_TOOLS, Kind.MINERAL_ISLET, Kind.MINERAL_ISLET};
String[] ids = {"balloon", "merchant_provisions", "merchant_tools", "mineral_islet_water", "mineral_islet_dry"};
Kind[] kinds = {Kind.MERCHANT_PROVISIONS, Kind.MERCHANT_TOOLS, Kind.MINERAL_ISLET, Kind.MINERAL_ISLET};
String[] ids = {"merchant_provisions", "merchant_tools", "mineral_islet_water", "mineral_islet_dry"};
double initialAngle = unit(mix(seed ^ 0xA24AE21AL)) * Math.PI * 2;
for (int ordinal = 0; ordinal < kinds.length; ordinal++) {
long siteSeed = ExpansionIsland.deriveSeed(seed ^ 0xA24AE21AL, ids[ordinal]);
int width = kinds[ordinal] == Kind.BALLOON ? 15 : ordinal < 3 ? 11 : 40 + 4 * (int) Math.floorMod(siteSeed, 7);
int depth = kinds[ordinal] == Kind.BALLOON ? 19 : ordinal < 3 ? 27 : width;
int bodyHeight = kinds[ordinal] == Kind.BALLOON ? 28 : ordinal < 3 ? 18 : 20 + (int) Math.floorMod(mix(siteSeed), 16);
int height = bodyHeight + (ordinal >= 3 ? AerialPlan24.VEGETATION_HEADROOM : 0);
boolean merchant = ordinal < 2;
int width = merchant ? 11 : 40 + 4 * (int) Math.floorMod(siteSeed, 7);
int depth = merchant ? 28 : width;
int bodyHeight = merchant ? 18 : 20 + (int) Math.floorMod(mix(siteSeed), 16);
int height = bodyHeight + (merchant ? 0 : AerialPlan24.VEGETATION_HEADROOM);
Site accepted = null;
for (int attempt = 0; attempt < MAX_ATTEMPTS_PER_SITE; attempt++) {
long draw = mix(siteSeed + attempt * 0x9e3779b97f4a7c15L);
double angle = initialAngle + ordinal * (Math.PI * 2 / 5) + (unit(draw) - .5) * .9
// Keep the four remaining bearings and seeded shapes; the former first sector is simply free.
double angle = initialAngle + (ordinal + 1) * (Math.PI * 2 / 5) + (unit(draw) - .5) * .9
+ (attempt / 16) * .41;
Shore shore = shore(probe, angle, radius);
if (shore == null) continue;
@@ -62,19 +64,18 @@ public final class AerialPlanner24 {
double distance = radius + 32 + Math.hypot(worldWidth, worldDepth) / 2 + 64 + (attempt % 4) * 20;
int cx = (int) Math.round(Math.cos(angle) * distance), cz = (int) Math.round(Math.sin(angle) * distance);
if (Math.hypot(cx - shore.x, cz - shore.z) > 500) continue;
int bottom = ordinal == 0 ? Math.clamp(shore.y + 24, 286, 321)
: ordinal < 3 ? Math.clamp(shore.y - 4 + (int) Math.floorMod(draw, 13), 180, 300)
int bottom = merchant ? Math.clamp(shore.y - 4 + (int) Math.floorMod(draw, 13), 180, 300)
: Math.clamp(shore.y + 20 - bodyHeight / 2, 210, 320 - bodyHeight);
bottom = Math.min(bottom, 350 - height);
var site = new Site(ids[ordinal], kinds[ordinal], cx - worldWidth / 2, bottom, cz - worldDepth / 2,
width, height, depth, facing, siteSeed, shore.x, shore.y, shore.z, ordinal == 3);
width, height, depth, facing, siteSeed, shore.x, shore.y, shore.z, ordinal == 2);
if (site.claimedChunks().stream().anyMatch(c -> claimed.contains(c.pack()))) continue;
boolean rootOverlap = site.claimedChunks().stream().anyMatch(c -> {
int nearestX = Math.clamp(0, c.getMinBlockX(), c.getMaxBlockX());
int nearestZ = Math.clamp(0, c.getMinBlockZ(), c.getMaxBlockZ());
return Math.hypot(nearestX, nearestZ) <= radius + 32;
});
if (rootOverlap || !visible(probe, shore, cx, bottom + (ordinal == 0 ? 18 : bodyHeight - 4), cz)) continue;
if (rootOverlap || !visible(probe, shore, cx, bottom + bodyHeight - 4, cz)) continue;
accepted = site; break;
}
if (accepted == null) throw new IllegalStateException("aerial_sites_unavailable: could not place " + ids[ordinal] + " within the bounded search");
@@ -41,7 +41,12 @@ public final class AerialRenderer24 {
BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(), state.toString());
}).toList();
result.add(Map.of("id", site.id(), "kind", site.kind().name(), "min", List.of(site.minX(), site.minY(), site.minZ()),
"size", List.of(site.worldWidth(), site.height(), site.worldDepth()), "seed", site.seed(), "facing", site.facing(), "blocks", list));
"size", List.of(site.worldWidth(), site.height(), site.worldDepth()), "seed", site.seed(), "facing", site.facing(), "blocks", list,
"native_template", site.merchant() ? Map.of("id", NativeMerchantShip27.TEMPLATE, "palette", NativeMerchantShip27.PALETTE,
"native_blocks", NativeMerchantShip27.restored().nativeBlocks(), "preserved_blocks", NativeMerchantShip27.restored().preservedBlocks(),
"added_blocks", NativeMerchantShip27.restored().addedBlocks(), "replaced_blocks", NativeMerchantShip27.restored().replacedBlocks(),
"drained_blocks", NativeMerchantShip27.restored().drainedBlocks(),
"restoration", "Native hull and stern cabin; repaired side planks/windows, mast and sail; merchant bed/workstation/light added afterwards") : Map.of()));
}
if (path.toAbsolutePath().getParent() != null) Files.createDirectories(path.toAbsolutePath().getParent());
Files.writeString(path, new com.google.gson.GsonBuilder().create().toJson(result) + "\n");
@@ -73,7 +78,6 @@ public final class AerialRenderer24 {
var destination = connected == null ? clipped : connected;
var b = new Builder(s, destination, connected == null ? clip : s.bounds());
switch (s.kind()) {
case BALLOON -> b.balloon();
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> b.ship();
case MINERAL_ISLET -> b.islet();
}
@@ -114,99 +118,16 @@ public final class AerialRenderer24 {
set(x, y, z, state.setValue(BlockStateProperties.BED_PART, BedPart.FOOT));
set(x, y, z + 1, state.setValue(BlockStateProperties.BED_PART, BedPart.HEAD));
}
void balloon() {
int cx = s.width() / 2, cz = s.depth() / 2;
// The envelope is a rounded shell. Continuous spars attach the four real suspensions.
Block accent = (s.seed() & 1) == 0 ? Blocks.WOOL.yellow() : Blocks.WOOL.lightBlue();
for (int y = 9; y <= 27; y++) for (int x = 0; x < s.width(); x++) for (int z = 0; z < s.depth(); z++) {
double q = Math.pow((x - cx) / 7.1, 2) + Math.pow((z - cz) / 9.1, 2) + Math.pow((y - 18) / 9.2, 2);
if (q <= 1.02 && q >= .73) set(x, y, z, Math.abs(x - cx) <= 1 || y == 19 ? accent : y % 6 == 0 ? Blocks.WOOL.lightGray() : Blocks.WOOL.white());
}
box(cx - 3, 0, cz - 4, cx + 3, 1, cz + 4, Blocks.OAK_PLANKS);
for (int x = cx - 3; x <= cx + 3; x++) for (int z = cz - 4; z <= cz + 4; z++) {
if (Math.abs(x - cx) == 3 || Math.abs(z - cz) == 4) {
set(x, 2, z, Blocks.OAK_PLANKS); slab(x, 3, z);
}
}
// One entry faces a usable standing deck, with a removable railing for boarding.
set(cx, 2, cz - 4, Blocks.OAK_FENCE_GATE); set(cx, 3, cz - 4, Blocks.AIR);
for (int x : new int[]{cx - 3, cx + 3}) for (int z : new int[]{cz - 4, cz + 4}) {
box(x, 3, z, x, 5, z, Blocks.STRIPPED_OAK_LOG);
box(x, 6, z, x, 12, z, Blocks.IRON_CHAIN);
}
for (int z : new int[]{cz - 4, cz + 4}) box(cx - 4, 12, z, cx + 4, 12, z, Blocks.STRIPPED_OAK_LOG);
for (int x : new int[]{cx - 3, cx + 3}) box(x, 12, cz - 5, x, 12, cz + 5, Blocks.STRIPPED_OAK_LOG);
box(cx - 3, 6, cz, cx + 3, 6, cz, Blocks.STRIPPED_OAK_LOG);
box(cx - 3, 5, cz - 4, cx - 3, 6, cz + 4, Blocks.STRIPPED_OAK_LOG);
box(cx + 3, 5, cz - 4, cx + 3, 6, cz + 4, Blocks.STRIPPED_OAK_LOG);
set(cx, 5, cz, Blocks.COPPER_BULB.waxed().unaffected().defaultBlockState().setValue(BlockStateProperties.LIT, true));
lantern(cx - 2, 5, cz); lantern(cx + 2, 5, cz);
set(cx + 2, 2, cz + 2, Blocks.CRAFTING_TABLE);
set(cx - 2, 2, cz - 2, Blocks.BARREL);
set(cx + 2, 2, cz - 2, Blocks.HAY_BLOCK);
}
int halfWidth(int y, int z) {
int end = Math.min(z, s.depth() - 1 - z);
return Math.max(0, Math.min(5, Math.min(1 + end, 1 + y)));
}
void ship() {
int cx = 5;
for (int z = 0; z < s.depth(); z++) for (int y = 0; y <= 5; y++) {
int half = halfWidth(y, z);
for (int x = cx - half; x <= cx + half; x++) {
boolean hull = y == 0 || y == 5 || Math.abs(x - cx) == half || z < 2 || z >= 25;
set(x, y, z, hull ? (y == 0 ? Blocks.STRIPPED_OAK_LOG : Blocks.OAK_PLANKS) : Blocks.AIR);
}
// The complete upright vanilla ship, offset one block in X for its saved envelope.
for (var entry : NativeMerchantShip27.restored().blocks().entrySet()) {
var at = entry.getKey(); set(at.getX() + 1, at.getY(), at.getZ(), entry.getValue());
}
// Curved gunwales remain closed; the wide centre stays clear for boarding small boats.
for (int z = 1; z <= 25; z++) {
int half = halfWidth(5, z);
for (int side : new int[]{-1, 1}) {
int x = cx + side * half;
set(x, 6, z, Blocks.OAK_FENCE); // Connection properties are completed below.
if (z % 5 == 0) set(x, 6, z, Blocks.STRIPPED_OAK_LOG);
}
}
for (int x = 3; x <= 7; x++) { set(x, 6, 1, Blocks.OAK_FENCE); set(x, 6, 25, Blocks.OAK_FENCE); }
// Roof, walls, threshold and cabin circulation share one continuous frame.
box(2, 6, 17, 8, 9, 24, Blocks.OAK_PLANKS);
box(3, 6, 18, 7, 9, 23, Blocks.AIR);
box(2, 10, 17, 8, 10, 24, Blocks.OAK_PLANKS);
for (int z = 17; z <= 24; z++) {
stair(1, 10, z, Direction.EAST, Half.BOTTOM); stair(9, 10, z, Direction.WEST, Half.BOTTOM);
}
for (int x : new int[]{2, 8}) for (int z : new int[]{17, 24}) box(x, 6, z, x, 10, z, Blocks.STRIPPED_OAK_LOG);
for (int z = 19; z <= 21; z++) for (int y = 7; y <= 8; y++) { set(2, y, z, Blocks.GLASS); set(8, y, z, Blocks.GLASS); }
for (int x = 4; x <= 6; x++) set(x, 8, 24, Blocks.GLASS);
door(5, 6, 17, Direction.SOUTH);
bed(3, 6, 22);
set(7, 6, 19, s.kind() == AerialPlan24.Kind.MERCHANT_PROVISIONS ? Blocks.COMPOSTER : Blocks.SMITHING_TABLE);
set(3, 6, 19, Blocks.CRAFTING_TABLE); set(3, 6, 20, Blocks.BARREL);
lantern(5, 9, 20); lantern(5, 9, 23);
// Cale reached by an actual supported stair run; no disconnected decorative floor.
for (int step = 0; step < 4; step++) {
int z = 10 + step, y = 4 - step;
box(5, 1, z, 5, y, z, Blocks.OAK_PLANKS);
box(5, y + 1, z, 5, 6, z, Blocks.AIR);
stair(5, y, z, Direction.NORTH, Half.BOTTOM);
}
box(3, 1, 7, 7, 1, 20, Blocks.OAK_PLANKS);
set(3, 2, 17, Blocks.BARREL); set(7, 2, 17, Blocks.BARREL);
lantern(3, 4, 8); lantern(7, 4, 8);
Block cargo = s.kind() == AerialPlan24.Kind.MERCHANT_PROVISIONS ? Blocks.HAY_BLOCK : Blocks.IRON_BLOCK;
box(2, 6, 5, 3, 7, 7, cargo); box(7, 6, 5, 8, 6, 7, Blocks.BARREL);
box(5, 6, 4, 5, 16, 4, Blocks.STRIPPED_OAK_LOG);
for (int x = 1; x <= 9; x++)
set(x, 16, 4, Blocks.STRIPPED_OAK_LOG.defaultBlockState().setValue(BlockStateProperties.AXIS, Direction.Axis.X));
for (int x = 2; x <= 8; x++)
set(x, 8, 4, Blocks.STRIPPED_OAK_LOG.defaultBlockState().setValue(BlockStateProperties.AXIS, Direction.Axis.X));
set(5, 17, 4, Blocks.LANTERN);
// Seven by seven sail around the continuous mast: yard above, boom below, clear deck beneath.
for (int y = 9; y <= 15; y++) for (int x = 2; x <= 8; x++)
if (x != cx) set(x, y, 4, y == 11 ? Blocks.WOOL.lightGray() : Blocks.WOOL.white());
// Stable handrails are explicit, rather than depending on neighbouring chunk updates.
for (int z = 2; z <= 24; z++) for (int x : new int[]{0, 10}) if (halfWidth(5, z) == 5 && z % 5 != 0 && !(x == 0 && z == 12))
set(x, 6, z, Blocks.OAK_FENCE.defaultBlockState().setValue(BlockStateProperties.NORTH, true).setValue(BlockStateProperties.SOUTH, true));
// Reuse the native stern cabin, with its original floor, roof, staircase and treasure-cache position.
set(3, 4, 23, s.kind() == AerialPlan24.Kind.MERCHANT_PROVISIONS ? Blocks.COMPOSTER : Blocks.SMITHING_TABLE);
bed(3, 4, 21);
set(7, 4, 21, Blocks.CRAFTING_TABLE);
lantern(5, 6, 22);
}
void islet() {
for (int x = 0; x < s.width(); x++) for (int z = 0; z < s.depth(); z++) {
@@ -17,7 +17,7 @@ public final class AerialStructure24 extends Structure {
private final String family;
public AerialStructure24(StructureSettings settings, String family) {
super(settings);
if (!Set.of("balloon", "merchant_ship", "mineral_islet").contains(family)) throw new IllegalArgumentException("Unknown aerial family");
if (!Set.of("merchant_ship", "mineral_islet").contains(family)) throw new IllegalArgumentException("Unknown aerial family");
this.family = family;
}
public String family() { return family; }
@@ -22,12 +22,11 @@ import net.minecraft.world.level.levelgen.structure.StructureType;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceType;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
/** Native starts for five immutable initial sites. Their void chunks need no terrain island owner. */
/** Native starts for four immutable initial sites. Their void chunks need no terrain island owner. */
public final class AerialStructures24 {
public static final ResourceKey<Structure> BALLOON = key("aerial_balloon");
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 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,
SanctuaryMod.id("aerial_v24"), () -> AerialStructure24.CODEC);
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
@@ -37,10 +36,10 @@ public final class AerialStructures24 {
private static ResourceKey<Structure> key(String id) { return ResourceKey.create(Registries.STRUCTURE, SanctuaryMod.id(id)); }
public static void register() {}
public static String family(AerialPlan24.Site site) {
return switch (site.kind()) { case BALLOON -> "balloon"; case MINERAL_ISLET -> "mineral_islet"; default -> "merchant_ship"; };
return switch (site.kind()) { case MINERAL_ISLET -> "mineral_islet"; default -> "merchant_ship"; };
}
public static ResourceKey<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 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.other_dimension": "Reserved for another dimension",
"sanctuary.mineral_sanctuary.entry": "Mineral sanctuary: %s %s %s",
"sanctuary.world_options.aerial24_hint": "Secrets, abandoned routes and five aerial sites. Vanilla structures remain independent.",
"sanctuary.world_options.aerial24_hint": "Secrets, abandoned routes, two ships and two floating islets. Vanilla structures remain independent.",
"sanctuary.structures.census.aerial.none": "No aerial sites reserved in this world.",
"sanctuary.structures.census.aerial.unknown_site": "Unknown aerial site: %s",
"sanctuary.structures.census.aerial.site": "%s · %s · %s · starts %s/%s · features %s/%s · full chunks %s/%s",
@@ -235,7 +235,7 @@
"sanctuary.structures.census.aerial.status.generated": "generated",
"sanctuary.structures.census.aerial.status.missing": "start missing after verification",
"sanctuary.structures.census.aerial.status.unknown": "unknown",
"structure.sanctuary.aerial_balloon": "Hot air balloon",
"structure.sanctuary.aerial_merchant_ship": "Merchant ship",
"structure.sanctuary.aerial_mineral_islet": "Mineral islet"
"structure.sanctuary.aerial_mineral_islet": "Mineral islet",
"biome.sanctuary.swamp27_caves": "Underground swamp"
}
@@ -226,7 +226,7 @@
"sanctuary.structures.census.status.incompatible": "Milieu incompatible",
"sanctuary.structures.census.status.other_dimension": "Réservée à une autre dimension",
"sanctuary.mineral_sanctuary.entry": "Sanctuaire minéral : %s %s %s",
"sanctuary.world_options.aerial24_hint": "Secrets, voies abandonnées et cinq lieux suspendus. Les structures vanilla restent indépendantes.",
"sanctuary.world_options.aerial24_hint": "Secrets, voies abandonnées, deux navires et deux îlots suspendus. Les structures vanilla restent indépendantes.",
"sanctuary.structures.census.aerial.none": "Aucun lieu suspendu réservé dans ce monde.",
"sanctuary.structures.census.aerial.unknown_site": "Lieu suspendu inconnu : %s",
"sanctuary.structures.census.aerial.site": "%s · %s · %s · départs %s/%s · décoration %s/%s · chunks complets %s/%s",
@@ -235,7 +235,7 @@
"sanctuary.structures.census.aerial.status.generated": "généré",
"sanctuary.structures.census.aerial.status.missing": "départ absent après vérification",
"sanctuary.structures.census.aerial.status.unknown": "inconnu",
"structure.sanctuary.aerial_balloon": "Montgolfière",
"structure.sanctuary.aerial_merchant_ship": "Bateau marchand",
"structure.sanctuary.aerial_mineral_islet": "Îlot minéral"
"structure.sanctuary.aerial_mineral_islet": "Îlot minéral",
"biome.sanctuary.swamp27_caves": "Marais souterrain"
}
@@ -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_dripstone_caves",
"sanctuary:lost_city",
"sanctuary:swamp27_caves",
"minecraft:ocean",
"minecraft:deep_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. */
public final class Aerial24Smoke {
public static void main(String[] args) {
public static void main(String[] args) throws java.io.IOException {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap();
BlockState[] column = new BlockState[384];
for (int y = 0; y < column.length; y++) column[y] = (y >= 80 && y <= 240 ? Blocks.STONE : Blocks.AIR).defaultBlockState();
@@ -27,10 +27,13 @@ public final class Aerial24Smoke {
int[] queries = {0};
var plan = AerialPlanner24.plan(seed, size, (x, z) -> { queries[0]++; return new NoiseColumn(0, column); });
var replay = AerialPlanner24.plan(seed, size, (x, z) -> new NoiseColumn(0, column));
String preview = System.getProperty("sanctuary.aerial.preview");
if (preview != null && size == 724 && seed == 42)
AerialRenderer24.writeDiagnostic(java.nio.file.Path.of(preview), plan.sites().stream().filter(AerialPlan24.Site::merchant).toList());
require(plan.sites().equals(replay.sites()) && plan.claimsChunks().equals(replay.claimsChunks()), "Plan does not depend on time or loaded chunks");
require(queries[0] <= AerialPlanner24.MAX_COLUMNS, "Finite terrain query budget");
require(plan.sites().size() == 5 && plan.sites().stream().filter(AerialPlan24.Site::merchant).count() == 2,
"Five complete sites, including two distinct merchant ships");
require(plan.sites().size() == 4 && plan.sites().stream().filter(AerialPlan24.Site::merchant).count() == 2,
"Four complete sites, including two distinct merchant ships");
require(plan.sites().stream().filter(AerialPlan24.Site::waterfall).count() == 1, "Exactly one waterfall site");
var allClaims = new HashSet<Long>();
var root = ExpansionIsland.origin(size, seed, 24);
@@ -68,7 +71,7 @@ public final class Aerial24Smoke {
require(AerialPiece24.decode(AerialPiece24.encode(site)).equals(site), "Saved site descriptor round-trips without changing its seed or rotation");
if (seed == 0) verifyRender(site);
}
require(allClaims.equals(plan.claimsChunks()), "Combined claims contain only the five finite site masks");
require(allClaims.equals(plan.claimsChunks()), "Combined claims contain only the four finite site masks");
var far = new ExpansionIsland("far", "sanctuary", "east", "temperate", 64, 4096, 4096, 1, "natural", "reserved", 24);
require(!plan.conflicts(far), "Aerial sites do not reserve an infinite ring around Sanctuary");
require(!plan.conflicts(root), "Root ownership is not treated as a future expansion");
@@ -78,11 +81,11 @@ public final class Aerial24Smoke {
catch (UnsupportedOperationException expected) { }
}
require(AerialPlan24.EMPTY.isEmpty() && AerialPlan24.EMPTY.claimsChunks().isEmpty(), "Disabled sites reserve no space");
try { new AerialPlan24(List.of(new AerialPlan24.Site("balloon", AerialPlan24.Kind.BALLOON,
500, 290, 0, 15, 28, 19, 0, 0, 250, 240, 0, false))); throw new AssertionError("Partial plan accepted"); }
try { new AerialPlan24(List.of(new AerialPlan24.Site("merchant_provisions", AerialPlan24.Kind.MERCHANT_PROVISIONS,
500, 290, 0, 11, 18, 28, 0, 0, 250, 240, 0, false))); throw new AssertionError("Partial plan accepted"); }
catch (IllegalArgumentException expected) { }
verifyReservedShoreAndView(column);
System.out.println("Aerial24Smoke: nine deterministic plans, five sites, four rotations, finite non-overlapping claims and pre-visit conflicts passed.");
System.out.println("Aerial24Smoke: nine deterministic plans, four sites, four rotations, finite non-overlapping claims and pre-visit conflicts passed.");
}
private static void verifyReservedShoreAndView(BlockState[] column) {
var original = AerialPlanner24.plan(0, 724, (x, z) -> new NoiseColumn(0, column)).sites().getFirst();
@@ -126,6 +129,24 @@ public final class Aerial24Smoke {
require(complete.equals(clipped) && clippedChests[0] == 1, "Chunk clipping reconstructs the complete authored structure without duplicate chests");
long sources = complete.values().stream().filter(state -> state.is(Blocks.WATER)).count();
require(sources == (site.waterfall() ? 1 : 0), "Renderer writes one source, never a fabricated full waterfall column");
if (site.merchant()) {
require(complete.values().stream().allMatch(state -> state.getFluidState().isEmpty()),
"Restored ships contain no fluid, including inside stairs, slabs, fences or trapdoors");
var nativeShip = NativeMerchantShip27.restored();
require(nativeShip.nativeBlocks() == 659, "The exact vanilla template contributes 662 authored blocks minus its three data markers");
require(nativeShip.preservedBlocks() + nativeShip.replacedBlocks() == nativeShip.nativeBlocks(), "No native hull position disappears during restoration");
require(nativeShip.blocks().values().stream().anyMatch(state -> state.is(Blocks.OAK_STAIRS))
&& nativeShip.blocks().values().stream().anyMatch(state -> state.is(Blocks.SPRUCE_SLAB)),
"The ship retains its native shaped stairs and slabs");
require(complete.values().stream().noneMatch(state -> state.is(Blocks.STRUCTURE_BLOCK)), "No native data marker is placed in the restored ship");
for (var at : AerialGeometry24.interiorChecks(site)) {
require(complete.getOrDefault(at, Blocks.AIR.defaultBlockState()).isAir()
&& complete.getOrDefault(at.above(), Blocks.AIR.defaultBlockState()).isAir(),
"Boarding, cabin circulation and merchant standing spaces remain open");
require(complete.getOrDefault(at.below(), Blocks.AIR.defaultBlockState()).isSolid(),
"Each boarding and interior witness retains its real floor");
}
}
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET) {
var chest = AerialGeometry24.chest(site);
for (int dy = 1; dy <= 3; dy++) require(complete.get(chest.above(dy)) != null && complete.get(chest.above(dy)).isSolid(), "Mineral chest is buried under three intact layers");
@@ -48,7 +48,7 @@ public final class AerialPersistence24Smoke {
require(!site.ownsChunk((site.maxX() >> 4) + 2, (site.maxZ() >> 4) + 2), "Unused corners of the old broad margin stay free");
}
require(!Files.exists(directory), "Manifest creation must not prematurely initialize the journal");
require(draws.get() == 1 && stored.plan().sites().size() == 5, "One complete initial plan is recorded");
require(draws.get() == 1 && stored.plan().sites().size() == 4, "One complete initial plan is recorded");
var journal = new ExpansionJournal(directory, SEED, 724, 24, stored.token());
var root = ExpansionIsland.origin(724, SEED, 24);
Path journalFile = directory.resolve(ExpansionJournal.FILE_NAME);
@@ -65,7 +65,7 @@ public final class AerialPersistence24Smoke {
journal.commit(List.of(root));
require(Arrays.equals(pristineJournal, Files.readAllBytes(journalFile)), "A no-op commit preserves bytes");
var collision = child("collision", 672, 0);
var collision = child("collision", "south", 0, 672);
require(!collision.conflicts(root), "Collision witness is outside initial island reservations");
require(stored.plan().conflicts(collision), "A never-generated satellite reserves its chunks");
reject(() -> journal.commit(List.of(root, collision)), "Reservation over an unvisited aerial site");
@@ -135,15 +135,14 @@ public final class AerialPersistence24Smoke {
}
}
private static ExpansionIsland child(String id, int x, int z) {
return new ExpansionIsland(id, "sanctuary", "east", "temperate", 64, x, z,
private static ExpansionIsland child(String id, String direction, int x, int z) {
return new ExpansionIsland(id, "sanctuary", direction, "temperate", 64, x, z,
ExpansionIsland.deriveSeed(SEED, id), "natural", "reserved", 24);
}
private static AerialPlan24 fixture() {
return new AerialPlan24(List.of(
new Site("balloon", Kind.BALLOON, 660, 290, 0, 15, 28, 19, 0, 1, 390, 240, 0, false),
new Site("merchant_provisions", Kind.MERCHANT_PROVISIONS, 0, 235, 660, 11, 18, 27, 0, 2, 0, 240, 390, false),
new Site("merchant_tools", Kind.MERCHANT_TOOLS, -740, 242, 0, 11, 18, 27, 0, 3, -390, 240, 0, false),
new Site("merchant_provisions", Kind.MERCHANT_PROVISIONS, 0, 235, 660, 11, 18, 28, 0, 2, 0, 240, 390, false),
new Site("merchant_tools", Kind.MERCHANT_TOOLS, -740, 242, 0, 11, 18, 28, 0, 3, -390, 240, 0, false),
new Site("mineral_islet_water", Kind.MINERAL_ISLET, -450, 255, -650, 48, 36, 48, 0, 4, -200, 240, -330, true),
new Site("mineral_islet_dry", Kind.MINERAL_ISLET, 460, 260, -600, 56, 40, 56, 0, 5, 250, 240, -300, false)));
}