From 076c9a6c16e93e46db4a16ed0dd8d6a5a22c5a85 Mon Sep 17 00:00:00 2001 From: koka Date: Sat, 12 Sep 2026 13:31:08 +0200 Subject: [PATCH] feat: balance native plateaus and 3D mountains for alpha30.3 --- CHANGELOG.md | 14 +++ README.md | 13 +-- docs/backlog.md | 10 ++ docs/generation-alpha30.3.md | 59 +++++++++++ docs/testing-alpha30.3.md | 97 +++++++++++++++++++ gradle.properties | 4 +- mods/sanctuary/build.gradle | 8 ++ .../gametest/Terraces29WorldGameTests.java | 43 ++++++-- .../sanctuary/worldgen/BalancedRelief303.java | 48 +++++++++ .../sanctuary/worldgen/DetachedRock302.java | 7 +- .../worldgen/PopulationIslandDensity.java | 30 +++--- .../worldgen/PopulationIslandShape.java | 8 +- .../worldgen/aerial/AerialPlanner24.java | 37 +++++-- .../has_structure/forgotten_networks.json | 3 +- .../density_function/population_10.json | 3 +- .../density_function/population_20.json | 3 +- .../density_function/population_5.json | 3 +- .../worldgen/BalancedRelief303Smoke.java | 38 ++++++++ packwiz/pack.toml | 2 +- 19 files changed, 387 insertions(+), 43 deletions(-) create mode 100644 docs/generation-alpha30.3.md create mode 100644 docs/testing-alpha30.3.md create mode 100644 mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/BalancedRelief303.java create mode 100644 mods/sanctuary/src/test/java/fr/koka/sanctuary/worldgen/BalancedRelief303Smoke.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 55f0515..920474f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # 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, l’arrivée, les cours d’eau 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 - Corniches locales dont la phase et l’espacement suivent un bruit 3D, sur le relief global conservé. diff --git a/README.md b/README.md index 6833d89..daf5258 100644 --- a/README.md +++ b/README.md @@ -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 [chantier et ses limites](docs/generation-alpha27.md). -## Alpha.30.2 — corniches et fragments +## Alpha.30.3 — plateaux et grands reliefs -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. +La passe de petites corniches de la 30.2 est retirée après le retour en jeu. +De larges provinces de relief font cohabiter les plateaux naturels issus de la +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) -et [les vérifications](docs/testing-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.3.md). ## Alpha.30.1 — rivières courbes et relief global diff --git a/docs/backlog.md b/docs/backlog.md index d599d76..88710a1 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -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 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). + +### 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 d’eau et les infrastructures ; limiter les petits fragments flottants. +Voir [le changement](generation-alpha30.3.md) et [les preuves](testing-alpha30.3.md). diff --git a/docs/generation-alpha30.3.md b/docs/generation-alpha30.3.md new file mode 100644 index 0000000..891f020 --- /dev/null +++ b/docs/generation-alpha30.3.md @@ -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 d’onde +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 n’est 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 d’eau 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 qu’il 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 n’ouvre et +ne régénère aucune sauvegarde personnelle. + +Les prévisualisations de densité servent à examiner les silhouettes ; elles +ne remplacent pas l’essai visuel du paysage décoré dans le client Minecraft. +Voir [les validations réalisées](testing-alpha30.3.md). diff --git a/docs/testing-alpha30.3.md b/docs/testing-alpha30.3.md new file mode 100644 index 0000000..f3de6a6 --- /dev/null +++ b/docs/testing-alpha30.3.md @@ -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 d’eau. + +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 n’est 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 n’est pas +une mesure du délai de chargement d’un 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 l’eau sous ticks réels ; +la région centrale de ce témoin n’admet 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 d’eau 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. L’altitude n’est 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, l’appui 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 l’agent. Les variations +de relief exigent un nouveau monde ; aucun monde personnel n’est régénéré. +Le filtrage local de connexité n’est pas un audit exhaustif de l’île entière. +La présence des mines, huttes et donjons reste conditionnelle au terrain. diff --git a/gradle.properties b/gradle.properties index 7779f86..62d7f29 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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.2 -pack_version=0.1.0-alpha.30.2 +mod_version=0.1.0-alpha.30.3 +pack_version=0.1.0-alpha.30.3 maven_group=fr.koka.sanctuary diff --git a/mods/sanctuary/build.gradle b/mods/sanctuary/build.gradle index 6ed02c9..129594a 100644 --- a/mods/sanctuary/build.gradle +++ b/mods/sanctuary/build.gradle @@ -674,3 +674,11 @@ tasks.register('surfaceRelief302Smoke', JavaExec) { mainClass = 'fr.koka.sanctuary.worldgen.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') } diff --git a/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Terraces29WorldGameTests.java b/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Terraces29WorldGameTests.java index ff375a1..2e5ea06 100644 --- a/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Terraces29WorldGameTests.java +++ b/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Terraces29WorldGameTests.java @@ -16,13 +16,13 @@ import net.minecraft.world.level.levelgen.densityfunction.SamplerContext; /** Compares registered native fields; does not place blocks or load additional chunks. */ public final class Terraces29WorldGameTests { @GameTest(maxTicks = 2400) - public void fineLedgesPreserveTheAcceptedMass(GameTestHelper helper) throws Exception { + public void broadProvincesRestorePlateausAndRaiseNativeMasses(GameTestHelper helper) throws Exception { var level = helper.getLevel(); var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator(); 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() && 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 current = random.getSampler(field); var previous = random.getSampler(field.legacy301()); var context = SamplerContext.builder().enableCaches().build(); @@ -31,25 +31,25 @@ public final class Terraces29WorldGameTests { report.put("process_id", ProcessHandle.current().pid()); int radius = Math.min(400, generator.initialDiameter() / 2), lowerSamples = 0, unchanged = 0, changed = 0; for (int z = -radius; z <= radius; z += 32) for (int x = -radius; x <= radius; x += 32) { - for (int y = 32; y <= 140; y += 13) { + for (int y = 32; y <= 170; y += 13) { helper.assertTrue(Float.floatToIntBits(current.sampleValue(context, x, y, z)) == Float.floatToIntBits(previous.sampleValue(context, x, y, z)), "Deep density is exactly alpha30.1"); 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 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, "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_upper_samples", unchanged); report.put("changed_upper_samples", changed); var sections = new LinkedHashMap(); for (int z : new int[]{0, 96}) { var before = new ArrayList(); var after = new ArrayList(); - for (int y = 305; y >= 145; y--) { + for (int y = 365; 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 ? '#' : '.'); @@ -57,7 +57,7 @@ public final class Terraces29WorldGameTests { } 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); // 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;} } var oldVolume=new ArrayList();var newVolume=new ArrayList(); - 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;z0)==expected,"Rock membership is independent of cache and reverse sample order");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(); + 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(); 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); - 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.writeString(path, new GsonBuilder().setPrettyPrinting().create().toJson(report)); helper.succeed(); diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/BalancedRelief303.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/BalancedRelief303.java new file mode 100644 index 0000000..54f555a --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/BalancedRelief303.java @@ -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); } +} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/DetachedRock302.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/DetachedRock302.java index a8fa289..c77504a 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/DetachedRock302.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/DetachedRock302.java @@ -13,11 +13,16 @@ public final class DetachedRock302 implements DensitySampler { private final DensitySampler input; private final double radius; private final long seed; + private final boolean broad; 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) { + 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; } @Override public float sampleValue(SamplerContext context,int x,int y,int z) { diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/PopulationIslandDensity.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/PopulationIslandDensity.java index 1602a59..30a2ebf 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/PopulationIslandDensity.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/PopulationIslandDensity.java @@ -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, boolean surface302) implements DensityFunction { + boolean terraces29, boolean relief30, boolean surface302, boolean balanced303) implements DensityFunction { public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group( DensityFunction.CODEC.fieldOf("terrain").forGetter(PopulationIslandDensity::terrain), 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("terraces_29", false).forGetter(PopulationIslandDensity::terraces29), 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)); 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, false); + DensityFunctions.zero(), DensityFunctions.zero(), DensityFunctions.zero(), false, false, false, false, false); } public PopulationIslandDensity(DensityFunction terrain, DensityFunction distortion, @@ -44,7 +45,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, false); + caves28, ridges28, erosion28, geology28, false, false, false, false); } @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 erosion = geology28 ? erosion28.compileSampler(context) : null; long seed = context.createRandom(SanctuaryMod.id("population_rifts_alpha10")).nextLong(); + var balanced = balanced303 ? new BalancedRelief303(coarse, capacity.radius(), seed) : null; var layouts = new ConcurrentHashMap(); DensitySampler field = new DensitySampler() { 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 fineValue = fine.sampleValue(sample, x, localY, z); 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; - 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()) * 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, erosion.sampleValue(sample, x + 173, y - 59, z - 89), ridges.sampleValue(sample, x, y, z), 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, coarse.sampleValue(sample,x-233,y+419,z+157)); 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. float natural = PopulationIslandShape.raisedDensity(x, y, z, terrainValue, 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; var region = capacity.regionAt(x, z); 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); } }; - 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. */ public PopulationIslandDensity legacy27() { @@ -126,11 +132,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, false); + capacity, caves28, ridges28, erosion28, geology28, false, false, false, false); } public PopulationIslandDensity legacy301() { 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) { 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) { 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, 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 int domainAxes() { return ALL_AXES; } diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/PopulationIslandShape.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/PopulationIslandShape.java index 27350b7..8cc077f 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/PopulationIslandShape.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/PopulationIslandShape.java @@ -7,6 +7,12 @@ public final class PopulationIslandShape { /** 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, 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); if (distance >= capacity.terrainLimit() || y <= 0 || y >= height) return -1.0F; double localY = (double) y - offset; @@ -21,7 +27,7 @@ public final class PopulationIslandShape { erosion += 2.0 * lower * lower * lower * lower; // 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. - 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((y - (height - 24.0)) / 24.0); return bounded(terrain, erosion); diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialPlanner24.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialPlanner24.java index 184752f..8590c93 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialPlanner24.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/aerial/AerialPlanner24.java @@ -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. */ public final class AerialPlanner24 { 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 MIN_ISLET_GAP = 16; private AerialPlanner24() {} @@ -58,11 +58,15 @@ public final class AerialPlanner24 { int bodyHeight = merchant ? 18 : 20 + (int) Math.floorMod(mix(siteSeed), 16); int height = bodyHeight + (merchant ? 0 : AerialPlan24.VEGETATION_HEADROOM); 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); // 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 + (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 worldWidth = facing % 2 == 0 ? width : depth, worldDepth = facing % 2 == 0 ? depth : width; Shore shore; @@ -85,13 +89,18 @@ public final class AerialPlanner24 { cx = (int) Math.round(Math.cos(angle) * distance); cz = (int) Math.round(Math.sin(angle) * distance); int top = probe.surface(cx, cz); - if (top < 120 || probe.terrain.reserved(cx, cz, top, top + 3)) continue; + if (top < 120 || top + MIN_ISLET_GAP + 1 + height > 350 + || probe.terrain.reserved(cx, cz, top, top + 3)) continue; shore = dryViewpoint(probe, cx, cz); if (shore == null) continue; int highest = top; for (int ox = -worldWidth / 2; ox <= worldWidth / 2; ox += 4) - for (int oz = -worldDepth / 2; oz <= worldDepth / 2; oz += 4) + for (int oz = -worldDepth / 2; oz <= worldDepth / 2; oz += 4) { 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; } if (bottom < 32 || bottom + height > 350) continue; @@ -104,19 +113,32 @@ public final class AerialPlanner24 { } else { // 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. + // 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; for (int x = 0; x < width; x++) for (int z = 0; z < depth; z++) { if (!isletColumn(site, x, z)) continue; int wx = site.worldX(x, z), wz = site.worldZ(x, z); 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++; count++; } if (supported * 4 < count * 3) continue; if (site.waterfall()) { var outlet = site.waterfallClaimBounds(); - 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)); + 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)); + if (highest + MIN_ISLET_GAP + 1 + height > 350) continue candidates; + } } bottom = Math.max(bottom, highest + MIN_ISLET_GAP + 1); if (bottom + height > 350) continue; @@ -217,13 +239,14 @@ public final class AerialPlanner24 { } private static final class Probe { final Terrain terrain; final int radius; + String candidate = "unassigned"; final Map columns = new HashMap<>(); Probe(Terrain terrain, int radius) { this.terrain = terrain; this.radius = radius; } NoiseColumn column(int x, int z) { if (Math.hypot(x, z) >= radius) return null; long key = ((long) x << 32) ^ (z & 0xffffffffL); var present = columns.get(key); if (present != null) return present; - if (columns.size() >= MAX_COLUMNS) throw new IllegalStateException("aerial_sites_unavailable: terrain column budget exhausted"); + if (columns.size() >= MAX_COLUMNS) throw new IllegalStateException("aerial_sites_unavailable: terrain column budget exhausted at " + candidate); var result = terrain.column(x, z); if (result == null) throw new IllegalStateException("Aerial terrain query returned no column"); columns.put(key, result); return result; diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/tags/worldgen/biome/has_structure/forgotten_networks.json b/mods/sanctuary/src/main/resources/data/sanctuary/tags/worldgen/biome/has_structure/forgotten_networks.json index 4468af2..158a085 100644 --- a/mods/sanctuary/src/main/resources/data/sanctuary/tags/worldgen/biome/has_structure/forgotten_networks.json +++ b/mods/sanctuary/src/main/resources/data/sanctuary/tags/worldgen/biome/has_structure/forgotten_networks.json @@ -50,6 +50,7 @@ "sanctuary:woodland_dry_woodland", "sanctuary:woodland_oak_forest", "sanctuary:woodland_rocky_heath", - "sanctuary:woodland_sulfur_depths" + "sanctuary:woodland_sulfur_depths", + "sanctuary:swamp27_caves" ] } diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/population_10.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/population_10.json index c59c8d2..ed570da 100644 --- a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/population_10.json +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/population_10.json @@ -66,5 +66,6 @@ }, "terraces_29": false, "relief_30": true, - "surface_302": true + "surface_302": false, + "balanced_303": true } diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/population_20.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/population_20.json index 24835c6..ef1b0ab 100644 --- a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/population_20.json +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/population_20.json @@ -66,5 +66,6 @@ }, "terraces_29": false, "relief_30": true, - "surface_302": true + "surface_302": false, + "balanced_303": true } diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/population_5.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/population_5.json index 67a40bc..3179eef 100644 --- a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/population_5.json +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/population_5.json @@ -66,5 +66,6 @@ }, "terraces_29": false, "relief_30": true, - "surface_302": true + "surface_302": false, + "balanced_303": true } diff --git a/mods/sanctuary/src/test/java/fr/koka/sanctuary/worldgen/BalancedRelief303Smoke.java b/mods/sanctuary/src/test/java/fr/koka/sanctuary/worldgen/BalancedRelief303Smoke.java new file mode 100644 index 0000000..2729998 --- /dev/null +++ b/mods/sanctuary/src/test/java/fr/koka/sanctuary/worldgen/BalancedRelief303Smoke.java @@ -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"); + } +} diff --git a/packwiz/pack.toml b/packwiz/pack.toml index df138d6..bfe925a 100644 --- a/packwiz/pack.toml +++ b/packwiz/pack.toml @@ -1,6 +1,6 @@ name = "Sanctuary" author = "KOKA99CAB" -version = "0.1.0-alpha.30.2" +version = "0.1.0-alpha.30.3" pack-format = "packwiz:1.1.0" [index]