feat(worldgen): irregular ledges and detached-rock filtering in alpha30.2
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-12 12:37:01 +02:00
parent a20d84811f
commit c5381b939a
16 changed files with 411 additions and 25 deletions
+8
View File
@@ -666,3 +666,11 @@ tasks.register('river30Smoke', JavaExec) {
mainClass = 'fr.koka.sanctuary.worldgen.River30Smoke'
}
tasks.named('check') { dependsOn('river30Smoke') }
tasks.register('surfaceRelief302Smoke', JavaExec) {
group = 'verification'
dependsOn tasks.named('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.SurfaceRelief302Smoke'
}
tasks.named('check') { dependsOn('surfaceRelief302Smoke') }
@@ -22,34 +22,34 @@ 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.relief30() && !field.terraces29(), "Alpha30.1 restores global deformation except a narrow plateau");
helper.assertTrue(field.relief30() && !field.terraces29() && field.surface302(), "Alpha30.2 adds non-periodic local relief to the global field");
var random = generator.starterContext().random();
var current = random.getSampler(field); var previous = random.getSampler(field.legacy28());
var current = random.getSampler(field); var previous = random.getSampler(field.legacy301());
var context = SamplerContext.builder().enableCaches().build();
var report = new LinkedHashMap<String, Object>();
report.put("seed", level.getSeed()); report.put("diameter", generator.initialDiameter());
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 <= 180; y += 13) {
for (int y = 32; y <= 140; 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");
== Float.floatToIntBits(previous.sampleValue(context, x, y, z)), "Deep density is exactly alpha30.1");
lowerSamples++;
}
for (int y = 181; y <= 320; y++) {
for (int y = 141; 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 * 3L, "A narrow plateau retains most of the accepted global relief");
helper.assertTrue(changed > 0 && unchanged > changed * 3L, "Local surface offsets retain most of the accepted rock volume");
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>();
for (int z : new int[]{0, 96}) {
var before = new ArrayList<String>(); var after = new ArrayList<String>();
for (int y = 305; y >= 205; y--) {
for (int y = 305; y >= 145; y--) {
var a = new StringBuilder(); var b = new StringBuilder();
for (int x = -radius; x <= radius; x++) {
a.append(previous.sampleValue(context, x, y, z) > 0 ? '#' : '.');
@@ -59,10 +59,67 @@ 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/alpha301", "relief-" + generator.initialDiameter() + "-" + level.getSeed() + ".json");
report.put("sections", sections);
// One block-resolution volume, chosen using the old field's surface transitions.
// Both versions sample the exact same coordinates, without structures or decoration.
int bestX=80,bestZ=80,bestScore=-1;
for(int cz=-radius+48;cz<=radius-48;cz+=64)for(int cx=-radius+48;cx<=radius-48;cx+=64){
if(Math.hypot(cx,cz)<80||Math.hypot(cx,cz)>generator.capacity().radius()*.78)continue;
int score=0;
for(int dz=-24;dz<=24;dz+=8)for(int dx=-24;dx<=24;dx+=8){
int last=-1;
for(int y=290;y>=175;y--){
int solid=previous.sampleValue(context,cx+dx,y,cz+dz)>0?1:0;
if(last>=0&&last!=solid)score++;last=solid;
}
}
if(score>bestScore){bestScore=score;bestX=cx;bestZ=cz;}
}
var oldVolume=new ArrayList<String>();var newVolume=new ArrayList<String>();
int size=64,minY=164,maxY=291;
for(int y=minY;y<=maxY;y++)for(int z=bestZ-size/2;z<bestZ+size/2;z++){
var a=new StringBuilder();var b=new StringBuilder();
for(int x=bestX-size/2;x<bestX+size/2;x++){
a.append(previous.sampleValue(context,x,y,z)>0?'#':'.');
b.append(current.sampleValue(context,x,y,z)>0?'#':'.');
}oldVolume.add(a.toString());newVolume.add(b.toString());
}
report.put("volume",Map.of("min_x",bestX-size/2,"min_z",bestZ-size/2,"min_y",minY,
"size_x",size,"size_z",size,"size_y",maxY-minY+1,"before",oldVolume,"after",newVolume));
int oldFragments=tinyComponents(oldVolume,size,maxY-minY+1);
int newFragments=tinyComponents(newVolume,size,maxY-minY+1);
report.put("tiny_components_before",oldFragments);report.put("tiny_components_after",newFragments);
helper.assertTrue(newFragments<=oldFragments,"The retouched volume must not introduce extra detached fragments");
var reverseContext=SamplerContext.builder().enableCaches().build();int reversed=0;
for(int y=maxY;y>=minY;y-=8)for(int z=bestZ+size/2-1;z>=bestZ-size/2;z-=8)
for(int x=bestX+size/2-1;x>=bestX-size/2;x-=8){
boolean expected=newVolume.get((y-minY)*size+z-(bestZ-size/2)).charAt(x-(bestX-size/2))=='#';
helper.assertTrue((current.sampleValue(reverseContext,x,y,z)>0)==expected,"Rock membership is independent of cache and reverse sample order");reversed++;
}
report.put("reverse_order_samples",reversed);
report.put("passed", true);
Path path = Path.of("diagnostics/alpha302", "relief-" + generator.initialDiameter() + "-" + level.getSeed() + ".json");
Files.createDirectories(path.getParent());
Files.writeString(path, new GsonBuilder().setPrettyPrinting().create().toJson(report));
helper.succeed();
}
private static int tinyComponents(List<String> rows,int size,int height){
int plane=size*size,total=plane*height,tiny=0;
boolean[] visited=new boolean[total];int[] queue=new int[total];
for(int start=0;start<total;start++){
if(visited[start]||rows.get(start/size).charAt(start%size)!='#')continue;
int head=0,count=1;queue[0]=start;visited[start]=true;boolean boundary=false;
while(head<count){
int p=queue[head++],x=p%size,z=(p/size)%size,y=p/plane;
if(x==0||x==size-1||z==0||z==size-1||y==0||y==height-1)boundary=true;
int[] neighbours={x>0?p-1:-1,x<size-1?p+1:-1,z>0?p-size:-1,z<size-1?p+size:-1,y>0?p-plane:-1,y<height-1?p+plane:-1};
for(int next:neighbours)if(next>=0&&!visited[next]&&rows.get(next/size).charAt(next%size)=='#'){
visited[next]=true;queue[count++]=next;
}
}
if(!boundary&&count<=32)tiny++;
}
return tiny;
}
}
@@ -0,0 +1,72 @@
package fr.koka.sanctuary.worldgen;
import java.lang.ref.WeakReference;
import net.minecraft.world.level.levelgen.densityfunction.*;
/** Removes detached components of at most 32 blocks in the locally retouched field.
* The flood search reads the unfiltered field and stops on its 33rd connected block.
* All coordinates are global: a chunk edge is never treated as the edge of a component. */
public final class DetachedRock302 implements DensitySampler {
private static final int MIN_BODY = 33;
private static final ThreadLocal<Cache> CACHE = ThreadLocal.withInitial(Cache::new);
private static final int[][] SIDES = {{1,0,0},{-1,0,0},{0,1,0},{0,-1,0},{0,0,1},{0,0,-1}};
private final DensitySampler input;
private final double radius;
private final long seed;
public DetachedRock302(DensitySampler input, double radius, long seed) {
this.input = input; this.radius = radius; this.seed = seed;
}
private boolean affected(int x, int y, int z) {
return SurfaceRelief302.weight(x,y,z,radius) * Relief30.influence(seed,x,z,radius) > 0;
}
@Override public float sampleValue(SamplerContext context,int x,int y,int z) {
if (!affected(x,y,z)) return input.sampleValue(context,x,y,z);
var cache=CACHE.get();cache.select(this,context);
int slot=cache.get(this,context,x,y,z);float value=cache.values[slot];
if(value<=0 || cache.status[slot]==2) return value;
if(cache.status[slot]==3) return -0.000001F;
int[] xs=new int[MIN_BODY],ys=new int[MIN_BODY],zs=new int[MIN_BODY];xs[0]=x;ys[0]=y;zs[0]=z;
int count=1;boolean attached=false;
search:for(int head=0;head<count;head++)for(var side:SIDES){
int nx=xs[head]+side[0],ny=ys[head]+side[1],nz=zs[head]+side[2];
boolean seen=false;
for(int i=0;i<count;i++)if(xs[i]==nx&&ys[i]==ny&&zs[i]==nz){seen=true;break;}
if(seen)continue;
int next=cache.get(this,context,nx,ny,nz);
if(cache.values[next]<=0 || cache.status[next]==3)continue;
if(cache.status[next]==2 || !affected(nx,ny,nz)){attached=true;break search;}
xs[count]=nx;ys[count]=ny;zs[count]=nz;
if(++count==MIN_BODY){attached=true;break search;}
}
for(int i=0;i<count;i++)cache.status[cache.get(this,context,xs[i],ys[i],zs[i])]=(byte)(attached?2:3);
return attached?value:-0.000001F;
}
@Override public void sampleVolume(SamplerContext context,DensityBuffer buffer,DensityVolume volume){
DensitySampler.sampleVolumeNaive(context,buffer,volume,this);
}
/** Bounded primitive cache; weak identities cannot retain a closed world or sample context. */
private static final class Cache {
private static final int SIZE=1<<16;
private WeakReference<DetachedRock302> owner=new WeakReference<>(null);
private WeakReference<SamplerContext> context=new WeakReference<>(null);
private final int[] xs=new int[SIZE],ys=new int[SIZE],zs=new int[SIZE],stamps=new int[SIZE];
private final float[] values=new float[SIZE];
private final byte[] status=new byte[SIZE];
private int generation;
void select(DetachedRock302 selected,SamplerContext selectedContext){
if(owner.get()==selected&&context.get()==selectedContext)return;
owner=new WeakReference<>(selected);context=new WeakReference<>(selectedContext);
if(++generation==0){java.util.Arrays.fill(stamps,0);generation=1;}
}
int get(DetachedRock302 sampler,SamplerContext sample,int x,int y,int z){
int h=x*0x9e3779b9^Integer.rotateLeft(y*0x85ebca6b,11)^Integer.rotateLeft(z*0xc2b2ae35,22);
int slot=(h^(h>>>16))&(SIZE-1);
if(stamps[slot]!=generation||xs[slot]!=x||ys[slot]!=y||zs[slot]!=z){
float value=sampler.input.sampleValue(sample,x,y,z);
xs[slot]=x;ys[slot]=y;zs[slot]=z;values[slot]=value;status[slot]=1;stamps[slot]=generation;
}
return slot;
}
}
}
@@ -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, boolean relief30) implements DensityFunction {
boolean terraces29, boolean relief30, boolean surface302) 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),
@@ -28,14 +28,15 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
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("relief_30", false).forGetter(PopulationIslandDensity::relief30)
Codec.BOOL.optionalFieldOf("relief_30", false).forGetter(PopulationIslandDensity::relief30),
Codec.BOOL.optionalFieldOf("surface_302", false).forGetter(PopulationIslandDensity::surface302)
).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, false);
DensityFunctions.zero(), DensityFunctions.zero(), DensityFunctions.zero(), false, false, false, false);
}
public PopulationIslandDensity(DensityFunction terrain, DensityFunction distortion,
@@ -43,7 +44,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, false);
caves28, ridges28, erosion28, geology28, false, false, false);
}
@Override public DensitySampler compileSampler(CompileContext context) {
@@ -56,7 +57,7 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
var erosion = geology28 ? erosion28.compileSampler(context) : null;
long seed = context.createRandom(SanctuaryMod.id("population_rifts_alpha10")).nextLong();
var layouts = new ConcurrentHashMap<IslandCapacity.Region, RiftShape.Layout>();
return new DensitySampler() {
DensitySampler field = new DensitySampler() {
private float terrainAt(SamplerContext sample, double x, double y, double z) {
int ix = (int) Math.floor(x), iy = (int) Math.floor(y), iz = (int) Math.floor(z);
double fx = x - ix, fy = y - iy, fz = z - iz;
@@ -79,12 +80,16 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
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,
double surfaceWeight = surface302 ? SurfaceRelief302.weight(x, y, z, capacity.radius())
* Relief30.influence(seed, x, z, capacity.radius()) : 0;
if (weight > 0 || surfaceWeight > 0) {
var shift = weight > 0 ? CavesRelief28.warp(weight,
erosion.sampleValue(sample, x + 173, y - 59, z - 89),
ridges.sampleValue(sample, x, y, z),
erosion.sampleValue(sample, x - 211, y + 137, z + 307));
erosion.sampleValue(sample, x - 211, y + 137, z + 307)) : new CavesRelief28.Warp(0,0,0);
double sourceY = y - 64 - shift.y();
if (surfaceWeight > 0) sourceY = SurfaceRelief302.sourceY(sourceY,surfaceWeight,
coarse.sampleValue(sample,x-233,y+419,z+157));
if (terraces29) sourceY = Terraces29.sourceY(sourceY, coarseValue, fineValue,
Terraces29.weight(y, weight));
terrainValue = terrainAt(sample, x - shift.x(), sourceY, z - shift.z());
@@ -111,6 +116,7 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
DensitySampler.sampleVolumeNaive(sample, buffer, volume, this);
}
};
return surface302 ? new DetachedRock302(field,capacity.radius(),seed) : field;
}
/** Native diagnostics can compile the exact accepted27 field without new adaptations. */
public PopulationIslandDensity legacy27() {
@@ -120,7 +126,11 @@ 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, false);
capacity, caves28, ridges28, erosion28, geology28, false, false, false);
}
public PopulationIslandDensity legacy301() {
return new PopulationIslandDensity(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail,
capacity, caves28, ridges28, erosion28, geology28, terraces29, relief30, false);
}
public static long regionSeed(long seed, IslandCapacity.Region region) {
long value = seed ^ ((long) region.x() << 32) ^ (region.z() & 0xffffffffL) ^ 0xA10CA7E1L;
@@ -131,7 +141,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, relief30);
rule.rewrite(caves28), rule.rewrite(ridges28), rule.rewrite(erosion28), geology28, terraces29, relief30, surface302);
}
@Override public Interval range() { return Interval.of(-1, 1); }
@Override public int domainAxes() { return ALL_AXES; }
@@ -0,0 +1,31 @@
package fr.koka.sanctuary.worldgen;
/** Locally phased ledges in the existing rock field, with no shared horizontal level. */
public final class SurfaceRelief302 {
private SurfaceRelief302() {}
public static double weight(int x, int y, int z, double radius) {
double distance = Math.hypot((double) x, z);
return smooth((y - 140) / 48.0) * (1 - smooth((y - 316) / 36.0))
* smooth((distance - 48) / 32.0)
* (1 - smooth((distance / radius - .86) / .12));
}
/** Coherent patches move together; narrow, continuous transitions form local ledges.
* No rounding of Y, shared layer height or per-block random displacement. */
public static double sourceY(double originalY, double weight, float noise) {
if(weight<=0)return originalY;
double period=6.5+1.5*Math.tanh(noise*1.5);
double phase=11*Math.tanh(noise);
double band=(originalY+phase)/period;
double fraction=band-Math.floor(band);
double ledge=smooth((fraction-.30)/.40);
double offset=Math.clamp((ledge-fraction)*period,-2.25,2.25);
return originalY+offset*weight;
}
private static double smooth(double value) {
double t = Math.clamp(value, 0, 1);
return t * t * (3 - 2 * t);
}
}
@@ -65,5 +65,6 @@
"y_scale": 2.0
},
"terraces_29": false,
"relief_30": true
"relief_30": true,
"surface_302": true
}
@@ -65,5 +65,6 @@
"y_scale": 2.0
},
"terraces_29": false,
"relief_30": true
"relief_30": true,
"surface_302": true
}
@@ -65,5 +65,6 @@
"y_scale": 2.0
},
"terraces_29": false,
"relief_30": true
"relief_30": true,
"surface_302": true
}
@@ -0,0 +1,37 @@
package fr.koka.sanctuary.worldgen;
import java.util.*;
import net.minecraft.world.level.levelgen.densityfunction.*;
public final class SurfaceRelief302Smoke {
private record P(int x,int y,int z) {}
private static DetachedRock302 filter(Set<P> blocks){
return new DetachedRock302(new DensitySampler(){
@Override public float sampleValue(SamplerContext c,int x,int y,int z){return blocks.contains(new P(x,y,z))?.5F:-.5F;}
@Override public void sampleVolume(SamplerContext c,DensityBuffer b,DensityVolume v){DensitySampler.sampleVolumeNaive(c,b,v,this);}
},362,0);
}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
public static void main(String[] args){
var context=SamplerContext.EMPTY_UNCACHED;var cube=new HashSet<P>();
for(int x=95;x<=96;x++)for(int y=230;y<=231;y++)for(int z=100;z<=101;z++)cube.add(new P(x,y,z));
var removed=filter(Set.copyOf(cube));
for(var p:cube)require(removed.sampleValue(context,p.x,p.y,p.z)<0,"Eight detached blocks crossing a chunk edge must disappear");
for(int x=95;x<=97;x++)for(int y=230;y<=232;y++)for(int z=100;z<=102;z++)cube.add(new P(x,y,z));
var clump=filter(Set.copyOf(cube));
for(var p:cube)require(clump.sampleValue(context,p.x,p.y,p.z)<0,"A detached 3 by 3 by 3 clump is also removed");
for(int z=100;z<=102;z++)for(int x=95;x<=97;x++)cube.add(new P(x,233,z));
var retained=filter(Set.copyOf(cube));
var order=new ArrayList<>(cube);Collections.reverse(order);
for(var p:order)require(retained.sampleValue(context,p.x,p.y,p.z)>0,"A connected body of 36 blocks must remain");
require(retained.sampleValue(context,94,230,100)<0,"Cleanup never fills surrounding air");
var singleton=filter(Set.of(new P(96,230,100)));
require(singleton.sampleValue(context,96,230,100)<0,"A different sampler cannot inherit another body's cached support");
var outside=new P(0,64,0);require(filter(Set.of(outside)).sampleValue(context,0,64,0)>.0,"The arrival and deep field are untouched");
for(double y=-10;y<400;y+=.125)for(float noise:new float[]{-2,-1,-.25F,0,.25F,1,2}){
require(Math.abs(SurfaceRelief302.sourceY(y,1,noise)-y)<=2.25001,"Local ledges must remain bounded");
require(SurfaceRelief302.sourceY(y,0,noise)==y,"An excluded region retains its exact field");
}
System.out.println("SurfaceRelief302Smoke: detached fragments across chunks removed, connected body and void retained, cache isolation and bounded local ledges passed");
}
}