feat(worldgen): add persistent aerial Sanctuary sites for alpha.24
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-11 18:29:38 +02:00
parent fbefb2e6d6
commit 27d7cba93f
61 changed files with 3469 additions and 132 deletions
+27 -2
View File
@@ -40,7 +40,11 @@ tasks.named('runGameTest') {
systemProperty('sanctuary.test.unified', providers.gradleProperty('sanctuaryUnifiedTests').getOrElse('true'))
systemProperty('sanctuary.test.diameter', providers.gradleProperty('sanctuaryTestDiameter').getOrElse('724'))
systemProperty('sanctuary.test.generation', providers.gradleProperty('sanctuaryTestGeneration').getOrElse(
providers.gradleProperty('sanctuaryLostCityTests').map { it.toBoolean() }.getOrElse(false) ? '14' : providers.gradleProperty('sanctuaryLostCity15Tests').map { it.toBoolean() }.getOrElse(false) ? '15' : providers.gradleProperty('sanctuaryLostCity16Tests').map { it.toBoolean() }.getOrElse(false) ? '16' : providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false) ? '17' : providers.gradleProperty('sanctuaryHeritage19Tests').map { it.toBoolean() }.getOrElse(false) ? '19' : (providers.gradleProperty('sanctuaryHeritage20Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryLostCity20Tests').map { it.toBoolean() }.getOrElse(false)) ? '20' : (providers.gradleProperty('sanctuaryHeritage21Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryLostCity21Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryTransit21Tests').map { it.toBoolean() }.getOrElse(false)) ? '21' : providers.gradleProperty('sanctuarySelective22Tests').map { it.toBoolean() }.getOrElse(false) ? '22' : '23'))
providers.gradleProperty('sanctuaryLostCityTests').map { it.toBoolean() }.getOrElse(false) ? '14' : providers.gradleProperty('sanctuaryLostCity15Tests').map { it.toBoolean() }.getOrElse(false) ? '15' : providers.gradleProperty('sanctuaryLostCity16Tests').map { it.toBoolean() }.getOrElse(false) ? '16' : providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false) ? '17' : providers.gradleProperty('sanctuaryHeritage19Tests').map { it.toBoolean() }.getOrElse(false) ? '19' : (providers.gradleProperty('sanctuaryHeritage20Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryLostCity20Tests').map { it.toBoolean() }.getOrElse(false)) ? '20' : (providers.gradleProperty('sanctuaryHeritage21Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryLostCity21Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryTransit21Tests').map { it.toBoolean() }.getOrElse(false)) ? '21' : providers.gradleProperty('sanctuarySelective22Tests').map { it.toBoolean() }.getOrElse(false) ? '22' : ['sanctuarySecrets23Tests', 'sanctuaryNative23Tests', 'sanctuaryOcean23Tests', 'sanctuaryAdmission23Tests'].any { providers.gradleProperty(it).map { v -> v.toBoolean() }.getOrElse(false) } ? '23' : '24'))
systemProperty('sanctuary.test.aerial24.flowAdmission', providers.gradleProperty('sanctuaryAerial24FlowAdmission').getOrElse('false'))
systemProperty('sanctuary.test.aerial24', providers.gradleProperty('sanctuaryAerial24Tests').getOrElse('false'))
systemProperty('sanctuary.test.aerial24.reverseChunks', providers.gradleProperty('sanctuaryAerial24ReverseChunks').getOrElse('false'))
systemProperty('sanctuary.test.aerial24.phase', providers.gradleProperty('sanctuaryAerial24Phase').getOrElse('create'))
systemProperty('sanctuary.test.secrets23', providers.gradleProperty('sanctuarySecrets23Tests').getOrElse('false'))
systemProperty('sanctuary.test.admission23', providers.gradleProperty('sanctuaryAdmission23Tests').getOrElse('false'))
systemProperty('sanctuary.test.admission23.reserveOnly', providers.gradleProperty('sanctuaryAdmission23ReserveOnly').getOrElse('false'))
@@ -94,6 +98,10 @@ tasks.named('processGametestResources') {
inputs.property('heritage21', heritage21)
def lostCity21 = providers.gradleProperty('sanctuaryLostCity21Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('lostCity21', lostCity21)
def aerial24Flow = providers.gradleProperty('sanctuaryAerial24FlowAdmission').map { it.toBoolean() }.getOrElse(false)
inputs.property('aerial24Flow', aerial24Flow)
def aerial24 = providers.gradleProperty('sanctuaryAerial24Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('aerial24', aerial24)
def secrets23 = providers.gradleProperty('sanctuarySecrets23Tests').map { it.toBoolean() }.getOrElse(false)
def admission23 = providers.gradleProperty('sanctuaryAdmission23Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('admission23', admission23)
@@ -111,7 +119,7 @@ tasks.named('processGametestResources') {
def voidReuse = providers.gradleProperty('sanctuaryVoidReuseTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('voidReuse', voidReuse)
filesMatching('fabric.mod.json') {
expand(gametest_entrypoint: admission23 ? 'fr.koka.sanctuary.gametest.Expansion23AdmissionGameTests' : ocean23 ? 'fr.koka.sanctuary.gametest.Ocean23WorldGameTests' : secrets23 ? 'fr.koka.sanctuary.gametest.Secrets23WorldGameTests' : native23 ? 'fr.koka.sanctuary.gametest.Native23WorldGameTests' : selective22 ? 'fr.koka.sanctuary.gametest.Selective22WorldGameTests' : transit21 ? 'fr.koka.sanctuary.gametest.Transit21WorldGameTests' : lostCity21 ? 'fr.koka.sanctuary.gametest.LostCity21WorldGameTests' : heritage21 ? 'fr.koka.sanctuary.gametest.Heritage21WorldGameTests' : lostCity20 ? 'fr.koka.sanctuary.gametest.LostCity20WorldGameTests' : heritage20 ? 'fr.koka.sanctuary.gametest.Heritage20WorldGameTests' : heritage19 ? 'fr.koka.sanctuary.gametest.Heritage19WorldGameTests' : ruins17 ? 'fr.koka.sanctuary.gametest.Ruins17WorldGameTests' : voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark
expand(gametest_entrypoint: aerial24Flow ? 'fr.koka.sanctuary.gametest.Aerial24FlowAdmissionGameTests' : aerial24 ? 'fr.koka.sanctuary.gametest.Aerial24WorldGameTests' : admission23 ? 'fr.koka.sanctuary.gametest.Expansion23AdmissionGameTests' : ocean23 ? 'fr.koka.sanctuary.gametest.Ocean23WorldGameTests' : secrets23 ? 'fr.koka.sanctuary.gametest.Secrets23WorldGameTests' : native23 ? 'fr.koka.sanctuary.gametest.Native23WorldGameTests' : selective22 ? 'fr.koka.sanctuary.gametest.Selective22WorldGameTests' : transit21 ? 'fr.koka.sanctuary.gametest.Transit21WorldGameTests' : lostCity21 ? 'fr.koka.sanctuary.gametest.LostCity21WorldGameTests' : heritage21 ? 'fr.koka.sanctuary.gametest.Heritage21WorldGameTests' : lostCity20 ? 'fr.koka.sanctuary.gametest.LostCity20WorldGameTests' : heritage20 ? 'fr.koka.sanctuary.gametest.Heritage20WorldGameTests' : heritage19 ? 'fr.koka.sanctuary.gametest.Heritage19WorldGameTests' : ruins17 ? 'fr.koka.sanctuary.gametest.Ruins17WorldGameTests' : voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark
? 'fr.koka.sanctuary.gametest.WorldgenPerformanceGameTests'
: unified ? 'fr.koka.sanctuary.gametest.UnifiedWorldGameTests'
: 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests')
@@ -588,3 +596,20 @@ tasks.named('check') { dependsOn('expansionOcean23Smoke') }
}
tasks.named('check') { dependsOn(taskName) }
}
// Versioned aerial planner: bounded search, native pieces and local claims.
tasks.register('aerial24Smoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.aerial.Aerial24Smoke'
}
tasks.named('check') { dependsOn('aerial24Smoke') }
tasks.register('aerialPersistence24Smoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.aerial.AerialPersistence24Smoke'
}
tasks.named('check') { dependsOn('aerialPersistence24Smoke') }
@@ -0,0 +1,179 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuary.knowledge.structures.StructureCensusMemory;
import fr.koka.sanctuary.knowledge.structures.StructureCensusService;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import fr.koka.sanctuary.worldgen.aerial.*;
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.gametest.framework.GameTestHelper;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.TicketType;
import net.minecraft.tags.FluidTags;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.structure.StructureStart;
/** Separate disposable world: real admission before visiting satellites, then an actual ticking waterfall. */
public final class Aerial24FlowAdmissionGameTests {
private record Request(String id, String direction, int diameter, ExpansionIsland raw, ExpansionIsland filtered) {}
@GameTest(maxTicks = 24000)
public void reservedSatellitesAndNativeWaterfall(GameTestHelper helper) throws Exception {
var report = new LinkedHashMap<String, Object>();
var level = helper.getLevel();
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
helper.assertTrue(generator.rootGeneration() == 24 && generator.experimentalStructures()
&& 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");
var evidence = StructureCensusService.snapshot(level.getServer()).evidence();
String dimension = level.dimension().identifier().toString();
for (var site : plan.sites()) for (var chunk : site.claimedChunks())
helper.assertTrue((evidence.chunks().getOrDefault(new StructureCensusMemory.ChunkKey(dimension, chunk.x(), chunk.z()), 0) & StructureCensusMemory.FULL) == 0,
"Satellite claims are protected before any of their chunks reach FULL");
report.put("seed", level.getSeed()); report.put("diameter", generator.initialDiameter()); report.put("process_id", ProcessHandle.current().pid());
report.put("unvisited_claims_verified", true); report.put("sites", plan.sites());
var admitted = new ArrayList<ExpansionIsland>();
try {
report.put("stage", "real admission toward an unseen satellite");
admitted.add(admit(helper, level, generator, session, plan, true, report));
report.put("stage", "real admission through an unclaimed interval");
admitted.add(admit(helper, level, generator, session, plan, false, report));
} catch (Exception error) { failure(report, error); throw error; }
var waterfall = plan.sites().stream().filter(AerialPlan24.Site::waterfall).findFirst().orElseThrow();
var requested = waterfall.claimedChunks(); var iterator = requested.iterator();
boolean[] tickets = {false}; int[] flowTicks = {0};
Runnable release = () -> {
if (tickets[0]) requested.forEach(chunk -> level.getChunkSource().removeTicketWithRadius(TicketType.DRAGON, chunk, 2));
tickets[0] = false;
};
Runnable[] step = new Runnable[1];
step[0] = () -> {
try {
if (admitted.stream().anyMatch(island -> !session.island(island.id()).status().equals("ready"))) {
report.put("stage", "prepare admitted expansions"); report.put("progress", session.progress());
helper.assertTrue(!session.progress().contains("failed") && !session.progress().contains("reload_required")
&& !session.progress().contains("unavailable"), "Real admission preparation progresses without reload or failure");
helper.runAtTickTime(helper.getTick() + 5, () -> step[0].run()); return;
}
if (!report.containsKey("native_ready_starts")) {
var nativeStarts = new TreeMap<String, Integer>();
for (var reserved : admitted) {
var ready = session.island(reserved.id());
helper.assertTrue(ready.equals(reserved.ready()) && !plan.conflicts(ready), "READY retains its immutable safe placement");
int count = 0;
for (var candidate : ExpansionStructures23.candidates(ready)) {
var chunk = level.getChunk(candidate.chunk().x(), candidate.chunk().z(), ChunkStatus.STRUCTURE_STARTS, true);
count += (int) chunk.getAllStarts().values().stream().filter(StructureStart::isValid)
.filter(start -> ((Object) start) instanceof NativeStructureMetadata23 metadata && metadata.sanctuary$native23() != null).count();
}
helper.assertTrue(count > 0, "Admitted generation24 expansion produces at least one real native start");
nativeStarts.put(ready.id(), count);
}
report.put("native_ready_starts", nativeStarts);
}
if (iterator.hasNext()) {
var chunk = iterator.next(); report.put("stage", "FULL waterfall " + chunk);
level.getChunk(chunk.x(), chunk.z(), ChunkStatus.FULL, true);
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
}
if (!tickets[0]) {
requested.forEach(chunk -> level.getChunkSource().addTicketWithRadius(TicketType.DRAGON, chunk, 2)); tickets[0] = true;
}
flowTicks[0] += 20; report.put("flow_wait_ticks", flowTicks[0]); report.put("stage", "native fluid ticks toward the void");
var source = AerialGeometry24.waterfallSource(waterfall);
helper.assertTrue(level.getFluidState(source).is(FluidTags.WATER) && level.getFluidState(source).isSource(), "The authored source remains a single native water source");
var below = waterAt(level, waterfall, waterfall.minY() - 16);
var low = waterAt(level, waterfall, 8);
report.put("water_below_islet", below == null ? List.of() : coords(below));
report.put("water_near_y0", low == null ? List.of() : coords(low));
if (below == null || low == null) {
helper.assertTrue(flowTicks[0] <= 4000, "Scheduled vanilla fluid ticks carry the waterfall below the island and down to y=8");
helper.runAtTickTime(helper.getTick() + 20, () -> step[0].run()); return;
}
helper.assertTrue(!level.getFluidState(low).isSource(), "Low waterfall water is flowing, not a fabricated source column");
report.put("expansions", admitted.stream().map(island -> session.island(island.id())).toList());
report.put("passed", true);
StructureCensusService.flush(level.getServer());
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Persist the disposable flow/admission witness");
write(report, "full.json"); release.run(); helper.succeed();
} catch (Exception error) { release.run(); failure(report, error); throw new IllegalStateException("Aerial24 flow/admission failed: " + error.getMessage(), error); }
};
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run());
}
private static ExpansionIsland admit(GameTestHelper helper, ServerLevel level, SanctuaryChunkGenerator generator,
ExpansionRuntime.Session session, AerialPlan24 plan, boolean collision, Map<String, Object> report) throws Exception {
var requests = requests(level.getSeed(), session.islands(), plan, collision);
helper.assertTrue(!requests.isEmpty(), "The finite candidate search contains a " + (collision ? "satellite overlap" : "free interval") + " witness");
var attempts = new ArrayList<Map<String, Object>>(); report.put(collision ? "collision_attempts" : "interval_attempts", attempts);
for (var request : requests.stream().limit(8).toList()) {
var before = List.copyOf(session.islands());
var filtered = session.candidate(request.id(), "sanctuary", request.direction(), "temperate", request.diameter(), "natural");
helper.assertTrue(filtered.equals(request.filtered()), "Real runtime candidate uses the same immutable aerial claims before any terrain inspection");
if (collision) helper.assertTrue(plan.conflicts(request.raw()) && !plan.conflicts(filtered) && !filtered.equals(request.raw()),
"The first overlapping candidate is rejected before the satellite is visited");
long start = System.nanoTime();
try {
var reserved = session.create(request.id(), "sanctuary", request.direction(), "temperate", request.diameter(), "natural");
long elapsed = (System.nanoTime() - start) / 1_000_000;
helper.assertTrue(elapsed <= 31_000, "Actual Session.create keeps its production thirty-second deadline");
helper.assertTrue(!plan.conflicts(reserved), "The admitted expansion cannot claim any satellite chunk");
helper.assertTrue(!ExpansionVoidReuse.requiresReload(level, reserved.id()), "Untouched candidate prepares in the same development session");
if (!collision) helper.assertTrue(inAerialBand(reserved, plan), "The admitted free interval lies among the nearby satellites, not beyond a blanket exclusion ring");
helper.assertTrue(session.create(request.id(), "sanctuary", request.direction(), "temperate", request.diameter(), "natural").equals(reserved),
"Reservation retry is idempotent");
attempts.add(Map.of("request", request, "reserved", reserved, "elapsed_ms", elapsed));
return reserved;
} catch (IllegalArgumentException refused) {
helper.assertTrue(before.equals(session.islands()), "Refused native admission writes no reservation");
attempts.add(Map.of("request", request, "refused", String.valueOf(refused.getMessage()), "elapsed_ms", (System.nanoTime() - start) / 1_000_000));
if (!"native_structure_unavailable".equals(refused.getMessage())) throw refused;
}
}
throw new IllegalStateException("No native expansion fits the bounded aerial admission witnesses");
}
private static List<Request> requests(long seed, List<ExpansionIsland> islands, AerialPlan24 plan, boolean collision) {
var result = new ArrayList<Request>();
for (int index = 0; index < 32 && result.size() < 16; index++) for (var direction : ExpansionIsland.DIRECTIONS) {
String id = (collision ? "aerial_collision_" : "aerial_interval_") + index + "_" + direction.replace("_", "");
var raw = ExpansionPlacement.candidates(seed, islands, id, "sanctuary", direction, "temperate", 128, "natural", 0);
if (raw.isEmpty() || plan.conflicts(raw.getFirst()) != collision) continue;
var filtered = raw.stream().filter(candidate -> !plan.conflicts(candidate)).findFirst().orElse(null);
if (filtered == null || !collision && !inAerialBand(filtered, plan)) continue;
result.add(new Request(id, direction, 128, raw.getFirst(), filtered));
}
return result;
}
private static boolean inAerialBand(ExpansionIsland island, AerialPlan24 plan) {
double outside = plan.claimsChunks().stream().map(ChunkPos::unpack).mapToDouble(chunk -> Math.hypot(chunk.getMiddleBlockX(), chunk.getMiddleBlockZ()) + 12).max().orElseThrow();
return Math.hypot(island.centerX(), island.centerZ()) - island.reservedRadius() < outside;
}
private static BlockPos waterAt(ServerLevel level, AerialPlan24.Site site, int y) {
for (int x = site.minX() - 3; x <= site.maxX() + 3; x++) for (int z = site.minZ() - 3; z <= site.maxZ() + 3; z++) {
var pos = new BlockPos(x, y, z);
if (level.getFluidState(pos).is(FluidTags.WATER)) return pos;
}
return null;
}
private static List<Integer> coords(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
private static void write(Map<String, Object> report, String name) throws java.io.IOException {
var directory = Path.of("diagnostics/aerial24-flow-admission"); Files.createDirectories(directory);
Files.writeString(directory.resolve(name), new GsonBuilder().setPrettyPrinting().create().toJson(report));
}
private static void failure(Map<String, Object> report, Exception error) {
report.put("passed", false); report.put("failure", error.toString());
var stack = new java.io.StringWriter(); error.printStackTrace(new java.io.PrintWriter(stack)); report.put("stacktrace", stack.toString());
SanctuaryMod.LOGGER.error("Aerial24 flow/admission failed at {}", report.get("stage"), error);
try { write(report, "failure.json"); } catch (java.io.IOException failed) { error.addSuppressed(failed); }
}
}
@@ -0,0 +1,398 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mojang.serialization.JsonOps;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import fr.koka.sanctuary.expansion.ExpansionStructures23;
import fr.koka.sanctuary.knowledge.structures.StructureCensusService;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import fr.koka.sanctuary.worldgen.aerial.*;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
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.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.TicketType;
import net.minecraft.world.entity.npc.villager.Villager;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.LevelResource;
import net.minecraft.world.level.storage.loot.LootTable;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
/** One real world per size/seed. Separate JVM phases preserve looting, mining and extracted occupants. */
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 {
var report = new LinkedHashMap<String, Object>();
try { begin(helper, report); }
catch (Exception failure) { failure(report, failure); throw failure; }
}
private static void begin(GameTestHelper helper, Map<String, Object> report) throws Exception {
var level = helper.getLevel();
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
helper.assertTrue(generator.rootGeneration() == 24, "Aerial24 requires a disposable generation24 world");
String phase = System.getProperty("sanctuary.test.aerial24.phase", "create");
helper.assertTrue(List.of("create", "reload1", "reload2").contains(phase), "Known aerial lifecycle phase");
boolean create = phase.equals("create");
var plan = generator.aerialPlan24();
report.put("phase", phase); report.put("seed", level.getSeed()); report.put("diameter", generator.initialDiameter());
report.put("process_id", ProcessHandle.current().pid()); report.put("experimental", generator.experimentalStructures());
report.put("generate_structures", level.structureManager().shouldGenerateStructures());
if (!level.structureManager().shouldGenerateStructures() || !generator.experimentalStructures()) {
helper.assertTrue(plan.isEmpty() && plan.claimsChunks().isEmpty(), "Disabled aerial generation reserves no space");
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().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)
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));
report.put("generator_codec", ChunkGenerator.CODEC.encodeStart(RegistryOps.create(JsonOps.INSTANCE, level.registryAccess()), generator).getOrThrow());
var journalBefore = files(level.getServer().getWorldPath(LevelResource.ROOT).resolve("data").resolve(generator.journalDirectory()));
verifyClaims(helper, generator, plan, report);
Path world = level.getServer().getWorldPath(LevelResource.ROOT);
JsonObject previous = create ? null : JsonParser.parseString(Files.readString(world.resolve("aerial24-" + (phase.equals("reload1") ? "create" : "reload1") + ".json"))).getAsJsonObject();
if (previous != null) {
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload occurs in a separate JVM after a successful phase");
helper.assertTrue(previous.get("geometry").equals(JSON.toJsonTree(plan.sites())), "Site geometry and palette seeds survive reload");
helper.assertTrue(previous.get("generator_codec").equals(report.get("generator_codec")), "Generator options survive reload");
}
var chunks = new TreeSet<ChunkPos>(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z));
plan.sites().forEach(site -> chunks.addAll(site.claimedChunks()));
plan.sites().forEach(site -> chunks.add(ChunkPos.containing(new BlockPos(site.shoreX(), site.shoreY(), site.shoreZ()))));
// The two extracted merchants are retained in a separate, existing root chunk.
chunks.add(new ChunkPos(0, 0));
var loadOrder = new ArrayList<>(chunks);
boolean reverseChunks = Boolean.getBoolean("sanctuary.test.aerial24.reverseChunks");
if (reverseChunks) Collections.reverse(loadOrder);
report.put("reverse_chunks", reverseChunks);
var iterator = loadOrder.iterator();
var entityChunks = new HashSet<ChunkPos>();
plan.sites().stream().filter(AerialPlan24.Site::merchant).forEach(site -> entityChunks.addAll(site.claimedChunks()));
entityChunks.add(new ChunkPos(0, 0));
boolean[] tickets = {false}; int[] wait = {0};
Runnable release = () -> {
if (tickets[0]) entityChunks.forEach(pos -> level.getChunkSource().removeTicketWithRadius(TicketType.DRAGON, pos, 2));
tickets[0] = false;
};
Runnable[] step = new Runnable[1];
step[0] = () -> {
try {
if (iterator.hasNext()) {
var chunk = iterator.next(); report.put("stage", "FULL " + chunk);
level.getChunk(chunk.x(), chunk.z(), ChunkStatus.FULL, true);
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
}
if (!tickets[0]) {
entityChunks.forEach(pos -> level.getChunkSource().addTicketWithRadius(TicketType.DRAGON, pos, 2)); tickets[0] = true;
}
var occupants = occupants(level, plan);
var readiness = new TreeMap<String, Object>(); boolean entitiesReady = true;
for (var pos : entityChunks) {
boolean loaded = level.areEntitiesLoaded(pos.pack()), ticking = level.areEntitiesActuallyLoadedAndTicking(pos);
readiness.put(pos.toString(), Map.of("loaded", loaded, "ticking", ticking));
entitiesReady &= loaded && ticking;
}
report.put("entity_chunks", readiness);
report.put("occupants_observed", occupants.stream().map(villager -> Map.of("uuid", villager.getUUID().toString(),
"position", List.of(villager.getX(), villager.getY(), villager.getZ()))).toList());
report.put("visible_occupants", occupants.size()); report.put("entity_wait_ticks", wait[0]);
if ((!entitiesReady || occupants.size() < 2) && wait[0]++ < 600) {
report.put("stage", "wait for persistent merchants");
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
}
helper.assertTrue(entitiesReady, "All ship chunks finish native entity loading and become ticking before occupant inspection");
helper.assertTrue(occupants.size() == 2, "Both one-time merchants are present after native entity loading");
report.put("stage", "inspect native structures");
var starts = starts(helper, level, plan);
var before = inspect(helper, level, plan, create, report);
if (create) {
verifyLocator(helper, level, plan, starts);
report.put("stage", "loot, extract and dismantle disposable witnesses");
report.put("mutations", mutate(helper, level, plan, occupants, before));
} else {
report.put("mutations", previous.get("mutations"));
verifyMutations(helper, level, plan, previous.getAsJsonObject("mutations"), occupants);
}
var tags = tags(level, starts);
if (!create) helper.assertTrue(previous.get("native_tags").equals(JSON.toJsonTree(tags)), "Native starts, references and occupant flags survive both reloads");
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");
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);
StructureCensusService.flush(level.getServer());
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save actual blocks, loot state, starts and occupants");
report.put("passed", true);
write(world.resolve("aerial24-" + phase + ".json"), report);
write(Path.of("diagnostics/aerial24/" + phase + ".json"), report);
release.run(); helper.succeed();
} catch (Exception error) {
release.run(); failure(report, error); throw new IllegalStateException("Aerial24 failed: " + error.getMessage(), error);
}
};
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run());
}
private static void verifyClaims(GameTestHelper helper, SanctuaryChunkGenerator generator, AerialPlan24 plan, Map<String, Object> report) {
var root = generator.source().islands().stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var views = new LinkedHashMap<String, Object>();
for (var site : plan.sites()) {
helper.assertTrue(site.claimedChunks().stream().noneMatch(chunk -> root.ownsChunk(chunk.x(), chunk.z())), "No aerial claim replaces natural starter chunks");
var overlap = new ExpansionIsland("aerial_overlap", root.id(), "east", "temperate", 64,
site.startChunkX() * 16, site.startChunkZ() * 16, 42, "natural", "reserved", 24);
helper.assertTrue(plan.conflicts(overlap), "An unseen site already prevents overlapping expansion ownership");
double distance = Math.hypot(site.locator().getX() - site.shoreX(), site.locator().getZ() - site.shoreZ());
helper.assertTrue(distance <= 512, "Aerial site remains within 512 blocks of a real shore witness");
views.put(site.id(), Map.of("shore", List.of(site.shoreX(), site.shoreY() + 2, site.shoreZ()),
"entry", coordinates(AerialGeometry24.entry(site)), "centre", coordinates(site.locator()), "shore_distance", distance,
"exterior_view", List.of(site.minX() - 24, site.minY() + site.height() / 2, site.minZ() - 24)));
}
helper.assertTrue(!plan.conflicts(new ExpansionIsland("aerial_far", root.id(), "east", "temperate", 64, 4096, 4096, 42, "natural", "reserved", 24)),
"Unclaimed space remains available; there is no blanket exclusion ring");
report.put("look_views", views);
}
private static List<Villager> occupants(ServerLevel level, AerialPlan24 plan) {
var expected = plan.sites().stream().filter(AerialPlan24.Site::merchant).map(AerialGeometry24::occupantUuid).collect(java.util.stream.Collectors.toSet());
return level.getEntitiesOfClass(Villager.class, new AABB(-2048, 0, -2048, 2048, 384, 2048))
.stream().filter(villager -> expected.contains(villager.getUUID())).toList();
}
private static Map<String, StructureStart> starts(GameTestHelper helper, ServerLevel level, AerialPlan24 plan) {
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
var result = new TreeMap<String, StructureStart>();
for (var site : plan.sites()) {
var type = registry.get(Identifier.parse(AerialStructures24.structureId(site))).orElseThrow().value();
var start = level.getChunk(site.startChunkX(), site.startChunkZ(), ChunkStatus.FULL, true).getStartForStructure(type);
helper.assertTrue(start != null && start.isValid(), "Planned aerial site has a real persisted native start: " + site.id());
helper.assertTrue(start.getPieces().stream().filter(piece -> piece instanceof AerialPiece24 aerial && aerial.site().equals(site)).count() == 1,
"Exactly one piece retains the site's immutable descriptor");
var context = StructurePieceSerializationContext.fromLevel(level);
var tag = start.createTag(context, start.getChunkPos());
var restored = StructureStart.loadStaticStart(context, tag, level.getSeed());
helper.assertTrue(restored != null && tag.equals(restored.createTag(context, start.getChunkPos())), "Aerial start round-trips through native serialization");
if (site.merchant()) helper.assertTrue(start.getPieces().stream().filter(AerialPiece24.class::isInstance).map(AerialPiece24.class::cast)
.allMatch(AerialPiece24::occupantPlaced), "Merchant placement flag is stored after its first creation");
result.put(site.id(), start);
}
return result;
}
private static Map<String, Map<BlockPos, BlockState>> inspect(GameTestHelper helper, ServerLevel level, AerialPlan24 plan,
boolean fresh, Map<String, Object> report) {
var expected = new LinkedHashMap<String, Map<BlockPos, BlockState>>(); int chestCount = 0, waterSources = 0;
var vegetation = new TreeMap<String, Object>();
var shores = new TreeMap<String, Object>(); report.put("shore_witnesses", shores);
for (var site : plan.sites()) {
var model = render(site); expected.put(site.id(), model);
var shore = new BlockPos(site.shoreX(), site.shoreY(), site.shoreZ());
var column = new TreeMap<Integer, String>();
for (int dy = -2; dy <= 20; dy++) column.put(shore.getY() + dy, level.getBlockState(shore.above(dy)).toString());
shores.put(site.id(), Map.of("position", coordinates(shore), "column", column));
helper.assertTrue(level.getBlockState(shore.below()).isSolid(), "Saved shore witness has a real FULL terrain support: " + site.id() + " " + shore);
helper.assertTrue(level.getBlockState(shore.above()).getCollisionShape(level, shore.above()).isEmpty(),
"Shore viewpoint is not buried inside the island: " + site.id() + " " + shore.above() + " " + level.getBlockState(shore.above()));
var chestAt = AerialGeometry24.chest(site);
helper.assertTrue(level.getBlockEntity(chestAt) instanceof RandomizableContainerBlockEntity, "Real chest at " + site.id()); chestCount++;
var chest = (RandomizableContainerBlockEntity) level.getBlockEntity(chestAt);
if (fresh) {
helper.assertTrue(AerialRenderer24.loot(site).equals(chest.getLootTable()), "Aerial chest retains its unopened site-specific loot table");
for (var entry : model.entrySet()) if (!entry.getValue().isAir() && !entry.getValue().is(Blocks.WATER))
helper.assertTrue(level.getBlockState(entry.getKey()).is(entry.getValue().getBlock()), "Native FULL keeps authored material at " + site.id() + " " + entry.getKey());
for (var pos : AerialGeometry24.interiorChecks(site)) {
helper.assertTrue(level.getBlockState(pos).getCollisionShape(level, pos).isEmpty()
&& level.getBlockState(pos.above()).getCollisionShape(level, pos.above()).isEmpty(), "Usable two-block headroom at " + site.id());
helper.assertTrue(!level.getBlockState(pos.below()).getCollisionShape(level, pos.below()).isEmpty(), "Interior stands on a real floor");
}
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET) for (int dy = 1; dy <= 3; dy++)
helper.assertTrue(level.getBlockState(chestAt.above(dy)).isSolid(), "Mineral chest needs excavation through three layers");
} else helper.assertTrue(chest.getLootTable() == null && chest.isEmpty(), "Looted chest never refills on reload");
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET)
vegetation.put(site.id(), vegetation(helper, level, site));
if (site.waterfall()) {
var source = AerialGeometry24.waterfallSource(site);
helper.assertTrue(level.getFluidState(source).isSource(), "The one real source drives the native waterfall"); waterSources++;
}
// These columns are beyond the authored box and well outside initial terrain ownership.
for (var pos : List.of(new BlockPos(site.minX() - 2, site.minY(), site.minZ() - 2), new BlockPos(site.maxX() + 2, site.minY(), site.maxZ() + 2)))
for (int y = site.minY(); y <= site.maxY() + 2; y++) {
var state = level.getBlockState(pos.atY(y));
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");
report.put("chests", chestCount); report.put("waterfall_sources", waterSources); report.put("blocks_before", countSites(level, plan));
report.put("vegetation", vegetation);
return expected;
}
private static Map<String, Object> vegetation(GameTestHelper helper, ServerLevel level, AerialPlan24.Site site) {
helper.assertTrue(site.rockHeight() >= 20 && site.rockHeight() <= 35
&& site.height() == site.rockHeight() + AerialPlan24.VEGETATION_HEADROOM,
"Tree headroom preserves the original stone height");
int surface = 0, grass = 0;
for (int x = 0; x < site.width(); x++) for (int z = 0; z < site.depth(); z++) {
int top = AerialGeometry24.terrainTop(site, x, z);
if (!AerialGeometry24.mineralBody(site, x, top, z)) continue;
surface++;
if (level.getBlockState(AerialGeometry24.world(site, x, top, z)).is(Blocks.GRASS_BLOCK)) grass++;
}
helper.assertTrue(grass * 2 > surface, "Most real intact surface columns carry grass: " + site.id());
var trees = AerialGeometry24.trees(site);
helper.assertTrue(!trees.isEmpty(), "At least one real tree on each mineral islet");
for (var tree : trees) {
var ground = AerialGeometry24.world(site, tree.x(), tree.groundY(), tree.z());
var floor = level.getBlockState(ground);
helper.assertTrue(floor.is(Blocks.GRASS_BLOCK) || floor.is(Blocks.DIRT), "Tree stands on soil, never in the air");
for (int y = 1; y <= tree.trunkHeight(); y++)
helper.assertTrue(level.getBlockState(ground.above(y)).is(tree.birch() ? Blocks.BIRCH_LOG : Blocks.OAK_LOG), "Native tree trunk stays continuous");
var crown = ground.above(tree.trunkHeight() + 1);
var leaves = level.getBlockState(crown);
helper.assertTrue(site.bounds().isInside(crown) && leaves.is(tree.birch() ? Blocks.BIRCH_LEAVES : Blocks.OAK_LEAVES)
&& !leaves.getValue(BlockStateProperties.PERSISTENT) && leaves.getValue(BlockStateProperties.DISTANCE) < 7,
"Complete natural crown survives native chunk decoration and reload");
}
return Map.of("rock_height", site.rockHeight(), "headroom", AerialPlan24.VEGETATION_HEADROOM,
"surface_columns", surface, "grass_columns", grass, "grass_percent", 100.0 * grass / surface, "trees", trees);
}
private static Map<String, Object> mutate(GameTestHelper helper, ServerLevel level, AerialPlan24 plan, List<Villager> occupants,
Map<String, Map<BlockPos, BlockState>> models) {
var emptied = new ArrayList<List<Integer>>(); var removed = new ArrayList<List<Integer>>(); var loot = new TreeMap<String, Integer>();
for (var site : plan.sites()) {
var at = AerialGeometry24.chest(site); var chest = (RandomizableContainerBlockEntity) level.getBlockEntity(at);
chest.unpackLootTable(null); int quantity = 0;
for (int slot = 0; slot < chest.getContainerSize(); slot++) quantity += chest.getItem(slot).getCount();
helper.assertTrue(quantity > 0, "Site-specific chest produces usable vanilla loot"); loot.put(site.id(), quantity);
chest.clearContent(); chest.setChanged(); emptied.add(coordinates(at));
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET) {
var ores = models.get(site.id()).entrySet().stream().filter(entry -> ore(entry.getValue())).sorted(Map.Entry.comparingByKey(
Comparator.<BlockPos>comparingInt(BlockPos::getX).thenComparingInt(BlockPos::getY).thenComparingInt(BlockPos::getZ))).limit(16).toList();
helper.assertTrue(!ores.isEmpty(), "Each mineral island contains actual finite veins");
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();
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;
for (var villager : occupants.stream().sorted(Comparator.comparing(v -> v.getUUID().toString())).toList()) {
// This fixture parks extracted merchants in an existing root chunk, outside both ships.
villager.setNoAi(true); villager.setNoGravity(true); villager.setDeltaMovement(Vec3.ZERO);
villager.snapTo(4.5 + i++ * 4, 330, 4.5); villager.setPersistenceRequired();
extracted.put(villager.getUUID().toString(), List.of(villager.getX(), villager.getY(), villager.getZ()));
}
return Map.of("emptied_chests", emptied, "removed_blocks", removed, "extracted_merchants", extracted, "loot_items_obtained", loot);
}
private static void verifyMutations(GameTestHelper helper, ServerLevel level, AerialPlan24 plan, JsonObject mutations, List<Villager> occupants) {
for (var value : mutations.getAsJsonArray("removed_blocks")) {
var pos = position(value.getAsJsonArray()); helper.assertTrue(level.getBlockState(pos).isAir(), "Mined ore and dismantled floor remain absent after reload");
}
var extracted = mutations.getAsJsonObject("extracted_merchants");
for (var villager : occupants) {
var at = extracted.getAsJsonArray(villager.getUUID().toString());
helper.assertTrue(at != null && Math.abs(villager.getX() - at.get(0).getAsDouble()) < .01
&& Math.abs(villager.getY() - at.get(1).getAsDouble()) < .01 && Math.abs(villager.getZ() - at.get(2).getAsDouble()) < .01,
"Extracted merchant stays outside its original ship without respawning");
}
for (var site : plan.sites()) if (site.merchant()) helper.assertTrue(level.getEntitiesOfClass(Villager.class, AABB.of(site.bounds())).isEmpty(),
"The visited ship does not replenish an extracted merchant");
}
private static void verifyLocator(GameTestHelper helper, ServerLevel level, AerialPlan24 plan, Map<String, StructureStart> starts) {
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
var holders = new ArrayList<Holder<Structure>>();
plan.sites().stream().map(AerialStructures24::structureId).distinct().forEach(id -> holders.add(registry.get(Identifier.parse(id)).orElseThrow()));
holders.add(registry.get(Identifier.withDefaultNamespace("stronghold")).orElseThrow());
var wanted = HolderSet.direct(holders); var origin = plan.sites().getFirst().locator();
var activeBefore = ((SanctuaryChunkGenerator) level.getChunkSource().getGenerator()).source().islands();
int before = starts.values().stream().mapToInt(StructureStart::getReferences).sum();
var found = ExpansionStructures23.findNearest(level, wanted, origin, 100, false);
helper.assertTrue(found != null, "Mixed vanilla/aerial locator finds a real accepted start");
helper.assertTrue(starts.values().stream().mapToInt(StructureStart::getReferences).sum() == before, "Ordinary locate never marks a site referenced");
var referenced = ExpansionStructures23.findNearest(level, wanted, origin, 100, true);
helper.assertTrue(referenced != null && starts.values().stream().mapToInt(StructureStart::getReferences).sum() == before + 1,
"Mixed map lookup references exactly its one winning aerial start");
helper.assertTrue(activeBefore.equals(((SanctuaryChunkGenerator) level.getChunkSource().getGenerator()).source().islands()), "Locator never activates an expansion");
}
private static Map<BlockPos, BlockState> render(AerialPlan24.Site site) {
var result = new HashMap<BlockPos, BlockState>();
AerialRenderer24.render(site, new LostCityRenderer.BlockWriter() {
public void set(int x, int y, int z, BlockState state) { result.put(new BlockPos(site.minX() + x, site.minY() + y, site.minZ() + z), state); }
public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { set(x, y, z, Blocks.CHEST.defaultBlockState()); }
});
return result;
}
private static Map<String, Map<String, Integer>> countSites(ServerLevel level, AerialPlan24 plan) {
var all = new TreeMap<String, Map<String, Integer>>();
for (var site : plan.sites()) {
var counts = new TreeMap<String, Integer>();
for (int x = site.minX(); x <= site.maxX(); x++) for (int z = site.minZ(); z <= site.maxZ(); z++) for (int y = site.minY(); y <= site.maxY(); y++) {
var state = level.getBlockState(new BlockPos(x, y, z));
if (!state.isAir()) counts.merge(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(), 1, Integer::sum);
}
all.put(site.id(), counts);
}
return all;
}
private static boolean ore(BlockState state) { return BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath().endsWith("_ore"); }
private static Map<String, String> tags(ServerLevel level, Map<String, StructureStart> starts) {
var result = new TreeMap<String, String>(); var context = StructurePieceSerializationContext.fromLevel(level);
starts.forEach((id, start) -> result.put(id, start.createTag(context, start.getChunkPos()).toString())); return result;
}
private static List<Integer> coordinates(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
private static BlockPos position(com.google.gson.JsonArray value) { return new BlockPos(value.get(0).getAsInt(), value.get(1).getAsInt(), value.get(2).getAsInt()); }
private static Map<String, byte[]> files(Path directory) throws java.io.IOException {
var found = new TreeMap<String, byte[]>();
if (Files.exists(directory)) try (var paths = Files.walk(directory)) {
for (var path : paths.filter(Files::isRegularFile).toList()) found.put(directory.relativize(path).toString(), Files.readAllBytes(path));
}
return found;
}
private static boolean equalFiles(Map<String, byte[]> a, Map<String, byte[]> b) {
return a.keySet().equals(b.keySet()) && a.entrySet().stream().allMatch(entry -> Arrays.equals(entry.getValue(), b.get(entry.getKey())));
}
private static void write(Path path, Map<String, Object> report) throws java.io.IOException { Files.createDirectories(path.toAbsolutePath().getParent()); Files.writeString(path, JSON.toJson(report)); }
private static void failure(Map<String, Object> report, Exception error) {
report.put("passed", false); report.put("failure", error.toString());
var stack = new java.io.StringWriter(); error.printStackTrace(new java.io.PrintWriter(stack)); report.put("stacktrace", stack.toString());
SanctuaryMod.LOGGER.error("Aerial24 fixture failed at {}", report.get("stage"), error);
try { write(Path.of("diagnostics/aerial24/failure.json"), report); } catch (java.io.IOException failed) { error.addSuppressed(failed); }
}
}
@@ -61,12 +61,12 @@ public final class PopulationDiagnostics {
if (level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator unified) {
var expected = new java.util.HashSet<>(SanctuaryBiomeSource.REQUIRED_BIOMES);
if (unified.source().citiesEnabled()) expected.add("sanctuary:lost_city");
if (unified.rootGeneration() == 23) fr.koka.sanctuary.expansion.ExpansionOcean23.BIOMES
if (unified.rootGeneration() == 23 || unified.rootGeneration() == 24) fr.koka.sanctuary.expansion.ExpansionOcean23.BIOMES
.forEach(name -> expected.add("minecraft:" + name));
helper.assertTrue(generator.getBiomeSource() instanceof SanctuaryBiomeSource
&& generator.getBiomeSource().possibleBiomes().stream().map(b -> b.getRegisteredName())
.collect(java.util.stream.Collectors.toSet()).equals(expected),
"Historical holders stay intact; only generation23 adds the expansion ocean and shore palette");
"Historical holders stay intact; generations23/24 add the expansion ocean and shore palette");
helper.assertTrue(binding.random() != level.getChunkSource().randomState(),
"The unified starter must use its real terrain state, never the expansion host fallback");
} else {
@@ -215,7 +215,7 @@ public final class PopulationDiagnostics {
void prepare() throws IOException {
boolean heritage = level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator sanctuary
&& (sanctuary.rootGeneration() == 23 || sanctuary.rootGeneration() == 22 || (sanctuary.rootGeneration() == 19 || sanctuary.rootGeneration() == 20 || sanctuary.rootGeneration() == 21) && sanctuary.experimentalStructures());
&& (sanctuary.rootGeneration() == 24 || sanctuary.rootGeneration() == 23 || sanctuary.rootGeneration() == 22 || (sanctuary.rootGeneration() == 19 || sanctuary.rootGeneration() == 20 || sanctuary.rootGeneration() == 21) && sanctuary.experimentalStructures());
// Generation19 explicitly prospects destinations across the root island before publishing them.
// A consulted hydrology region is a bounded immutable plan, not a loaded/generated chunk.
// Historical generators retain their original near-spawn contract.
@@ -61,7 +61,10 @@ final class UnifiedWorldSettingsDiagnostics {
var historical22 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v22")));
helper.assertTrue(historical22.value().createWorldDimensions().overworld() instanceof SanctuaryChunkGenerator old
&& old.rootGeneration() == 22 && old.source().generation() == 22, "Historical22 remains separately readable");
helper.assertTrue(generator.rootGeneration() == 23 && generator.source().generation() == 23 && generator.experimentalStructures(), "The public preset uses generation23 with its new-world option");
var historical23 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v23")));
helper.assertTrue(historical23.value().createWorldDimensions().overworld() instanceof SanctuaryChunkGenerator old
&& old.rootGeneration() == 23 && old.source().generation() == 23, "Historical23 remains separately readable");
helper.assertTrue(generator.rootGeneration() == 24 && generator.source().generation() == 24 && generator.experimentalStructures(), "The public preset uses generation24 with its new-world option");
helper.assertTrue(generator.initialDiameter() == 724, "The public preset defaults to Medium / 724 blocks");
verifyConstructionMasks(helper, (SanctuaryChunkGenerator) historical21.value().createWorldDimensions().overworld(), generator);
for (var tag : List.of(WorldPresetTags.NORMAL, WorldPresetTags.EXTENDED)) {
@@ -72,11 +75,11 @@ final class UnifiedWorldSettingsDiagnostics {
}
var ops = RegistryOps.create(JsonOps.INSTANCE, access);
var activeCodec = generator.rootGeneration() == 23 ? SanctuaryChunkGenerator.SECRETS23_CODEC : generator.rootGeneration() == 22 ? SanctuaryChunkGenerator.SELECTIVE22_CODEC : generator.rootGeneration() == 21 ? SanctuaryChunkGenerator.NETWORK21_CODEC : generator.rootGeneration() == 20 ? SanctuaryChunkGenerator.ARCHITECTURE20_CODEC : generator.rootGeneration() == 19 ? SanctuaryChunkGenerator.HERITAGE19_CODEC : generator.rootGeneration() == 17 ? SanctuaryChunkGenerator.RUINS17_CODEC : generator.rootGeneration() == 16 ? SanctuaryChunkGenerator.CITY16_CODEC : generator.rootGeneration() == 15 ? SanctuaryChunkGenerator.CITY15_CODEC : generator.rootGeneration() == 14 ? SanctuaryChunkGenerator.CITY_CODEC : SanctuaryChunkGenerator.CODEC;
var activeCodec = generator.rootGeneration() == 24 ? SanctuaryChunkGenerator.AERIAL24_CODEC : generator.rootGeneration() == 23 ? SanctuaryChunkGenerator.SECRETS23_CODEC : generator.rootGeneration() == 22 ? SanctuaryChunkGenerator.SELECTIVE22_CODEC : generator.rootGeneration() == 21 ? SanctuaryChunkGenerator.NETWORK21_CODEC : generator.rootGeneration() == 20 ? SanctuaryChunkGenerator.ARCHITECTURE20_CODEC : generator.rootGeneration() == 19 ? SanctuaryChunkGenerator.HERITAGE19_CODEC : generator.rootGeneration() == 17 ? SanctuaryChunkGenerator.RUINS17_CODEC : generator.rootGeneration() == 16 ? SanctuaryChunkGenerator.CITY16_CODEC : generator.rootGeneration() == 15 ? SanctuaryChunkGenerator.CITY15_CODEC : generator.rootGeneration() == 14 ? SanctuaryChunkGenerator.CITY_CODEC : SanctuaryChunkGenerator.CODEC;
var serialized = activeCodec.codec().encodeStart(ops, generator).getOrThrow().getAsJsonObject();
serialized.remove("experimental_structures");
helper.assertTrue(activeCodec.codec().parse(ops, serialized).getOrThrow().experimentalStructures(),
"Generation23 omitted experimental option defaults to enabled");
"Generation24 omitted experimental option defaults to enabled");
helper.assertTrue(!((SanctuaryChunkGenerator) historical16.overworld()).withExperimentalStructures(true).experimentalStructures(),
"The new-world switch cannot activate experimental generation in a historical16 generator");
serialized.remove("initial_diameter");
@@ -135,7 +138,7 @@ final class UnifiedWorldSettingsDiagnostics {
}
// Reuse the attached world's real plans: no extra native-noise planning or chunk loads.
if (!(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator active)
|| active.rootGeneration() != 21 && active.rootGeneration() != 22 && active.rootGeneration() != 23) return;
|| active.rootGeneration() != 21 && active.rootGeneration() != 22 && active.rootGeneration() != 23 && active.rootGeneration() != 24) return;
helper.assertTrue(!active.protectsCityDecoration(outside)
&& !active.intersectsExperimentalStructures(outside.getX(), outside.getY(), outside.getZ(),
outside.getX() + 1, outside.getY() + 1, outside.getZ() + 1),
@@ -145,7 +148,7 @@ final class UnifiedWorldSettingsDiagnostics {
interior.add(new net.minecraft.core.BlockPos(module.centerX(), module.floorY(), module.centerZ()));
for (var network : active.undergroundHall22().networks()) for (var module : network.modules())
interior.add(new net.minecraft.core.BlockPos(module.centerX(), module.floorY(), module.centerZ()));
if ((active.rootGeneration() == 22 || active.rootGeneration() == 23) && helper.getLevel().structureManager().shouldGenerateStructures())
if ((active.rootGeneration() == 22 || active.rootGeneration() == 23 || active.rootGeneration() == 24) && helper.getLevel().structureManager().shouldGenerateStructures())
helper.assertTrue(!interior.isEmpty(), "The attached generation22 world has core construction to protect");
for (var position : interior) helper.assertTrue(active.protectsCityDecoration(position)
&& active.intersectsExperimentalStructures(position.getX(), position.getY(), position.getZ(),
@@ -21,7 +21,7 @@ public abstract class SanctuaryGameTestServerMixin {
// generation test's deadline before its IO/workers can complete. This is test-only pacing.
@Inject(method = "waitUntilNextTick", at = @At("RETURN"))
private void sanctuary$paceExpansionWorkers(CallbackInfo ci) {
if (Boolean.getBoolean("sanctuary.test.admission23") || Boolean.getBoolean("sanctuary.test.ocean23") || Boolean.getBoolean("sanctuary.test.secrets23") || Boolean.getBoolean("sanctuary.test.native23") || Boolean.getBoolean("sanctuary.test.selective22") || (Boolean.getBoolean("sanctuary.test.heritage20") || Boolean.getBoolean("sanctuary.test.heritage21") || Boolean.getBoolean("sanctuary.test.lostCity21") || Boolean.getBoolean("sanctuary.test.transit21")) || Boolean.getBoolean("sanctuary.test.lostCity20")
if (Boolean.getBoolean("sanctuary.test.aerial24.flowAdmission") || Boolean.getBoolean("sanctuary.test.aerial24") || Boolean.getBoolean("sanctuary.test.admission23") || Boolean.getBoolean("sanctuary.test.ocean23") || Boolean.getBoolean("sanctuary.test.secrets23") || Boolean.getBoolean("sanctuary.test.native23") || Boolean.getBoolean("sanctuary.test.selective22") || (Boolean.getBoolean("sanctuary.test.heritage20") || Boolean.getBoolean("sanctuary.test.heritage21") || Boolean.getBoolean("sanctuary.test.lostCity21") || Boolean.getBoolean("sanctuary.test.transit21")) || Boolean.getBoolean("sanctuary.test.lostCity20")
|| Boolean.getBoolean("sanctuary.test.heritage19") || Boolean.getBoolean("sanctuary.test.ruins17") || Boolean.getBoolean("sanctuary.test.expansion") || Boolean.getBoolean("sanctuary.test.lostCity")
|| Boolean.getBoolean("sanctuary.test.lostCity16") || Boolean.getBoolean("sanctuary.test.lostCity15") || Boolean.getBoolean("sanctuary.test.voidReuse")) {
long until = System.nanoTime() + 20_000_000L;
@@ -47,9 +47,9 @@ public abstract class SanctuaryGameTestServerMixin {
"Lnet/minecraft/world/level/levelgen/presets/WorldPresets;FLAT_ALL_DIMENSIONS:Lnet/minecraft/resources/ResourceKey;"))
private static ResourceKey<WorldPreset> sanctuary$selectActualPreset() {
if (Boolean.getBoolean("sanctuary.test.unified")) {
int generation = Integer.getInteger("sanctuary.test.generation", 23);
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && generation != 23) throw new IllegalArgumentException("Unsupported test generation");
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 23 ? "sanctuary" : "sanctuary_v" + generation));
int generation = Integer.getInteger("sanctuary.test.generation", 24);
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && generation != 23 && generation != 24) throw new IllegalArgumentException("Unsupported test generation");
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 24 ? "sanctuary" : "sanctuary_v" + generation));
}
if (Boolean.getBoolean("sanctuary.test.expansion")) return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("expansion_lab"));
int players = Integer.getInteger("sanctuary.test.players", 10);
@@ -76,6 +76,9 @@ public final class SanctuaryMod implements ModInitializer {
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v22"), SanctuaryBiomeSource.SELECTIVE22_CODEC);
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v23"), SanctuaryChunkGenerator.SECRETS23_CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v23"), SanctuaryBiomeSource.SECRETS23_CODEC);
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v24"), SanctuaryChunkGenerator.AERIAL24_CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v24"), SanctuaryBiomeSource.AERIAL24_CODEC);
fr.koka.sanctuary.worldgen.aerial.AerialStructures24.register();
fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.register();
fr.koka.sanctuary.worldgen.transit.TransitStructures21.register();
fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.register();
@@ -51,11 +51,11 @@ public final class SanctuaryWorldOptionsScreen extends Screen {
contents.addChild(sizeButton);
diameterHint = contents.addChild(new MultiLineTextWidget(diameterHint(), font)
.setMaxWidth(310).setCentered(true));
if (generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || generator.rootGeneration() == 23) {
if (generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) {
contents.addChild(CycleButton.builder((Boolean enabled) -> enabled ? CommonComponents.OPTION_ON : CommonComponents.OPTION_OFF, experimental)
.withValues(true, false).create(0, 0, 280, 20, Component.translatable("sanctuary.world_options.experimental"),
(button, value) -> experimental = value));
contents.addChild(new MultiLineTextWidget(Component.translatable(generator.rootGeneration() == 23 ? "sanctuary.world_options.secrets23_hint" : generator.rootGeneration() == 22 ? "sanctuary.world_options.heritage22_hint" : (generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || generator.rootGeneration() == 23) ? "sanctuary.world_options.heritage_hint" : "sanctuary.world_options.experimental_hint"), font)
contents.addChild(new MultiLineTextWidget(Component.translatable(generator.rootGeneration() == 24 ? "sanctuary.world_options.aerial24_hint" : generator.rootGeneration() == 23 ? "sanctuary.world_options.secrets23_hint" : generator.rootGeneration() == 22 ? "sanctuary.world_options.heritage22_hint" : (generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) ? "sanctuary.world_options.heritage_hint" : "sanctuary.world_options.experimental_hint"), font)
.setMaxWidth(310).setCentered(true));
}
layout.addToContents(contents);
@@ -129,6 +129,7 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
fr.koka.sanctuary.worldgen.transit.TransitStructures21.generate(this, registries, state, structures, chunk, templates, dimension);
fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.generate(this, registries, state, structures, chunk, templates, dimension);
fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.generate(this, registries, state, structures, chunk, templates, dimension);
fr.koka.sanctuary.worldgen.aerial.AerialStructures24.generate(this, registries, state, structures, chunk, templates, dimension);
ExpansionStructures.generate(this, registries, state, structures, chunk, templates, dimension);
}
@Override public void spawnOriginalMobs(WorldGenRegion region) {
@@ -45,7 +45,7 @@ public record ExpansionIsland(String id, String parent, String direction, String
public ExpansionIsland {
if (!id.matches("[a-z][a-z0-9_]{0,31}") || !parent.matches("[a-z][a-z0-9_]{0,31}")) throw new IllegalArgumentException("Invalid island identifier");
if (!DIRECTIONS.contains(direction) || !CLIMATES.contains(climate) || !reliefs(generation).contains(relief)) throw new IllegalArgumentException("Unknown island profile");
if (relief.equals("ocean") && (generation != 23 || id.equals("sanctuary") || diameter != 512 && diameter != 1024))
if (relief.equals("ocean") && ((generation != 23 && generation != 24) || id.equals("sanctuary") || diameter != 512 && diameter != 1024))
throw new IllegalArgumentException("Ocean relief requires a generation23 expansion of diameter512 or1024");
if (!((spatialGeneration(generation) ? DIAMETERS : LEGACY_DIAMETERS).contains(diameter) || (id.equals("sanctuary") && (diameter == 724 || (generation == 13 || spatialGeneration(generation)) && diameter == 1024)))) throw new IllegalArgumentException("Unsupported nominal diameter");
if (Math.abs((long) centerX) > 100_000 || Math.abs((long) centerZ) > 100_000 || (centerX & 15) != 0 || (centerZ & 15) != 0) throw new IllegalArgumentException("Invalid island centre");
@@ -59,7 +59,7 @@ public record ExpansionIsland(String id, String parent, String direction, String
var priorities = new java.util.TreeSet<>(structurePriorities);
if (priorities.size() != structurePriorities.size()) throw new IllegalArgumentException("Duplicate structure priority family");
structurePriorities = List.copyOf(priorities);
if (!structurePriorities.isEmpty() && (generation != 23 || id.equals("sanctuary")))
if (!structurePriorities.isEmpty() && ((generation != 23 && generation != 24) || id.equals("sanctuary")))
throw new IllegalArgumentException("Structure priorities are only available for generation23 expansions");
}
@@ -70,8 +70,8 @@ public record ExpansionIsland(String id, String parent, String direction, String
return new ExpansionIsland("sanctuary", "sanctuary", "north", "temperate", diameter, 0, 0, seed, "natural", "ready", generation);
}
public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23; }
public static List<String> reliefs(int generation) { return generation == 23 ? RELIEFS23 : RELIEFS; }
public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24); }
public static List<String> reliefs(int generation) { return (generation == 23 || generation == 24) ? RELIEFS23 : RELIEFS; }
public boolean origin() { return id.equals("sanctuary"); }
public double referenceDiameter() { return origin() && diameter == 724 ? Math.sqrt(2) * 512 : 512; }
public double horizontalScale() { return origin() ? 1.0 : diameter / referenceDiameter(); }
@@ -10,6 +10,8 @@ import com.google.gson.JsonPrimitive;
import com.google.gson.Strictness;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import fr.koka.sanctuary.worldgen.aerial.AerialPlan24;
import fr.koka.sanctuary.worldgen.aerial.AerialStore24;
import java.io.IOException;
import java.io.StringReader;
import java.math.BigDecimal;
@@ -41,6 +43,8 @@ public final class ExpansionJournal {
private final int rootDiameter;
private final int rootGeneration;
private final DirectorySync directorySync;
private String aerialToken = "";
private AerialPlan24 aerialPlan24 = AerialPlan24.EMPTY;
private List<ExpansionIsland> islands;
private boolean failed;
@@ -52,7 +56,17 @@ public final class ExpansionJournal {
directorySyncFor(directory.getFileSystem().getSeparator(), ExpansionJournal::forceDirectory));
}
public ExpansionJournal(Path directory, long seed, int rootDiameter, int rootGeneration, String initialAerialToken) throws IOException {
this(directory, seed, rootDiameter, rootGeneration,
directorySyncFor(directory.getFileSystem().getSeparator(), ExpansionJournal::forceDirectory), initialAerialToken);
}
ExpansionJournal(Path directory, long seed, int rootDiameter, int rootGeneration, DirectorySync directorySync) throws IOException {
this(directory, seed, rootDiameter, rootGeneration, directorySync, "");
}
private ExpansionJournal(Path directory, long seed, int rootDiameter, int rootGeneration, DirectorySync directorySync,
String initialAerialToken) throws IOException {
if (rootGeneration != 12 && rootGeneration != 13 && !ExpansionIsland.spatialGeneration(rootGeneration)) throw new IOException("Unsupported root generation");
if (rootDiameter != 512 && rootDiameter != 724 && !((rootGeneration == 13 || ExpansionIsland.spatialGeneration(rootGeneration)) && rootDiameter == 1024))
throw new IOException("Unsupported expansion root diameter");
@@ -62,10 +76,13 @@ public final class ExpansionJournal {
this.file = this.directory.resolve(FILE_NAME);
this.seed = seed;
this.rootDiameter = rootDiameter;
if (rootGeneration == 24 && !initialAerialToken.isEmpty()) bindAerial(initialAerialToken);
else if (rootGeneration != 24 && !initialAerialToken.isEmpty()) throw new IOException("Historical journals cannot bind aerial sites");
if (Files.exists(this.directory, LinkOption.NOFOLLOW_LINKS)) {
if (!Files.isDirectory(this.directory, LinkOption.NOFOLLOW_LINKS)) throw new IOException("Expansion journal path is not a directory");
this.islands = read();
} else {
if (rootGeneration == 24 && aerialToken.isEmpty()) throw new IOException("New generation24 journal requires its immutable aerial manifest");
// Creating the directory is the bootstrap marker. An interrupted bootstrap requires repair.
Files.createDirectories(this.directory.getParent());
Files.createDirectory(this.directory);
@@ -75,6 +92,8 @@ public final class ExpansionJournal {
}
public synchronized List<ExpansionIsland> islands() { return islands; }
public synchronized AerialPlan24 aerialPlan24() { return aerialPlan24; }
public synchronized String aerialToken() { return aerialToken; }
/** False after uncertain persistence or external damage; only a successful reload can recover. */
public synchronized boolean healthy() { return !failed; }
@@ -150,17 +169,20 @@ public final class ExpansionJournal {
reader.setStrictness(Strictness.STRICT);
JsonObject root = readJson(reader, 0).getAsJsonObject();
if (reader.peek() != JsonToken.END_DOCUMENT) throw new IOException("Trailing data in expansion journal");
requireFields(root, JOURNAL_FIELDS);
if (integer(root, "schema") != 1 || number(root, "world_seed").longValueExact() != seed || integer(root, "root_diameter") != rootDiameter) {
var journalFields = new java.util.HashSet<>(JOURNAL_FIELDS);
if (rootGeneration == 24) journalFields.add("aerial_plan");
requireFields(root, journalFields);
if (integer(root, "schema") != (rootGeneration == 24 ? 2 : 1) || number(root, "world_seed").longValueExact() != seed || integer(root, "root_diameter") != rootDiameter) {
throw new IOException("Expansion journal schema, world seed or root diameter does not match");
}
if (rootGeneration == 24) bindAerial(string(root, "aerial_plan"));
JsonArray entries = root.getAsJsonArray("islands");
List<ExpansionIsland> decoded = new ArrayList<>();
for (JsonElement element : entries) {
JsonObject entry = element.getAsJsonObject();
var fields = new java.util.HashSet<>(ISLAND_FIELDS);
if (ExpansionIsland.spatialGeneration(rootGeneration) && entry.has("reuse_token")) fields.add("reuse_token");
if (rootGeneration == 23 && entry.has("structure_priorities")) fields.add("structure_priorities");
if ((rootGeneration == 23 || rootGeneration == 24) && entry.has("structure_priorities")) fields.add("structure_priorities");
requireFields(entry, fields);
decoded.add(new ExpansionIsland(string(entry, "id"), string(entry, "parent"), string(entry, "direction"), string(entry, "climate"),
integer(entry, "diameter"), integer(entry, "center_x"), integer(entry, "center_z"), number(entry, "seed").longValueExact(),
@@ -191,6 +213,7 @@ public final class ExpansionJournal {
long dx = (long) island.centerX() - parent.centerX(), dz = (long) island.centerZ() - parent.centerZ();
if (!(ExpansionIsland.spatialGeneration(island.generation()) ? ExpansionSpatialPlacement14.followsDirection(island.direction(), dx, dz) : followsDirection(island.direction(), dx, dz))) throw new IOException("Expansion position does not follow its direction: " + island.id());
for (ExpansionIsland other : previous.values()) if (island.conflicts(other)) throw new IOException("Expansion reservations overlap: " + island.id() + " / " + other.id());
if (rootGeneration == 24 && aerialPlan24.conflicts(island)) throw new IOException("Expansion reservation overlaps a permanent aerial site: " + island.id());
previous.put(island.id(), island);
}
return checked;
@@ -212,7 +235,8 @@ public final class ExpansionJournal {
private JsonObject encode(List<ExpansionIsland> entries) {
JsonObject root = new JsonObject();
root.addProperty("schema", 1); root.addProperty("world_seed", seed); root.addProperty("root_diameter", rootDiameter);
root.addProperty("schema", rootGeneration == 24 ? 2 : 1); root.addProperty("world_seed", seed); root.addProperty("root_diameter", rootDiameter);
if (rootGeneration == 24) root.addProperty("aerial_plan", aerialToken);
JsonArray array = new JsonArray();
for (ExpansionIsland island : entries) {
JsonObject entry = new JsonObject();
@@ -232,6 +256,14 @@ public final class ExpansionJournal {
return root;
}
private void bindAerial(String token) throws IOException {
if (!aerialToken.isEmpty() && !aerialToken.equals(token)) throw new IOException("Aerial manifest identity changed outside this journal");
var stored = AerialStore24.load(AerialStore24.directory(directory), token, seed, rootDiameter);
if (!aerialToken.isEmpty() && !aerialPlan24.sites().equals(stored.plan().sites())) throw new IOException("Aerial layout changed outside this journal");
aerialToken = token;
aerialPlan24 = stored.plan();
}
private static JsonElement readJson(JsonReader reader, int depth) throws IOException {
if (depth > 4) throw new IOException("Expansion journal nesting exceeds its schema");
return switch (reader.peek()) {
@@ -24,14 +24,14 @@ public final class ExpansionOcean23 {
}
private ExpansionOcean23() {}
public static boolean active(ExpansionIsland island) {
return island != null && island.generation() == 23 && !island.origin() && island.relief().equals("ocean");
return island != null && (island.generation() == 23 || island.generation() == 24) && !island.origin() && island.relief().equals("ocean");
}
public static int seaLevel(ExpansionIsland island) {
return (int) Math.round(ExpansionRelief.worldY(island, 220));
}
/** Quick expansions retain their size/climate draw; only unspecified relief participates. */
public static String quickRelief(long seed, String id, int diameter, String climate, int generation) {
if (generation != 23 || diameter < 512 || climate.equals("lost_city")) return "natural";
if ((generation != 23 && generation != 24) || diameter < 512 || climate.equals("lost_city")) return "natural";
return Long.remainderUnsigned(ExpansionIsland.deriveSeed(seed ^ 0x0CEA231L, id), 3) == 0 ? "ocean" : "natural";
}
public static Column column(ExpansionIsland island, int x, int z) {
@@ -1,6 +1,9 @@
package fr.koka.sanctuary.expansion;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import fr.koka.sanctuary.worldgen.aerial.AerialPlanner24;
import fr.koka.sanctuary.worldgen.aerial.AerialStore24;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
@@ -37,7 +40,19 @@ public final class ExpansionRuntime {
if (server.getWorldData().overworldData().isInitialized()
&& !java.nio.file.Files.isDirectory(directory, java.nio.file.LinkOption.NOFOLLOW_LINKS))
throw new IOException("Initialized laboratory world is missing its expansion journal directory");
var journal = new ExpansionJournal(directory, level.getSeed(), generator.initialDiameter(), generator.rootGeneration());
String initialAerialToken = "";
if (generator.rootGeneration() == 24 && !java.nio.file.Files.exists(directory, java.nio.file.LinkOption.NOFOLLOW_LINKS)) {
if (!(generator instanceof SanctuaryChunkGenerator sanctuary)) throw new IOException("Aerial world requires the Sanctuary generator");
initialAerialToken = AerialStore24.create(AerialStore24.directory(directory), level.getSeed(), generator.initialDiameter(),
() -> AerialPlanner24.forWorld(level, sanctuary)).token();
}
var journal = generator.rootGeneration() == 24
? new ExpansionJournal(directory, level.getSeed(), generator.initialDiameter(), 24, initialAerialToken)
: new ExpansionJournal(directory, level.getSeed(), generator.initialDiameter(), generator.rootGeneration());
if (generator.rootGeneration() == 24) {
if (!(generator instanceof SanctuaryChunkGenerator sanctuary)) throw new IOException("Aerial world requires the Sanctuary generator");
sanctuary.setAerialPlan24(journal.aerialPlan24());
}
ExpansionVoidReuse.open(level, generator, journal.islands());
generator.initialize(level, journal.islands());
var session = new Session(level, generator, journal);
@@ -87,7 +102,7 @@ public final class ExpansionRuntime {
public ExpansionIsland candidate(String id, String parentId, String direction, String climate, int diameter, String relief,
int minimumDistance) {
return ExpansionPlacement.candidates(level.getSeed(), islands(), id, parentId, direction, climate, diameter, relief, minimumDistance)
.stream().findFirst().orElseThrow(() -> new IllegalArgumentException("no_free_space"));
.stream().filter(value -> !aerialConflicts(value)).findFirst().orElseThrow(() -> new IllegalArgumentException("no_free_space"));
}
public ExpansionIsland create(String id, String parent, String direction, String climate, int diameter, String relief) throws IOException {
return create(id, parent, direction, climate, diameter, relief, 0);
@@ -109,15 +124,15 @@ public final class ExpansionRuntime {
if (islands().size() >= 64) throw new IllegalArgumentException("island_limit");
// Capture one immutable decision before any candidate is probed or certified. A resumed
// reservation uses its recorded profile even if the world's census has since changed.
var priorities = generator.rootGeneration() == 23
var priorities = generator.rootGeneration() == 23 || generator.rootGeneration() == 24
? fr.koka.sanctuary.knowledge.structures.StructureCensusService.priorities(level) : List.<String>of();
var candidates = ExpansionPlacement.candidates(level.getSeed(), islands(), id, parent, direction, climate, diameter, relief, minimumDistance)
.stream().map(value -> value.withStructurePriorities(priorities)).toList();
.stream().filter(value -> !aerialConflicts(value)).map(value -> value.withStructurePriorities(priorities)).toList();
boolean nativeRefused = false;
for (var value : candidates) {
ExpansionAdmission.checkDeadline(deadline);
try {
if (value.generation() == 23) ExpansionStructures23.verifyExpansion(level, value, deadline);
if (value.generation() == 23 || value.generation() == 24) ExpansionStructures23.verifyExpansion(level, value, deadline);
var inspection = ExpansionIsland.spatialGeneration(value.generation()) ? ExpansionVoidReuse.inspect(level, value, deadline) : null;
if (inspection == null) ExpansionAdmission.verify(level, value, deadline);
if (probe(value) == null) throw new IllegalArgumentException("no_ground");
@@ -143,6 +158,10 @@ public final class ExpansionRuntime {
ExpansionAdmission.checkDeadline(deadline);
throw new IllegalArgumentException(nativeRefused ? "native_structure_unavailable" : "no_free_space");
}
private boolean aerialConflicts(ExpansionIsland island) {
return generator.rootGeneration() == 24 && generator instanceof SanctuaryChunkGenerator sanctuary
&& sanctuary.aerialPlan24().conflicts(island);
}
public void resume(String id) {
requireHealthyJournal();
var island = island(id);
@@ -44,7 +44,7 @@ public final class ExpansionStructures {
public static void generate(ExpansionChunkGenerator generator, RegistryAccess registries,
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> level) {
if (generator.rootGeneration() == 23) {
if ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) {
ExpansionStructures23.generate(generator, registries, state, manager, chunk, templates, level);
return;
}
@@ -61,6 +61,7 @@ public final class ExpansionStructures23 {
public static List<Entry> catalogue() { return ExpansionStructureCatalog23.entries(); }
public static String family(String id) { return ExpansionStructureCatalog23.family(id); }
public static boolean isPlacing() { return NativeStructurePlacement23.seaLevel() != null; }
private static boolean nativeGeneration(int generation) { return generation == 23 || generation == 24; }
public record Candidate(ChunkPos chunk, long roll) {}
public record Accepted(Holder<Structure> structure, StructureStart start) {}
public static final class NoSuitableStructureException extends IllegalArgumentException {
@@ -90,7 +91,7 @@ public final class ExpansionStructures23 {
StructureTemplateManager templates, ResourceKey<Level> dimension) {
if (!Level.OVERWORLD.equals(dimension) || !manager.shouldGenerateStructures()) return;
ExpansionIsland island = generator.islandAt(chunk.getPos().getMiddleBlockX(), chunk.getPos().getMiddleBlockZ());
if (island == null || island.generation() != 23) return;
if (island == null || !nativeGeneration(island.generation())) return;
Candidate candidate = candidates(island).stream().filter(c -> c.chunk().equals(chunk.getPos())).findFirst().orElse(null);
if (candidate == null) return;
for (var old : chunk.getAllStarts().values()) {
@@ -110,7 +111,7 @@ public final class ExpansionStructures23 {
* No active source snapshot is mutated and no real world chunk is requested.
*/
public static void verifyExpansion(ServerLevel level, ExpansionIsland island, long deadlineNanos) {
if (island.generation() != 23 || island.origin() || !level.structureManager().shouldGenerateStructures()) return;
if (!nativeGeneration(island.generation()) || island.origin() || !level.structureManager().shouldGenerateStructures()) return;
if (!(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator host))
throw new IllegalArgumentException("Native structure preflight requires a Sanctuary world");
ExpansionBiomeSource isolated = isolatedSource(host, island, level.registryAccess());
@@ -384,7 +385,7 @@ public final class ExpansionStructures23 {
public static Pair<BlockPos, Holder<Structure>> findNearest(ServerLevel level, HolderSet<Structure> wanted,
BlockPos origin, int radius, boolean skipReferenced) {
if (!(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator generator)
|| generator.rootGeneration() != 23 || !level.structureManager().shouldGenerateStructures()) return null;
|| !nativeGeneration(generator.rootGeneration()) || !level.structureManager().shouldGenerateStructures()) return null;
boolean nativeRequested = wanted.stream().anyMatch(h -> {
var e = ExpansionStructureCatalog23.entry(h.unwrapKey().orElseThrow().identifier().toString());
return e != null && e.availability() == Availability.OVERWORLD;
@@ -392,7 +393,7 @@ public final class ExpansionStructures23 {
boolean sanctuaryRequested = wanted.stream().anyMatch(h -> h.unwrapKey().orElseThrow().identifier().getNamespace().equals("sanctuary"));
var candidates = new ArrayList<Candidate>();
for (ExpansionIsland island : generator.source().islands()) {
if (!nativeRequested || island.generation() != 23 || !island.status().equals("ready")) continue;
if (!nativeRequested || !nativeGeneration(island.generation()) || !island.status().equals("ready")) continue;
for (Candidate candidate : candidates(island)) {
if (Math.abs((long) candidate.chunk().x() - (origin.getX() >> 4)) <= (long) radius * CELL_CHUNKS
&& Math.abs((long) candidate.chunk().z() - (origin.getZ() >> 4)) <= (long) radius * CELL_CHUNKS) candidates.add(candidate);
@@ -429,7 +430,7 @@ public final class ExpansionStructures23 {
private static void addSanctuaryCandidates(SanctuaryChunkGenerator generator, List<Candidate> positions) {
for (ExpansionIsland island : generator.source().islands()) {
if (island.generation() != 23 || !island.status().equals("ready")) continue;
if (!nativeGeneration(island.generation()) || !island.status().equals("ready")) continue;
for (var district : generator.lostCityPlan(island).districts()) addPosition(positions, district.startChunkX(), district.startChunkZ());
}
generator.heritagePlan19().sites().forEach(s -> addPosition(positions, s.startChunkX(), s.startChunkZ()));
@@ -439,6 +440,8 @@ public final class ExpansionStructures23 {
generator.undergroundHall22().networks().forEach(s -> addPosition(positions, s.startChunkX(), s.startChunkZ()));
var mineral = generator.mineralSanctuary23();
if (!mineral.isEmpty()) addPosition(positions, mineral.chamber().startChunkX(), mineral.chamber().startChunkZ());
if (generator.rootGeneration() == 24) generator.aerialPlan24().sites()
.forEach(site -> addPosition(positions, site.startChunkX(), site.startChunkZ()));
}
private static void addPosition(List<Candidate> positions, int x, int z) { positions.add(new Candidate(new ChunkPos(x, z), 0)); }
private static BlockPos locator(StructureStart start, BlockPos origin) {
@@ -536,7 +539,7 @@ public final class ExpansionStructures23 {
NativeView(ExpansionChunkGenerator host, ExpansionIsland island, ExpansionBiomeSource source, RegistryAccess registries) {
super(source, host.initialDiameter(), true); this.host = host; this.island = island; this.registries = registries;
}
@Override public int rootGeneration() { return 23; }
@Override public int rootGeneration() { return island.generation(); }
@Override public Holder<Biome> structureBiome(Holder<Biome> biome) { return host.structureBiome(biome); }
@Override public int getSeaLevel() { return ExpansionOcean23.active(island) ? ExpansionOcean23.seaLevel(island) : 192; }
@Override public RandomState randomState(ExpansionIsland candidate) { return island.origin() ? host.randomState(candidate) : super.randomState(candidate); }
@@ -86,6 +86,9 @@ public final class ExpansionVoidReuse {
if (!(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator generator)
|| !ExpansionIsland.spatialGeneration(generator.rootGeneration()) || island.generation() != generator.rootGeneration() || island.origin())
throw new IOException("Void reuse is restricted to a matching generation-14/15 expansion");
if (generator.rootGeneration() == 24 && generator instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator sanctuary
&& sanctuary.aerialPlan24().conflicts(island))
throw new ExpansionAdmission.OccupiedException("aerial_reserved: expansion overlaps a permanent initial site");
List<ChunkPos> positions = positions(island);
var entityStorage = entityRegion(level);
var poiStorage = ((ExpansionVoidPoiStorageAccess) level.getPoiManager()).sanctuary$simpleRegionStorage();
@@ -173,6 +176,9 @@ public final class ExpansionVoidReuse {
public static void open(ServerLevel level, ExpansionChunkGenerator generator, List<ExpansionIsland> islands) throws IOException {
requireServerThread(level); close(level);
if (!ExpansionIsland.spatialGeneration(generator.rootGeneration())) return;
if (generator.rootGeneration() == 24 && generator instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator sanctuary)
for (var island : islands) if (sanctuary.aerialPlan24().conflicts(island))
throw new IOException("Saved expansion overlaps a permanent aerial site: " + island.id());
var state = new State();
for (var island : islands) if (!island.reuseToken().isEmpty()) add(state, loadManifest(level, island));
// Validate every transaction before writing even an EMPTY placeholder or publishing any island.
@@ -16,6 +16,12 @@ public final class StructureCensusCommands {
public static void register() {
CommandRegistrationCallback.EVENT.register((dispatcher, registries, environment) -> {
var structures = Commands.literal("structures").executes(context -> report(context.getSource(), null))
.then(Commands.literal("aerial").executes(context -> aerial(context.getSource(), null))
.then(Commands.argument("site", StringArgumentType.word())
.suggests((context, builder) -> SharedSuggestionProvider.suggest(
StructureCensusService.aerialSites(context.getSource().getLevel()).stream()
.map(StructureCensusService.AerialSite::id), builder))
.executes(context -> aerial(context.getSource(), StringArgumentType.getString(context, "site")))))
.then(Commands.literal("island").then(Commands.argument("id", StringArgumentType.word())
.suggests((context, builder) -> SharedSuggestionProvider.suggest(
StructureCensusService.snapshot(context.getSource().getServer()).regions().stream()
@@ -51,6 +57,23 @@ public final class StructureCensusCommands {
send(source, "help");
return 1;
}
private static int aerial(CommandSourceStack source, String selected) {
var sites = StructureCensusService.aerialSites(source.getLevel()).stream()
.filter(site -> selected == null || site.id().equals(selected)).toList();
if (sites.isEmpty()) {
source.sendFailure(selected == null ? Component.translatable(TEXT + "aerial.none")
: Component.translatable(TEXT + "aerial.unknown_site", selected));
return 0;
}
for (var site : sites) {
var coverage = site.coverage();
send(source, "aerial.site", site.id(), site.structure(),
Component.translatable(TEXT + "aerial.status." + site.status().name().toLowerCase(java.util.Locale.ROOT)),
coverage.startsChunks(), coverage.expectedChunks(), coverage.featurePassChunks(), coverage.expectedChunks(),
coverage.fullChunks(), coverage.expectedChunks());
}
return 1;
}
private static int dimension(CommandSourceStack source, String id) {
var snapshot = StructureCensusService.snapshot(source.getServer());
if (!snapshot.available()) { source.sendFailure(Component.translatable(TEXT + "unavailable")); return 0; }
@@ -83,7 +83,8 @@ public final class StructureCensusService {
public static List<String> priorities(ServerLevel level) {
assertServerThread(level.getServer());
if (!level.dimension().equals(Level.OVERWORLD) || !level.structureManager().shouldGenerateStructures()
|| !(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator generator) || generator.rootGeneration() != 23)
|| !(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator generator)
|| generator.rootGeneration() != 23 && generator.rootGeneration() != 24)
return List.of();
return confirmedPriorities(snapshot(level.getServer()), level.dimension().identifier().toString());
}
@@ -156,7 +157,7 @@ public final class StructureCensusService {
}).toList();
if (matching.isEmpty()) return Presence.OTHER_DIMENSION;
// This is an explicit generation23 habitat requirement, not an inferred lack of biomes.
if (island != null && island.generation() == 23 && !island.relief().equals("ocean")
if (island != null && (island.generation() == 23 || island.generation() == 24) && !island.relief().equals("ocean")
&& matching.stream().allMatch(entry -> entry.habitat() == ExpansionStructureCatalog23.Habitat.OCEAN)) return Presence.INCOMPATIBLE;
return null;
}
@@ -172,6 +173,31 @@ public final class StructureCensusService {
.map(ExpansionStructureCatalog23.Entry::family).findFirst().orElse(id);
}
/** Planned aerial sites have their own finite scopes. Planning is never recorded as
* generated evidence, and these sites never enter the vanilla absence-priority pool. */
public static List<AerialSite> aerialSites(ServerLevel level) {
assertServerThread(level.getServer());
if (!level.dimension().equals(Level.OVERWORLD) || !level.structureManager().shouldGenerateStructures()
|| !(level.getChunkSource().getGenerator() instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 24) return List.of();
var session = current(level.getServer());
var evidence = session.memory.snapshot();
String dimension = level.dimension().identifier().toString();
var result = new ArrayList<AerialSite>();
for (var site : generator.aerialPlan24().sites()) {
String structure = fr.koka.sanctuary.worldgen.aerial.AerialStructures24.structureId(site);
var scope = site.claimedChunks().stream().map(chunk -> new ChunkKey(dimension, chunk.x(), chunk.z())).toList();
var coverage = evidence.coverage(scope);
var key = new SiteKey(dimension, structure, site.startChunkX(), 0, site.startChunkZ(), false);
var observed = evidence.sites().get(key);
AerialStatus status = session.failed ? AerialStatus.UNKNOWN : observed != null
? evidence.generated(observed) ? AerialStatus.GENERATED : AerialStatus.ACCEPTED
: coverage.startsComplete() ? AerialStatus.MISSING : AerialStatus.PLANNED;
result.add(new AerialSite(site.id(), structure, dimension, coverage, status));
}
return List.copyOf(result);
}
/** Finite ownership is derived from the immutable expansion journal descriptors,
* rather than duplicated in the sidecar or inferred from currently loaded chunks. */
public static List<ChunkKey> scope(String dimension, ExpansionIsland island) {
@@ -222,6 +248,8 @@ public final class StructureCensusService {
Session(StructureCensusStore store) { this.store = store; }
}
public enum Presence { PRESENT, ABSENT, UNKNOWN, INCOMPATIBLE, OTHER_DIMENSION }
public enum AerialStatus { PLANNED, ACCEPTED, GENERATED, MISSING, UNKNOWN }
public record AerialSite(String id, String structure, String dimension, Coverage coverage, AerialStatus status) {}
public record FamilyCount(int accepted, int generated, Presence presence) {}
public record Region(String dimension, String island, int generation, String status, Coverage coverage, Map<String, FamilyCount> families) {
public Region { families = Map.copyOf(families); }
@@ -20,6 +20,11 @@ public abstract class UnifiedDecorationProtectionMixin {
var region = (WorldGenRegion) (Object) this;
if (!(region.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| !fr.koka.sanctuary.expansion.ExpansionIsland.spatialGeneration(generator.rootGeneration())) return;
// Aerial pieces write in genuine void chunks, within their own immutable site and current chunk only.
if (generator.rootGeneration() == 24 && fr.koka.sanctuary.worldgen.aerial.AerialStructures24.isPlacing()) {
if (!fr.koka.sanctuary.worldgen.aerial.AerialStructures24.mayWrite(pos)) callback.setReturnValue(false);
return; // Vanilla still applies its write-zone and build-height checks.
}
var center = region.getCenter();
var owner = generator.islandForChunk(center.x(), center.z());
var destination = generator.islandForChunk(pos.getX() >> 4, pos.getZ() >> 4);
@@ -39,7 +44,11 @@ public abstract class UnifiedDecorationProtectionMixin {
if (!(region.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)) return;
// Vanilla rejects writes outside its region before reading the destination chunk.
if (!region.isWithinWriteZone(pos)) return;
if (generator.rootGeneration() == 23) {
if (generator.rootGeneration() == 24 && fr.koka.sanctuary.worldgen.aerial.AerialStructures24.isPlacing()) {
if (!fr.koka.sanctuary.worldgen.aerial.AerialStructures24.mayWrite(pos)) callback.setReturnValue(false);
return;
}
if ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) {
if (fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.isPlacing()) return;
var island = generator.islandAt(pos.getX(), pos.getZ());
if (fr.koka.sanctuary.expansion.ExpansionOcean23.active(island)
@@ -55,7 +64,7 @@ public abstract class UnifiedDecorationProtectionMixin {
if (state.is(Blocks.SUGAR_CANE)) return;
// Generation16 reserves its architectural volume before decoration. Only its own
// native pieces may replace water there; published generations retain the old rule.
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || generator.rootGeneration() == 23)
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))
&& fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
&& generator.protectsCityDecoration(pos)) return;
if (generator.protectsDecoration(pos) && !region.getBlockState(pos).equals(state)) {
@@ -62,6 +62,10 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 23)));
public static final MapCodec<SanctuaryBiomeSource> AERIAL24_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 24)));
private final List<Holder<Biome>> allBiomes;
private final PopulationIslandBiomeSource population;
private final boolean cities;
@@ -77,7 +81,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
this.generation = generation;
this.cities = ExpansionIsland.spatialGeneration(generation);
allBiomes = List.copyOf(biomes);
if (generation == 23) 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,
net.minecraft.resources.Identifier.withDefaultNamespace(name)));
cityBiome = cities ? required(ResourceKey.create(net.minecraft.core.registries.Registries.BIOME,
@@ -98,7 +102,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
cityPlans = java.util.Map.copyOf(plans);
}
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 23 ? SECRETS23_CODEC : generation == 22 ? SELECTIVE22_CODEC : generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; }
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 24 ? AERIAL24_CODEC : generation == 23 ? SECRETS23_CODEC : generation == 22 ? SELECTIVE22_CODEC : generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; }
@Override public BiomeResolver createResolver(Climate.Sampler sampler) {
BiomeResolver woodlands = population.createResolver(sampler);
@@ -75,6 +75,41 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 23, experimental)));
public static final MapCodec<SanctuaryChunkGenerator> AERIAL24_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
SanctuaryBiomeSource.AERIAL24_CODEC.fieldOf("biome_source").forGetter(SanctuaryChunkGenerator::source),
DIAMETER_CODEC.optionalFieldOf("initial_diameter", 724).forGetter(SanctuaryChunkGenerator::initialDiameter),
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 24, experimental)));
private volatile fr.koka.sanctuary.worldgen.aerial.AerialPlan24 aerialPlan24 = fr.koka.sanctuary.worldgen.aerial.AerialPlan24.EMPTY;
public fr.koka.sanctuary.worldgen.aerial.AerialPlan24 aerialPlan24() { return aerialPlan24; }
public void setAerialPlan24(fr.koka.sanctuary.worldgen.aerial.AerialPlan24 plan) {
if (generation != 24) throw new IllegalStateException("Aerial plans require generation24");
aerialPlan24 = java.util.Objects.requireNonNull(plan);
}
/** Plan root secrets before certifying aerial shore viewpoints, without requesting chunks. */
public void prepareAerialSampling24(ServerLevel level) {
if (generation != 24) throw new IllegalStateException("Aerial sampling requires generation24");
heightAccessor = level;
cityStructuresEnabled = level.structureManager().shouldGenerateStructures();
var root = java.util.List.of(ExpansionIsland.origin(initialDiameter(), level.getSeed(), 24));
super.initialize(level, root);
context(root.getFirst());
// Same order as initialize. Prepared plans are reused after the manifest is persisted.
prepareCities(root);
prepareTransit(root);
prepareUndergroundHall22(root);
prepareMineral23(root);
prepareRuins(root);
prepareHeritage(root);
prepareTransit(root);
}
public net.minecraft.world.level.NoiseColumn aerialBaseColumn24(int x, int z) {
var current = originContext;
if (current == null || heightAccessor == null) throw new IllegalStateException("Aerial sampling before world attachment");
return current.generator().getBaseColumn(x, z, heightAccessor, current.random());
}
private volatile fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlan23 mineralSanctuary23 = fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlan23.EMPTY;
private boolean mineralPrepared23;
public fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlan23 mineralSanctuary23() { return mineralSanctuary23; }
@@ -130,12 +165,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
}
private SanctuaryChunkGenerator(SanctuaryBiomeSource source, int initialDiameter, int generation, boolean experimental) {
super(source, initialDiameter, true);
this.experimentalStructures = (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && experimental;
this.experimentalStructures = (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && experimental;
this.generation = generation;
if (generation != source.generation()) throw new IllegalArgumentException("Sanctuary generator and biome source versions must agree");
}
@Override public SanctuaryBiomeSource source() { return (SanctuaryBiomeSource) super.source(); }
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 23 ? SECRETS23_CODEC : generation == 22 ? SELECTIVE22_CODEC : generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; }
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 24 ? AERIAL24_CODEC : generation == 23 ? SECRETS23_CODEC : generation == 22 ? SELECTIVE22_CODEC : generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; }
public SanctuaryChunkGenerator withInitialDiameter(int diameter) {
return new SanctuaryChunkGenerator(source().copy(), diameter, generation, experimentalStructures);
}
@@ -146,11 +181,15 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
public fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17() { return ruinsPlan17; }
public fr.koka.sanctuary.worldgen.heritage.HeritagePlan19 heritagePlan19() { return heritagePlan19; }
public boolean intersectsExperimentalStructures(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
return generation == 23 && mineralSanctuary23.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && heritagePlan19.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 21 || generation == 22 || generation == 23) && transitPlan21.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 22 || generation == 23) && undergroundHallProtection22.intersects(minX, minY, minZ, maxX, maxY, maxZ);
if (generation == 24) for (var site : aerialPlan24.sites()) {
if (minX <= site.maxX() + 1 && maxX >= site.minX() - 1 && minZ <= site.maxZ() + 1 && maxZ >= site.minZ() - 1
&& minY <= site.maxY() + 1 && maxY >= (site.waterfall() ? 0 : site.minY() - 1)) return true;
}
return (generation == 23 || generation == 24) && mineralSanctuary23.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && heritagePlan19.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && transitPlan21.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 22 || (generation == 23 || generation == 24)) && undergroundHallProtection22.intersects(minX, minY, minZ, maxX, maxY, maxZ);
}
public IslandCapacity capacity() {
return switch (initialDiameter()) {
@@ -185,7 +224,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
// Publish one root context before any generation workers use it.
context(islands.getFirst());
prepareCities(islands);
if (generation == 22 || generation == 23) { prepareTransit(islands); prepareUndergroundHall22(islands); }
if (generation == 22 || (generation == 23 || generation == 24)) { prepareTransit(islands); prepareUndergroundHall22(islands); }
prepareMineral23(islands);
prepareRuins(islands);
prepareHeritage(islands);
@@ -194,7 +233,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
@Override public void setIslands(List<ExpansionIsland> islands) {
// Complete and publish city geometry before workers can observe the new owner list.
prepareCities(islands);
if (generation == 22 || generation == 23) { prepareTransit(islands); prepareUndergroundHall22(islands); }
if (generation == 22 || (generation == 23 || generation == 24)) { prepareTransit(islands); prepareUndergroundHall22(islands); }
prepareMineral23(islands);
prepareRuins(islands);
prepareHeritage(islands);
@@ -209,7 +248,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
source().setCityPlans(java.util.Map.of());
return;
}
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && !cityStructuresEnabled) {
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && !cityStructuresEnabled) {
cityProtection16 = fr.koka.sanctuary.worldgen.city.LostCityProtection16.EMPTY;
activeCityPlans = java.util.Map.of();
source().setCityPlans(java.util.Map.of());
@@ -225,7 +264,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
}
cityProtection15 = protection.build();
}
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) {
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) {
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
for (var island : islands) {
protection.add(published.get(island.id()), undergroundPlan16(island));
@@ -240,13 +279,13 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
source().setCityPlans(published);
}
private void prepareRuins(List<ExpansionIsland> islands) {
if ((generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && generation != 23) || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return;
if ((generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24)) || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var terrain = context(root);
var sampled = new fr.koka.sanctuary.worldgen.ruins.RuinsTerrain17(root, terrain.generator(), terrain.random(),
heightAccessor, pos -> protectsCore22(pos) || protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()));
long start = System.nanoTime();
var plan = generation == 23
var plan = (generation == 23 || generation == 24)
? fr.koka.sanctuary.worldgen.ruins.RuinsPlanner23.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
: fr.koka.sanctuary.worldgen.ruins.RuinsPlanner17.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
ruinsPlan17 = plan;
@@ -255,16 +294,16 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
root.seed(), plan.networks().size(), plan.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000);
}
private void prepareHeritage(List<ExpansionIsland> islands) {
if ((generation != 19 && generation != 20 && generation != 21 && generation != 22 && generation != 23) || !experimentalStructures || !cityStructuresEnabled || heritagePrepared) return;
if ((generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24)) || !experimentalStructures || !cityStructuresEnabled || heritagePrepared) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var terrain = context(root);
if (generation == 21 || generation == 22 || generation == 23) {
if (generation == 21 || generation == 22 || (generation == 23 || generation == 24)) {
var sampled = new fr.koka.sanctuary.worldgen.heritage.HeritageTerrain21(root, terrain.generator(), terrain.random(),
heightAccessor, pos -> protectsCore22(pos) || protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()));
long start = System.nanoTime();
sampled.discoverLandmarks(lostCityPlan(root), ruinsPlan17);
var plan = generation == 23
var plan = (generation == 23 || generation == 24)
? fr.koka.sanctuary.worldgen.heritage.HeritagePlanner23.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
: generation == 22
? fr.koka.sanctuary.worldgen.heritage.HeritagePlanner22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
@@ -289,7 +328,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
(System.nanoTime() - start) / 1_000_000, plan.diagnostics());
}
private void prepareTransit(List<ExpansionIsland> islands) {
if ((generation != 21 && generation != 22 && generation != 23) || generation == 21 && !experimentalStructures || !cityStructuresEnabled || transitPrepared) return;
if ((generation != 21 && generation != 22 && (generation != 23 && generation != 24)) || generation == 21 && !experimentalStructures || !cityStructuresEnabled || transitPrepared) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var terrain = context(root);
var sampled = new fr.koka.sanctuary.worldgen.transit.TransitTerrain21(root, terrain.generator(), terrain.random(),
@@ -300,7 +339,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
? fr.koka.sanctuary.worldgen.transit.TransitPlanner22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
: fr.koka.sanctuary.worldgen.transit.TransitPlanner21.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
int directColumns = 0;
if ((generation == 22 || generation == 23) && transitPlan21.isEmpty()) {
if ((generation == 22 || (generation == 23 || generation == 24)) && transitPlan21.isEmpty()) {
directColumns = sampled.columnsUsed();
sampled = new fr.koka.sanctuary.worldgen.transit.TransitTerrain21(root, terrain.generator(), terrain.random(),
heightAccessor, pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()));
@@ -311,12 +350,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
transitPlan21.modules().size(), transitPlan21.samples(), directColumns + sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000, transitPlan21.diagnostic());
}
private boolean protectsCore22(net.minecraft.core.BlockPos pos) {
return (generation == 22 || generation == 23) && (transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ())
return (generation == 22 || (generation == 23 || generation == 24)) && (transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ())
|| undergroundHallProtection22.contains(pos.getX(), pos.getY(), pos.getZ())
|| generation == 23 && mineralSanctuary23.protects(pos.getX(), pos.getY(), pos.getZ()));
|| (generation == 23 || generation == 24) && mineralSanctuary23.protects(pos.getX(), pos.getY(), pos.getZ()));
}
private void prepareUndergroundHall22(List<ExpansionIsland> islands) {
if ((generation != 22 && generation != 23) || !cityStructuresEnabled || undergroundHallPrepared22) return;
if ((generation != 22 && (generation != 23 && generation != 24)) || !cityStructuresEnabled || undergroundHallPrepared22) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
if (undergroundPlan16(root).networks().stream().flatMap(n -> n.modules().stream())
.anyMatch(m -> m.kind() == fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Kind.EXPANSION_HALL)) {
@@ -333,7 +372,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
@Override public boolean protectedAt(int x, int y, int z) { return sampled.protectedAt(x, y, z); }
@Override public boolean exteriorAir(int x, int y, int z, int dx, int dz) { return sampled.exteriorAir(x, y, z, dx, dz); }
};
undergroundHall22 = generation == 23
undergroundHall22 = (generation == 23 || generation == 24)
? fr.koka.sanctuary.worldgen.city.LostCityUnderground23.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), hallTerrain)
: fr.koka.sanctuary.worldgen.city.LostCityUnderground22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), hallTerrain);
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
@@ -343,7 +382,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
undergroundHall22.networks().size(), undergroundHall22.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000);
}
private void prepareMineral23(List<ExpansionIsland> islands) {
if (generation != 23 || !experimentalStructures || !cityStructuresEnabled || mineralPrepared23) return;
if ((generation != 23 && generation != 24) || !experimentalStructures || !cityStructuresEnabled || mineralPrepared23) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var terrain = context(root);
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain21(root, terrain.generator(), terrain.random(), heightAccessor,
@@ -381,13 +420,13 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
}
}
public fr.koka.sanctuary.worldgen.city.LostCityPlan lostCityPlan(ExpansionIsland island) {
if (generation == 23 && island.origin()) return fr.koka.sanctuary.worldgen.city.LostCityPlan.EMPTY;
if ((generation == 23 || generation == 24) && island.origin()) return fr.koka.sanctuary.worldgen.city.LostCityPlan.EMPTY;
if (!ExpansionIsland.spatialGeneration(generation) || island.generation() != generation || (!island.origin() && !island.climate().equals("lost_city")))
return fr.koka.sanctuary.worldgen.city.LostCityPlan.EMPTY;
return cityPlans.computeIfAbsent(cityIdentity(island), ignored -> {
long start = System.nanoTime();
var terrain = context(island);
if (generation == 21 || generation == 22 || generation == 23) {
if (generation == 21 || generation == 22 || (generation == 23 || generation == 24)) {
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain21(island, terrain.generator(), terrain.random(),
heightAccessor, pos -> island.origin() && protectsDecoration(pos));
var placement = generation >= 22
@@ -492,12 +531,13 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
});
}
public boolean protectsCityDecoration(net.minecraft.core.BlockPos pos) {
if (generation == 23 && mineralSanctuary23.protects(pos.getX(), pos.getY(), pos.getZ())) return true;
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 21 || generation == 22 || generation == 23) && transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 22 || generation == 23) && undergroundHallProtection22.contains(pos.getX(), pos.getY(), pos.getZ());
if (generation == 24 && aerialPlan24.protects(pos)) return true;
if ((generation == 23 || generation == 24) && mineralSanctuary23.protects(pos.getX(), pos.getY(), pos.getZ())) return true;
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 22 || (generation == 23 || generation == 24)) && undergroundHallProtection22.contains(pos.getX(), pos.getY(), pos.getZ());
if (generation == 15) return cityProtection15.contains(pos.getX(), pos.getY(), pos.getZ());
if (generation != 14) return false;
var island = islandAt(pos.getX(), pos.getZ());
@@ -519,20 +559,20 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
return citySupports.getOrDefault(cityIdentity(island), List.of());
}
public boolean intersectsCityConstruction16(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
return (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
return (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
}
public fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan undergroundPlan16(ExpansionIsland island) {
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && generation != 23) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24)) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
lostCityPlan(island);
return undergroundPlans16.getOrDefault(cityIdentity(island), new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0));
}
public List<LostCityPlanner16.Support> citySupports16(ExpansionIsland island) {
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && generation != 23) return List.of();
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24)) return List.of();
lostCityPlan(island);
return citySupports16.getOrDefault(cityIdentity(island), List.of());
}
public LostCityPlanner16.Fallback cityReservation16(ExpansionIsland island) {
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && generation != 23) return null;
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24)) return null;
lostCityPlan(island);
return cityReservations16.get(cityIdentity(island));
}
@@ -542,16 +582,16 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
@Override public com.mojang.datafixers.util.Pair<net.minecraft.core.BlockPos, net.minecraft.core.Holder<net.minecraft.world.level.levelgen.structure.Structure>> findNearestMapStructure(
ServerLevel level, net.minecraft.core.HolderSet<net.minecraft.world.level.levelgen.structure.Structure> structures,
net.minecraft.core.BlockPos origin, int radius, boolean skipReferenced) {
if (generation == 23) return fr.koka.sanctuary.expansion.ExpansionStructures23.findNearest(level, structures, origin, radius, skipReferenced);
if ((generation == 23 || generation == 24)) return fr.koka.sanctuary.expansion.ExpansionStructures23.findNearest(level, structures, origin, radius, skipReferenced);
if (generation == 22 && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.KEY))
return fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.findNearest(level, structures, origin, radius, skipReferenced);
if (structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.city.LostCityStructures.KEY))
return fr.koka.sanctuary.worldgen.city.LostCityStructures.findNearest(level, structures, origin, radius, skipReferenced);
if ((generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.KEY))
if ((generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.KEY))
return fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.findNearest(level, structures, origin, radius, skipReferenced);
if ((generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.KEY))
if ((generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.KEY))
return fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.findNearest(level, structures, origin, radius, skipReferenced);
if ((generation == 21 || generation == 22 || generation == 23) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.transit.TransitStructures21.KEY))
if ((generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.transit.TransitStructures21.KEY))
return fr.koka.sanctuary.worldgen.transit.TransitStructures21.findNearest(level, structures, origin, radius, skipReferenced);
return super.findNearestMapStructure(level, structures, origin, radius, skipReferenced);
}
@@ -580,7 +620,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
@Override protected void afterTerrain(ExpansionIsland island, ChunkAccess chunk) {
// Root water/lava are already applied by the Population hook on the named delegate.
if (!island.origin()) super.afterTerrain(island, chunk);
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && cityStructuresEnabled) {
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && cityStructuresEnabled) {
var reserved = cityReservations16.get(cityIdentity(island));
if (reserved != null) {
var mutable = new net.minecraft.core.BlockPos.MutableBlockPos();
@@ -0,0 +1,139 @@
package fr.koka.sanctuary.worldgen.aerial;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import net.minecraft.core.BlockPos;
/** Shared geometry for rendering, native locators and executable interior witnesses. */
public final class AerialGeometry24 {
private AerialGeometry24() {}
public static BlockPos world(AerialPlan24.Site site, int x, int y, int z) {
return new BlockPos(site.worldX(x, z), site.minY() + y, site.worldZ(x, z));
}
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 MINERAL_ISLET -> {
int x = s.width() / 2 + 4, z = s.depth() / 2 + 2;
yield world(s, x, terrainTop(s, x, z) - 3, z);
}
};
}
public static BlockPos occupant(AerialPlan24.Site s) {
if (!s.merchant()) throw new IllegalArgumentException("Only merchant ships have occupants");
return world(s, 5, 6, 20);
}
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 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 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));
return List.of(entry(s));
}
public static int terrainTop(AerialPlan24.Site s, int x, int z) {
int height = rockHeight(s);
if (s.waterfall() && Math.abs(x - s.width() / 2) <= 2 && z <= 6) return height - 4;
if (radial(s, x, z) < .13) return height - 1;
double phase = (s.seed() & 1023) * .01;
return Math.clamp(height - 3 + (int) Math.round(2 * Math.sin(x * .17 + phase) + 1.5 * Math.cos(z * .13 - phase)), height - 7, height - 1);
}
public static int rockHeight(AerialPlan24.Site s) { return s.rockHeight(); }
public static double radial(AerialPlan24.Site s, int x, int z) {
double dx = (x - (s.width() - 1) * .5) / (s.width() * .48);
double dz = (z - (s.depth() - 1) * .5) / (s.depth() * .48);
double phase = (s.seed() & 1023) * .01, angle = Math.atan2(dz, dx);
double edge = .84 + .105 * Math.sin(angle * 3 + phase) + .055 * Math.cos(angle * 5 - phase)
+ .025 * Math.sin(angle * 9 + phase * .7);
if (s.waterfall()) edge = Math.max(edge, .9 - 2.5 * Math.pow(angle + Math.PI / 2, 2));
return Math.hypot(dx, dz) / edge;
}
public static int terrainBottom(AerialPlan24.Site s, int x, int z) {
double r = Math.min(1, radial(s, x, z));
if (r < .065) return 0;
int top = terrainTop(s, x, z);
double taper = (r - .065) / .935;
double angle = Math.atan2(z - s.depth() * .5, x - s.width() * .5);
double ribs = 2 * Math.sin(angle * 5 + (s.seed() & 255) * .02) * taper * (1 - taper);
// The underside meets the rim within two blocks, instead of ending in a vertical cylinder.
return Math.clamp((int) Math.round((top - 1) * Math.pow(taper, .85) + ribs), 0, top - 1);
}
/** Open rock fractures deepen towards the rim; the central arrival and buried cache stay intact. */
public static int fractureFloor(AerialPlan24.Site s, int x, int z) {
if (s.waterfall() && Math.abs(x - s.width() / 2) <= 3 && z <= 8) return Integer.MAX_VALUE;
double dx = x - (s.width() - 1) * .5, dz = z - (s.depth() - 1) * .5;
double phase = (s.seed() & 1023) * .006, start = Math.min(s.width(), s.depth()) * .21;
int floor = Integer.MAX_VALUE;
for (int fracture = 0; fracture < 2; fracture++) {
double angle = phase + fracture * 2.45;
double along = dx * Math.cos(angle) + dz * Math.sin(angle);
double across = -dx * Math.sin(angle) + dz * Math.cos(angle);
double bend = 1.1 * Math.sin(along * .24 + fracture);
if (along > start && Math.abs(across + bend) < 1.05 + (along - start) * .09)
floor = Math.min(floor, terrainTop(s, x, z) - 2 - (int) ((along - start) * 1.3));
}
return floor;
}
public static boolean mineralBody(AerialPlan24.Site s, int x, int y, int z) {
if (x < 0 || z < 0 || x >= s.width() || z >= s.depth()) return false;
if (s.waterfall() && Math.abs(x - s.width() / 2) <= 2 && z <= 6)
return y >= rockHeight(s) - 8 + Math.max(0, 3 - z) && y <= rockHeight(s) - 4;
return radial(s, x, z) <= 1 && y >= terrainBottom(s, x, z) && y <= terrainTop(s, x, z)
&& y < fractureFloor(s, x, z);
}
/** Continuous soil cover broken by rocky edges, exposed seams and open fractures. */
public static boolean naturalSurface(AerialPlan24.Site s, int x, int z) {
if (s.waterfall() && Math.abs(x - s.width() / 2) <= 4 && z <= 9) return false;
int top = terrainTop(s, x, z);
if (radial(s, x, z) > .97 || !mineralBody(s, x, top, z) || !mineralBody(s, x, top - 1, z)) return false;
double phase = (s.seed() & 1023) * .01;
double patches = .6 * Math.sin(x * .17 + phase) + .45 * Math.cos(z * .14 - phase) + .25 * Math.sin((x + z) * .24);
return patches > -.75;
}
public static boolean arrivalColumn(AerialPlan24.Site s, int x, int z) {
return Math.abs(x - s.width() / 2) <= 1 && Math.abs(z - s.depth() / 2) <= 1;
}
public record Tree(int x, int groundY, int z, int trunkHeight, boolean birch) {}
/** Small woodland groups are planned against the whole site before any chunk is rendered. */
public static List<Tree> trees(AerialPlan24.Site s) {
if (s.kind() != AerialPlan24.Kind.MINERAL_ISLET) return List.of();
var trees = new ArrayList<Tree>();
int target = 2 + (s.width() - 40) / 10;
for (int attempt = 0; attempt < 256 && trees.size() < target; attempt++) {
long hash = mix(s.seed() ^ 0x24A11F0AE57L ^ (attempt * 0x9e3779b97f4a7c15L));
int x = 6 + (int) Math.floorMod(hash, s.width() - 12);
int z = 6 + (int) Math.floorMod(mix(hash), s.depth() - 12);
int dx = x - s.width() / 2, dz = z - s.depth() / 2;
if (dx * dx + dz * dz < 36 || (dx - 4) * (dx - 4) + (dz - 2) * (dz - 2) < 25) continue;
if (trees.stream().anyMatch(t -> (t.x() - x) * (t.x() - x) + (t.z() - z) * (t.z() - z) < 81)) continue;
int ground = terrainTop(s, x, z);
if (!naturalSurface(s, x, z)) continue;
boolean supported = true;
for (int ox = -2; ox <= 2 && supported; ox++) for (int oz = -2; oz <= 2; oz++) {
int neighbour = terrainTop(s, x + ox, z + oz);
if (Math.abs(neighbour - ground) > 1 || !mineralBody(s, x + ox, neighbour, z + oz)) {
supported = false; break;
}
}
if (supported) trees.add(new Tree(x, ground, z, 4 + (int) (hash & 1), trees.size() % 3 == 1));
}
return List.copyOf(trees);
}
static long mix(long n) { n = (n ^ n >>> 30) * 0xbf58476d1ce4e5b9L; n = (n ^ n >>> 27) * 0x94d049bb133111ebL; return n ^ n >>> 31; }
}
@@ -0,0 +1,113 @@
package fr.koka.sanctuary.worldgen.aerial;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import java.util.Set;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.ByteTag;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.IntTag;
import net.minecraft.nbt.LongTag;
import net.minecraft.nbt.StringTag;
import net.minecraft.resources.ResourceKey;
import net.minecraft.util.RandomSource;
import net.minecraft.world.entity.EntitySpawnReason;
import net.minecraft.world.entity.EntityTypes;
import net.minecraft.world.entity.npc.villager.VillagerProfession;
import net.minecraft.world.entity.npc.villager.VillagerType;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.loot.LootTable;
/** Native geometry and one-shot occupant flag; neither removed loot nor an extracted merchant is respawned. */
public final class AerialPiece24 extends StructurePiece {
private final AerialPlan24.Site site;
private boolean occupantPlaced;
public AerialPiece24(AerialPlan24.Site site) {
super(AerialStructures24.PIECE, 0, site.bounds()); this.site = site; setOrientation(null);
}
public AerialPiece24(StructurePieceSerializationContext context, CompoundTag tag) {
super(AerialStructures24.PIECE, tag);
site = decode(tag.getCompound("SanctuaryAerial").orElseThrow());
if (!(tag.get("OccupantPlaced") instanceof ByteTag)) throw new IllegalArgumentException("Missing occupant marker");
byte value = tag.getByte("OccupantPlaced").orElseThrow();
if (value != 0 && value != 1) throw new IllegalArgumentException("Invalid occupant marker");
occupantPlaced = value == 1;
if (occupantPlaced && !site.merchant()) throw new IllegalArgumentException("Non-merchant site has occupant marker");
if (!getBoundingBox().equals(site.bounds())) throw new IllegalArgumentException("Aerial piece bounding box differs from saved geometry");
setOrientation(null);
}
public AerialPlan24.Site site() { return site; }
public boolean occupantPlaced() { return occupantPlaced; }
public static CompoundTag encode(AerialPlan24.Site s) {
var tag = new CompoundTag(); tag.putInt("Schema", 1); tag.putString("Id", s.id()); tag.putString("Kind", s.kind().name());
tag.putInt("X", s.minX()); tag.putInt("Y", s.minY()); tag.putInt("Z", s.minZ());
tag.putInt("Width", s.width()); tag.putInt("Height", s.height()); tag.putInt("Depth", s.depth());
tag.putInt("Facing", s.facing()); tag.putLong("Seed", s.seed()); tag.putInt("ShoreX", s.shoreX());
tag.putInt("ShoreY", s.shoreY()); tag.putInt("ShoreZ", s.shoreZ()); tag.putBoolean("Waterfall", s.waterfall()); return tag;
}
public static AerialPlan24.Site decode(CompoundTag tag) {
if (!tag.keySet().equals(Set.of("Schema", "Id", "Kind", "X", "Y", "Z", "Width", "Height", "Depth", "Facing", "Seed", "ShoreX", "ShoreY", "ShoreZ", "Waterfall")))
throw new IllegalArgumentException("Unexpected aerial piece fields");
for (String name : new String[]{"Schema", "X", "Y", "Z", "Width", "Height", "Depth", "Facing", "ShoreX", "ShoreY", "ShoreZ"})
if (!(tag.get(name) instanceof IntTag)) throw new IllegalArgumentException("Expected aerial integer " + name);
if (!(tag.get("Id") instanceof StringTag) || !(tag.get("Kind") instanceof StringTag) || !(tag.get("Seed") instanceof LongTag)
|| !(tag.get("Waterfall") instanceof ByteTag) || tag.getInt("Schema").orElseThrow() != 1)
throw new IllegalArgumentException("Invalid aerial schema or fields");
byte waterfall = tag.getByte("Waterfall").orElseThrow();
if (waterfall != 0 && waterfall != 1) throw new IllegalArgumentException("Invalid waterfall flag");
return new AerialPlan24.Site(tag.getString("Id").orElseThrow(), AerialPlan24.Kind.valueOf(tag.getString("Kind").orElseThrow()),
tag.getInt("X").orElseThrow(), tag.getInt("Y").orElseThrow(), tag.getInt("Z").orElseThrow(),
tag.getInt("Width").orElseThrow(), tag.getInt("Height").orElseThrow(), tag.getInt("Depth").orElseThrow(),
tag.getInt("Facing").orElseThrow(), tag.getLong("Seed").orElseThrow(), tag.getInt("ShoreX").orElseThrow(),
tag.getInt("ShoreY").orElseThrow(), tag.getInt("ShoreZ").orElseThrow(), waterfall == 1);
}
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
tag.put("SanctuaryAerial", encode(site)); tag.putBoolean("OccupantPlaced", occupantPlaced);
}
@Override public BlockPos getLocatorPosition() { return AerialGeometry24.entry(site); }
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
AerialStructures24.placing(site, () -> {
AerialRenderer24.render(site, new LostCityRenderer.BlockWriter() {
private BlockPos position(int x, int y, int z) { return new BlockPos(site.minX() + x, site.minY() + y, site.minZ() + z); }
@Override public void set(int x, int y, int z, BlockState state) {
var pos = position(x, y, z);
if (site.bounds().isInside(pos) && clip.isInside(pos)) {
placeBlock(level, state, pos.getX(), pos.getY(), pos.getZ(), clip);
if (state.is(Blocks.WATER)) level.getChunk(pos).markPosForPostProcessing(pos);
}
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
var pos = position(x, y, z);
if (site.bounds().isInside(pos) && clip.isInside(pos)) createChest(level, clip, RandomSource.create(seed), pos, table, null);
}
}, clip);
placeOccupant(level, clip);
});
}
private synchronized void placeOccupant(WorldGenLevel level, BoundingBox clip) {
if (!site.merchant() || occupantPlaced) return;
var at = AerialGeometry24.occupant(site);
if (!clip.isInside(at)) return;
// Match vanilla structure-owned occupants: the persisted flag records placement, not continued presence.
var villager = EntityTypes.VILLAGER.create(level.getLevel(), EntitySpawnReason.STRUCTURE);
if (villager == null) throw new IllegalStateException("Merchant entity could not be created");
villager.snapTo(at.getX() + .5, at.getY(), at.getZ() + .5, site.facing() * 90, 0);
villager.finalizeSpawn(level, level.getCurrentDifficultyAt(at), EntitySpawnReason.STRUCTURE, null);
var profession = site.kind() == AerialPlan24.Kind.MERCHANT_PROVISIONS ? VillagerProfession.FARMER : VillagerProfession.TOOLSMITH;
villager.setVillagerData(villager.getVillagerData().withType(level.registryAccess(), VillagerType.PLAINS)
.withProfession(level.registryAccess(), profession).withLevel(1));
villager.setVillagerDataFinalized(true);
villager.setVillagerXp(1); // A previously practising merchant keeps the profession when carried away from its workstation.
villager.setPersistenceRequired(); villager.setUUID(AerialGeometry24.occupantUuid(site));
occupantPlaced = true;
level.addFreshEntityWithPassengers(villager);
}
}
@@ -0,0 +1,129 @@
package fr.koka.sanctuary.worldgen.aerial;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
/** Immutable initial sites. Claims reserve only their columns, never new terrain or a surrounding ring. */
public final class AerialPlan24 {
/** Vegetation occupies additional height; it never replaces the twenty to thirty-five blocks of rock. */
public static final int VEGETATION_HEADROOM = 8;
/** Overworld water can spread seven blocks horizontally after landing. Only the outlet reserves this neighbourhood. */
public static final int WATERFALL_SPREAD = 7;
public static final AerialPlan24 EMPTY = new AerialPlan24(List.of());
private final List<Site> sites;
private final Set<Long> claims;
public enum Kind { BALLOON, 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
|| 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");
var ids = new HashSet<String>();
var columns = new HashSet<Long>();
for (Site site : sites) {
if (!ids.add(site.id())) throw new IllegalArgumentException("Duplicate aerial site");
for (ChunkPos chunk : site.claimedChunks()) if (!columns.add(chunk.pack()))
throw new IllegalArgumentException("Overlapping aerial claims");
}
if (columns.size() > 512) throw new IllegalArgumentException("Aerial claims exceed their finite budget");
claims = Set.copyOf(columns);
}
public List<Site> sites() { return sites; }
public boolean isEmpty() { return sites.isEmpty(); }
public Set<Long> claimsChunks() { return claims; }
public List<Site> atStartChunk(int x, int z) {
return sites.stream().filter(s -> s.startChunkX() == x && s.startChunkZ() == z).toList();
}
public boolean ownsChunk(int x, int z) { return claims.contains(ChunkPos.pack(x, z)); }
public boolean conflicts(ExpansionIsland island) {
if (island.origin()) return false;
for (long packed : claims) {
var chunk = ChunkPos.unpack(packed);
if (island.ownsChunk(chunk.x(), chunk.z())) return true;
}
return false;
}
public boolean protects(BlockPos pos) { return sites.stream().anyMatch(s -> s.mayWrite(pos)); }
/** Width and depth are local dimensions; facing rotates them around the world-space bounding-box minimum. */
public record Site(String id, Kind kind, int minX, int minY, int minZ, int width, int height, int depth,
int facing, long seed, int shoreX, int shoreY, int shoreZ, boolean waterfall) {
public Site {
if (id == null || !id.matches("[a-z][a-z0-9_]{0,47}") || kind == null || facing < 0 || facing > 3
|| Math.abs((long) minX) > 2048 || Math.abs((long) minZ) > 2048
|| Math.abs((long) shoreX) > 1024 || Math.abs((long) shoreZ) > 1024
|| shoreY < 1 || shoreY > 383 || minY < 32 || height < 8 || height > 35 + VEGETATION_HEADROOM || minY + height > 350
|| width < 7 || width > 64 || depth < 7 || depth > 64
|| 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))
throw new IllegalArgumentException("Aerial site differs from its version24 gabarit");
int worldWidth = facing % 2 == 0 ? width : depth;
int worldDepth = facing % 2 == 0 ? depth : width;
double cx = minX + (worldWidth - 1) / 2.0, cz = minZ + (worldDepth - 1) / 2.0;
if (Math.hypot(cx - shoreX, cz - shoreZ) > 512)
throw new IllegalArgumentException("Aerial site is farther than512 blocks from its shore witness");
}
public int worldWidth() { return facing % 2 == 0 ? width : depth; }
public int worldDepth() { return facing % 2 == 0 ? depth : width; }
public int rockHeight() {
if (kind != Kind.MINERAL_ISLET) throw new IllegalStateException("Only mineral islets have a rock height");
return height - VEGETATION_HEADROOM;
}
public int maxX() { return minX + worldWidth() - 1; }
public int maxY() { return minY + height - 1; }
public int maxZ() { return minZ + worldDepth() - 1; }
public int worldX(int x, int z) {
return minX + switch (facing) { case 0 -> x; case 1 -> depth - 1 - z; case 2 -> width - 1 - x; default -> z; };
}
public int worldZ(int x, int z) {
return minZ + switch (facing) { case 0 -> z; case 1 -> x; case 2 -> depth - 1 - z; default -> width - 1 - x; };
}
public BoundingBox bounds() { return new BoundingBox(minX, minY, minZ, maxX(), maxY(), maxZ()); }
public int startChunkX() { return (minX + worldWidth() / 2) >> 4; }
public int startChunkZ() { return (minZ + worldDepth() / 2) >> 4; }
public BlockPos locator() { return AerialGeometry24.entry(this); }
public boolean merchant() { return kind == Kind.MERCHANT_PROVISIONS || kind == Kind.MERCHANT_TOOLS; }
public BoundingBox waterfallClaimBounds() {
if (!waterfall) throw new IllegalStateException("This site has no waterfall");
int x = worldX(width / 2, -1), z = worldZ(width / 2, -1);
return new BoundingBox(x - WATERFALL_SPREAD, 0, z - WATERFALL_SPREAD,
x + WATERFALL_SPREAD, AerialGeometry24.waterfallSource(this).getY(), z + WATERFALL_SPREAD);
}
public List<ChunkPos> claimedChunks() {
var result = new LinkedHashSet<ChunkPos>();
addChunks(bounds(), result);
if (waterfall) addChunks(waterfallClaimBounds(), result);
return List.copyOf(result);
}
public boolean ownsChunk(int x, int z) {
return includesChunk(bounds(), x, z) || waterfall && includesChunk(waterfallClaimBounds(), x, z);
}
public boolean mayWrite(BlockPos pos) {
return bounds().isInside(pos) || waterfall && waterfallClaimBounds().isInside(pos);
}
private static boolean includesChunk(BoundingBox bounds, int x, int z) {
return x >= (bounds.minX() >> 4) && x <= (bounds.maxX() >> 4)
&& z >= (bounds.minZ() >> 4) && z <= (bounds.maxZ() >> 4);
}
private static void addChunks(BoundingBox bounds, Set<ChunkPos> output) {
for (int x = bounds.minX() >> 4; x <= bounds.maxX() >> 4; x++)
for (int z = bounds.minZ() >> 4; z <= bounds.maxZ() >> 4; z++) output.add(new ChunkPos(x, z));
}
}
}
@@ -0,0 +1,150 @@
package fr.koka.sanctuary.worldgen.aerial;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.NoiseColumn;
import fr.koka.sanctuary.worldgen.aerial.AerialPlan24.Kind;
import fr.koka.sanctuary.worldgen.aerial.AerialPlan24.Site;
/** Fixed work limits, not wall-clock-dependent draws. No chunk is requested or modified by this planner. */
public final class AerialPlanner24 {
public static final int MAX_ATTEMPTS_PER_SITE = 64;
public static final int MAX_COLUMNS = 8192;
private AerialPlanner24() {}
public interface Terrain {
NoiseColumn column(int x, int z);
/** Planned structures may add rock above the original noise column or cut its support. */
default boolean reserved(int x, int z, int minY, int maxY) { return false; }
}
public static AerialPlan24 forWorld(ServerLevel level, SanctuaryChunkGenerator generator) {
if (generator.rootGeneration() != 24) throw new IllegalArgumentException("Aerial planning requires generation24");
if (!level.structureManager().shouldGenerateStructures() || !generator.experimentalStructures()) return AerialPlan24.EMPTY;
generator.prepareAerialSampling24(level);
return plan(level.getSeed(), generator.initialDiameter(), new Terrain() {
@Override public NoiseColumn column(int x, int z) { return generator.aerialBaseColumn24(x, z); }
@Override public boolean reserved(int x, int z, int minY, int maxY) {
return generator.intersectsExperimentalStructures(x, minY, z, x, maxY, z);
}
});
}
public static AerialPlan24 plan(long seed, int diameter, Terrain terrain) {
if (diameter != 512 && diameter != 724 && diameter != 1024) throw new IllegalArgumentException("Unsupported aerial root size");
int radius = diameter == 724 ? 395 : diameter / 2 + 32;
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"};
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);
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
+ (attempt / 16) * .41;
Shore shore = shore(probe, angle, radius);
if (shore == null) continue;
int facing = (int) Math.floorMod(mix(draw), 4);
int worldWidth = facing % 2 == 0 ? width : depth, worldDepth = facing % 2 == 0 ? depth : width;
// The full claim remains outside initial terrain ownership, including corner chunks.
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)
: 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);
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;
accepted = site; break;
}
if (accepted == null) throw new IllegalStateException("aerial_sites_unavailable: could not place " + ids[ordinal] + " within the bounded search");
sites.add(accepted);
accepted.claimedChunks().forEach(c -> claimed.add(c.pack()));
}
var result = new AerialPlan24(sites);
System.getLogger(AerialPlanner24.class.getName()).log(System.Logger.Level.INFO,
"Aerial24 seed=" + seed + " diameter=" + diameter + " sites=" + result.sites().size()
+ " claims=" + result.claimsChunks().size() + " columns=" + probe.columns.size());
return result;
}
private static Shore shore(Probe probe, double angle, int radius) {
double dx = Math.cos(angle), dz = Math.sin(angle);
// An outside-in scan finds the outer shore, never an internal pond. Refine its edge to a block.
for (int r = radius; r >= 32; r -= 8) {
int x = (int) Math.round(dx * r), z = (int) Math.round(dz * r), top = probe.surface(x, z);
if (top < 120 || top > 326 || probe.terrain.reserved(x, z, top, top + 3)) continue;
Shore best = new Shore(x, top + 1, z);
for (int fine = 1; fine <= 8; fine++) {
int fx = (int) Math.round(dx * (r + fine)), fz = (int) Math.round(dz * (r + fine));
int ft = probe.surface(fx, fz);
if (ft < 120 || ft > 326 || probe.terrain.reserved(fx, fz, ft, ft + 3)) break;
best = new Shore(fx, ft + 1, fz);
}
return best;
}
return null;
}
private static boolean visible(Probe probe, Shore shore, int targetX, int targetY, int targetZ) {
double distance = Math.hypot(targetX - shore.x, targetZ - shore.z);
int steps = (int) Math.ceil(distance);
for (int step = 1; step < steps; step++) {
double t = (double) step / steps;
int x = (int) Math.floor(shore.x + .5 + (targetX - shore.x) * t);
int z = (int) Math.floor(shore.z + .5 + (targetZ - shore.z) * t);
int y = (int) Math.floor(shore.y + 2.0 + (targetY - shore.y - 2.0) * t);
if (probe.solid(x, y, z) || probe.terrain.reserved(x, z, y, y)) return false;
}
return true;
}
private record Shore(int x, int y, int z) {}
private static final class Probe {
final Terrain terrain; final int radius;
final Map<Long, NoiseColumn> columns = new HashMap<>();
Probe(Terrain terrain, int radius) { this.terrain = terrain; this.radius = radius; }
NoiseColumn column(int x, int z) {
if (Math.hypot(x, z) >= radius) return null;
long key = ((long) x << 32) ^ (z & 0xffffffffL);
var present = columns.get(key); if (present != null) return present;
if (columns.size() >= MAX_COLUMNS) throw new IllegalStateException("aerial_sites_unavailable: terrain column budget exhausted");
var result = terrain.column(x, z);
if (result == null) throw new IllegalStateException("Aerial terrain query returned no column");
columns.put(key, result); return result;
}
int surface(int x, int z) {
var column = column(x, z); if (column == null) return 0;
for (int y = 382; y > 0; y--) if (column.getBlock(y).isSolid() && column.getBlock(y).getFluidState().isEmpty()) return y;
return 0;
}
boolean solid(int x, int y, int z) {
var column = column(x, z); return column != null && !column.getBlock(y).isAir();
}
}
private static long mix(long value) {
value = (value ^ value >>> 30) * 0xbf58476d1ce4e5b9L;
value = (value ^ value >>> 27) * 0x94d049bb133111ebL;
return value ^ value >>> 31;
}
private static double unit(long value) { return (value >>> 11) * 0x1.0p-53; }
}
@@ -0,0 +1,276 @@
package fr.koka.sanctuary.worldgen.aerial;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BedPart;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
import net.minecraft.world.level.block.state.properties.Half;
import net.minecraft.world.level.block.state.properties.SlabType;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.storage.loot.LootTable;
/** Complete supported silhouettes; no terrain pillars, remote writes, or fabricated flowing-water columns. */
public final class AerialRenderer24 {
private AerialRenderer24() {}
/** Export actual final block states for inspection, including rotations and generated chest positions. */
public static void writeDiagnostic(Path path, List<AerialPlan24.Site> sites) throws IOException {
var result = new ArrayList<Map<String, Object>>();
for (var site : sites) {
var blocks = blocks(site);
var list = blocks.entrySet().stream().filter(e -> !e.getValue().isAir()).map(e -> {
var p = e.getKey(); var state = e.getValue();
return List.of(p.getX() - site.minX(), p.getY() - site.minY(), p.getZ() - site.minZ(),
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));
}
if (path.toAbsolutePath().getParent() != null) Files.createDirectories(path.toAbsolutePath().getParent());
Files.writeString(path, new com.google.gson.GsonBuilder().create().toJson(result) + "\n");
}
public static Map<BlockPos, BlockState> blocks(AerialPlan24.Site site) {
var blocks = new LinkedHashMap<BlockPos, BlockState>();
render(site, new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(new BlockPos(site.minX() + x, site.minY() + y, site.minZ() + z), state); }
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) { set(x, y, z, Blocks.CHEST.defaultBlockState()); }
});
return Map.copyOf(blocks);
}
public static ResourceKey<LootTable> loot(AerialPlan24.Site s) {
return ResourceKey.create(Registries.LOOT_TABLE, SanctuaryMod.id("chests/aerial24/" + s.kind().name().toLowerCase(java.util.Locale.ROOT)));
}
public static void render(AerialPlan24.Site s, LostCityRenderer.BlockWriter out) { render(s, out, s.bounds()); }
public static void render(AerialPlan24.Site s, LostCityRenderer.BlockWriter out, BoundingBox clip) {
// Connections see the complete airship before the native destination clips its own chunk.
// Mineral columns need no connection buffer and keep their bounded per-chunk scan.
LostCityRenderer.BlockWriter clipped = new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) {
if (clip.isInside(s.minX() + x, s.minY() + y, s.minZ() + z)) out.set(x, y, z, state);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
if (clip.isInside(s.minX() + x, s.minY() + y, s.minZ() + z)) out.chest(x, y, z, table, seed);
}
};
var connected = s.kind() == AerialPlan24.Kind.MINERAL_ISLET ? null : ConnectedBlocks20.buffer(clipped);
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();
}
var chest = AerialGeometry24.chest(s);
destination.chest(chest.getX() - s.minX(), chest.getY() - s.minY(), chest.getZ() - s.minZ(), loot(s), s.seed() ^ 0x24C4E57L);
if (connected != null) connected.flush();
}
private static final class Builder {
final AerialPlan24.Site s;
final LostCityRenderer.BlockWriter out;
final BoundingBox clip;
final Rotation rotation;
Builder(AerialPlan24.Site s, LostCityRenderer.BlockWriter out, BoundingBox clip) {
this.s = s; this.out = out; this.clip = clip;
rotation = switch (s.facing()) { case 1 -> Rotation.CLOCKWISE_90; case 2 -> Rotation.CLOCKWISE_180; case 3 -> Rotation.COUNTERCLOCKWISE_90; default -> Rotation.NONE; };
}
void set(int x, int y, int z, Block block) { set(x, y, z, block.defaultBlockState()); }
void set(int x, int y, int z, BlockState state) {
if (x < 0 || y < 0 || z < 0 || x >= s.width() || y >= s.height() || z >= s.depth()) return;
int wx = s.worldX(x, z), wz = s.worldZ(x, z);
if (clip.isInside(wx, s.minY() + y, wz)) out.set(wx - s.minX(), y, wz - s.minZ(), state.rotate(rotation));
}
void box(int x0, int y0, int z0, int x1, int y1, int z1, Block block) {
for (int x = x0; x <= x1; x++) for (int z = z0; z <= z1; z++) for (int y = y0; y <= y1; y++) set(x, y, z, block);
}
void slab(int x, int y, int z) { set(x, y, z, Blocks.OAK_SLAB.defaultBlockState().setValue(BlockStateProperties.SLAB_TYPE, SlabType.BOTTOM)); }
void stair(int x, int y, int z, Direction direction, Half half) {
set(x, y, z, Blocks.OAK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, direction).setValue(BlockStateProperties.HALF, half));
}
void lantern(int x, int y, int z) { set(x, y, z, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true)); }
void door(int x, int y, int z, Direction direction) {
var state = Blocks.OAK_DOOR.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, direction);
set(x, y, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.LOWER));
set(x, y + 1, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.UPPER));
}
void bed(int x, int y, int z) {
var state = Blocks.BED.white().defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH);
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);
}
}
// 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));
}
void islet() {
for (int x = 0; x < s.width(); x++) for (int z = 0; z < s.depth(); z++) {
if (!clip.isInside(s.worldX(x, z), s.minY(), s.worldZ(x, z))
&& (s.worldX(x,z) < clip.minX() || s.worldX(x,z) > clip.maxX() || s.worldZ(x,z) < clip.minZ() || s.worldZ(x,z) > clip.maxZ())) continue;
int top = AerialGeometry24.terrainTop(s, x, z);
for (int y = 0; y <= top; y++) if (AerialGeometry24.mineralBody(s, x, y, z)) set(x, y, z, ore(x, y, z, top));
if (AerialGeometry24.naturalSurface(s, x, z)) {
set(x, top, z, Blocks.GRASS_BLOCK);
int soil = 2 + (int) Math.floorMod(AerialGeometry24.mix(s.seed() ^ x * 19349663L ^ z * 83492791L), 2);
for (int layer = 1; layer <= soil && top - layer > AerialGeometry24.terrainBottom(s, x, z); layer++)
if (AerialGeometry24.mineralBody(s, x, top - layer, z)) set(x, top - layer, z, Blocks.DIRT);
if (!AerialGeometry24.arrivalColumn(s, x, z)) {
long foliage = AerialGeometry24.mix(s.seed() ^ x * 374761393L ^ z * 668265263L);
int roll = (int) Math.floorMod(foliage, 100);
if (roll < 8) set(x, top + 1, z, switch ((int) Math.floorMod(foliage >>> 16, 4)) {
case 0 -> Blocks.DANDELION; case 1 -> Blocks.POPPY; case 2 -> Blocks.OXEYE_DAISY; default -> Blocks.CORNFLOWER;
});
else if (roll < 44) set(x, top + 1, z, Blocks.SHORT_GRASS);
}
}
}
if (s.waterfall()) {
int cx = s.width() / 2, floor = AerialGeometry24.rockHeight(s) - 4;
for (int z = 0; z <= 4; z++) {
set(cx, floor, z, Blocks.STONE);
set(cx - 1, floor + 1, z, Blocks.STONE); set(cx + 1, floor + 1, z, Blocks.STONE);
box(cx, floor + 1, z, cx, floor + 3, z, Blocks.AIR);
}
set(cx, floor + 1, 4, Blocks.STONE);
set(cx, floor + 1, 3, Blocks.WATER);
}
// A true buried chest: intact roof and floor, no decorative open loot pedestal.
var c = AerialGeometry24.chest(s);
int x = s.width() / 2 + 4, z = s.depth() / 2 + 2, y = c.getY() - s.minY();
box(x - 1, y - 1, z - 1, x + 1, y + 2, z + 1, Blocks.STONE);
for (var tree : AerialGeometry24.trees(s)) tree(tree);
}
void tree(AerialGeometry24.Tree tree) {
Block leaves = tree.birch() ? Blocks.BIRCH_LEAVES : Blocks.OAK_LEAVES;
int lastLog = tree.groundY() + tree.trunkHeight();
for (int y = lastLog - 2; y <= lastLog + 1; y++) {
int radius = y < lastLog ? 2 : 1;
for (int dx = -radius; dx <= radius; dx++) for (int dz = -radius; dz <= radius; dz++) {
if (Math.abs(dx) + Math.abs(dz) > (radius == 2 ? 3 : 1)) continue;
int distance = Math.max(1, Math.abs(dx) + Math.abs(dz) + Math.max(0, y - lastLog));
set(tree.x() + dx, y, tree.z() + dz, leaves.defaultBlockState()
.setValue(BlockStateProperties.DISTANCE, distance).setValue(BlockStateProperties.PERSISTENT, false));
}
}
box(tree.x(), tree.groundY() + 1, tree.z(), tree.x(), lastLog, tree.z(), tree.birch() ? Blocks.BIRCH_LOG : Blocks.OAK_LOG);
}
Block ore(int x, int y, int z, int top) {
double depth = (double) (top - y) / Math.max(1, top);
long cell = AerialGeometry24.mix(s.seed() ^ ((long)(x / 4) << 42) ^ ((long)(z / 4) << 21) ^ (y / 3));
int roll = (int) Math.floorMod(cell, 100);
// Relative strata make the lower suspended rock useful without pretending it is y=-64.
if (roll < 37) {
if (depth < .27) return roll < 15 ? Blocks.COAL_ORE : roll < 27 ? Blocks.COPPER_ORE : Blocks.IRON_ORE;
if (depth < .60) return roll < 13 ? Blocks.IRON_ORE : roll < 21 ? Blocks.GOLD_ORE : roll < 29 ? Blocks.LAPIS_ORE : Blocks.COAL_ORE;
return roll < 10 ? Blocks.DEEPSLATE_IRON_ORE : roll < 19 ? Blocks.DEEPSLATE_REDSTONE_ORE : roll < 27 ? Blocks.DEEPSLATE_GOLD_ORE : roll < 34 ? Blocks.DEEPSLATE_DIAMOND_ORE : Blocks.DEEPSLATE_EMERALD_ORE;
}
if (depth > .6) return roll < 59 ? Blocks.TUFF : Blocks.DEEPSLATE;
return roll < 53 ? Blocks.ANDESITE : roll < 59 ? Blocks.COBBLESTONE : Blocks.STONE;
}
}
}
@@ -0,0 +1,166 @@
package fr.koka.sanctuary.worldgen.aerial;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.Strictness;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import java.io.IOException;
import java.io.StringReader;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.Set;
import java.util.function.Supplier;
/** Content-addressed initial layout. Its hash is authoritative only once bound by the expansion journal24. */
public final class AerialStore24 {
private static final int MAX_BYTES = 32768;
private static final Set<String> ROOT_FIELDS = Set.of("schema", "world_seed", "root_diameter", "generation", "sites");
private static final Set<String> SITE_FIELDS = Set.of("id", "kind", "min_x", "min_y", "min_z", "width", "height", "depth",
"facing", "seed", "shore_x", "shore_y", "shore_z", "waterfall");
private AerialStore24() {}
public record Stored(String token, AerialPlan24 plan) {}
/** Store lives beside sanctuary-world-v24, so creating an orphan manifest never looks like an initialized journal. */
public static Path directory(Path journalDirectory) { return journalDirectory.resolveSibling("sanctuary-aerial-v24"); }
public static Stored create(Path directory, long seed, int diameter, Supplier<AerialPlan24> factory) throws IOException {
AerialPlan24 plan = factory.get();
validateExtent(plan, diameter);
String encoded = encode(plan, seed, diameter).toString() + "\n";
byte[] bytes = encoded.getBytes(StandardCharsets.UTF_8);
if (bytes.length > MAX_BYTES) throw new IOException("Aerial layout exceeds its size limit");
String token = digest(bytes);
if (Files.exists(directory, LinkOption.NOFOLLOW_LINKS) && !Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS))
throw new IOException("Aerial manifest path is not a directory");
Files.createDirectories(directory);
Path file = directory.resolve(token + ".json");
if (!Files.exists(file, LinkOption.NOFOLLOW_LINKS)) {
Path temporary = Files.createTempFile(directory, ".aerial-", ".tmp");
try {
try (FileChannel output = FileChannel.open(temporary, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
var buffer = ByteBuffer.wrap(bytes); while (buffer.hasRemaining()) output.write(buffer); output.force(true);
}
Files.move(temporary, file, StandardCopyOption.ATOMIC_MOVE);
if (!file.getFileSystem().getSeparator().equals("\\")) {
try (FileChannel parent = FileChannel.open(directory, StandardOpenOption.READ)) { parent.force(true); }
}
} finally { Files.deleteIfExists(temporary); }
}
Stored verified = load(directory, token, seed, diameter);
if (!verified.plan().sites().equals(plan.sites())) throw new IOException("Aerial manifest read-back differs from its plan");
return verified;
}
public static Stored load(Path directory, String token, long seed, int diameter) throws IOException {
if (token == null || !token.matches("[0-9a-f]{64}")) throw new IOException("Invalid aerial manifest token");
Path file = directory.resolve(token + ".json");
if (!Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS) || !Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)
|| Files.size(file) > MAX_BYTES) throw new IOException("Missing or invalid saved aerial manifest");
byte[] bytes = Files.readAllBytes(file);
if (!digest(bytes).equals(token)) throw new IOException("Saved aerial manifest has changed");
try (var reader = new JsonReader(new StringReader(new String(bytes, StandardCharsets.UTF_8)))) {
reader.setStrictness(Strictness.STRICT);
JsonObject root = readJson(reader, 0).getAsJsonObject();
if (reader.peek() != JsonToken.END_DOCUMENT) throw new IOException("Trailing aerial manifest data");
fields(root, ROOT_FIELDS);
if (integer(root, "schema") != 1 || integer(root, "generation") != 24
|| number(root, "world_seed").longValueExact() != seed || integer(root, "root_diameter") != diameter)
throw new IOException("Aerial manifest differs from its world contract");
var sites = new ArrayList<AerialPlan24.Site>();
for (JsonElement item : root.getAsJsonArray("sites")) {
JsonObject site = item.getAsJsonObject(); fields(site, SITE_FIELDS);
sites.add(new AerialPlan24.Site(string(site, "id"), AerialPlan24.Kind.valueOf(string(site, "kind")),
integer(site, "min_x"), integer(site, "min_y"), integer(site, "min_z"), integer(site, "width"),
integer(site, "height"), integer(site, "depth"), integer(site, "facing"), number(site, "seed").longValueExact(),
integer(site, "shore_x"), integer(site, "shore_y"), integer(site, "shore_z"), bool(site, "waterfall")));
}
var plan = new AerialPlan24(sites); validateExtent(plan, diameter);
return new Stored(token, plan);
} catch (RuntimeException error) { throw new IOException("Invalid aerial manifest", error); }
}
private static void validateExtent(AerialPlan24 plan, int diameter) throws IOException {
if (diameter != 512 && diameter != 724 && diameter != 1024) throw new IOException("Unsupported aerial root size");
int radius = (diameter == 724 ? 395 : diameter / 2 + 32) + 32;
for (long packed : plan.claimsChunks()) {
var c = net.minecraft.world.level.ChunkPos.unpack(packed);
int x = Math.clamp(0, c.getMinBlockX(), c.getMaxBlockX());
int z = Math.clamp(0, c.getMinBlockZ(), c.getMaxBlockZ());
if (Math.hypot(x, z) <= radius) throw new IOException("Aerial claim overlaps initial terrain ownership");
}
}
private static JsonObject encode(AerialPlan24 plan, long seed, int diameter) {
var root = new JsonObject();
root.addProperty("schema", 1); root.addProperty("world_seed", seed); root.addProperty("root_diameter", diameter); root.addProperty("generation", 24);
var sites = new JsonArray();
for (var s : plan.sites()) {
var site = new JsonObject(); site.addProperty("id", s.id()); site.addProperty("kind", s.kind().name());
site.addProperty("min_x", s.minX()); site.addProperty("min_y", s.minY()); site.addProperty("min_z", s.minZ());
site.addProperty("width", s.width()); site.addProperty("height", s.height()); site.addProperty("depth", s.depth());
site.addProperty("facing", s.facing()); site.addProperty("seed", s.seed());
site.addProperty("shore_x", s.shoreX()); site.addProperty("shore_y", s.shoreY()); site.addProperty("shore_z", s.shoreZ());
site.addProperty("waterfall", s.waterfall()); sites.add(site);
}
root.add("sites", sites); return root;
}
private static String digest(byte[] bytes) {
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes)); }
catch (NoSuchAlgorithmException impossible) { throw new IllegalStateException(impossible); }
}
private static void fields(JsonObject object, Set<String> expected) throws IOException {
if (!object.keySet().equals(expected)) throw new IOException("Unexpected aerial manifest fields");
}
private static String string(JsonObject object, String key) {
var value = object.getAsJsonPrimitive(key); if (!value.isString()) throw new IllegalArgumentException("Expected aerial text");
return value.getAsString();
}
private static BigDecimal number(JsonObject object, String key) {
var value = object.getAsJsonPrimitive(key); if (!value.isNumber()) throw new IllegalArgumentException("Expected aerial number");
return value.getAsBigDecimal();
}
private static int integer(JsonObject object, String key) { return number(object, key).intValueExact(); }
private static boolean bool(JsonObject object, String key) {
var value = object.getAsJsonPrimitive(key); if (!value.isBoolean()) throw new IllegalArgumentException("Expected aerial boolean");
return value.getAsBoolean();
}
private static JsonElement readJson(JsonReader reader, int depth) throws IOException {
if (depth > 3) throw new IOException("Aerial manifest exceeds its nesting limit");
return switch (reader.peek()) {
case BEGIN_OBJECT -> {
reader.beginObject(); var object = new JsonObject();
while (reader.hasNext()) {
String name = reader.nextName();
if (object.has(name) || object.size() >= 16) throw new IOException("Duplicate or excessive aerial fields");
object.add(name, readJson(reader, depth + 1));
}
reader.endObject(); yield object;
}
case BEGIN_ARRAY -> {
reader.beginArray(); var array = new JsonArray();
while (reader.hasNext()) {
if (array.size() >= 5) throw new IOException("Too many aerial sites");
array.add(readJson(reader, depth + 1));
}
reader.endArray(); yield array;
}
case STRING -> new JsonPrimitive(reader.nextString());
case NUMBER -> new JsonPrimitive(new BigDecimal(reader.nextString()));
case BOOLEAN -> new JsonPrimitive(reader.nextBoolean());
default -> throw new IOException("Invalid aerial JSON value");
};
}
}
@@ -0,0 +1,35 @@
package fr.koka.sanctuary.worldgen.aerial;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.util.Optional;
import java.util.Set;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureType;
/** Each family uses its own native registry identity, independent of any terrain-island owner. */
public final class AerialStructure24 extends Structure {
public static final MapCodec<AerialStructure24> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
settingsCodec(instance), Codec.STRING.fieldOf("family").forGetter(AerialStructure24::family)
).apply(instance, AerialStructure24::new));
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");
this.family = family;
}
public String family() { return family; }
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator) || generator.rootGeneration() != 24
|| !generator.experimentalStructures()) return Optional.empty();
var sites = generator.aerialPlan24().atStartChunk(context.chunkPos().x(), context.chunkPos().z()).stream()
.filter(site -> AerialStructures24.family(site).equals(family)).toList();
if (sites.isEmpty()) return Optional.empty();
return Optional.of(new GenerationStub(AerialGeometry24.entry(sites.getFirst()), builder -> {
for (var site : sites) builder.addPiece(new AerialPiece24(site));
}));
}
@Override public StructureType<?> type() { return AerialStructures24.TYPE; }
}
@@ -0,0 +1,75 @@
package fr.koka.sanctuary.worldgen.aerial;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.util.List;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
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. */
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 StructureType<AerialStructure24> TYPE = Registry.register(BuiltInRegistries.STRUCTURE_TYPE,
SanctuaryMod.id("aerial_v24"), () -> AerialStructure24.CODEC);
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
SanctuaryMod.id("aerial_piece_v24"), AerialPiece24::new);
private static final ThreadLocal<AerialPlan24.Site> PLACING = new ThreadLocal<>();
private 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"; };
}
public static ResourceKey<Structure> key(AerialPlan24.Site site) {
return switch (site.kind()) { case BALLOON -> BALLOON; 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; }
public static boolean mayWrite(BlockPos pos) { var site = PLACING.get(); return site != null && site.bounds().isInside(pos); }
static void placing(AerialPlan24.Site site, Runnable action) {
var old = PLACING.get(); PLACING.set(site);
try { action.run(); } finally { if (old == null) PLACING.remove(); else PLACING.set(old); }
}
public static void generate(ExpansionChunkGenerator generator, RegistryAccess registries,
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> level) {
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || generator.rootGeneration() != 24
|| !sanctuary.experimentalStructures()) return;
var sites = sanctuary.aerialPlan24().atStartChunk(chunk.getPos().x(), chunk.getPos().z());
if (sites.isEmpty()) return;
// Climate evaluation may use the original island's random state; geometry belongs only to the saved site.
var origin = generator.source().islands().stream().filter(i -> i.origin()).findFirst().orElseThrow();
var random = generator.randomState(origin);
var sampler = random.createClimateSampler(SamplerContext.builder().enableCaches().build());
for (var key : sites.stream().map(AerialStructures24::key).distinct().toList()) {
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(key);
Structure structure = holder.value();
StructureStart previous = manager.getStartForStructure(structure, chunk);
if (previous != null && previous.isValid()) continue;
var start = structure.generate(holder, level, registries, generator, generator.getBiomeSource(), sampler,
random, templates, origin.seed(), chunk.getPos(), previous == null ? 0 : previous.getReferences(),
chunk, structure.biomes()::contains);
if (start.isValid()) manager.setStartForStructure(structure, start, chunk);
}
}
}
@@ -29,18 +29,18 @@ public final class LostCityStructure extends Structure {
throw new IllegalStateException("Lost City district exceeds native structure reference range");
}
return Optional.of(new GenerationStub(LostCityStructures.locator(district), pieces -> {
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || generator.rootGeneration() == 23) && generator.cityReservation16(island) != null
&& ((generator.rootGeneration() != 21 && generator.rootGeneration() != 22 && generator.rootGeneration() != 23) || generator.cityPlacement21(island).core().surface().districts().contains(district)))
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) && generator.cityReservation16(island) != null
&& ((generator.rootGeneration() != 21 && generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)) || generator.cityPlacement21(island).core().surface().districts().contains(district)))
pieces.addPiece(new LostCityFoundationPiece16(generator.cityReservation16(island), district.seed()));
if (generator.rootGeneration() == 15) for (var support : generator.citySupports(island)) {
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
pieces.addPiece(new LostCitySupportPiece15(support));
}
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || generator.rootGeneration() == 23)) for (var support : generator.citySupports16(island)) {
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) for (var support : generator.citySupports16(island)) {
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
pieces.addPiece(new LostCitySupportPiece16(support));
}
district.parcels().forEach(parcel -> pieces.addPiece((generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || generator.rootGeneration() == 23) ? new LostCityPiece21(parcel) : generator.rootGeneration() == 20 ? new LostCityPiece20(parcel) : (generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || generator.rootGeneration() == 23) ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
district.parcels().forEach(parcel -> pieces.addPiece((generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) ? new LostCityPiece21(parcel) : generator.rootGeneration() == 20 ? new LostCityPiece20(parcel) : (generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
? new LostCityPiece15(parcel) : new LostCityPiece(parcel)));
if (generator.rootGeneration() == 15) for (var network : generator.undergroundPlan(island).networks()) {
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
@@ -50,13 +50,13 @@ public final class LostCityStructure extends Structure {
network.modules().stream().filter(m -> m.kind() == LostCityUnderground15.Kind.ACCESS)
.forEach(m -> pieces.addPiece(new LostCityUndergroundPiece15(m)));
}
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || generator.rootGeneration() == 23)) for (var network : generator.undergroundPlan16(island).networks()) {
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) for (var network : generator.undergroundPlan16(island).networks()) {
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
// Surface comes first, rooms/corridors next, shafts last so every entrance remains open.
network.modules().stream().filter(m -> m.kind() != LostCityUnderground16.Kind.ACCESS)
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || generator.rootGeneration() == 23) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
network.modules().stream().filter(m -> m.kind() == LostCityUnderground16.Kind.ACCESS)
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || generator.rootGeneration() == 23) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
}
}));
}
@@ -14,9 +14,9 @@ public final class LostCityUndergroundStructure22 extends Structure {
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) return Optional.empty();
|| (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) return Optional.empty();
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
if (island == null || !island.origin() || (island.generation() != 22 && island.generation() != 23)) return Optional.empty();
if (island == null || !island.origin() || (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24))) return Optional.empty();
var networks = generator.undergroundHall22().networks().stream()
.filter(n -> n.startChunkX() == context.chunkPos().x() && n.startChunkZ() == context.chunkPos().z()).toList();
if (networks.isEmpty()) return Optional.empty();
@@ -41,9 +41,9 @@ public final class LostCityUndergroundStructures22 {
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> level) {
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) return;
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || (island.generation() != 22 && island.generation() != 23)) return;
if (island == null || !island.origin() || (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24))) return;
if (sanctuary.undergroundHall22().networks().stream().noneMatch(n -> n.startChunkX() == chunk.getPos().x()
&& n.startChunkZ() == chunk.getPos().z())) return;
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
@@ -63,14 +63,14 @@ public final class LostCityUndergroundStructures22 {
BlockPos origin, int radius, boolean skipReferenced) {
if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 22 && generator.rootGeneration() != 23) || !level.structureManager().shouldGenerateStructures()) return null;
|| (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)) || !level.structureManager().shouldGenerateStructures()) return null;
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
if (!structures.contains(holder)) return null;
var candidates = new ArrayList<LostCityUnderground16.Network>();
int originChunkX = origin.getX() >> 4, originChunkZ = origin.getZ() >> 4;
for (var network : generator.undergroundHall22().networks()) {
var island = generator.islandForChunk(network.startChunkX(), network.startChunkZ());
if (island == null || !island.origin() || (island.generation() != 22 && island.generation() != 23) || !island.status().equals("ready")) continue;
if (island == null || !island.origin() || (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24)) || !island.status().equals("ready")) continue;
if (Math.abs((long) network.startChunkX() - originChunkX) <= radius
&& Math.abs((long) network.startChunkZ() - originChunkZ) <= radius) candidates.add(network);
}
@@ -19,7 +19,7 @@ public final class HeritageCommands19 {
var level = source.getLevel();
if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) || (generator.rootGeneration() != 22 && generator.rootGeneration() != 23) && !generator.experimentalStructures()) {
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)) && !generator.experimentalStructures()) {
source.sendFailure(Component.translatable("sanctuary.heritage.unavailable"));
return 0;
}
@@ -28,20 +28,20 @@ public final class HeritageCommands19 {
for (var site : plan.sites()) source.sendSuccess(() -> Component.translatable("sanctuary.heritage.entry",
Component.translatable("sanctuary.heritage.kind." + site.kind().name().toLowerCase(Locale.ROOT)),
site.entry().x(), site.entry().y(), site.entry().z()), false);
if (generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || generator.rootGeneration() == 23)) for (var module : generator.transitPlan21().modules()) {
if (generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) for (var module : generator.transitPlan21().modules()) {
if (module.kind() != fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind.ACCESS) continue;
var at = fr.koka.sanctuary.worldgen.transit.TransitPiece21.locator(module);
source.sendSuccess(() -> Component.translatable("sanctuary.transit.entry", at.getX(), at.getY(), at.getZ()), false);
}
if ((generator.rootGeneration() == 22 || generator.rootGeneration() == 23)) for (var network : generator.undergroundHall22().networks()) {
if ((generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) for (var network : generator.undergroundHall22().networks()) {
var at = fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.locator(network);
source.sendSuccess(() -> Component.translatable("sanctuary.underground_hall.entry", at.getX(), at.getY(), at.getZ()), false);
}
if (generator.rootGeneration() == 23 && !generator.mineralSanctuary23().isEmpty()) {
if ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24) && !generator.mineralSanctuary23().isEmpty()) {
var at = generator.mineralSanctuary23().chamber().locator();
source.sendSuccess(() -> Component.translatable("sanctuary.mineral_sanctuary.entry", at.getX(), at.getY(), at.getZ()), false);
}
return plan.sites().size() + (generator.rootGeneration() == 23 && !generator.mineralSanctuary23().isEmpty() ? 1 : 0) + ((generator.rootGeneration() == 22 || generator.rootGeneration() == 23)
return plan.sites().size() + ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24) && !generator.mineralSanctuary23().isEmpty() ? 1 : 0) + ((generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))
? (int) generator.transitPlan21().modules().stream().filter(m -> m.kind() == fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind.ACCESS).count()
+ generator.undergroundHall22().networks().size() : 0);
}))));
@@ -14,7 +14,7 @@ public final class HeritageStructure19 extends Structure {
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) || !generator.experimentalStructures()) return Optional.empty();
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || !generator.experimentalStructures()) return Optional.empty();
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
if (island == null || !island.origin() || (island.generation() != generator.rootGeneration())) return Optional.empty();
var plan = generator.heritagePlan19();
@@ -23,9 +23,9 @@ public final class HeritageStructure19 extends Structure {
// Local route segments render before buildings. No piece needs a distant link endpoint loaded.
for (var segment : plan.segments())
if (segment.startChunkX() == x && segment.startChunkZ() == z)
pieces.add((generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || generator.rootGeneration() == 23)) ? new HeritagePiece21(segment) : generator.rootGeneration() == 20 ? new HeritagePiece20(segment) : new HeritagePiece19(segment));
pieces.add((generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) ? new HeritagePiece21(segment) : generator.rootGeneration() == 20 ? new HeritagePiece20(segment) : new HeritagePiece19(segment));
for (var site : plan.sites()) if (site.startChunkX() == x && site.startChunkZ() == z)
pieces.add((generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || generator.rootGeneration() == 23)) ? new HeritagePiece21(site) : generator.rootGeneration() == 20 ? new HeritagePiece20(site) : new HeritagePiece19(site));
pieces.add((generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) ? new HeritagePiece21(site) : generator.rootGeneration() == 20 ? new HeritagePiece20(site) : new HeritagePiece19(site));
if (pieces.isEmpty()) return Optional.empty();
for (var piece : pieces) {
var box = piece.getBoundingBox();
@@ -57,7 +57,7 @@ public final class HeritageStructures19 {
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> level) {
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23))
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)))
|| !sanctuary.experimentalStructures()) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
@@ -79,7 +79,7 @@ public final class HeritageStructures19 {
BlockPos origin, int radius, boolean skipReferenced) {
if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) || !generator.experimentalStructures()
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || !generator.experimentalStructures()
|| !level.structureManager().shouldGenerateStructures()) return null;
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
if (!structures.contains(holder)) return null;
@@ -11,9 +11,9 @@ public final class MineralSanctuaryStructure23 extends Structure {
public MineralSanctuaryStructure23(StructureSettings settings) { super(settings); }
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 23 || !generator.experimentalStructures()) return Optional.empty();
|| (generator.rootGeneration() != 23 && generator.rootGeneration() != 24) || !generator.experimentalStructures()) return Optional.empty();
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
if (island == null || !island.origin() || island.generation() != 23) return Optional.empty();
if (island == null || !island.origin() || (island.generation() != 23 && island.generation() != 24)) return Optional.empty();
var plan = generator.mineralSanctuary23();
if (plan.isEmpty()) return Optional.empty();
var c = plan.chamber();
@@ -40,9 +40,9 @@ public final class MineralSanctuaryStructures23 {
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> level) {
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || generator.rootGeneration() != 23 || !sanctuary.experimentalStructures()) return;
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 23 && generator.rootGeneration() != 24) || !sanctuary.experimentalStructures()) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || island.generation() != 23) return;
if (island == null || !island.origin() || (island.generation() != 23 && island.generation() != 24)) return;
var plan = sanctuary.mineralSanctuary23();
if (plan.isEmpty() || plan.chamber().startChunkX() != chunk.getPos().x()
|| plan.chamber().startChunkZ() != chunk.getPos().z()) return;
@@ -63,14 +63,14 @@ public final class MineralSanctuaryStructures23 {
BlockPos origin, int radius, boolean skipReferenced) {
if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 23 || !level.structureManager().shouldGenerateStructures()) return null;
|| (generator.rootGeneration() != 23 && generator.rootGeneration() != 24) || !level.structureManager().shouldGenerateStructures()) return null;
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
if (!structures.contains(holder)) return null;
var plan = generator.mineralSanctuary23();
if (plan.isEmpty() || !generator.experimentalStructures()) return null;
var c = plan.chamber();
var island = generator.islandForChunk(c.startChunkX(), c.startChunkZ());
if (island == null || !island.origin() || island.generation() != 23 || !island.status().equals("ready")) return null;
if (island == null || !island.origin() || (island.generation() != 23 && island.generation() != 24) || !island.status().equals("ready")) return null;
if (Math.abs((long) c.startChunkX() - (origin.getX() >> 4)) > radius
|| Math.abs((long) c.startChunkZ() - (origin.getZ() >> 4)) > radius) return null;
var chunk = level.getChunk(c.startChunkX(), c.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
@@ -13,7 +13,7 @@ public final class RuinsStructure17 extends Structure {
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) || !generator.experimentalStructures()) return Optional.empty();
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || !generator.experimentalStructures()) return Optional.empty();
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return Optional.empty();
var networks = generator.ruinsPlan17().networks().stream()
@@ -30,9 +30,9 @@ public final class RuinsStructure17 extends Structure {
return Optional.of(new GenerationStub(RuinsStructures17.locator(networks.getFirst()), pieces -> {
// Shafts come last so their surface hatches cannot be filled by a connected room.
networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() != RuinsPlan17.Kind.ACCESS)
.forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || generator.rootGeneration() == 23)) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
.forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS)
.forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || generator.rootGeneration() == 23)) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
.forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
}));
}
@Override public StructureType<?> type() { return RuinsStructures17.TYPE; }
@@ -55,7 +55,7 @@ public final class RuinsStructures17 {
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> level) {
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23))
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)))
|| !sanctuary.experimentalStructures()) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
@@ -78,7 +78,7 @@ public final class RuinsStructures17 {
BlockPos origin, int radius, boolean skipReferenced) {
if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) || !generator.experimentalStructures()
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || !generator.experimentalStructures()
|| !level.structureManager().shouldGenerateStructures()) return null;
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
if (!structures.contains(holder)) return null;
@@ -12,9 +12,9 @@ public final class TransitStructure21 extends Structure {
public TransitStructure21(StructureSettings settings) { super(settings); }
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) || (generator.rootGeneration() != 22 && generator.rootGeneration() != 23) && !generator.experimentalStructures()) return Optional.empty();
|| (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)) && !generator.experimentalStructures()) return Optional.empty();
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
if (island == null || !island.origin() || (island.generation() != 21 && (island.generation() != 22 && island.generation() != 23))) return Optional.empty();
if (island == null || !island.origin() || (island.generation() != 21 && (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24)))) return Optional.empty();
var modules = generator.transitPlan21().modulesForChunk(context.chunkPos().x(), context.chunkPos().z());
if (modules.isEmpty()) return Optional.empty();
for (var module : modules) {
@@ -25,7 +25,7 @@ public final class TransitStructure21 extends Structure {
throw new IllegalStateException("Transit module exceeds local native reference range");
}
return Optional.of(new GenerationStub(TransitPiece21.locator(modules.getFirst()), pieces ->
modules.forEach(module -> pieces.addPiece((generator.rootGeneration() == 22 || generator.rootGeneration() == 23)
modules.forEach(module -> pieces.addPiece((generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))
&& module.kind() == TransitPlan21.Kind.JUNCTION && Integer.bitCount(module.openings()) == 2
? new TransitPiece22(module) : new TransitPiece21(module)))));
}
@@ -46,10 +46,10 @@ public final class TransitStructures21 {
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> level) {
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23))
|| (generator.rootGeneration() != 22 && generator.rootGeneration() != 23) && !sanctuary.experimentalStructures()) return;
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)))
|| (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)) && !sanctuary.experimentalStructures()) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || (island.generation() != 21 && (island.generation() != 22 && island.generation() != 23))) return;
if (island == null || !island.origin() || (island.generation() != 21 && (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24)))) return;
if (sanctuary.transitPlan21().modulesForChunk(chunk.getPos().x(), chunk.getPos().z()).isEmpty()) return;
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
StructureStart previous = manager.getStartForStructure(holder.value(), chunk);
@@ -66,7 +66,7 @@ public final class TransitStructures21 {
BlockPos origin, int radius, boolean skipReferenced) {
if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) || (generator.rootGeneration() != 22 && generator.rootGeneration() != 23) && !generator.experimentalStructures()
|| (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)) && !generator.experimentalStructures()
|| !level.structureManager().shouldGenerateStructures()) return null;
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
if (!structures.contains(holder)) return null;
@@ -225,5 +225,17 @@
"sanctuary.structures.census.unknown_dimension": "Unknown dimension: %s",
"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.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.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",
"sanctuary.structures.census.aerial.status.planned": "planned",
"sanctuary.structures.census.aerial.status.accepted": "placement accepted",
"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"
}
@@ -225,5 +225,17 @@
"sanctuary.structures.census.unknown_dimension": "Dimension inconnue : %s",
"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.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.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",
"sanctuary.structures.census.aerial.status.planned": "planifié",
"sanctuary.structures.census.aerial.status.accepted": "implantation acceptée",
"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"
}
@@ -0,0 +1,127 @@
{
"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,176 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:bread",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 24,
"max": 40
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:carrot",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 16,
"max": 32
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:potato",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 16,
"max": 32
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:wheat_seeds",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 16,
"max": 32
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:pumpkin_seeds",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 8,
"max": 16
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:melon_seeds",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 8,
"max": 16
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:cooked_beef",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 8,
"max": 16
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:golden_apple",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
}
}
]
}
]
},
{
"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,146 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:iron_pickaxe"
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:iron_axe"
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:iron_shovel"
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:iron_ingot",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 24,
"max": 40
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:copper_ingot",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 24,
"max": 40
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:coal",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 24,
"max": 40
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:redstone",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 16,
"max": 32
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:diamond",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 2,
"max": 5
}
}
]
}
]
},
{
"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,157 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:diamond",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 3,
"max": 7
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:emerald",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 8,
"max": 20
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:gold_ingot",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 12,
"max": 24
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:iron_ingot",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 24,
"max": 48
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:lapis_lazuli",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 24,
"max": 48
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:redstone",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 24,
"max": 48
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:amethyst_shard",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 12,
"max": 24
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:golden_apple",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,10 @@
{
"type": "sanctuary:aerial_v24",
"family": "balloon",
"biomes": [
"minecraft:the_void"
],
"step": "top_layer_modification",
"terrain_adaptation": "none",
"spawn_overrides": {}
}
@@ -0,0 +1,10 @@
{
"type": "sanctuary:aerial_v24",
"family": "merchant_ship",
"biomes": [
"minecraft:the_void"
],
"step": "top_layer_modification",
"terrain_adaptation": "none",
"spawn_overrides": {}
}
@@ -0,0 +1,10 @@
{
"type": "sanctuary:aerial_v24",
"family": "mineral_islet",
"biomes": [
"minecraft:the_void"
],
"step": "top_layer_modification",
"terrain_adaptation": "none",
"spawn_overrides": {}
}
@@ -3,7 +3,7 @@
"minecraft:overworld": {
"type": "sanctuary:sanctuary_384",
"generator": {
"type": "sanctuary:island_v23",
"type": "sanctuary:island_v24",
"biome_source": {
"biomes": [
"minecraft:the_void",
@@ -64,7 +64,7 @@
"minecraft:snowy_beach",
"minecraft:stony_shore"
],
"type": "sanctuary:island_v23"
"type": "sanctuary:island_v24"
},
"initial_diameter": 724
}
@@ -0,0 +1,94 @@
{
"dimensions": {
"minecraft:overworld": {
"type": "sanctuary:sanctuary_384",
"generator": {
"type": "sanctuary:island_v23",
"biome_source": {
"biomes": [
"minecraft:the_void",
"minecraft:forest",
"minecraft:birch_forest",
"minecraft:flower_forest",
"minecraft:plains",
"minecraft:meadow",
"minecraft:taiga",
"minecraft:snowy_taiga",
"minecraft:snowy_plains",
"minecraft:grove",
"minecraft:ice_spikes",
"minecraft:frozen_peaks",
"minecraft:old_growth_spruce_taiga",
"minecraft:dark_forest",
"minecraft:swamp",
"minecraft:pale_garden",
"minecraft:jungle",
"minecraft:sparse_jungle",
"minecraft:bamboo_jungle",
"minecraft:mangrove_swamp",
"minecraft:savanna",
"minecraft:savanna_plateau",
"minecraft:desert",
"minecraft:badlands",
"minecraft:eroded_badlands",
"minecraft:wooded_badlands",
"minecraft:windswept_hills",
"minecraft:windswept_gravelly_hills",
"minecraft:stony_peaks",
"minecraft:dappled_forest",
"minecraft:cherry_grove",
"minecraft:lush_caves",
"minecraft:dripstone_caves",
"minecraft:sulfur_caves",
"sanctuary:population_oak_forest",
"sanctuary:population_birch_forest",
"sanctuary:population_clearing",
"sanctuary:population_dry_woodland",
"sanctuary:population_rocky_heath",
"sanctuary:population_dark_grove",
"sanctuary:population_bamboo_grove",
"sanctuary:population_sulfur_depths",
"sanctuary:population_lush_caves",
"sanctuary:population_dripstone_caves",
"sanctuary:lost_city",
"minecraft:ocean",
"minecraft:deep_ocean",
"minecraft:cold_ocean",
"minecraft:deep_cold_ocean",
"minecraft:frozen_ocean",
"minecraft:deep_frozen_ocean",
"minecraft:warm_ocean",
"minecraft:lukewarm_ocean",
"minecraft:deep_lukewarm_ocean",
"minecraft:beach",
"minecraft:snowy_beach",
"minecraft:stony_shore"
],
"type": "sanctuary:island_v23"
},
"initial_diameter": 724
}
},
"minecraft:the_end": {
"type": "minecraft:the_end",
"generator": {
"type": "minecraft:noise",
"biome_source": {
"type": "minecraft:the_end"
},
"settings": "minecraft:end"
}
},
"minecraft:the_nether": {
"type": "minecraft:the_nether",
"generator": {
"type": "minecraft:noise",
"biome_source": {
"type": "minecraft:multi_noise",
"preset": "minecraft:nether"
},
"settings": "minecraft:nether"
}
}
}
}
@@ -0,0 +1,170 @@
package fr.koka.sanctuary.worldgen.aerial;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import java.util.HashSet;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.server.Bootstrap;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
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) {
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();
for (int size : new int[]{512, 724, 1024}) for (long seed : new long[]{0, 42, 2026}) {
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));
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().stream().filter(AerialPlan24.Site::waterfall).count() == 1, "Exactly one waterfall site");
var allClaims = new HashSet<Long>();
var root = ExpansionIsland.origin(size, seed, 24);
for (var site : plan.sites()) {
var physicalChunks = new HashSet<>(site.bounds().intersectingChunks().toList());
if (site.waterfall()) physicalChunks.addAll(site.waterfallClaimBounds().intersectingChunks().toList());
require(physicalChunks.equals(new HashSet<>(site.claimedChunks())),
"Claim contains only body chunks and the local waterfall outlet, without a generic margin");
require(site.maxY() < 350 && site.minY() >= 32, "The entire site fits the dimension height");
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET)
require(site.rockHeight() >= 20 && site.rockHeight() <= 35
&& site.height() == site.rockHeight() + AerialPlan24.VEGETATION_HEADROOM,
"Vegetation adds eight blocks of headroom without enlarging the stone body");
double distance = Math.hypot(site.locator().getX() - site.shoreX(), site.locator().getZ() - site.shoreZ());
require(distance <= 512, "Every site has a nearby shore witness");
require(plan.atStartChunk(site.startChunkX(), site.startChunkZ()).contains(site), "Native departure belongs to its saved site");
for (var chunk : site.claimedChunks()) {
require(allClaims.add(chunk.pack()), "Site claims do not overlap");
require(site.ownsChunk(chunk.x(), chunk.z()) && plan.ownsChunk(chunk.x(), chunk.z()), "Claim query agrees with the immutable mask");
require(!root.ownsChunk(chunk.x(), chunk.z()), "Claim does not replace initial island ownership");
}
var overlapping = new ExpansionIsland("overlap", "sanctuary", "east", "temperate", 64,
site.startChunkX() * 16, site.startChunkZ() * 16, 42, "natural", "reserved", 24);
require(plan.conflicts(overlapping), "Expansion conflicts are known before visiting or generating a site");
var local = new HashSet<BlockPos>();
for (int face = 0; face < 4; face++) {
var rotated = rotate(site, face); local.clear();
for (int x = 0; x < site.width(); x++) for (int z = 0; z < site.depth(); z++) {
var at = new BlockPos(rotated.worldX(x, z), site.minY(), rotated.worldZ(x, z));
require(rotated.bounds().isInside(at) && rotated.mayWrite(at), "Rotations stay inside their world bounds");
require(local.add(at), "Rotation never folds two local columns together");
}
}
require(!site.mayWrite(new BlockPos(site.maxX() + 2, site.minY(), site.maxZ() + 2)), "No writes outside the finite envelope");
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");
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");
try { plan.sites().clear(); throw new AssertionError("Mutable site list"); }
catch (UnsupportedOperationException expected) { }
try { plan.claimsChunks().clear(); throw new AssertionError("Mutable claims"); }
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"); }
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.");
}
private static void verifyReservedShoreAndView(BlockState[] column) {
var original = AerialPlanner24.plan(0, 724, (x, z) -> new NoiseColumn(0, column)).sites().getFirst();
for (boolean obstructView : new boolean[]{false, true}) {
int cx = original.minX() + original.worldWidth() / 2;
int cz = original.minZ() + original.worldDepth() / 2;
int blockedX = obstructView ? (original.shoreX() + cx) / 2 : original.shoreX();
int blockedZ = obstructView ? (original.shoreZ() + cz) / 2 : original.shoreZ();
int[] refused = {0}, columns = {0};
var terrain = new AerialPlanner24.Terrain() {
public NoiseColumn column(int x, int z) { columns[0]++; return new NoiseColumn(0, column); }
public boolean reserved(int x, int z, int minY, int maxY) {
boolean blocked = Math.abs(x - blockedX) <= 8 && Math.abs(z - blockedZ) <= 8;
if (blocked) refused[0]++;
return blocked;
}
};
var replacement = AerialPlanner24.plan(0, 724, terrain);
require(refused[0] > 0 && !replacement.sites().getFirst().equals(original),
obstructView ? "A secret across the view segment rejects the original candidate" : "A secret over the shore rejects the original witness");
require(columns[0] <= AerialPlanner24.MAX_COLUMNS, "Reservations preserve the bounded terrain sampling budget");
for (var site : replacement.sites())
require(!terrain.reserved(site.shoreX(), site.shoreZ(), site.shoreY() - 1, site.shoreY() + 2),
"Every accepted shore stays outside planned root structures");
}
}
private static AerialPlan24.Site rotate(AerialPlan24.Site site, int facing) {
return new AerialPlan24.Site(site.id(), site.kind(), site.minX(), site.minY(), site.minZ(), site.width(), site.height(), site.depth(),
facing, site.seed(), site.shoreX(), site.shoreY(), site.shoreZ(), site.waterfall());
}
private static void verifyRender(AerialPlan24.Site site) {
Map<BlockPos, BlockState> complete = new HashMap<>(), clipped = new HashMap<>();
int[] chests = {0};
var writer = writer(site, complete, chests);
AerialRenderer24.render(site, writer);
require(chests[0] == 1 && complete.get(AerialGeometry24.chest(site)).is(Blocks.CHEST), "Each site has exactly one real chest");
int[] clippedChests = {0};
for (var chunk : site.bounds().intersectingChunks().toList())
AerialRenderer24.render(site, writer(site, clipped, clippedChests),
new BoundingBox(chunk.getMinBlockX(), 0, chunk.getMinBlockZ(), chunk.getMaxBlockX(), 383, chunk.getMaxBlockZ()));
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.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");
require(complete.values().stream().anyMatch(state -> state.is(Blocks.COAL_ORE)), "Real mineable coal is available on the mineral islet");
require(complete.values().stream().anyMatch(state -> state.is(Blocks.IRON_ORE) || state.is(Blocks.DEEPSLATE_IRON_ORE)), "Real mineable iron is available");
int surface = 0, grass = 0;
for (int x = 0; x < site.width(); x++) for (int z = 0; z < site.depth(); z++) {
int top = AerialGeometry24.terrainTop(site, x, z);
if (!AerialGeometry24.mineralBody(site, x, top, z)) continue;
surface++;
if (complete.getOrDefault(AerialGeometry24.world(site, x, top, z), Blocks.AIR.defaultBlockState()).is(Blocks.GRASS_BLOCK)) grass++;
}
require(grass * 2 > surface, "Grass covers most intact surface columns while fissures retain exposed rock");
var trees = AerialGeometry24.trees(site);
require(!trees.isEmpty(), "Each mineral islet supports a small woodland");
for (var tree : trees) {
var ground = AerialGeometry24.world(site, tree.x(), tree.groundY(), tree.z());
require(complete.get(ground).is(Blocks.GRASS_BLOCK), "Tree trunk is rooted in the real soil surface");
for (int y = 1; y <= tree.trunkHeight(); y++)
require(complete.get(ground.above(y)).is(tree.birch() ? Blocks.BIRCH_LOG : Blocks.OAK_LOG), "Tree trunk remains continuous");
require(complete.get(ground.above(tree.trunkHeight() + 1)).is(tree.birch() ? Blocks.BIRCH_LEAVES : Blocks.OAK_LEAVES),
"Tree crown fits entirely within its saved envelope");
}
for (var state : complete.values()) if (state.is(Blocks.OAK_LEAVES) || state.is(Blocks.BIRCH_LEAVES))
require(!state.getValue(BlockStateProperties.PERSISTENT) && state.getValue(BlockStateProperties.DISTANCE) < 7,
"Natural leaves have a log connection and may decay after players cut the tree");
}
}
private static LostCityRenderer.BlockWriter writer(AerialPlan24.Site site, Map<BlockPos, BlockState> blocks, int[] chests) {
return new LostCityRenderer.BlockWriter() {
public void set(int x, int y, int z, BlockState state) {
var pos = new BlockPos(site.minX() + x, site.minY() + y, site.minZ() + z);
require(site.bounds().isInside(pos), "Authored placement stays inside its saved native bounds"); blocks.put(pos, state);
}
public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
chests[0]++; require(loot.equals(AerialRenderer24.loot(site)), "Each chest uses its site's distinct loot family");
set(x, y, z, Blocks.CHEST.defaultBlockState());
}
};
}
private static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); }
}
@@ -0,0 +1,157 @@
package fr.koka.sanctuary.worldgen.aerial;
import com.google.gson.JsonParser;
import com.mojang.serialization.JsonOps;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import fr.koka.sanctuary.expansion.ExpansionJournal;
import fr.koka.sanctuary.expansion.ExpansionPlacement;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HexFormat;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import fr.koka.sanctuary.worldgen.aerial.AerialPlan24.Kind;
import fr.koka.sanctuary.worldgen.aerial.AerialPlan24.Site;
/** Real filesystem transaction checks; no Minecraft save, chunk or personal instance is opened. */
public final class AerialPersistence24Smoke {
private static final long SEED = Long.MIN_VALUE + 824;
private AerialPersistence24Smoke() {}
public static void main(String[] args) throws Exception {
Path scratch = Files.createTempDirectory("sanctuary-aerial-persistence24-");
try {
Path legacyDirectory = scratch.resolve("legacy23");
var legacy = new ExpansionJournal(legacyDirectory, SEED, 724, 23);
Path legacyFile = legacyDirectory.resolve(ExpansionJournal.FILE_NAME);
byte[] legacyBytes = Files.readAllBytes(legacyFile);
Path directory = scratch.resolve("sanctuary-world-v24");
Path storeDirectory = AerialStore24.directory(directory);
reject(() -> new ExpansionJournal(directory, SEED, 724, 24), "New journal without an immutable plan");
require(!Files.exists(directory), "Rejected unbound journal must not leave an initialized directory");
var draws = new AtomicInteger();
var stored = AerialStore24.create(storeDirectory, SEED, 724, () -> { draws.incrementAndGet(); return fixture(); });
for (var site : stored.plan().sites()) {
if (site.kind() == Kind.MINERAL_ISLET) {
require(site.rockHeight() >= 20 && site.rockHeight() <= 35, "Vegetation does not reduce the rock thickness");
require(site.height() == site.rockHeight() + 8 && site.maxY() < 350, "The persisted envelope includes eight blocks of vegetation below the ceiling");
}
var expectedClaims = new java.util.HashSet<>(site.bounds().intersectingChunks().toList());
if (site.waterfall()) expectedClaims.addAll(site.waterfallClaimBounds().intersectingChunks().toList());
require(expectedClaims.equals(java.util.Set.copyOf(site.claimedChunks())), "Claims contain only body chunks and the local waterfall outlet");
for (var chunk : expectedClaims) require(site.ownsChunk(chunk.x(), chunk.z()), "Ownership agrees with the exact finite mask");
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");
var journal = new ExpansionJournal(directory, SEED, 724, 24, stored.token());
var root = ExpansionIsland.origin(724, SEED, 24);
Path journalFile = directory.resolve(ExpansionJournal.FILE_NAME);
byte[] pristineJournal = Files.readAllBytes(journalFile);
require(JsonParser.parseString(Files.readString(journalFile)).getAsJsonObject().get("schema").getAsInt() == 2,
"Only the new generation24 journal uses its new schema");
var reloaded = new ExpansionJournal(directory, SEED, 724, 24);
require(reloaded.aerialToken().equals(stored.token()) && reloaded.aerialPlan24().sites().equals(stored.plan().sites()),
"Reload obtains the exact stored sites and token without calling a planner");
require(draws.get() == 1, "Reload must not draw another layout");
require(reloaded.aerialPlan24().sites().stream().filter(s -> s.kind() == Kind.MINERAL_ISLET)
.map(Site::rockHeight).toList().equals(List.of(28, 32)),
"Reload preserves the original rock heights independently from vegetation headroom");
journal.commit(List.of(root));
require(Arrays.equals(pristineJournal, Files.readAllBytes(journalFile)), "A no-op commit preserves bytes");
var collision = child("collision", 672, 0);
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");
require(journal.healthy() && Arrays.equals(pristineJournal, Files.readAllBytes(journalFile)),
"Ordinary rejected geometry preserves the healthy journal");
var free = ExpansionPlacement.candidates(SEED, List.of(root), "free_space", "sanctuary", "south", "temperate", 64, "natural", 0)
.stream().filter(value -> !stored.plan().conflicts(value)).findFirst().orElseThrow();
journal.commit(List.of(root, free));
require(new ExpansionJournal(directory, SEED, 724, 24).islands().equals(List.of(root, free)),
"Claims do not immobilize the complete surrounding ring");
byte[] withFree = Files.readAllBytes(journalFile);
var poisoned = JsonParser.parseString(Files.readString(journalFile)).getAsJsonObject();
poisoned.getAsJsonArray("islands").add(ExpansionIsland.CODEC.encodeStart(JsonOps.INSTANCE, collision).getOrThrow());
Files.writeString(journalFile, poisoned.toString());
byte[] poisonedBytes = Files.readAllBytes(journalFile);
reject(() -> new ExpansionJournal(directory, SEED, 724, 24), "Reload with an externally introduced claim overlap");
require(Arrays.equals(poisonedBytes, Files.readAllBytes(journalFile)), "Rejected reload does not repair or rewrite history");
Files.write(journalFile, withFree);
Path manifestFile = storeDirectory.resolve(stored.token() + ".json");
byte[] manifestBytes = Files.readAllBytes(manifestFile);
Files.writeString(manifestFile, "{}");
reject(() -> AerialStore24.load(storeDirectory, stored.token(), SEED, 724), "Changed manifest bytes");
reject(() -> new ExpansionJournal(directory, SEED, 724, 24), "Changed manifest linked by an existing journal");
require(Arrays.equals(withFree, Files.readAllBytes(journalFile)), "Manifest corruption leaves the journal untouched");
Files.write(manifestFile, manifestBytes);
Files.delete(manifestFile);
reject(() -> new ExpansionJournal(directory, SEED, 724, 24), "Missing manifest cannot be regenerated on reload");
reject(() -> journal.commit(List.of(root, free.ready())), "Missing manifest stops later commits too");
require(!journal.healthy(), "Uncertain manifest authority stops the current session");
Files.write(manifestFile, manifestBytes);
require(new ExpansionJournal(directory, SEED, 724, 24).islands().equals(List.of(root, free)), "Explicit reload restores a repaired authority");
reject(() -> AerialStore24.load(storeDirectory, stored.token(), SEED + 1, 724), "Other world seed");
reject(() -> AerialStore24.load(storeDirectory, stored.token(), SEED, 1024), "Other initial size");
reject(() -> AerialStore24.load(storeDirectory, "../manifest", SEED, 724), "Non-digest token");
String duplicate = new String(manifestBytes, StandardCharsets.UTF_8).replaceFirst("\\{", "{\"schema\":1,");
String duplicateToken = digest(duplicate.getBytes(StandardCharsets.UTF_8));
Files.writeString(storeDirectory.resolve(duplicateToken + ".json"), duplicate);
reject(() -> AerialStore24.load(storeDirectory, duplicateToken, SEED, 724), "A correctly hashed manifest still rejects duplicate fields");
for (String option : List.of("global-off", "experimental-off")) {
Path offDirectory = scratch.resolve(option).resolve("sanctuary-world-v24");
var empty = AerialStore24.create(AerialStore24.directory(offDirectory), SEED, 724, () -> AerialPlan24.EMPTY);
var off = new ExpansionJournal(offDirectory, SEED, 724, 24, empty.token());
require(off.aerialPlan24().isEmpty() && off.aerialPlan24().claimsChunks().isEmpty(), "Disabled plan reserves no aerial sites");
require(new ExpansionJournal(offDirectory, SEED, 724, 24).aerialPlan24().isEmpty(), "Disabled plan stays disabled after reload");
off.commit(List.of(root, collision));
}
var alternative = AerialStore24.create(storeDirectory, SEED, 724, () -> AerialPlan24.EMPTY);
var swapped = JsonParser.parseString(Files.readString(journalFile)).getAsJsonObject();
swapped.addProperty("aerial_plan", alternative.token());
var healthy = new ExpansionJournal(directory, SEED, 724, 24);
Files.writeString(journalFile, swapped.toString());
reject(() -> healthy.commit(List.of(root, free.ready())), "Another valid manifest cannot replace the current session's immutable layout");
Files.write(journalFile, withFree);
reject(() -> new ExpansionJournal(legacyDirectory, SEED, 724, 24), "No implicit migration from23");
reject(() -> new ExpansionJournal(directory, SEED, 724, 23), "No implicit downgrade to23");
legacy.commit(legacy.islands());
require(Arrays.equals(legacyBytes, Files.readAllBytes(legacyFile)), "Historical23 bytes remain identical");
require(!Files.readString(legacyFile).contains("aerial_plan") && !legacy.aerialPlan24().conflicts(collision), "No aerial authority added to23");
System.out.println("Aerial persistence24: manifest-first bootstrap, strict reload, claim conflicts, disabled plans and legacy23 passed");
} finally {
try (var paths = Files.walk(scratch)) {
for (Path path : paths.sorted(Comparator.reverseOrder()).toList()) Files.deleteIfExists(path);
}
}
}
private static ExpansionIsland child(String id, int x, int z) {
return new ExpansionIsland(id, "sanctuary", "east", "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("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)));
}
private static String digest(byte[] bytes) throws Exception { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes)); }
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
private static void reject(IOAction action, String message) throws Exception {
try { action.run(); } catch (IOException expected) { return; }
throw new AssertionError("Expected rejection: " + message);
}
@FunctionalInterface private interface IOAction { void run() throws Exception; }
}