Deliver alpha.30 plateaus, open river banks and underground routes
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -657,3 +657,11 @@ tasks.named('check') { dependsOn('terraces29Smoke') }
|
||||
}
|
||||
tasks.named('check') { dependsOn(taskName) }
|
||||
}
|
||||
|
||||
tasks.register('river30Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn tasks.named('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.River30Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('river30Smoke') }
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka.sanctuary.worldgen.*;
|
||||
import java.nio.file.*;
|
||||
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.TicketType;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
|
||||
public final class River30WorldGameTests {
|
||||
@GameTest(maxTicks=12000)
|
||||
public void riverAndOpenBanksUseRealTerrain(GameTestHelper helper) throws Exception {
|
||||
var level=helper.getLevel(); var generator=(SanctuaryChunkGenerator)level.getChunkSource().getGenerator();
|
||||
var binding=generator.starterContext();var plan=PopulationHydrologyRuntime.river30(binding.generator(),binding.random());
|
||||
var report=new LinkedHashMap<String,Object>();report.put("seed",level.getSeed());report.put("diameter",generator.initialDiameter());
|
||||
report.put("process_id",ProcessHandle.current().pid());report.put("river_cells",plan.cells().size());report.put("features",plan.features());report.put("spills",plan.spills());
|
||||
report.put("mineral",generator.mineralSanctuary23());
|
||||
helper.assertTrue(plan.features().stream().anyMatch(f->f.kind()==PopulationHydrology.Kind.RIVER),"An island-wide river must be present in the release witness");
|
||||
if(level.structureManager().shouldGenerateStructures()&&generator.experimentalStructures())
|
||||
helper.assertFalse(generator.mineralSanctuary23().isEmpty(),"The mineral sanctuary must be present in the release witness");
|
||||
var selected=plan.cells().stream().filter(PopulationHydrology.Cell::hasWater).toList();
|
||||
var witnesses=new ArrayList<PopulationHydrology.Cell>();
|
||||
for(int i=0;i<6;i++)witnesses.add(selected.get(i*(selected.size()-1)/5));
|
||||
var falls=plan.spills().stream().limit(2).toList();var chunks=new LinkedHashSet<ChunkPos>();
|
||||
witnesses.forEach(c->chunks.add(new ChunkPos(c.x()>>4,c.z()>>4)));
|
||||
for(var fall:falls){chunks.add(new ChunkPos(fall.source().x()>>4,fall.source().z()>>4));chunks.add(new ChunkPos(fall.outlet().x()>>4,fall.outlet().z()>>4));}
|
||||
var mineral=generator.mineralSanctuary23().chamber();
|
||||
if(mineral!=null){
|
||||
var key=fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.KEY;
|
||||
var structure=level.registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.STRUCTURE).getOrThrow(key).value();
|
||||
var cp=new ChunkPos(mineral.startChunkX(),mineral.startChunkZ());
|
||||
var start=level.getChunk(cp.x(),cp.z(),ChunkStatus.STRUCTURE_STARTS,true).getStartForStructure(structure);
|
||||
helper.assertTrue(start!=null&&start.isValid()&&start.getPieces().size()==1,"The mineral plan produces its actual native structure start");
|
||||
chunks.add(cp);
|
||||
}
|
||||
var iterator=chunks.iterator();Runnable[] step=new Runnable[1];
|
||||
step[0]=()->{
|
||||
if(iterator.hasNext()) {var c=iterator.next();level.getChunk(c.x(),c.z(),ChunkStatus.FULL,true);helper.runAtTickTime(helper.getTick()+1,()->step[0].run());return;}
|
||||
chunks.forEach(c->level.getChunkSource().addTicketWithRadius(TicketType.DRAGON,c,2));
|
||||
helper.runAtTickTime(helper.getTick()+120,()->{
|
||||
try {
|
||||
if(mineral!=null){
|
||||
var floor=new BlockPos(mineral.centerX(),mineral.minY()+2,mineral.centerZ());
|
||||
helper.assertTrue(level.getBlockState(floor).isSolid()&&level.getBlockState(floor.above()).isAir(),"The central mineral floor and guardian space are really generated");
|
||||
report.put("mineral_native_floor",List.of(floor.getX(),floor.getY(),floor.getZ()));
|
||||
}
|
||||
for(var c:witnesses)helper.assertTrue(!level.getFluidState(new BlockPos(c.x(),c.waterY(),c.z())).isEmpty(),"Generated river water remains at its supported local bed");
|
||||
int flowing=0;
|
||||
for(var fall:falls)for(int dx=-1;dx<=1;dx++)for(int dz=-1;dz<=1;dz++)
|
||||
if(!level.getFluidState(new BlockPos(fall.outlet().x()+dx,fall.outlet().y()-3,fall.outlet().z()+dz)).isEmpty()){flowing++;}
|
||||
if(!falls.isEmpty())helper.assertTrue(flowing>0,"Vanilla fluid ticks carry water below at least one opened bank");
|
||||
report.put("full_chunks",chunks.size());report.put("flowing_below_openings",flowing);report.put("passed",true);
|
||||
Path out=Path.of("diagnostics/alpha30/river.json");Files.createDirectories(out.getParent());Files.writeString(out,new GsonBuilder().setPrettyPrinting().create().toJson(report));helper.succeed();
|
||||
}catch(Exception error){throw new RuntimeException(error);}finally{chunks.forEach(c->level.getChunkSource().removeTicketWithRadius(TicketType.DRAGON,c,2));}
|
||||
});
|
||||
};step[0].run();
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -22,7 +22,7 @@ public final class Terraces29WorldGameTests {
|
||||
var field = (PopulationIslandDensity) level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION)
|
||||
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION,
|
||||
SanctuaryMod.id("population_" + generator.capacity().players()))).value();
|
||||
helper.assertTrue(field.terraces29(), "Current resources enable the fine ledges");
|
||||
helper.assertTrue(field.relief30() && !field.terraces29(), "Alpha30 localizes deformation and disables periodic micro-ledges");
|
||||
var random = generator.starterContext().random();
|
||||
var current = random.getSampler(field); var previous = random.getSampler(field.legacy28());
|
||||
var context = SamplerContext.builder().enableCaches().build();
|
||||
@@ -31,19 +31,19 @@ public final class Terraces29WorldGameTests {
|
||||
report.put("process_id", ProcessHandle.current().pid());
|
||||
int radius = Math.min(400, generator.initialDiameter() / 2), lowerSamples = 0, unchanged = 0, changed = 0;
|
||||
for (int z = -radius; z <= radius; z += 32) for (int x = -radius; x <= radius; x += 32) {
|
||||
for (int y = 32; y <= 214; y += 13) {
|
||||
for (int y = 32; y <= 180; y += 13) {
|
||||
helper.assertTrue(Float.floatToIntBits(current.sampleValue(context, x, y, z))
|
||||
== Float.floatToIntBits(previous.sampleValue(context, x, y, z)), "Lower density is exactly alpha28");
|
||||
lowerSamples++;
|
||||
}
|
||||
for (int y = 215; y <= 320; y++) {
|
||||
for (int y = 181; y <= 320; y++) {
|
||||
boolean old = previous.sampleValue(context, x, y, z) > 0;
|
||||
boolean now = current.sampleValue(context, x, y, z) > 0;
|
||||
if (old == now) unchanged++; else changed++;
|
||||
if (Math.hypot(x, z) <= 48) helper.assertTrue(old == now, "Arrival silhouette is unchanged");
|
||||
}
|
||||
}
|
||||
helper.assertTrue(changed > 0 && unchanged > changed * 20L, "The edit produces small ledges, not a replacement of the upper mass");
|
||||
helper.assertTrue(changed > 0 && unchanged > changed * 3L, "Localized relief retains most of the accepted upper mass");
|
||||
report.put("unchanged_lower_density_samples", lowerSamples);
|
||||
report.put("unchanged_upper_samples", unchanged); report.put("changed_upper_samples", changed);
|
||||
var sections = new LinkedHashMap<String, Object>();
|
||||
@@ -60,7 +60,7 @@ public final class Terraces29WorldGameTests {
|
||||
sections.put("z" + z, Map.of("before", before, "after", after, "min_x", -radius, "max_y", 305, "step", 1));
|
||||
}
|
||||
report.put("sections", sections); report.put("passed", true);
|
||||
Path path = Path.of("diagnostics/alpha29", "terraces-" + generator.initialDiameter() + "-" + level.getSeed() + ".json");
|
||||
Path path = Path.of("diagnostics/alpha30", "relief-" + generator.initialDiameter() + "-" + level.getSeed() + ".json");
|
||||
Files.createDirectories(path.getParent());
|
||||
Files.writeString(path, new GsonBuilder().setPrettyPrinting().create().toJson(report));
|
||||
helper.succeed();
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"environment": "*",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"entrypoints": {
|
||||
"fabric-gametest": ["${gametest_entrypoint}", "fr.koka.sanctuary.gametest.Swamp27WorldGameTests", "fr.koka.sanctuary.gametest.Caves28WorldGameTests", "fr.koka.sanctuary.gametest.Terraces29WorldGameTests", "fr.koka.sanctuary.gametest.Transit29WorldGameTests", "fr.koka.sanctuary.gametest.Alpha28NativeStructuresGameTests", "fr.koka.sanctuary.gametest.BlockKnowledgeGameTests", "fr.koka.sanctuary.gametest.CensusGameTestsTerrain", "fr.koka.sanctuary.gametest.CensusStockGameTests", "fr.koka.sanctuary.gametest.MaterialActivityGameTests"],
|
||||
"fabric-gametest": ["${gametest_entrypoint}", "fr.koka.sanctuary.gametest.Swamp27WorldGameTests", "fr.koka.sanctuary.gametest.Caves28WorldGameTests", "fr.koka.sanctuary.gametest.Terraces29WorldGameTests", "fr.koka.sanctuary.gametest.River30WorldGameTests", "fr.koka.sanctuary.gametest.Transit29WorldGameTests", "fr.koka.sanctuary.gametest.Alpha28NativeStructuresGameTests", "fr.koka.sanctuary.gametest.BlockKnowledgeGameTests", "fr.koka.sanctuary.gametest.CensusGameTestsTerrain", "fr.koka.sanctuary.gametest.CensusStockGameTests", "fr.koka.sanctuary.gametest.MaterialActivityGameTests"],
|
||||
"fabric-client-gametest": ["fr.koka.sanctuary.gametest.SanctuaryClientRenderTests"]
|
||||
},
|
||||
"mixins": ["sanctuary-gametest.mixins.json"],
|
||||
|
||||
@@ -31,7 +31,7 @@ public final class PopulationHydrology {
|
||||
@FunctionalInterface
|
||||
public interface Sampler { float sample(int x, int y, int z); }
|
||||
|
||||
public enum Kind { POND, LAKE, RIVER, TERRACE }
|
||||
public enum Kind { POND, LAKE, RIVER, TERRACE, OUTLET }
|
||||
public enum ShoreMaterial { SAND, GRAVEL, CLAY, STONE, GRASS }
|
||||
public record Point(int x, int z) {}
|
||||
public record Bounds(int minX, int minZ, int maxX, int maxZ) {}
|
||||
@@ -133,6 +133,23 @@ public final class PopulationHydrology {
|
||||
public List<Feature> features() { return features; }
|
||||
public boolean isRiverCell(Cell cell) { return features.stream().anyMatch(f -> f.kind() == Kind.RIVER && f.id() == cell.featureId()); }
|
||||
public boolean isTerraceCell(Cell cell) { return features.stream().anyMatch(f -> f.kind() == Kind.TERRACE && f.id() == cell.featureId()); }
|
||||
public boolean isOutletCell(Cell cell) { return features.stream().anyMatch(f -> f.kind() == Kind.OUTLET && f.id() == cell.featureId()); }
|
||||
/** Add an island-wide channel or short bank cuts, retaining the deeper water terraces. */
|
||||
public Plan overlay30(Plan other) {
|
||||
var surface = new LinkedHashMap<Long, Cell>();
|
||||
var lowerCells = new ArrayList<Cell>();
|
||||
for (var cell : allCells) {
|
||||
if (isTerraceCell(cell)) lowerCells.add(cell);
|
||||
else surface.put(key(cell.x(), cell.z()), cell);
|
||||
}
|
||||
for (var cell : other.allCells) surface.put(key(cell.x(), cell.z()), cell);
|
||||
var mergedFeatures = new LinkedHashMap<Long, Feature>();
|
||||
features.forEach(f -> mergedFeatures.put(f.id(), f));
|
||||
other.features.forEach(f -> mergedFeatures.put(f.id(), f));
|
||||
var mergedSpills = new ArrayList<>(spills); mergedSpills.addAll(other.spills);
|
||||
var mergedCells = new ArrayList<>(surface.values()); mergedCells.addAll(lowerCells);
|
||||
return rebuild(mergedCells, List.copyOf(mergedFeatures.values()), springs, springFlow, terraces, mergedSpills);
|
||||
}
|
||||
public List<Terrace> terraces() { return terraces; }
|
||||
public List<Spill> spills() { return spills; }
|
||||
public boolean isSpillOpening(int x, int y, int z) { return spillOpenings.contains(new Position(x, y, z)); }
|
||||
@@ -254,6 +271,10 @@ public final class PopulationHydrology {
|
||||
public static Plan create(long seed, Sampler sampler) {
|
||||
return new Planner(seed, sampler).build();
|
||||
}
|
||||
static Plan additional30(Map<Long, Cell> cells, List<Feature> features, List<Spill> spills, int columns, int samples) {
|
||||
return new Plan(cells, features, List.of(), Set.of(), columns, samples,
|
||||
new PopulationTerraces.Result(List.of(), List.of(), List.of(), spills, Set.of()));
|
||||
}
|
||||
|
||||
private record Column(int top, int solidBottom) {}
|
||||
private record Candidate(int x, int z, double score) {}
|
||||
|
||||
+24
@@ -17,6 +17,25 @@ public final class PopulationHydrologyRuntime {
|
||||
public record RegionPlan(IslandCapacity.Region region, PopulationHydrology.Plan water, PopulationLavaDeposit.Plan lava) {}
|
||||
private static final Map<RandomState, PopulationPlanCache<IslandCapacity.Region, RegionPlan>> WORLDS =
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
private static final Map<RandomState, PopulationHydrology.Plan> RIVERS30 = new WeakHashMap<>();
|
||||
public static boolean current30(NoiseBasedChunkGenerator generator) {
|
||||
var settings = generator.generatorSettings();
|
||||
return settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_5"))
|
||||
|| settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_10"))
|
||||
|| settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_20"));
|
||||
}
|
||||
public static PopulationHydrology.Plan river30(NoiseBasedChunkGenerator generator, RandomState state) {
|
||||
synchronized (RIVERS30) {
|
||||
return RIVERS30.computeIfAbsent(state, ignored -> {
|
||||
var sampler = state.getSampler(generator.generatorSettings().value().noiseRouter().finalDensity());
|
||||
var plan = River30.create(state.seed(), (int) capacity(generator).radius(),
|
||||
new SignMemo((x,y,z) -> sampler.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)));
|
||||
SanctuaryMod.LOGGER.info("River30 seed {}: {} cells, {} outlets, {} path points", state.seed(),
|
||||
plan.cells().size(), plan.spills().size(), plan.features().stream().mapToInt(f -> f.path().size()).sum());
|
||||
return plan;
|
||||
});
|
||||
}
|
||||
}
|
||||
private PopulationHydrologyRuntime() {}
|
||||
public static boolean enabled(NoiseBasedChunkGenerator generator) { return IslandCapacity.forGenerator(generator) != null; }
|
||||
public static IslandCapacity capacity(NoiseBasedChunkGenerator generator) {
|
||||
@@ -47,6 +66,10 @@ public final class PopulationHydrologyRuntime {
|
||||
});
|
||||
long seed = PopulationIslandDensity.regionSeed(state.seed(), region);
|
||||
var water = PopulationHydrology.create(seed, memo).admitted();
|
||||
if (current30(generator)) {
|
||||
water = River30.openBanks(seed, water, memo, 1)
|
||||
.overlay30(river30(generator, state).translated(-region.originX(), -region.originZ()));
|
||||
}
|
||||
var lava = PopulationLavaDeposit.create(seed, memo, water).admitted();
|
||||
var result = new RegionPlan(region, water.translated(region.originX(), region.originZ()),
|
||||
lava.translated(region.originX(), region.originZ()));
|
||||
@@ -133,6 +156,7 @@ public final class PopulationHydrologyRuntime {
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
// Validate all replacements and two intact support layers before modifying this chunk.
|
||||
for (var cell : cells) {
|
||||
if (plan.isOutletCell(cell)) continue;
|
||||
for (int y = cell.bedY() - cell.sedimentDepth() - 1; y <= cell.bedY(); y++) {
|
||||
requireSolid(chunk, pos.set(cell.x(), y, cell.z()), randomState.seed());
|
||||
}
|
||||
|
||||
+8
-6
@@ -13,7 +13,7 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
|
||||
DensityFunction sculpt, DensityFunction detail, DensityFunction underside,
|
||||
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity,
|
||||
DensityFunction caves28, DensityFunction ridges28, DensityFunction erosion28, boolean geology28,
|
||||
boolean terraces29) implements DensityFunction {
|
||||
boolean terraces29, boolean relief30) implements DensityFunction {
|
||||
public static final MapCodec<PopulationIslandDensity> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||
DensityFunction.CODEC.fieldOf("terrain").forGetter(PopulationIslandDensity::terrain),
|
||||
DensityFunction.CODEC.fieldOf("distortion").forGetter(PopulationIslandDensity::distortion),
|
||||
@@ -27,14 +27,15 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
|
||||
DensityFunction.CODEC.optionalFieldOf("ridges_28", DensityFunctions.zero()).forGetter(PopulationIslandDensity::ridges28),
|
||||
DensityFunction.CODEC.optionalFieldOf("erosion_28", DensityFunctions.zero()).forGetter(PopulationIslandDensity::erosion28),
|
||||
Codec.BOOL.optionalFieldOf("geology_28", false).forGetter(PopulationIslandDensity::geology28),
|
||||
Codec.BOOL.optionalFieldOf("terraces_29", false).forGetter(PopulationIslandDensity::terraces29)
|
||||
Codec.BOOL.optionalFieldOf("terraces_29", false).forGetter(PopulationIslandDensity::terraces29),
|
||||
Codec.BOOL.optionalFieldOf("relief_30", false).forGetter(PopulationIslandDensity::relief30)
|
||||
).apply(instance, PopulationIslandDensity::new));
|
||||
|
||||
public PopulationIslandDensity(DensityFunction terrain, DensityFunction distortion,
|
||||
DensityFunction sculpt, DensityFunction detail, DensityFunction underside,
|
||||
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity) {
|
||||
this(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail, capacity,
|
||||
DensityFunctions.zero(), DensityFunctions.zero(), DensityFunctions.zero(), false, false);
|
||||
DensityFunctions.zero(), DensityFunctions.zero(), DensityFunctions.zero(), false, false, false);
|
||||
}
|
||||
|
||||
public PopulationIslandDensity(DensityFunction terrain, DensityFunction distortion,
|
||||
@@ -42,7 +43,7 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
|
||||
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity,
|
||||
DensityFunction caves28, DensityFunction ridges28, DensityFunction erosion28, boolean geology28) {
|
||||
this(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail, capacity,
|
||||
caves28, ridges28, erosion28, geology28, false);
|
||||
caves28, ridges28, erosion28, geology28, false, false);
|
||||
}
|
||||
|
||||
@Override public DensitySampler compileSampler(CompileContext context) {
|
||||
@@ -77,6 +78,7 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
|
||||
float fineValue = fine.sampleValue(sample, x, localY, z);
|
||||
float terrainValue;
|
||||
double weight = geology28 ? CavesRelief28.deformationWeight(x, y, z, capacity.radius()) : 0;
|
||||
if (relief30) weight *= Relief30.influence(seed, x, z, capacity.radius());
|
||||
if (weight > 0) {
|
||||
var shift = CavesRelief28.warp(weight,
|
||||
erosion.sampleValue(sample, x + 173, y - 59, z - 89),
|
||||
@@ -118,7 +120,7 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
|
||||
/** Exact accepted28 deformation and caves, without the fine local ledges. */
|
||||
public PopulationIslandDensity legacy28() {
|
||||
return new PopulationIslandDensity(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail,
|
||||
capacity, caves28, ridges28, erosion28, geology28, false);
|
||||
capacity, caves28, ridges28, erosion28, geology28, false, false);
|
||||
}
|
||||
public static long regionSeed(long seed, IslandCapacity.Region region) {
|
||||
long value = seed ^ ((long) region.x() << 32) ^ (region.z() & 0xffffffffL) ^ 0xA10CA7E1L;
|
||||
@@ -129,7 +131,7 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
|
||||
@Override public DensityFunction rewriteChildren(DfRewriteRule rule) {
|
||||
return new PopulationIslandDensity(rule.rewrite(terrain), rule.rewrite(distortion), rule.rewrite(sculpt),
|
||||
rule.rewrite(detail), rule.rewrite(underside), rule.rewrite(riftWarp), rule.rewrite(riftDetail), capacity,
|
||||
rule.rewrite(caves28), rule.rewrite(ridges28), rule.rewrite(erosion28), geology28, terraces29);
|
||||
rule.rewrite(caves28), rule.rewrite(ridges28), rule.rewrite(erosion28), geology28, terraces29, relief30);
|
||||
}
|
||||
@Override public Interval range() { return Interval.of(-1, 1); }
|
||||
@Override public int domainAxes() { return ALL_AXES; }
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
/** Three broad areas of signed deformation; the intervening rock keeps the original plateau field. */
|
||||
public final class Relief30 {
|
||||
private Relief30() {}
|
||||
public static double influence(long seed, int x, int z, double radius) {
|
||||
long mixed = seed ^ 0x30BADC0FFEEL;
|
||||
mixed = (mixed ^ (mixed >>> 30)) * 0xbf58476d1ce4e5b9L;
|
||||
double rotation = (mixed >>> 11) * 0x1.0p-53 * Math.PI * 2;
|
||||
double strongest = 0;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
double angle = rotation + i * Math.PI * 2 / 3;
|
||||
double cx = Math.cos(angle) * radius * .52, cz = Math.sin(angle) * radius * .52;
|
||||
double dx = x - cx, dz = z - cz;
|
||||
double along = (dx * Math.cos(angle) + dz * Math.sin(angle)) / (radius * .47);
|
||||
double across = (-dx * Math.sin(angle) + dz * Math.cos(angle)) / (radius * .34);
|
||||
double distance = Math.hypot(along, across);
|
||||
double t = Math.clamp((1 - distance) / .70, 0, 1);
|
||||
strongest = Math.max(strongest, t * t * (3 - 2 * t));
|
||||
}
|
||||
return strongest;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.PopulationHydrology.*;
|
||||
import java.util.*;
|
||||
|
||||
/** One island-scale route follows actual rock, with local water levels and short open banks. */
|
||||
public final class River30 {
|
||||
private static final int STEP = 8;
|
||||
private static final int[][] DIR = {{1,0},{-1,0},{0,1},{0,-1}};
|
||||
private River30() {}
|
||||
private static long key(int x, int z) { return ((long)x << 32) ^ (z & 0xffffffffL); }
|
||||
private static int x(long k) { return (int)(k >> 32); }
|
||||
private static int z(long k) { return (int)k; }
|
||||
private record Node(long key, double cost) {}
|
||||
private static final class Rock {
|
||||
final Sampler sampler; final Map<Long,Integer> tops = new HashMap<>(); int samples;
|
||||
Rock(Sampler sampler) { this.sampler = sampler; }
|
||||
boolean solid(int x,int y,int z) { samples++; return y > 1 && y < 384 && sampler.sample(x,y,z) > 0; }
|
||||
int top(int x,int z) {
|
||||
return tops.computeIfAbsent(key(x,z), unused -> {
|
||||
for (int y=358;y>=140;y--) if (solid(x,y,z)) {
|
||||
for(int d=1;d<=6;d++) if(!solid(x,y-d,z)) return -1;
|
||||
return y;
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
}
|
||||
}
|
||||
public static Plan create(long seed, int radius, Sampler sampler) {
|
||||
var rock = new Rock(sampler); var nodes = new TreeMap<Long,Integer>();
|
||||
int limit = radius / STEP * STEP;
|
||||
for(int x=-limit;x<=limit;x+=STEP) for(int z=-limit;z<=limit;z+=STEP) {
|
||||
if(Math.hypot(x,z)>radius*.95 || Math.hypot(x,z)<52) continue;
|
||||
int y=rock.top(x,z); if(y>=140) nodes.put(key(x,z),y);
|
||||
}
|
||||
// Ignore isolated peripheral rock when selecting opposite banks: route in the largest connected mass.
|
||||
var adjacency=new HashMap<Long,List<Long>>();
|
||||
for(long k:nodes.keySet()) {
|
||||
var neighbours=new ArrayList<Long>();
|
||||
for(var d:DIR) {
|
||||
long next=key(x(k)+d[0]*STEP,z(k)+d[1]*STEP);
|
||||
if(!nodes.containsKey(next)||Math.abs(nodes.get(next)-nodes.get(k))>48)continue;
|
||||
int gaps=0;for(int n=1;n<STEP;n++)if(rock.top(x(k)+d[0]*n,z(k)+d[1]*n)<140)gaps++;
|
||||
if(gaps<=2)neighbours.add(next);
|
||||
}
|
||||
adjacency.put(k,neighbours);
|
||||
}
|
||||
var remaining=new TreeSet<>(nodes.keySet());Set<Long> largest=Set.of();
|
||||
while(!remaining.isEmpty()) {
|
||||
var component=new HashSet<Long>();var queue=new ArrayDeque<Long>();queue.add(remaining.first());
|
||||
while(!queue.isEmpty()){long k=queue.remove();if(!component.add(k))continue;queue.addAll(adjacency.get(k));}
|
||||
remaining.removeAll(component);if(component.size()>largest.size())largest=component;
|
||||
}
|
||||
nodes.keySet().retainAll(largest);
|
||||
List<Long> best = List.of(); double longest = 0;
|
||||
// Four deterministic orientations avoid choosing an axis whose shore is entirely broken.
|
||||
for(int axis=0;axis<4;axis++) {
|
||||
double angle=(seed & 1023)*Math.PI/512 + axis*Math.PI/4, dx=Math.cos(angle), dz=Math.sin(angle);
|
||||
var ordered=nodes.keySet().stream().sorted(Comparator.<Long>comparingDouble(k->x(k)*dx+z(k)*dz).thenComparingLong(k->k)).toList();
|
||||
if(ordered.isEmpty()) break;
|
||||
long start=ordered.getFirst(); var costs=new HashMap<Long,Double>(); var parent=new HashMap<Long,Long>();
|
||||
var queue=new PriorityQueue<Node>(Comparator.comparingDouble(Node::cost).thenComparingLong(Node::key));
|
||||
costs.put(start,0.0);parent.put(start,start);queue.add(new Node(start,0));
|
||||
while(!queue.isEmpty()) {
|
||||
var at=queue.remove(); if(at.cost()!=costs.get(at.key())) continue;
|
||||
for(long next:adjacency.get(at.key())) {
|
||||
if(!nodes.containsKey(next)) continue;
|
||||
int difference=Math.abs(nodes.get(next)-nodes.get(at.key()));
|
||||
double cost=at.cost()+STEP+difference*difference*.18+Math.max(0,nodes.get(next)-215)*.13;
|
||||
if(cost>=costs.getOrDefault(next,Double.POSITIVE_INFINITY)) continue;
|
||||
costs.put(next,cost);parent.put(next,at.key());queue.add(new Node(next,cost));
|
||||
}
|
||||
}
|
||||
for(int i=ordered.size()-1;i>=0;i--) {
|
||||
long end=ordered.get(i); if(!parent.containsKey(end)) continue;
|
||||
double span=Math.hypot(x(end)-x(start),z(end)-z(start));
|
||||
if(span>longest) { var path=new ArrayList<Long>();for(long k=end;;k=parent.get(k)){path.add(k);if(k==start)break;}Collections.reverse(path);best=path;longest=span; }
|
||||
break;
|
||||
}
|
||||
}
|
||||
var cells=new LinkedHashMap<Long,Cell>();var path=new ArrayList<Point>(); long id=0x300000000L;
|
||||
if(longest>=radius*.7) for(int i=1;i<best.size();i++) {
|
||||
long a=best.get(i-1),b=best.get(i);int dx=Integer.signum(x(b)-x(a)),dz=Integer.signum(z(b)-z(a));
|
||||
for(int n=0;n<=STEP;n++) {
|
||||
int cx=x(a)+dx*n,cz=z(a)+dz*n;path.add(new Point(cx,cz));
|
||||
int center=rock.top(cx,cz);
|
||||
for(int side=-2;side<=2;side++) {
|
||||
int px=cx+dz*side,pz=cz-dx*side,top=rock.top(px,pz);
|
||||
if(top<140 || Math.abs(top-center)>5) continue;
|
||||
// The bed stays in existing rock. Higher neighbouring faces become natural banks.
|
||||
int level=Math.min(top-1,center-1),bed=level-2;boolean support=true;
|
||||
for(int y=bed-3;y<=bed;y++) if(!rock.solid(px,y,pz)) {support=false;break;}
|
||||
if(support) cells.put(key(px,pz),new Cell(px,pz,level,bed,top,side==0?ShoreMaterial.GRAVEL:ShoreMaterial.CLAY,id,2));
|
||||
}
|
||||
}
|
||||
}
|
||||
System.getLogger(River30.class.getName()).log(System.Logger.Level.INFO,"River30 connectedNodes="+nodes.size()+" span="+longest+" columns="+rock.tops.size());
|
||||
var features=new ArrayList<Feature>();
|
||||
if(!cells.isEmpty()) features.add(feature(id,Kind.RIVER,cells.values(),path));
|
||||
var plan=PopulationHydrology.additional30(cells,features,List.of(),rock.tops.size(),rock.samples);
|
||||
return openBanks(seed,plan,sampler,6);
|
||||
}
|
||||
/** Cut only a short bank. Nothing is built beneath a fall and its landing is unrestricted. */
|
||||
public static Plan openBanks(long seed,Plan water,Sampler sampler,int maximum) {
|
||||
var cuts=new LinkedHashMap<Long,Cell>();var features=new ArrayList<Feature>();var spills=new ArrayList<Spill>();
|
||||
var mouths=new ArrayList<Point>();
|
||||
var candidates=water.cells().stream().filter(Cell::hasWater).sorted(Comparator
|
||||
.<Cell>comparingLong(c->mix(seed^key(c.x(),c.z()))).thenComparingInt(Cell::x).thenComparingInt(Cell::z)).toList();
|
||||
for(var wet:candidates) {
|
||||
if(mouths.size()>=maximum) break;
|
||||
if(mouths.stream().anyMatch(p->Math.hypot(p.x()-wet.x(),p.z()-wet.z())<48)) continue;
|
||||
for(var d:DIR) {
|
||||
if(water.cellAt(wet.x()+d[0],wet.z()+d[1])!=null) continue;
|
||||
int reach=0;
|
||||
for(int n=1;n<=5;n++) {
|
||||
int px=wet.x()+d[0]*n,pz=wet.z()+d[1]*n;
|
||||
if(sampler.sample(px,wet.waterY(),pz)<=0 && sampler.sample(px,wet.waterY()-3,pz)<=0
|
||||
&& sampler.sample(px,wet.waterY()-6,pz)<=0) {reach=n;break;}
|
||||
}
|
||||
if(reach==0) continue;
|
||||
long id=0x310000000L+(mix(seed)&0xfffffffL)*16+mouths.size();var local=new LinkedHashMap<Long,Cell>();
|
||||
boolean conflict=false;
|
||||
for(int n=1;n<=reach;n++) for(int side=-1;side<=1;side++) {
|
||||
int px=wet.x()+d[0]*n+d[1]*side,pz=wet.z()+d[1]*n-d[0]*side;
|
||||
var old=water.cellAt(px,pz);
|
||||
if(old!=null && old.hasWater()) {conflict=true;continue;}
|
||||
local.put(key(px,pz),new Cell(px,pz,-1,wet.waterY()-1,wet.waterY()+2,ShoreMaterial.STONE,id,0));
|
||||
}
|
||||
if(conflict) continue;
|
||||
cuts.putAll(local);features.add(feature(id,Kind.OUTLET,local.values(),List.of()));mouths.add(new Point(wet.x(),wet.z()));
|
||||
var source=new Position(wet.x(),wet.waterY(),wet.z());
|
||||
var outlet=new Position(wet.x()+d[0]*reach,wet.waterY(),wet.z()+d[1]*reach);
|
||||
var bounds=new FlowBounds(Math.min(source.x(),outlet.x())-3,0,Math.min(source.z(),outlet.z())-3,
|
||||
Math.max(source.x(),outlet.x())+3,wet.waterY()+3,Math.max(source.z(),outlet.z())+3);
|
||||
spills.add(new Spill(id,wet.featureId(),-1,source,outlet,List.of(source,outlet),bounds));break;
|
||||
}
|
||||
}
|
||||
return water.overlay30(PopulationHydrology.additional30(cuts,features,spills,0,0));
|
||||
}
|
||||
private static Feature feature(long id,Kind kind,Collection<Cell> cells,List<Point> path) {
|
||||
int x0=Integer.MAX_VALUE,z0=x0,x1=Integer.MIN_VALUE,z1=x1,water=-1,count=0;
|
||||
for(var c:cells){x0=Math.min(x0,c.x());z0=Math.min(z0,c.z());x1=Math.max(x1,c.x());z1=Math.max(z1,c.z());if(c.hasWater()){water=c.waterY();count++;}}
|
||||
return new Feature(id,kind,water,path,count,new Bounds(x0,z0,x1,z1));
|
||||
}
|
||||
private static long mix(long n){n=(n^(n>>>30))*0xbf58476d1ce4e5b9L;n=(n^(n>>>27))*0x94d049bb133111ebL;return n^(n>>>31);}
|
||||
}
|
||||
+8
-1
@@ -109,6 +109,13 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
if (current == null || heightAccessor == null) throw new IllegalStateException("Aerial sampling before world attachment");
|
||||
return current.generator().getBaseColumn(x, z, heightAccessor, current.random());
|
||||
}
|
||||
public boolean aerialDryWitness30(int x, int groundY, int z) {
|
||||
var current = originContext;
|
||||
if (current == null) throw new IllegalStateException("Aerial sampling before world attachment");
|
||||
for (var cell : PopulationHydrologyRuntime.cellsAt(current.generator(), current.random(), x, z))
|
||||
if (groundY >= cell.bedY() && groundY <= Math.max(cell.carveTop(), cell.waterY()) + 1) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private volatile fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlan23 mineralSanctuary23 = fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlan23.EMPTY;
|
||||
private boolean mineralPrepared23;
|
||||
@@ -399,7 +406,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain21(root, terrain.generator(), terrain.random(), heightAccessor,
|
||||
pos -> protectsDecoration(pos) || protectsCore22(pos));
|
||||
long started = System.nanoTime();
|
||||
mineralSanctuary23 = fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlanner23.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(),
|
||||
mineralSanctuary23 = fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlanner23.plan30(root.seed(), root.centerX(), root.centerZ(), root.diameter(),
|
||||
new fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlanner23.Terrain() {
|
||||
@Override public boolean owns(int x, int z) { return sampled.owns(x, z); }
|
||||
@Override public int surfaceY(int x, int z) { return sampled.ground(x, z).top(); }
|
||||
|
||||
+20
-1
@@ -22,6 +22,8 @@ public final class AerialPlanner24 {
|
||||
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; }
|
||||
/** The raw noise surface may subsequently be cut into a pond or an open river bank. */
|
||||
default boolean dryWitness(int x, int groundY, int z) { return true; }
|
||||
}
|
||||
|
||||
public static AerialPlan24 forWorld(ServerLevel level, SanctuaryChunkGenerator generator) {
|
||||
@@ -30,6 +32,9 @@ public final class AerialPlanner24 {
|
||||
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 dryWitness(int x, int groundY, int z) {
|
||||
return generator.aerialDryWitness30(x, groundY, z);
|
||||
}
|
||||
@Override public boolean reserved(int x, int z, int minY, int maxY) {
|
||||
return generator.intersectsExperimentalStructures(x, minY, z, x, maxY, z);
|
||||
}
|
||||
@@ -81,7 +86,8 @@ public final class AerialPlanner24 {
|
||||
cz = (int) Math.round(Math.sin(angle) * distance);
|
||||
int top = probe.surface(cx, cz);
|
||||
if (top < 120 || probe.terrain.reserved(cx, cz, top, top + 3)) continue;
|
||||
shore = new Shore(cx, top + 1, cz);
|
||||
shore = dryViewpoint(probe, cx, cz);
|
||||
if (shore == null) continue;
|
||||
int highest = top;
|
||||
for (int ox = -worldWidth / 2; ox <= worldWidth / 2; ox += 4)
|
||||
for (int oz = -worldDepth / 2; oz <= worldDepth / 2; oz += 4)
|
||||
@@ -196,6 +202,19 @@ public final class AerialPlanner24 {
|
||||
return true;
|
||||
}
|
||||
private record Shore(int x, int y, int z) {}
|
||||
private static Shore dryViewpoint(Probe probe, int x, int z) {
|
||||
// Keep the site above the same terrain. Move only its saved walking viewpoint
|
||||
// to a nearby dry bank when the centre is subsequently carved by hydrology.
|
||||
for (int r = 0; r <= 12; r += 2) for (int dx = -r; dx <= r; dx += 2)
|
||||
for (int dz = -r; dz <= r; dz += 2) {
|
||||
if (Math.max(Math.abs(dx), Math.abs(dz)) != r) continue;
|
||||
int top = probe.surface(x + dx, z + dz);
|
||||
if (top >= 120 && probe.terrain.dryWitness(x + dx, top, z + dz)
|
||||
&& !probe.terrain.reserved(x + dx, z + dz, top, top + 3))
|
||||
return new Shore(x + dx, top + 1, z + dz);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private static final class Probe {
|
||||
final Terrain terrain; final int radius;
|
||||
final Map<Long, NoiseColumn> columns = new HashMap<>();
|
||||
|
||||
+20
-13
@@ -5,7 +5,7 @@ import java.util.Map;
|
||||
|
||||
/** Finite terrain search: at most48 horizontal candidates, four orientations, three depths. */
|
||||
public final class MineralSanctuaryPlanner23 {
|
||||
public static final int MAX_COLUMNS = 10_000, MAX_SAMPLES = 1_200_000;
|
||||
public static final int MAX_COLUMNS = 24_000, MAX_SAMPLES = 2_400_000;
|
||||
private MineralSanctuaryPlanner23() {}
|
||||
public interface Terrain {
|
||||
boolean owns(int x, int z);
|
||||
@@ -14,12 +14,19 @@ public final class MineralSanctuaryPlanner23 {
|
||||
boolean protectedAt(int x, int y, int z);
|
||||
}
|
||||
public static MineralSanctuaryPlan23 plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
|
||||
return search(seed, cx, cz, diameter, terrain, false);
|
||||
}
|
||||
public static MineralSanctuaryPlan23 plan30(long seed, int cx, int cz, int diameter, Terrain terrain) {
|
||||
return search(seed, cx, cz, diameter, terrain, true);
|
||||
}
|
||||
private static MineralSanctuaryPlan23 search(long seed, int cx, int cz, int diameter, Terrain terrain, boolean exposedFallback) {
|
||||
if (diameter != 512 && diameter != 724 && diameter != 1024) throw new IllegalArgumentException("Unsupported mineral island size");
|
||||
var p = new Probe(terrain);
|
||||
try {
|
||||
for (int attempt = 0; attempt < 48; attempt++) {
|
||||
for (int attempt = 0; attempt < (exposedFallback ? 96 : 48); attempt++) {
|
||||
double angle = unit(mix(seed ^ 0x23CA7EL)) * Math.PI * 2 + attempt * 2.399963229728653;
|
||||
double radius = diameter * (.17 + .045 * (attempt % 4));
|
||||
p.exposed = attempt >= 48;
|
||||
double radius = diameter * (p.exposed ? .27 + .04 * (attempt % 5) : .17 + .045 * (attempt % 4));
|
||||
int x = cx + (int) Math.round(Math.cos(angle) * radius), z = cz + (int) Math.round(Math.sin(angle) * radius);
|
||||
int surface = p.surface(x, z);
|
||||
if (surface < 80) { p.reject("candidate-surface"); continue; }
|
||||
@@ -39,7 +46,7 @@ public final class MineralSanctuaryPlanner23 {
|
||||
var result = new MineralSanctuaryPlan23(c, p.samples, p.columns.size(), p.rejections);
|
||||
System.getLogger(MineralSanctuaryPlanner23.class.getName()).log(System.Logger.Level.INFO,
|
||||
"Mineral23 accepted=" + (c != null) + " columns=" + p.columns.size() + " samples=" + p.samples
|
||||
+ " chamber=" + c + " rejected=" + p.rejections);
|
||||
+ " exposed=" + p.exposed + " chamber=" + c + " rejected=" + p.rejections);
|
||||
return result;
|
||||
}
|
||||
/** The fissure ends in existing traversable air (a cave or the exposed island side), not sealed rock. */
|
||||
@@ -81,9 +88,9 @@ public final class MineralSanctuaryPlanner23 {
|
||||
tested++;
|
||||
}
|
||||
}
|
||||
if (columns == 0 || covered * 5 < columns * 3 || coveredQuadrants != 15)
|
||||
if (columns == 0 || covered * 5 < columns * (p.exposed ? 1 : 3) || (p.exposed ? Integer.bitCount(coveredQuadrants) < 2 : coveredQuadrants != 15))
|
||||
return p.reject("coarse-cover-distribution");
|
||||
return tested > 0 && rock * 5 >= tested * 3 || p.reject("coarse-rock");
|
||||
return tested > 0 && rock * 5 >= tested * (p.exposed ? 1 : 3) || p.reject("coarse-rock");
|
||||
}
|
||||
/** Reject unsupported candidates using a small shared column lattice before reading the full floor. */
|
||||
private static boolean coarseBearings(MineralSanctuaryPlan23.Chamber c, Probe p) {
|
||||
@@ -98,12 +105,12 @@ public final class MineralSanctuaryPlanner23 {
|
||||
else gaps.add(point);
|
||||
}
|
||||
// Sampling is deliberately permissive about the proportion; dense certification still requires60%.
|
||||
if (anchors.size() < gaps.size()) return p.reject("coarse-anchor-majority");
|
||||
if (anchors.size() * (p.exposed ? 2 : 1) < gaps.size()) return p.reject("coarse-anchor-majority");
|
||||
for (var gap : gaps) {
|
||||
boolean supported = false;
|
||||
for (var anchor : anchors) {
|
||||
int dx = gap[0] - anchor[0], dz = gap[1] - anchor[1];
|
||||
if (dx * dx + dz * dz <= 100) { supported = true; break; }
|
||||
if (dx * dx + dz * dz <= (p.exposed ? 324 : 100)) { supported = true; break; }
|
||||
}
|
||||
if (!supported) return p.reject("coarse-anchor-distance");
|
||||
}
|
||||
@@ -137,14 +144,14 @@ public final class MineralSanctuaryPlanner23 {
|
||||
anchored[x][z] = true; direct++;
|
||||
}
|
||||
}
|
||||
if (columns == 0 || direct * 5 < columns * 3) return p.reject("floor-anchor-majority");
|
||||
if (covered * 5 < columns * 3) return p.reject("full-cover-distribution");
|
||||
if (columns == 0 || direct * 5 < columns * (p.exposed ? 2 : 3)) return p.reject("floor-anchor-majority");
|
||||
if (covered * 5 < columns * (p.exposed ? 1 : 3)) return p.reject("full-cover-distribution");
|
||||
for (int z = 0; z < c.depth(); z++) for (int x = 0; x < c.width(); x++) {
|
||||
if (!footprint[x][z] || anchored[x][z]) continue;
|
||||
boolean nearby = false;
|
||||
for (int dz = -10; dz <= 10 && !nearby; dz++) for (int dx = -10; dx <= 10; dx++) {
|
||||
for (int dz = p.exposed ? -18 : -10; dz <= (p.exposed ? 18 : 10) && !nearby; dz++) for (int dx = p.exposed ? -18 : -10; dx <= (p.exposed ? 18 : 10); dx++) {
|
||||
int ax = x + dx, az = z + dz;
|
||||
if (dx * dx + dz * dz <= 100 && ax >= 0 && ax < c.width() && az >= 0 && az < c.depth()
|
||||
if (dx * dx + dz * dz <= (p.exposed ? 324 : 100) && ax >= 0 && ax < c.width() && az >= 0 && az < c.depth()
|
||||
&& anchored[ax][az]) { nearby = true; break; }
|
||||
}
|
||||
if (!nearby) return p.reject("floor-anchor-distance");
|
||||
@@ -158,7 +165,7 @@ public final class MineralSanctuaryPlanner23 {
|
||||
}
|
||||
private static final class Probe {
|
||||
final Terrain terrain; final Map<Long, Integer> columns = new HashMap<>();
|
||||
final Map<String, Integer> rejections = new java.util.TreeMap<>(); int samples;
|
||||
final Map<String, Integer> rejections = new java.util.TreeMap<>(); int samples; boolean exposed;
|
||||
Probe(Terrain terrain) { this.terrain = java.util.Objects.requireNonNull(terrain); }
|
||||
int surface(int x, int z) {
|
||||
if (!terrain.owns(x, z)) return -1;
|
||||
|
||||
+5
-5
@@ -22,17 +22,17 @@ public final class TransitPlanner29 {
|
||||
if (radius * 2 >= diameter / 4) radii.add(radius);
|
||||
try {
|
||||
// Cheap, cached entrance scans precede any expensive underground volume scan.
|
||||
search: for (int shift : new int[]{-diameter / 8, diameter / 8, -diameter / 16, diameter / 16, 0,
|
||||
-3 * diameter / 16, 3 * diameter / 16})
|
||||
for (int advance : new int[]{0, -17, 17, -34, 34, -51, 51})
|
||||
for (int radius : radii) for (boolean alongX : new boolean[]{true, false}) {
|
||||
search: for (int advance : new int[]{0, -17, 17, -34, 34, -51, 51})
|
||||
for (int radius : radii) for (boolean alongX : new boolean[]{true, false})
|
||||
for (int shift : new int[]{-diameter / 8, diameter / 8, -diameter / 16, diameter / 16, 0,
|
||||
-3 * diameter / 16, 3 * diameter / 16}) {
|
||||
if (++layouts > 320 || p.columns.size() >= 3_000) break search;
|
||||
int ax = cx + (alongX ? advance - radius : shift), az = cz + (alongX ? shift : advance - radius);
|
||||
int bx = cx + (alongX ? advance + radius : shift), bz = cz + (alongX ? shift : advance + radius);
|
||||
int a = entrance(ax, az, p), b = entrance(bx, bz, p);
|
||||
if (a < 0 || b < 0 || Math.abs(a - b) > 95) continue;
|
||||
entries.add(new Alignment(ax, az, bx, bz, a, b, alongX));
|
||||
if (entries.size() >= 24) break search;
|
||||
if (entries.size() >= 48) break search;
|
||||
}
|
||||
// Preserve every complete route already accepted by the primary search. Only after
|
||||
// its failure may an incompatible optional footing be omitted, using other bearings.
|
||||
|
||||
+2
-1
@@ -64,5 +64,6 @@
|
||||
"xz_scale": 2.0,
|
||||
"y_scale": 2.0
|
||||
},
|
||||
"terraces_29": true
|
||||
"terraces_29": false,
|
||||
"relief_30": true
|
||||
}
|
||||
|
||||
+2
-1
@@ -64,5 +64,6 @@
|
||||
"xz_scale": 2.0,
|
||||
"y_scale": 2.0
|
||||
},
|
||||
"terraces_29": true
|
||||
"terraces_29": false,
|
||||
"relief_30": true
|
||||
}
|
||||
|
||||
+2
-1
@@ -64,5 +64,6 @@
|
||||
"xz_scale": 2.0,
|
||||
"y_scale": 2.0
|
||||
},
|
||||
"terraces_29": true
|
||||
"terraces_29": false,
|
||||
"relief_30": true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
public final class River30Smoke {
|
||||
public static void main(String[] args) {
|
||||
for (int radius : new int[]{256,362,512}) {
|
||||
PopulationHydrology.Sampler rock=(x,y,z)->Math.hypot(x,z)<radius*.82 && y>=60 && y<=230 ? 1 : -1;
|
||||
var plan=River30.create(42,radius,rock);
|
||||
var river=plan.features().stream().filter(f->f.kind()==PopulationHydrology.Kind.RIVER).findFirst().orElseThrow();
|
||||
require(river.bounds().maxX()-river.bounds().minX()>radius || river.bounds().maxZ()-river.bounds().minZ()>radius,"Island-wide route");
|
||||
require(plan.spills().size()>0,"A nearby edge can discharge into native void");
|
||||
for(var c:plan.cells()) {
|
||||
if(plan.isOutletCell(c)) {require(c.sedimentDepth()==0&&!c.hasWater(),"Outlet only removes its bank");continue;}
|
||||
require(Math.hypot(c.x(),c.z())>=49,"Arrival remains natural");
|
||||
for(int y=c.bedY()-c.sedimentDepth()-1;y<=c.bedY();y++) require(rock.sample(c.x(),y,c.z())>0,"Bed consists of native rock");
|
||||
}
|
||||
for(var spill:plan.spills()) require(Math.abs(spill.outlet().x()-spill.source().x())+Math.abs(spill.outlet().z()-spill.source().z())<=5,"Bank cut at most five blocks");
|
||||
require(plan.cells().equals(River30.create(42,radius,rock).cells()),"Immutable deterministic plan");
|
||||
}
|
||||
for(long seed:new long[]{0,42,2026,-7228211907433324401L}) {
|
||||
int untouched=0;
|
||||
for(int x=-350;x<=350;x+=10)for(int z=-350;z<=350;z+=10)if(Math.hypot(x,z)<350 && Relief30.influence(seed,x,z,362)==0)untouched++;
|
||||
require(untouched>1000,"Wide undeformed regions remain between the three deformation areas");
|
||||
}
|
||||
System.out.println("River30: island-scale supported channels, five-block open banks, natural arrival, deterministic plans and separated relief regions passed.");
|
||||
}
|
||||
static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
|
||||
}
|
||||
Reference in New Issue
Block a user