feat: balance native plateaus and 3D mountains for alpha30.3
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-12 13:31:08 +02:00
parent 2a5b4cb51b
commit 076c9a6c16
19 changed files with 387 additions and 43 deletions
+14
View File
@@ -1,5 +1,19 @@
# Changelog # Changelog
## 0.1.0-alpha.30.3
- Retire les microcorniches de la 30.2, jugées trop bruitées en jeu.
- Répartit de grandes zones de plateaux naturels et de montagnes déformées en 3D,
avec une échelle relative commune à Petit, Moyen et Grand.
- Étend les hauts reliefs vers Y360 sans changer la hauteur du monde ; conserve
les profondeurs, larrivée, les cours deau courbes et les infrastructures.
- Conserve un filtrage des fragments rocheux détachés jusque dans les sommets.
- Adapte la recherche aérienne aux plateaux disponibles : rejet grossier des
emprises trop creuses, contrôle de hauteur précoce et recherche autour de l’île.
- Corrige le veto de biome qui empêchait un sanctuaire minéral pourtant admis
de se poser dans les marais souterrains.
- Nouveau monde nécessaire pour tester le nouveau terrain ; aucun monde régénéré.
## 0.1.0-alpha.30.2 ## 0.1.0-alpha.30.2
- Corniches locales dont la phase et lespacement suivent un bruit 3D, sur le relief global conservé. - Corniches locales dont la phase et lespacement suivent un bruit 3D, sur le relief global conservé.
+7 -6
View File
@@ -21,14 +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 marais souterrains et la restauration de vrais modèles d’épaves vanilla. Voir le
[chantier et ses limites](docs/generation-alpha27.md). [chantier et ses limites](docs/generation-alpha27.md).
## Alpha.30.2corniches et fragments ## Alpha.30.3plateaux et grands reliefs
Les grandes masses et arches sont conservées. Une passe locale de corniches La passe de petites corniches de la 30.2 est retirée après le retour en jeu.
irrégulières casse les pentes répétitives ; les petits composants rocheux De larges provinces de relief font cohabiter les plateaux naturels issus de la
entièrement détachés sont filtrés dans la zone retouchée. 24 et les montagnes déformées en 3D, jusqu’à proximité de Y360. Leur répartition
est proportionnée aux trois tailles. Les petits fragments détachés restent filtrés.
Créer un **nouveau monde**. Voir [la génération](docs/generation-alpha30.2.md) Créer un **nouveau monde**. Voir [la génération](docs/generation-alpha30.3.md)
et [les vérifications](docs/testing-alpha30.2.md). et [les vérifications](docs/testing-alpha30.3.md).
## Alpha.30.1 — rivières courbes et relief global ## Alpha.30.1 — rivières courbes et relief global
+10
View File
@@ -1017,3 +1017,13 @@ arches, casser la régularité des pentes par des corniches localisées, et reti
les petits fragments détachés issus du champ de terrain retouché. Comparer les 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. 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). Voir [la génération](generation-alpha30.2.md) et [les essais](testing-alpha30.2.md).
### WG-35 — Plateaux naturels et grandes montagnes — alpha.30.3
Branche `codex/balanced-relief-alpha30-3`. Le rendu de la 30.2 est rejeté en jeu :
les métriques locales de marches ne constituaient pas une validation esthétique.
Retirer ce microrelief, retrouver des plateaux issus de la 24 et réserver une
part importante aux grandes déformations 3D, vers Y360. Équilibrer les trois
tailles par une distribution à l’échelle de l’île. Préserver les profondeurs,
les cours deau et les infrastructures ; limiter les petits fragments flottants.
Voir [le changement](generation-alpha30.3.md) et [les preuves](testing-alpha30.3.md).
+59
View File
@@ -0,0 +1,59 @@
# Alpha.30.3 — plateaux et grands reliefs
## Choix retenu
Le créateur a rejeté la 30.2 après essai : la retouche fine produisait un aspect
rongé et trop bruité. Son nombre réduit de longues marches ne garantissait pas
un meilleur paysage. Cette version désactive `surface_302` et `terraces_29`.
La roche reste le champ volumétrique Minecraft de la base alpha.24, accompagné
des grottes et fractures adaptées en 28. Un bruit de grande longueur donde
sélectionne des provinces montagneuses ; il ne dessine ni cônes ni salles.
Les provinces sont échantillonnées dans un repère proportionnel au rayon :
Petit 512, Moyen 724 et Grand 1 024 conservent une distribution comparable,
avec des masses physiquement plus vastes lorsque l’île grandit.
Le seuil médian du champ intérieur conserve une grande part des plateaux ;
la transition vers les provinces hautes est continue. La bande de plateau
utile aux infrastructures reste protégée, ainsi que les 48 blocs autour du
centre. Aucun pavage périodique ou bruit supplémentaire à l’échelle du bloc.
Dans les provinces hautes, le domaine vertical s’étend progressivement
au-dessus de Y170, avec les déplacements signés sur trois axes de la 28.
Cela relève la roche existante et conserve sa structure volumétrique, ses
surplombs et ses vides. Une extinction douce sous Y360 laisse de la place
sous le plafond de construction Y384. La hauteur atteinte dépend du terrain
et de la graine ; aucun sommet nest forcé à une altitude identique.
Le filtre de connexité travaille sur le champ rocheux avant décoration :
composants de 32 blocs au plus, connexions par faces, coordonnées globales
même à cheval sur plusieurs chunks. Il ne crée aucun pilier ni roche de
remplissage. Les grandes masses flottantes naturelles et les îlots aériens
ne sont pas supprimés.
Les grottes sous Y170 conservent exactement leur densité précédente. Les
anciens cours deau courbes et leurs brèches de débordement, la traversée,
le sanctuaire minéral, les secrets et les quatre lieux aériens sont conservés.
Leurs recherches utilisent le terrain courant ; leurs emplacements peuvent
changer. Pas de retour de la montgolfière ou des rivières angulaires.
Le biome `sanctuary:swamp27_caves` rejoint les biomes autorisés des réseaux
secrets : un sanctuaire minéral déjà certifié ne doit plus être annulé au
stade du départ natif simplement parce quil se trouve dans ce marais.
La recherche des îlots refuse immédiatement les volumes trop hauts, dès le
premier obstacle incompatible, et filtre grossièrement les emprises au-dessus
de trop grands trous avant leur certification bloc par bloc. Après le secteur initial, les candidats parcourent tout le tour de l’île.
La recherche reste bornée à 64 candidats par site et 16 384 colonnes au total ;
la certification détaillée des sites acceptés est conservée.
## Essai
Minecraft 26.3-pre-2, mêmes dépendances et bouton natif Personnaliser.
Créer un **nouveau monde Sanctuary**. Le codec reste `sanctuary:island_v24`,
conformément au workflow de mondes de test jetables demandé par le créateur.
Les ressources des trois profils sont mises à jour : ne pas prolonger une
ancienne sauvegarde pour comparer des raccords. Cette livraison nouvre et
ne régénère aucune sauvegarde personnelle.
Les prévisualisations de densité servent à examiner les silhouettes ; elles
ne remplacent pas lessai visuel du paysage décoré dans le client Minecraft.
Voir [les validations réalisées](testing-alpha30.3.md).
+97
View File
@@ -0,0 +1,97 @@
# Vérification alpha.30.3
## Premier témoin natif — Mac
Monde Moyen 724, graine `0`, processus `68244` : 24/24 GameTests réussis.
La comparaison utilise les densités natives enregistrées, pas un terrain simulé.
La correction finale de bornage numérique à -1, sans effet sur les blocs solides,
a ensuite été incluse dans le build de livraison.
| Profil mesuré (même graine) | Hauts terrains proches de la base 24* | Plus haut échantillon |
|---|---:|---:|
| Petit 512 | 330 / 463 (71 %) | Y344 |
| Moyen 724 | 913 / 1 288 (71 %) | Y344 |
| Grand 1 024 | 1 930 / 2 853 (68 %) | Y344 |
*Colonnes échantillonnées tous les 12 blocs, ancienne surface au-dessus de Y200,
écart final de deux blocs au plus. La base de comparaison est le relief 24 repris
en 27. Cela ne mesure ni la surface constructible exacte ni une qualité esthétique.
Les trois profils sont compilés avec le même état aléatoire natif ; seul le
monde Moyen de ce premier essai est parcouru en chunks FULL.
Le volume local contrôlé passe de cinq petits composants détachés en 30.1 à
zéro en 30.3. Le parcours inversé des échantillons donne les mêmes appartenances.
La densité sous Y170 est identique à la 30.1 aux points vérifiés.
La traversée a 19 départs de structure, 29 chunks FULL visités, portée 272.
Le sanctuaire minéral est admis et son sol natif est inspecté. Les deux navires
et deux îlots sont générés ; les tests du Blocodex et des minerais réussissent.
La rivière courbe centrale est présente. Ce témoin ne fournit pas de nouvelle
brèche de berge admissible : il ne constitue donc pas une preuve supplémentaire
de cascade de surface sous ticks, malgré le succès du test des cellules deau.
Preuves ignorées : `build/alpha303-native-medium.log`,
`build/alpha303-evidence/medium-0/`, `build/alpha303-preview/`.
Les cartes et coupes visualisent la roche seule ; aucune capture client ni
validation esthétique du joueur nest revendiquée.
## Build de livraison et témoin Petit — Mac
`./gradlew check build -PsanctuaryTestDiameter=512 -PsanctuaryTestSeed=42
-PsanctuaryTransit29Tests=true -PsanctuaryAerial24Tests=true` : réussite,
24/24 tests natifs et contrôles JVM, 4 min 53 au total sur cette machine Mac.
Ce temps comprend compilation, anciens tests et inspections ; ce nest pas
une mesure du délai de chargement dun client.
Processus `69553`. Les 15 modules de traversée possèdent leurs départs natifs,
32 chunks FULL sont parcourus, portée 204. Le sanctuaire minéral apparaît
réellement dans le marais. Quatre sites aériens, quatre coffres, deux villageois,
une source de cascade sur un îlot ; leurs coffres sont ouverts et les habitants
déplacés dans ce monde jetable par le test. Aucun petit composant détaché dans
le volume contrôlé ; 1 536 échantillons restent identiques en ordre inversé.
Les trois profils sous la graine 42 conservent 60 %, 59 % et 58 % des hauts
terrains à deux blocs au plus de la base 24. Sommets échantillonnés :
Y344 / Y346 / Y346. Une brèche de bassin produit de leau sous ticks réels ;
la région centrale de ce témoin nadmet pas de rivière courbe.
Preuves : `build/alpha303-check-build.log`, `build/alpha303-evidence/small-42/`.
## Témoin Grand signalé par le créateur — Mac
Grand 1024, graine `-7228211907433324401`, processus `69750` : 24/24 tests
natifs réussis. La traversée compte 23 départs, portée 374, avec 52 chunks FULL
parcourus et des accès vérifiés. Localisation témoin : `-187,249,-72`.
Le sanctuaire minéral a un départ valide et un véritable sol central ;
les quatre lieux aériens génèrent leurs quatre coffres et deux marchands.
La rivière courbe centrale est présente ; huit cellules deau sont constatées
sous les ouvertures après propagation native. Le volume local passe de 12
petits composants détachés à zéro. Les1 536 points contrôlés en ordre inversé
restent identiques et les 7 436 échantillons profonds sont inchangés.
Sur cette graine, les plus hauts échantillons des profils 512/724/1024 atteignent
Y322 / Y344 / Y346. Laltitude nest pas forcée à 360 sur toutes les graines.
Preuves : `build/alpha303-native-large.log`, `build/alpha303-evidence/large-user/`.
Les fichiers `verified-run.json` désignent les rapports de chaque processus ;
les autres anciens diagnostics copiés ne comptent pas comme des essais 30.3.
## Défauts corrigés pendant la vérification
Petit 512, graine 42, révélait deux défauts distincts :
- La recherche aérienne épuisait ses colonnes sur des emprises trop hautes ou
trop creuses. Le rejet de hauteur est maintenant immédiat, lappui est filtré
grossièrement avant certification et les candidats de repli couvrent le tour
de l’île. Le plan témoin obtenu emploie 8 058 colonnes, avec quatre sites.
- La chambre minérale était admise en `sanctuary:swamp27_caves`, mais ce biome
manquait au tag `has_structure/forgotten_networks`. Le départ natif était donc
refusé. Le tag inclut désormais ce biome, sans contourner la certification.
Les premiers journaux en échec sont conservés sous `build/alpha303-small42-*failure.log`.
## Limites
Essais exécutés sur Mac ; aucun essai natif Windows par lagent. Les variations
de relief exigent un nouveau monde ; aucun monde personnel nest régénéré.
Le filtrage local de connexité nest pas un audit exhaustif de l’île entière.
La présence des mines, huttes et donjons reste conditionnelle au terrain.
+2 -2
View File
@@ -8,6 +8,6 @@ loader_version=0.19.5
loom_version=1.17.20 loom_version=1.17.20
fabric_api_version=0.160.0+26.3 fabric_api_version=0.160.0+26.3
mod_version=0.1.0-alpha.30.2 mod_version=0.1.0-alpha.30.3
pack_version=0.1.0-alpha.30.2 pack_version=0.1.0-alpha.30.3
maven_group=fr.koka.sanctuary maven_group=fr.koka.sanctuary
+8
View File
@@ -674,3 +674,11 @@ tasks.register('surfaceRelief302Smoke', JavaExec) {
mainClass = 'fr.koka.sanctuary.worldgen.SurfaceRelief302Smoke' mainClass = 'fr.koka.sanctuary.worldgen.SurfaceRelief302Smoke'
} }
tasks.named('check') { dependsOn('surfaceRelief302Smoke') } tasks.named('check') { dependsOn('surfaceRelief302Smoke') }
tasks.register('balancedRelief303Smoke', JavaExec) {
group = 'verification'
dependsOn tasks.named('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.BalancedRelief303Smoke'
}
tasks.named('check') { dependsOn('balancedRelief303Smoke') }
@@ -16,13 +16,13 @@ import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Compares registered native fields; does not place blocks or load additional chunks. */ /** Compares registered native fields; does not place blocks or load additional chunks. */
public final class Terraces29WorldGameTests { public final class Terraces29WorldGameTests {
@GameTest(maxTicks = 2400) @GameTest(maxTicks = 2400)
public void fineLedgesPreserveTheAcceptedMass(GameTestHelper helper) throws Exception { public void broadProvincesRestorePlateausAndRaiseNativeMasses(GameTestHelper helper) throws Exception {
var level = helper.getLevel(); var level = helper.getLevel();
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator(); var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
var field = (PopulationIslandDensity) level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION) var field = (PopulationIslandDensity) level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION)
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, .getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION,
SanctuaryMod.id("population_" + generator.capacity().players()))).value(); SanctuaryMod.id("population_" + generator.capacity().players()))).value();
helper.assertTrue(field.relief30() && !field.terraces29() && field.surface302(), "Alpha30.2 adds non-periodic local relief to the global field"); helper.assertTrue(field.balanced303() && !field.terraces29() && !field.surface302(), "Alpha30.3 removes fine bands and uses broad geological provinces");
var random = generator.starterContext().random(); var random = generator.starterContext().random();
var current = random.getSampler(field); var previous = random.getSampler(field.legacy301()); var current = random.getSampler(field); var previous = random.getSampler(field.legacy301());
var context = SamplerContext.builder().enableCaches().build(); var context = SamplerContext.builder().enableCaches().build();
@@ -31,25 +31,25 @@ public final class Terraces29WorldGameTests {
report.put("process_id", ProcessHandle.current().pid()); report.put("process_id", ProcessHandle.current().pid());
int radius = Math.min(400, generator.initialDiameter() / 2), lowerSamples = 0, unchanged = 0, changed = 0; 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 z = -radius; z <= radius; z += 32) for (int x = -radius; x <= radius; x += 32) {
for (int y = 32; y <= 140; y += 13) { for (int y = 32; y <= 170; y += 13) {
helper.assertTrue(Float.floatToIntBits(current.sampleValue(context, x, y, z)) helper.assertTrue(Float.floatToIntBits(current.sampleValue(context, x, y, z))
== Float.floatToIntBits(previous.sampleValue(context, x, y, z)), "Deep density is exactly alpha30.1"); == Float.floatToIntBits(previous.sampleValue(context, x, y, z)), "Deep density is exactly alpha30.1");
lowerSamples++; lowerSamples++;
} }
for (int y = 141; y <= 320; y++) { for (int y = 171; y <= 380; y++) {
boolean old = previous.sampleValue(context, x, y, z) > 0; boolean old = previous.sampleValue(context, x, y, z) > 0;
boolean now = current.sampleValue(context, x, y, z) > 0; boolean now = current.sampleValue(context, x, y, z) > 0;
if (old == now) unchanged++; else changed++; if (old == now) unchanged++; else changed++;
if (Math.hypot(x, z) <= 48) helper.assertTrue(old == now, "Arrival silhouette is unchanged"); if (Math.hypot(x, z) <= 48) helper.assertTrue(old == now, "Arrival silhouette is unchanged");
} }
} }
helper.assertTrue(changed > 0 && unchanged > changed * 3L, "Local surface offsets retain most of the accepted rock volume"); helper.assertTrue(changed > 0, "The upper field now combines broad plateaus and raised rock");
report.put("unchanged_lower_density_samples", lowerSamples); report.put("unchanged_lower_density_samples", lowerSamples);
report.put("unchanged_upper_samples", unchanged); report.put("changed_upper_samples", changed); report.put("unchanged_upper_samples", unchanged); report.put("changed_upper_samples", changed);
var sections = new LinkedHashMap<String, Object>(); var sections = new LinkedHashMap<String, Object>();
for (int z : new int[]{0, 96}) { for (int z : new int[]{0, 96}) {
var before = new ArrayList<String>(); var after = new ArrayList<String>(); var before = new ArrayList<String>(); var after = new ArrayList<String>();
for (int y = 305; y >= 145; y--) { for (int y = 365; y >= 145; y--) {
var a = new StringBuilder(); var b = new StringBuilder(); var a = new StringBuilder(); var b = new StringBuilder();
for (int x = -radius; x <= radius; x++) { for (int x = -radius; x <= radius; x++) {
a.append(previous.sampleValue(context, x, y, z) > 0 ? '#' : '.'); a.append(previous.sampleValue(context, x, y, z) > 0 ? '#' : '.');
@@ -57,7 +57,7 @@ public final class Terraces29WorldGameTests {
} }
before.add(a.toString()); after.add(b.toString()); before.add(a.toString()); after.add(b.toString());
} }
sections.put("z" + z, Map.of("before", before, "after", after, "min_x", -radius, "max_y", 305, "step", 1)); sections.put("z" + z, Map.of("before", before, "after", after, "min_x", -radius, "max_y", 365, "step", 1));
} }
report.put("sections", sections); report.put("sections", sections);
// One block-resolution volume, chosen using the old field's surface transitions. // One block-resolution volume, chosen using the old field's surface transitions.
@@ -76,7 +76,7 @@ public final class Terraces29WorldGameTests {
if(score>bestScore){bestScore=score;bestX=cx;bestZ=cz;} if(score>bestScore){bestScore=score;bestX=cx;bestZ=cz;}
} }
var oldVolume=new ArrayList<String>();var newVolume=new ArrayList<String>(); var oldVolume=new ArrayList<String>();var newVolume=new ArrayList<String>();
int size=64,minY=164,maxY=291; int size=64,minY=171,maxY=362;
for(int y=minY;y<=maxY;y++)for(int z=bestZ-size/2;z<bestZ+size/2;z++){ 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(); var a=new StringBuilder();var b=new StringBuilder();
for(int x=bestX-size/2;x<bestX+size/2;x++){ for(int x=bestX-size/2;x<bestX+size/2;x++){
@@ -97,8 +97,33 @@ public final class Terraces29WorldGameTests {
helper.assertTrue((current.sampleValue(reverseContext,x,y,z)>0)==expected,"Rock membership is independent of cache and reverse sample order");reversed++; 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("reverse_order_samples",reversed);
// Broad top views compare all three supported sizes under the same native noise seed.
var sizes = new ArrayList<Object>();
for (int players : new int[]{5,10,20}) {
var f = (PopulationIslandDensity) level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION)
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, SanctuaryMod.id("population_"+players))).value();
var now = random.getSampler(f); var old = random.getSampler(f.legacy27());
var global = random.getSampler(f.legacy301());
int r = (int)Math.round(f.capacity().radius());
var columns = new ArrayList<Object>(); int retained=0,land=0,highest=0;
for(int z=-r;z<=r;z+=12)for(int x=-r;x<=r;x+=12){
int a=0,b=0,c=0;
for(int y=380;y>=32;y-=2){
if(a==0 && old.sampleValue(context,x,y,z)>0)a=y;
if(b==0 && now.sampleValue(context,x,y,z)>0)b=y;
if(c==0 && global.sampleValue(context,x,y,z)>0)c=y;
if(a>0&&b>0&&c>0)break;
}
if(a>=200){land++;if(Math.abs(a-b)<=2)retained++;}
highest=Math.max(highest,b); columns.add(List.of(x,z,a,b,c));
}
helper.assertTrue(highest<=358,"Natural summits leave room below the build ceiling");
helper.assertTrue(retained>land*.25,"Each size retains substantial original uplands, rather than changing every slope");
sizes.add(Map.of("diameter",r*2,"columns",columns,"unchanged_uplands",retained,"uplands",land,"highest_sample",highest));
}
report.put("sizes",sizes);
report.put("passed", true); report.put("passed", true);
Path path = Path.of("diagnostics/alpha302", "relief-" + generator.initialDiameter() + "-" + level.getSeed() + ".json"); Path path = Path.of("diagnostics/alpha303", "relief-" + generator.initialDiameter() + "-" + level.getSeed() + ".json");
Files.createDirectories(path.getParent()); Files.createDirectories(path.getParent());
Files.writeString(path, new GsonBuilder().setPrettyPrinting().create().toJson(report)); Files.writeString(path, new GsonBuilder().setPrettyPrinting().create().toJson(report));
helper.succeed(); helper.succeed();
@@ -0,0 +1,48 @@
package fr.koka.sanctuary.worldgen;
import java.util.Arrays;
import net.minecraft.world.level.levelgen.densityfunction.*;
/** Broad geological provinces, sampled once in island-relative coordinates.
* The mask selects where the existing 3D rock field can rise; it never defines a summit shape. */
public final class BalancedRelief303 {
private static final int SIZE = 65;
private final float[] samples = new float[SIZE * SIZE];
private final double radius, low, high;
private final long seed;
public BalancedRelief303(DensitySampler noise, double radius, long seed) {
this.radius = radius; this.seed = seed;
float[] distribution = new float[SIZE * SIZE]; int count = 0;
for (int z = 0; z < SIZE; z++) for (int x = 0; x < SIZE; x++) {
float value = noise.sampleValue(SamplerContext.EMPTY_UNCACHED, x * 2 - 64, 87, z * 2 - 64);
samples[z * SIZE + x] = value;
if (Math.hypot(x - 32, z - 32) <= 26) distribution[count++] = value;
}
Arrays.sort(distribution, 0, count);
low = distribution[count / 2]; high = Math.max(low + .0001, distribution[(int)(count * .82)]);
}
public double province(int x, int z) {
double gx = Math.clamp((x / radius + 1) * 32, 0, 63.999999);
double gz = Math.clamp((z / radius + 1) * 32, 0, 63.999999);
int ix = (int)gx, iz = (int)gz; double fx = gx - ix, fz = gz - iz;
double n = lerp(fz, lerp(fx, samples[iz*SIZE+ix], samples[iz*SIZE+ix+1]),
lerp(fx, samples[(iz+1)*SIZE+ix], samples[(iz+1)*SIZE+ix+1]));
double distance = Math.hypot(x, z);
return smooth((n - low) / (high - low)) * smooth((distance - 48) / 40)
* (1 - smooth((distance / radius - .68) / .22))
* Relief30.influence(seed, x, z, radius);
}
/** Continuous, strictly increasing vertical domain: no bands, ledges or added fine noise. */
public static double sourceY(int y, double province) {
if (y <= 170 || province <= 0) return y;
double d = y - 170;
// The first 30 blocks blend into untouched lower caves with a matching first derivative.
return 170 + d / (1 + .95 * province * smooth(d / 30));
}
public static double ceilingErosion(int y) { return 2 * smooth((y - 340) / 36.0); }
private static double lerp(double t, double a, double b) { return a + t * (b - a); }
private static double smooth(double t) { t = Math.clamp(t, 0, 1); return t*t*(3-2*t); }
}
@@ -13,11 +13,16 @@ public final class DetachedRock302 implements DensitySampler {
private final DensitySampler input; private final DensitySampler input;
private final double radius; private final double radius;
private final long seed; private final long seed;
private final boolean broad;
public DetachedRock302(DensitySampler input, double radius, long seed) { public DetachedRock302(DensitySampler input, double radius, long seed) {
this.input = input; this.radius = radius; this.seed = seed; this(input, radius, seed, false);
}
public DetachedRock302(DensitySampler input, double radius, long seed, boolean broad) {
this.input = input; this.radius = radius; this.seed = seed; this.broad = broad;
} }
private boolean affected(int x, int y, int z) { private boolean affected(int x, int y, int z) {
if (broad) return y > 170 && y < 376 && Math.hypot(x, z) > 48 && Math.hypot(x,z) < radius + 32;
return SurfaceRelief302.weight(x,y,z,radius) * Relief30.influence(seed,x,z,radius) > 0; 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) { @Override public float sampleValue(SamplerContext context,int x,int y,int z) {
@@ -13,7 +13,7 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
DensityFunction sculpt, DensityFunction detail, DensityFunction underside, DensityFunction sculpt, DensityFunction detail, DensityFunction underside,
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity, DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity,
DensityFunction caves28, DensityFunction ridges28, DensityFunction erosion28, boolean geology28, DensityFunction caves28, DensityFunction ridges28, DensityFunction erosion28, boolean geology28,
boolean terraces29, boolean relief30, boolean surface302) implements DensityFunction { boolean terraces29, boolean relief30, boolean surface302, boolean balanced303) implements DensityFunction {
public static final MapCodec<PopulationIslandDensity> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group( public static final MapCodec<PopulationIslandDensity> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
DensityFunction.CODEC.fieldOf("terrain").forGetter(PopulationIslandDensity::terrain), DensityFunction.CODEC.fieldOf("terrain").forGetter(PopulationIslandDensity::terrain),
DensityFunction.CODEC.fieldOf("distortion").forGetter(PopulationIslandDensity::distortion), DensityFunction.CODEC.fieldOf("distortion").forGetter(PopulationIslandDensity::distortion),
@@ -29,14 +29,15 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
Codec.BOOL.optionalFieldOf("geology_28", false).forGetter(PopulationIslandDensity::geology28), 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), Codec.BOOL.optionalFieldOf("relief_30", false).forGetter(PopulationIslandDensity::relief30),
Codec.BOOL.optionalFieldOf("surface_302", false).forGetter(PopulationIslandDensity::surface302) Codec.BOOL.optionalFieldOf("surface_302", false).forGetter(PopulationIslandDensity::surface302),
Codec.BOOL.optionalFieldOf("balanced_303", false).forGetter(PopulationIslandDensity::balanced303)
).apply(instance, PopulationIslandDensity::new)); ).apply(instance, PopulationIslandDensity::new));
public PopulationIslandDensity(DensityFunction terrain, DensityFunction distortion, public PopulationIslandDensity(DensityFunction terrain, DensityFunction distortion,
DensityFunction sculpt, DensityFunction detail, DensityFunction underside, DensityFunction sculpt, DensityFunction detail, DensityFunction underside,
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity) { DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity) {
this(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail, capacity, this(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail, capacity,
DensityFunctions.zero(), DensityFunctions.zero(), DensityFunctions.zero(), false, false, false, false); DensityFunctions.zero(), DensityFunctions.zero(), DensityFunctions.zero(), false, false, false, false, false);
} }
public PopulationIslandDensity(DensityFunction terrain, DensityFunction distortion, public PopulationIslandDensity(DensityFunction terrain, DensityFunction distortion,
@@ -44,7 +45,7 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity, DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity,
DensityFunction caves28, DensityFunction ridges28, DensityFunction erosion28, boolean geology28) { DensityFunction caves28, DensityFunction ridges28, DensityFunction erosion28, boolean geology28) {
this(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail, capacity, this(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail, capacity,
caves28, ridges28, erosion28, geology28, false, false, false); caves28, ridges28, erosion28, geology28, false, false, false, false);
} }
@Override public DensitySampler compileSampler(CompileContext context) { @Override public DensitySampler compileSampler(CompileContext context) {
@@ -56,6 +57,7 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
var ridges = geology28 ? ridges28.compileSampler(context) : null; var ridges = geology28 ? ridges28.compileSampler(context) : null;
var erosion = geology28 ? erosion28.compileSampler(context) : null; var erosion = geology28 ? erosion28.compileSampler(context) : null;
long seed = context.createRandom(SanctuaryMod.id("population_rifts_alpha10")).nextLong(); long seed = context.createRandom(SanctuaryMod.id("population_rifts_alpha10")).nextLong();
var balanced = balanced303 ? new BalancedRelief303(coarse, capacity.radius(), seed) : null;
var layouts = new ConcurrentHashMap<IslandCapacity.Region, RiftShape.Layout>(); var layouts = new ConcurrentHashMap<IslandCapacity.Region, RiftShape.Layout>();
DensitySampler field = new DensitySampler() { DensitySampler field = new DensitySampler() {
private float terrainAt(SamplerContext sample, double x, double y, double z) { private float terrainAt(SamplerContext sample, double x, double y, double z) {
@@ -78,16 +80,19 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
float coarseValue = coarse.sampleValue(sample, x, localY, z); float coarseValue = coarse.sampleValue(sample, x, localY, z);
float fineValue = fine.sampleValue(sample, x, localY, z); float fineValue = fine.sampleValue(sample, x, localY, z);
float terrainValue; float terrainValue;
double province = balanced != null ? balanced.province(x, z) : 0;
double mappedY = balanced != null ? BalancedRelief303.sourceY(y, province) : y;
double weight = geology28 ? CavesRelief28.deformationWeight(x, y, z, capacity.radius()) : 0; double weight = geology28 ? CavesRelief28.deformationWeight(x, y, z, capacity.radius()) : 0;
if (relief30) weight *= Relief30.influence(seed, x, z, capacity.radius()); if (balanced != null) weight *= province;
else if (relief30) weight *= Relief30.influence(seed, x, z, capacity.radius());
double surfaceWeight = surface302 ? SurfaceRelief302.weight(x, y, z, capacity.radius()) double surfaceWeight = surface302 ? SurfaceRelief302.weight(x, y, z, capacity.radius())
* Relief30.influence(seed, x, z, capacity.radius()) : 0; * Relief30.influence(seed, x, z, capacity.radius()) : 0;
if (weight > 0 || surfaceWeight > 0) { if (weight > 0 || surfaceWeight > 0 || mappedY != y) {
var shift = weight > 0 ? CavesRelief28.warp(weight, var shift = weight > 0 ? CavesRelief28.warp(weight,
erosion.sampleValue(sample, x + 173, y - 59, z - 89), erosion.sampleValue(sample, x + 173, y - 59, z - 89),
ridges.sampleValue(sample, x, y, z), ridges.sampleValue(sample, x, y, z),
erosion.sampleValue(sample, x - 211, y + 137, z + 307)) : new CavesRelief28.Warp(0,0,0); erosion.sampleValue(sample, x - 211, y + 137, z + 307)) : new CavesRelief28.Warp(0,0,0);
double sourceY = y - 64 - shift.y(); double sourceY = mappedY - 64 - shift.y();
if (surfaceWeight > 0) sourceY = SurfaceRelief302.sourceY(sourceY,surfaceWeight, if (surfaceWeight > 0) sourceY = SurfaceRelief302.sourceY(sourceY,surfaceWeight,
coarse.sampleValue(sample,x-233,y+419,z+157)); coarse.sampleValue(sample,x-233,y+419,z+157));
if (terraces29) sourceY = Terraces29.sourceY(sourceY, coarseValue, fineValue, if (terraces29) sourceY = Terraces29.sourceY(sourceY, coarseValue, fineValue,
@@ -97,7 +102,8 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
// Keep the island envelope at actual coordinates, independent from displaced rock. // Keep the island envelope at actual coordinates, independent from displaced rock.
float natural = PopulationIslandShape.raisedDensity(x, y, z, terrainValue, float natural = PopulationIslandShape.raisedDensity(x, y, z, terrainValue,
edge.sampleValue(sample, x, 0, z), coarseValue, edge.sampleValue(sample, x, 0, z), coarseValue,
fineValue, bottom.sampleValue(sample, x, localY, z), 64, 384, capacity); fineValue, bottom.sampleValue(sample, x, localY, z), 64, 384, capacity, mappedY - 64);
if (balanced != null) natural = Math.max(-1, natural - (float) BalancedRelief303.ceilingErosion(y));
if (natural <= 0) return natural; if (natural <= 0) return natural;
var region = capacity.regionAt(x, z); var region = capacity.regionAt(x, z);
var layout = layouts.computeIfAbsent(region, r -> RiftShape.layout(regionSeed(seed, r))); var layout = layouts.computeIfAbsent(region, r -> RiftShape.layout(regionSeed(seed, r)));
@@ -116,7 +122,7 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
DensitySampler.sampleVolumeNaive(sample, buffer, volume, this); DensitySampler.sampleVolumeNaive(sample, buffer, volume, this);
} }
}; };
return surface302 ? new DetachedRock302(field,capacity.radius(),seed) : field; return surface302 || balanced303 ? new DetachedRock302(field,capacity.radius(),seed, balanced303) : field;
} }
/** Native diagnostics can compile the exact accepted27 field without new adaptations. */ /** Native diagnostics can compile the exact accepted27 field without new adaptations. */
public PopulationIslandDensity legacy27() { public PopulationIslandDensity legacy27() {
@@ -126,11 +132,11 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
/** Exact accepted28 deformation and caves, without the fine local ledges. */ /** Exact accepted28 deformation and caves, without the fine local ledges. */
public PopulationIslandDensity legacy28() { public PopulationIslandDensity legacy28() {
return new PopulationIslandDensity(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail, return new PopulationIslandDensity(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail,
capacity, caves28, ridges28, erosion28, geology28, false, false, false); capacity, caves28, ridges28, erosion28, geology28, false, false, false, false);
} }
public PopulationIslandDensity legacy301() { public PopulationIslandDensity legacy301() {
return new PopulationIslandDensity(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail, return new PopulationIslandDensity(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail,
capacity, caves28, ridges28, erosion28, geology28, terraces29, relief30, false); capacity, caves28, ridges28, erosion28, geology28, terraces29, relief30, false, false);
} }
public static long regionSeed(long seed, IslandCapacity.Region region) { public static long regionSeed(long seed, IslandCapacity.Region region) {
long value = seed ^ ((long) region.x() << 32) ^ (region.z() & 0xffffffffL) ^ 0xA10CA7E1L; long value = seed ^ ((long) region.x() << 32) ^ (region.z() & 0xffffffffL) ^ 0xA10CA7E1L;
@@ -141,7 +147,7 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
@Override public DensityFunction rewriteChildren(DfRewriteRule rule) { @Override public DensityFunction rewriteChildren(DfRewriteRule rule) {
return new PopulationIslandDensity(rule.rewrite(terrain), rule.rewrite(distortion), rule.rewrite(sculpt), 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(detail), rule.rewrite(underside), rule.rewrite(riftWarp), rule.rewrite(riftDetail), capacity,
rule.rewrite(caves28), rule.rewrite(ridges28), rule.rewrite(erosion28), geology28, terraces29, relief30, surface302); rule.rewrite(caves28), rule.rewrite(ridges28), rule.rewrite(erosion28), geology28, terraces29, relief30, surface302, balanced303);
} }
@Override public Interval range() { return Interval.of(-1, 1); } @Override public Interval range() { return Interval.of(-1, 1); }
@Override public int domainAxes() { return ALL_AXES; } @Override public int domainAxes() { return ALL_AXES; }
@@ -7,6 +7,12 @@ public final class PopulationIslandShape {
/** Generation v3. The natural field and lateral sculpture are sampled in their original frame. */ /** Generation v3. The natural field and lateral sculpture are sampled in their original frame. */
public static float raisedDensity(int x, int y, int z, float terrain, float distortion, public static float raisedDensity(int x, int y, int z, float terrain, float distortion,
float sculpt, float detail, float underside, int offset, int height, IslandCapacity capacity) { float sculpt, float detail, float underside, int offset, int height, IslandCapacity capacity) {
return raisedDensity(x, y, z, terrain, distortion, sculpt, detail, underside, offset, height, capacity, (double)y - offset);
}
/** A raised province relaxes only the old upper fade; lateral and lower coast cuts stay in place. */
public static float raisedDensity(int x, int y, int z, float terrain, float distortion,
float sculpt, float detail, float underside, int offset, int height, IslandCapacity capacity, double upperLocalY) {
double distance = Math.hypot((double) x, (double) z); double distance = Math.hypot((double) x, (double) z);
if (distance >= capacity.terrainLimit() || y <= 0 || y >= height) return -1.0F; if (distance >= capacity.terrainLimit() || y <= 0 || y >= height) return -1.0F;
double localY = (double) y - offset; double localY = (double) y - offset;
@@ -21,7 +27,7 @@ public final class PopulationIslandShape {
erosion += 2.0 * lower * lower * lower * lower; erosion += 2.0 * lower * lower * lower * lower;
// Upper silhouette is exactly the old one translated by offset. Absolute-world fades are // Upper silhouette is exactly the old one translated by offset. Absolute-world fades are
// safety margins, outside the normal terrain, rather than the island's visible underside. // safety margins, outside the normal terrain, rather than the island's visible underside.
erosion += 2.0 * smoothstep((localY - 224.0) / 32.0); erosion += 2.0 * smoothstep((upperLocalY - 224.0) / 32.0);
erosion += 2.0 * smoothstep((24.0 - y) / 24.0); erosion += 2.0 * smoothstep((24.0 - y) / 24.0);
erosion += 2.0 * smoothstep((y - (height - 24.0)) / 24.0); erosion += 2.0 * smoothstep((y - (height - 24.0)) / 24.0);
return bounded(terrain, erosion); return bounded(terrain, erosion);
@@ -14,7 +14,7 @@ import fr.koka.sanctuary.worldgen.aerial.AerialPlan24.Site;
/** Fixed work limits, not wall-clock-dependent draws. No chunk is requested or modified by this planner. */ /** Fixed work limits, not wall-clock-dependent draws. No chunk is requested or modified by this planner. */
public final class AerialPlanner24 { public final class AerialPlanner24 {
public static final int MAX_ATTEMPTS_PER_SITE = 64; public static final int MAX_ATTEMPTS_PER_SITE = 64;
public static final int MAX_COLUMNS = 8192; public static final int MAX_COLUMNS = 16384;
public static final int ARRIVAL_RADIUS = 96; public static final int ARRIVAL_RADIUS = 96;
public static final int MIN_ISLET_GAP = 16; public static final int MIN_ISLET_GAP = 16;
private AerialPlanner24() {} private AerialPlanner24() {}
@@ -58,11 +58,15 @@ public final class AerialPlanner24 {
int bodyHeight = merchant ? 18 : 20 + (int) Math.floorMod(mix(siteSeed), 16); int bodyHeight = merchant ? 18 : 20 + (int) Math.floorMod(mix(siteSeed), 16);
int height = bodyHeight + (merchant ? 0 : AerialPlan24.VEGETATION_HEADROOM); int height = bodyHeight + (merchant ? 0 : AerialPlan24.VEGETATION_HEADROOM);
Site accepted = null; Site accepted = null;
for (int attempt = 0; attempt < MAX_ATTEMPTS_PER_SITE; attempt++) { candidates: for (int attempt = 0; attempt < MAX_ATTEMPTS_PER_SITE; attempt++) {
probe.candidate = ids[ordinal] + " attempt=" + attempt;
long draw = mix(siteSeed + attempt * 0x9e3779b97f4a7c15L); long draw = mix(siteSeed + attempt * 0x9e3779b97f4a7c15L);
// Keep the four remaining bearings and seeded shapes; the former first sector is simply free. // Keep the four remaining bearings and seeded shapes; the former first sector is simply free.
double angle = initialAngle + (ordinal + 1) * (Math.PI * 2 / 5) + (unit(draw) - .5) * .9 double angle = initialAngle + (ordinal + 1) * (Math.PI * 2 / 5) + (unit(draw) - .5) * .9
+ (attempt / 16) * .41; + (attempt / 16) * .41;
// After the initial sector, cover the whole island instead of repeatedly inspecting
// the same mountain. First-choice locations and all geometry rules remain intact.
if (!merchant && attempt >= 4) angle = initialAngle + (attempt - 4) * 2.399963229728653;
int facing = (int) Math.floorMod(mix(draw), 4); int facing = (int) Math.floorMod(mix(draw), 4);
int worldWidth = facing % 2 == 0 ? width : depth, worldDepth = facing % 2 == 0 ? depth : width; int worldWidth = facing % 2 == 0 ? width : depth, worldDepth = facing % 2 == 0 ? depth : width;
Shore shore; Shore shore;
@@ -85,13 +89,18 @@ public final class AerialPlanner24 {
cx = (int) Math.round(Math.cos(angle) * distance); cx = (int) Math.round(Math.cos(angle) * distance);
cz = (int) Math.round(Math.sin(angle) * distance); cz = (int) Math.round(Math.sin(angle) * distance);
int top = probe.surface(cx, cz); int top = probe.surface(cx, cz);
if (top < 120 || probe.terrain.reserved(cx, cz, top, top + 3)) continue; if (top < 120 || top + MIN_ISLET_GAP + 1 + height > 350
|| probe.terrain.reserved(cx, cz, top, top + 3)) continue;
shore = dryViewpoint(probe, cx, cz); shore = dryViewpoint(probe, cx, cz);
if (shore == null) continue; if (shore == null) continue;
int highest = top; int highest = top;
for (int ox = -worldWidth / 2; ox <= worldWidth / 2; ox += 4) for (int ox = -worldWidth / 2; ox <= worldWidth / 2; ox += 4)
for (int oz = -worldDepth / 2; oz <= worldDepth / 2; oz += 4) for (int oz = -worldDepth / 2; oz <= worldDepth / 2; oz += 4) {
highest = Math.max(highest, probe.highest(cx + ox, cz + oz)); highest = Math.max(highest, probe.highest(cx + ox, cz + oz));
// One obstruction is enough to refuse a candidate. Do not consume the
// remaining column budget on an island that already cannot fit vertically.
if (highest + MIN_ISLET_GAP + 1 + height > 350) continue candidates;
}
bottom = highest + MIN_ISLET_GAP + 1; bottom = highest + MIN_ISLET_GAP + 1;
} }
if (bottom < 32 || bottom + height > 350) continue; if (bottom < 32 || bottom + height > 350) continue;
@@ -104,19 +113,32 @@ public final class AerialPlanner24 {
} else { } else {
// Certify every physical rock column, not only the coarse samples used to // Certify every physical rock column, not only the coarse samples used to
// choose a height. This may raise the whole unchanged island, never the ground. // choose a height. This may raise the whole unchanged island, never the ground.
// A coarse footprint rejects candidates mostly over holes before the full
// per-block certification. Matching the final75% rule avoids exhausting the
// shared budget on the same recessed coast in a small island.
int coarseCount = 0, coarseSupported = 0;
for (int x = 0; x < width; x += 4) for (int z = 0; z < depth; z += 4) {
if (!isletColumn(site, x, z)) continue;
coarseCount++;
if (probe.surface(site.worldX(x,z),site.worldZ(x,z)) >= 120) coarseSupported++;
}
if (coarseSupported * 4 < coarseCount * 3) continue;
int highest = 0, supported = 0, count = 0; int highest = 0, supported = 0, count = 0;
for (int x = 0; x < width; x++) for (int z = 0; z < depth; z++) { for (int x = 0; x < width; x++) for (int z = 0; z < depth; z++) {
if (!isletColumn(site, x, z)) continue; if (!isletColumn(site, x, z)) continue;
int wx = site.worldX(x, z), wz = site.worldZ(x, z); int wx = site.worldX(x, z), wz = site.worldZ(x, z);
highest = Math.max(highest, probe.highest(wx, wz)); highest = Math.max(highest, probe.highest(wx, wz));
if (highest + MIN_ISLET_GAP + 1 + height > 350) continue candidates;
if (probe.surface(wx, wz) >= 120) supported++; if (probe.surface(wx, wz) >= 120) supported++;
count++; count++;
} }
if (supported * 4 < count * 3) continue; if (supported * 4 < count * 3) continue;
if (site.waterfall()) { if (site.waterfall()) {
var outlet = site.waterfallClaimBounds(); var outlet = site.waterfallClaimBounds();
for (int x = outlet.minX(); x <= outlet.maxX(); x++) for (int z = outlet.minZ(); z <= outlet.maxZ(); z++) for (int x = outlet.minX(); x <= outlet.maxX(); x++) for (int z = outlet.minZ(); z <= outlet.maxZ(); z++) {
highest = Math.max(highest, probe.highest(x, z)); highest = Math.max(highest, probe.highest(x, z));
if (highest + MIN_ISLET_GAP + 1 + height > 350) continue candidates;
}
} }
bottom = Math.max(bottom, highest + MIN_ISLET_GAP + 1); bottom = Math.max(bottom, highest + MIN_ISLET_GAP + 1);
if (bottom + height > 350) continue; if (bottom + height > 350) continue;
@@ -217,13 +239,14 @@ public final class AerialPlanner24 {
} }
private static final class Probe { private static final class Probe {
final Terrain terrain; final int radius; final Terrain terrain; final int radius;
String candidate = "unassigned";
final Map<Long, NoiseColumn> columns = new HashMap<>(); final Map<Long, NoiseColumn> columns = new HashMap<>();
Probe(Terrain terrain, int radius) { this.terrain = terrain; this.radius = radius; } Probe(Terrain terrain, int radius) { this.terrain = terrain; this.radius = radius; }
NoiseColumn column(int x, int z) { NoiseColumn column(int x, int z) {
if (Math.hypot(x, z) >= radius) return null; if (Math.hypot(x, z) >= radius) return null;
long key = ((long) x << 32) ^ (z & 0xffffffffL); long key = ((long) x << 32) ^ (z & 0xffffffffL);
var present = columns.get(key); if (present != null) return present; var present = columns.get(key); if (present != null) return present;
if (columns.size() >= MAX_COLUMNS) throw new IllegalStateException("aerial_sites_unavailable: terrain column budget exhausted"); if (columns.size() >= MAX_COLUMNS) throw new IllegalStateException("aerial_sites_unavailable: terrain column budget exhausted at " + candidate);
var result = terrain.column(x, z); var result = terrain.column(x, z);
if (result == null) throw new IllegalStateException("Aerial terrain query returned no column"); if (result == null) throw new IllegalStateException("Aerial terrain query returned no column");
columns.put(key, result); return result; columns.put(key, result); return result;
@@ -50,6 +50,7 @@
"sanctuary:woodland_dry_woodland", "sanctuary:woodland_dry_woodland",
"sanctuary:woodland_oak_forest", "sanctuary:woodland_oak_forest",
"sanctuary:woodland_rocky_heath", "sanctuary:woodland_rocky_heath",
"sanctuary:woodland_sulfur_depths" "sanctuary:woodland_sulfur_depths",
"sanctuary:swamp27_caves"
] ]
} }
@@ -66,5 +66,6 @@
}, },
"terraces_29": false, "terraces_29": false,
"relief_30": true, "relief_30": true,
"surface_302": true "surface_302": false,
"balanced_303": true
} }
@@ -66,5 +66,6 @@
}, },
"terraces_29": false, "terraces_29": false,
"relief_30": true, "relief_30": true,
"surface_302": true "surface_302": false,
"balanced_303": true
} }
@@ -66,5 +66,6 @@
}, },
"terraces_29": false, "terraces_29": false,
"relief_30": true, "relief_30": true,
"surface_302": true "surface_302": false,
"balanced_303": true
} }
@@ -0,0 +1,38 @@
package fr.koka.sanctuary.worldgen;
import net.minecraft.world.level.levelgen.densityfunction.*;
public final class BalancedRelief303Smoke {
private static void require(boolean condition,String message){if(!condition)throw new AssertionError(message);}
public static void main(String[] args){
DensitySampler noise=new DensitySampler(){
@Override public float sampleValue(SamplerContext c,int x,int y,int z){return (float)(Math.sin(x*.05)+Math.cos(z*.04));}
@Override public void sampleVolume(SamplerContext c,DensityBuffer b,DensityVolume v){DensitySampler.sampleVolumeNaive(c,b,v,this);}
};
for(int radius:new int[]{256,362,512}){
var p=new BalancedRelief303(noise,radius,0);int flat=0,mountain=0,n=0;
for(int z=-radius;z<=radius;z+=8)for(int x=-radius;x<=radius;x+=8){
if(Math.hypot(x,z)>radius*.8)continue;
double w=p.province(x,z);if(w==0)flat++;if(w>.5)mountain++;n++;
if(Math.hypot(x,z)<=48)require(w==0,"Natural arrival remains untouched");
}
require(flat>n*.4 && mountain>n*.05,"Every size combines broad natural plateaus and mountains");
}
for(double w=0;w<=1;w+=.05){
double previous=Double.NEGATIVE_INFINITY;
for(int y=0;y<=384;y++){
double now=BalancedRelief303.sourceY(y,w);
require(now>previous,"Vertical mapping cannot fold rock into repeated layers");previous=now;
if(y<=170||w==0)require(now==y,"Zero mountain influence and lower caves preserve original coordinates");
}
}
require(BalancedRelief303.ceilingErosion(358)>=1,"Even fully dense rock fades before Y360");
// New fragment cleanup also covers raised summits, outside the former302 height range.
DensitySampler singleton=new DensitySampler(){
@Override public float sampleValue(SamplerContext c,int x,int y,int z){return x==96&&y==355&&z==100?1:-1;}
@Override public void sampleVolume(SamplerContext c,DensityBuffer b,DensityVolume v){DensitySampler.sampleVolumeNaive(c,b,v,this);}
};
require(new DetachedRock302(singleton,362,0,true).sampleValue(SamplerContext.EMPTY_UNCACHED,96,355,100)<0,"A detached summit block is removed");
System.out.println("BalancedRelief303Smoke: scale balance, untouched arrival/depths, monotonic domains, ceiling and detached summit passed");
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary" name = "Sanctuary"
author = "KOKA99CAB" author = "KOKA99CAB"
version = "0.1.0-alpha.30.2" version = "0.1.0-alpha.30.3"
pack-format = "packwiz:1.1.0" pack-format = "packwiz:1.1.0"
[index] [index]