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
+7
View File
@@ -1,5 +1,12 @@
# Changelog
## 0.1.0-alpha.30.2
- Corniches locales dont la phase et lespacement suivent un bruit 3D, sur le relief global conservé.
- Filtrage borné des petits composants rocheux détachés dans la zone retouchée, indépendant des frontières de chunks.
- Rivières courbes, tunnels, sanctuaire minéral et lieux aériens conservés ; nouveau monde pour lessai.
- Minecraft 26.3-pre-2 et les dépendances restent inchangés. Voir `docs/testing-alpha30.2.md` pour les validations effectivement réalisées.
## 0.1.0-alpha.30.1 — 2026-09-12
Retrait des rivières sur grille de la 30 ; brèches de cascade sur les anciennes
+9
View File
@@ -21,6 +21,15 @@ le relief et les souterrains de la 24, retire la montgolfière et travaille les
marais souterrains et la restauration de vrais modèles d’épaves vanilla. Voir le
[chantier et ses limites](docs/generation-alpha27.md).
## Alpha.30.2 — corniches et fragments
Les grandes masses et arches sont conservées. Une passe locale de corniches
irrégulières casse les pentes répétitives ; les petits composants rocheux
entièrement détachés sont filtrés dans la zone retouchée.
Créer un **nouveau monde**. Voir [la génération](docs/generation-alpha30.2.md)
et [les vérifications](docs/testing-alpha30.2.md).
## Alpha.30.1 — rivières courbes et relief global
Les rivières angulaires de la 30 sont retirées. Les anciens cours deau et
+8
View File
@@ -1009,3 +1009,11 @@ retrouver la déformation globale de la 28 avec une zone restreinte de plateau,
et ouvrir les berges des anciens cours deau vers les espaces vides proches.
Conserver les recherches de traversées et de sanctuaire améliorées en 30.
Voir [le correctif](generation-alpha30.1.md) et [les essais](testing-alpha30.1.md).
### WG-34 — Corniches irrégulières et fragments — alpha.30.2
Branche `codex/irregular-relief-alpha30-2`. Conserver les grandes masses et
arches, casser la régularité des pentes par des corniches localisées, et retirer
les petits fragments détachés issus du champ de terrain retouché. Comparer les
mêmes volumes natifs avant/après ; contrôler les raccords aux frontières de chunks.
Voir [la génération](generation-alpha30.2.md) et [les essais](testing-alpha30.2.md).
+50
View File
@@ -0,0 +1,50 @@
# Alpha.30.2 — corniches irrégulières et petits fragments
Retour visuel du 12 septembre 2026 : conserver les grandes masses et les arches
acceptées, mais réduire leurs contours trop réguliers et les longues pentes
en marches répétées. Le rendu des captures sert de contre-exemple ; il ne
sagit pas de remplacer les massifs par de nouvelles formes prédéfinies.
## Passe locale
Le relief global de la 28/30.1 reste actif, ainsi que sa zone limitée de plateau.
Une adaptation locale du point d’échantillonnage vertical crée de courtes
corniches : leur phase et leur espacement suivent un bruit 3D existant, à
l’échelle de quelques dizaines de blocs. Les couches ne partagent pas un même
niveau horizontal à travers toute l’île. Le déplacement supplémentaire du
point d’échantillonnage reste borné à **2,25 blocs**.
La passe satténue sous Y188 et disparaît sous Y140, près de larrivée, dans le
cœur de la bande de plateau, à lextrémité du contour et au-dessus du terrain.
Les volumes profonds et les paramètres des grottes natives ne sont pas changés.
Les rivières courbes, sorties de berge et leurs écoulements gardent leur
algorithme. Comme pour toute modification de densité, la recherche dun lieu
compatible peut aboutir à des coordonnées différentes sur une même graine.
## Petits blocs flottants
Une recherche de connexion dans le **champ naturel avant décoration** retire
les composants détachés de **32 blocs au maximum** dans la zone retouchée.
Elle sarrête dès quelle rejoint 33 blocs connectés ou la roche conservée hors
retouche. Les six faces servent de voisinage ; les diagonales ne suffisent pas.
Les coordonnées sont globales : les bords de chunks ne coupent pas une attache.
Ce contrôle ne remplit aucun trou, ne rajoute aucun pilier, ne visite aucune
sauvegarde personnelle et ne retire pas les îlots aériens. Il précède la pose
ultérieure des arbres, structures et fluides. Il ne constitue pas une promesse
quaucun élément flottant de quelque origine que ce soit ne puisse exister.
Le cache est borné, propre au thread et invalidé selon le sampler et son contexte,
avec des références faibles aux identités de monde.
## Livraison
Version mod et pack **0.1.0-alpha.30.2**, branche
`codex/irregular-relief-alpha30-2`. Nouveau champ de codec optionnel
`surface_302`, faux lorsquil est absent. Activé dans les trois profils actuels
512 / 724 / 1 024 ; Moyen reste le choix par défaut. Le prototype garde
`sanctuary:island_v24`, Minecraft 26.3-pre-2 et le bouton natif Personnaliser.
**Créer un nouveau monde pour essayer le rendu** ; aucune régénération des
sauvegardes personnelles. Pas de nouvelle interface nécessitant une traduction.
Les vérifications et limites sont dans [les essais](testing-alpha30.2.md).
La publication et la synchronisation suivent [packwiz](packwiz.md).
+94
View File
@@ -0,0 +1,94 @@
# Alpha.30.2 — vérifications et limites
Essais sur **Mac**, le 12 septembre 2026, dans le serveur de développement.
Aucun essai Windows natif de ce correctif nest revendiqué. Les comparaisons
visuelles sont des volumes natifs de roche, sans textures, arbres ou structures,
et ne remplacent pas lessai du rendu complet dans Minecraft.
## Méthode
Le témoin principal est **Moyen 724, graine 0**. Le test compare le champ
30.1 au champ retouché aux mêmes coordonnées. Il exporte deux coupes et un
volume de **64 × 128 × 64 blocs**, choisi uniquement selon les transitions du
champ précédent. Le choix du volume ne recherche pas le meilleur résultat de
la nouvelle passe. La profondeur sous Y140 et larrivée restent inchangées.
Le volume permet de compter les composants de roche connectés par leurs six
faces. Les composants touchant ses plans de coupe sont exclus de linventaire
des fragments, car leur attache peut continuer hors du volume. Les longues
marches sont comptées sur les surfaces supérieures complètes, le long des deux
axes : suites dau moins six différences de hauteur identiques de ±1 bloc.
Ces mesures portent sur cet échantillon, pas sur toutes les graines du jeu.
Deux essais de déplacement plus simple ont été écartés avant publication : le
premier introduisait trop de fragments, le second ne réduisait pas les longues
marches du témoin. Les aperçus intermédiaires restent dans les dossiers ignorés
`build/alpha302-preview-first-pass/` et `build/alpha302-preview-second-pass/`.
## Contrôle ciblé des fragments
`surfaceRelief302Smoke` vérifie les petits amas à cheval sur un bord de chunk,
la conservation des corps connectés plus grands, labsence de remplissage du
vide, lisolation des caches entre deux samplers et la limite de déplacement
local. Le filtre sapplique au champ de roche retouché, avant les décorations ;
il ne modifie pas les constructions ni les sauvegardes personnelles.
## Témoin Moyen 724, graine 0
Le processus **67135** passe les **24/24 GameTests** ; `check build` réussit
en **5 min 18 s**. Reçus dans `build/alpha302-evidence/medium-0/`.
Dans le volume natif choisi (**X-154, Y164, Z-218**, 64 × 128 × 64), **96,72 %**
des positions restent identiques. La quantité de roche passe de **174 157 à
173 881 blocs**, soit une différence de **-0,16 %**. Les pas appartenant aux
longues suites de marches passent de **424 à 81**. Le comptage indépendant
observe **0 petit composant isolé de huit blocs ou moins**, contre cinq
composants réunissant six blocs avant la passe. Lanalyse ne compte pas les
composants coupés par la frontière du volume. Le filtre lui-même traite les
composants de 32 blocs ou moins.
Deux sorties de lancienne rivière sont vérifiées sous ticks réels :
**-105 246 -65** et **-24 246 -45**. La traversée possède **19 départs natifs**,
**29 chunks FULL** contrôlés, **272 blocs** entre ses accès et **678 pas** dans
le parcours testé. Entrées : **-136 234 82** et **136 236 82**. Le sanctuaire
possède son départ et son sol central ; les quatre satellites et une mine
vanilla sont également observés dans leurs tests respectifs.
Les mesures de planification sont distinctes du temps de rendu client : la
région hydrologique centrale prend **32,1 s**, et la recherche de traversée,
qui inclut la préparation de son terrain, **40,8 s**. Ces durées ne sajoutent
pas simplement. Le filtre a un coût ; cette livraison ne revendique pas un
gain de performance. Le test dexport exhaustif du volume bloque ponctuellement
le serveur de test ; cet export nest pas activé dans le mod livré.
## Témoin Grand 1 024, graine -7228211907433324401
Le processus **67379** passe les **24/24 GameTests** en **4 min 23 s** pour la
commande complète. Le volume natif contient **0 composant détaché de 32 blocs
ou moins**, contre **12 avant**. Le test exige de ne pas augmenter ce nombre ;
**1 024 points** rééchantillonnés dans lordre inverse, avec un autre contexte de
cache, conservent le même état roche/air.
La traversée est réellement posée : **21 départs**, **48 chunks FULL**,
**340 blocs entre les accès** et **574 pas** sur le parcours testé. Entrées :
**192 238 -195** et **192 224 145**. Le sanctuaire et les quatre satellites
passent leurs contrôles. La région centrale admet ici une rivière courbe ;
l’écoulement des brèches est contrôlé sous ticks réels dans huit chunks FULL.
Reçus : `build/alpha302-evidence/large-user/`. Les deux commandes natives
activent `sanctuaryAerial24Tests` et `sanctuaryTransit29Tests` ; le témoin Moyen
exige aussi `sanctuaryRequireCurvedRiverBank=true`. Les trois tailles conservent
leurs profils et le même filtre. Petit na pas fait lobjet dune nouvelle
visite native complète dans cette livraison. Les sauvegardes personnelles
ne sont pas régénérées ; les scénarios natifs utilisent des mondes jetables.
Les reçus de distribution sont consignés dans [packwiz](packwiz.md).
## Distribution
`assemblePack` passe après la vérification complète ; `:sanctuary:check` est
exclu de cette seconde commande pour ne pas répéter la suite inchangée. Le JAR,
le MRpack et le ZIP damorçage sont vérifiés localement : versions, index,
empreintes et contenu concordent. Reçus : `build/alpha302-artifacts.json` et
`build/alpha302-delivery-local.json`. Les reçus publics et la mise à jour Prism
sont consignés dans [Distribution](packwiz.md).
+2 -2
View File
@@ -8,6 +8,6 @@ loader_version=0.19.5
loom_version=1.17.20
fabric_api_version=0.160.0+26.3
mod_version=0.1.0-alpha.30.1
pack_version=0.1.0-alpha.30.1
mod_version=0.1.0-alpha.30.2
pack_version=0.1.0-alpha.30.2
maven_group=fr.koka.sanctuary
+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");
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "0.1.0-alpha.30.1"
version = "0.1.0-alpha.30.2"
pack-format = "packwiz:1.1.0"
[index]