Compare commits

...
Author SHA1 Message Date
koka 79d3185cba feat(worldgen): add volumetric caves and nearby aerial sites for alpha28
Build Sanctuary / build (push) Canceled after 0s
2026-09-12 01:36:05 +02:00
40 changed files with 1324 additions and 83 deletions
+8
View File
@@ -1,5 +1,13 @@
# Changelog
## 0.1.0-alpha.28 — 2026-09-12
Spéléologie par bruits natifs, déformations volumiques 3D et failles obliques bornées ;
bateaux rapprochés et îlots au-dessus de Sanctuary. Marais réduits. Mines,
huttes et donjons vanilla adaptés aux souterrains ; filons natifs selon la
profondeur dans la roche, dans l’île et les expansions. Nouveau monde de test.
Voir [génération](docs/generation-alpha28.md) et [validation](docs/testing-alpha28.md).
## 0.1.0-alpha.27 — 2026-09-12
Retour à la base alpha.24 après rejet visuel de lalpha.25. Le relief et les
+15 -2
View File
@@ -21,6 +21,19 @@ 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.28 — grottes et lieux proches
Cette passe prolonge la 27 validée en jeu : bruits de grottes vanilla dans
la roche, failles moins traversantes, reliefs déplacés et affaissés par du bruit 3D,
bateaux près des berges et deux îlots au-dessus de Sanctuary. Les marais
occupent moins de place. Mines, huttes et donjons vanilla bénéficient de
contrôles adaptés aux cavités ; les filons suivent la profondeur de la masse
rocheuse, y compris dans les expansions.
Créer un **nouveau monde**. Voir le [périmètre](docs/generation-alpha28.md),
les [résultats effectivement vérifiés](docs/testing-alpha28.md) et la
[distribution](docs/packwiz.md).
## Alpha.27 — retour au terrain de la 24
La base est le commit `e7d1f29`, sans les massifs coniques, chambres creusées
@@ -572,7 +585,7 @@ sera une mise à jour explicite, avec vérification des API et des sauvegardes.
| Fabric API | 0.160.0+26.3 |
| Fabric Loom | 1.17.20 |
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
| Sanctuary / pack | 0.1.0-alpha.23 |
| Sanctuary / pack | 0.1.0-alpha.28 |
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
uniquement la bibliothèque standard et repère aussi une installation Python
@@ -601,7 +614,7 @@ décrits dans [Validation](docs/testing.md).
Résultats :
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.23.jar` : mod à installer avec
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.28.jar` : mod à installer avec
Fabric API sur la version Minecraft indiquée.
- `build/packwiz/` : pack de développement complet, avec le mod construit et
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
+12
View File
@@ -966,3 +966,15 @@ marais volumétriques dans les cavités existantes et affiner les coques. Voir l
[contrat](generation-alpha27.md). Les étagements, l’érosion, les nouvelles grottes
et les petites sorties deau seront traités un à un, avec retour en jeu entre
les changements ; ils ne sont pas déclarés implémentés dans cette passe.
### WG-30 — Spéléologie, reliefs et satellites proches — alpha.28
Branche `codex/caves-relief-alpha28`, depuis la 27 validée visuellement par le
créateur. Ajouter les bruits de grottes vanilla dans les masses rocheuses,
limiter et incliner les failles, développer des hauteurs avec ridge/erosion,
rapprocher les bateaux des vraies berges et placer les îlots au-dessus du terrain.
Moins de marais ; mines, huttes et donjons vanilla adaptés aux cavités réelles.
Les filons natifs doivent aussi alimenter les continents ouverts ensuite.
Aucun retour des salles ellipsoïdales ni des cônes de lalpha.25.
Résultats et limites : [génération](generation-alpha28.md), [essais](testing-alpha28.md).
+61
View File
@@ -0,0 +1,61 @@
# Alpha.28 — roche à explorer
La 27 est la base validée par le créateur. Cette passe garde son île, les
secrets minéraux et les réseaux, puis ajoute du creusement dans la roche.
Les nouvelles cavités ne sont pas des salles de structure aux volumes prescrits.
## Terrain
Le champ commun compose les fonctions de bruit de Minecraft 26.3-pre-2 :
cheese et cave_layer, entrées/spaghetti 3D, spaghetti 2D et roughness, piliers
et noodle. Leur altitude est déplacée pour la masse flottante. Les coupes
restent soustractives et satténuent vers les limites verticales ; il ne
sagit pas de recopier toute la génération Overworld ou ses aquifères.
Les carvers historiques restent désactivés pour ne pas ajouter une seconde
passe étrangère aux colonnes de certification.
Le relief supérieur déforme le volume rocheux dans les trois axes avec les
bruits ridge/erosion natifs échantillonnés en 3D. Les déplacements sont signés :
soulèvements, affaissements et cisaillements varient aussi avec la profondeur.
Le déplacement est borné à ±16 blocs horizontalement et ±40 verticalement,
avec une transition progressive de Y180 à Y228. Lenveloppe de l’île reste
évaluée aux coordonnées réelles ; seul le champ rocheux interne est déplacé.
Ce nest pas une simulation physique de plaques tectoniques. Aucun cône ni
salle géométrique ne sert de forme à ajouter au terrain.
Larrivée et les côtes gardent leur place. Les anciennes failles deviennent
obliques, interrompues et limitées en hauteur, avec de la roche au-dessus et
en dessous. Les expansions utilisent la même source de terrain ; leur climat
et leur déformation propre sappliquent ensuite.
Le seuil du champ de marais passe de 0,04 à 0,25 : les anciens secteurs se
réduisent au profit des biomes de grottes précédents, dont les champignons.
La hauteur du monde, les nuages et les trois diamètres sont conservés.
## Exploration et ressources
Les bateaux vanilla restaurés recherchent désormais les vraies berges proches.
Les deux îlots sont au-dessus de Sanctuary, avec un espace libre sous leur
roche et un seul exutoire deau. Leurs corps et leurs chutes deau ont des
emprises distinctes ; rien ne réserve toute la colonne sous un îlot sec.
Les mines emploient les pièces vanilla, leurs supports, rails, coffres et
spawners. Une partie exposée peut passer si lensemble reste ancré. Les huttes
cherchent aussi des berges souterraines de marais : sol local, couverture de
la cavité et quatre pilotis natifs. Les salles à spawner sont de vraies
MonsterRoomFeature, posées sur des candidats réels et soumises aux refus
natifs. Leur présence nest pas garantie sur chaque île.
Une passe de filons native complète les distributions existantes dans les
chunks de l’île et des expansions. Charbon, fer, cuivre, or, lapis, redstone,
diamant et émeraude utilisent les formes OreFeature vanilla. La profondeur
se mesure dans lenveloppe rocheuse entière de la colonne, sans recommencer
le calcul à chaque plafond de grotte. Les stocks restent finis. Les minerais
du Nether restent dans le Nether ; le Deep Dark nest pas ajouté à l’île.
## Livraison de test
Mod et pack `0.1.0-alpha.28`, Minecraft `26.3-pre-2`, codec de prototype
`sanctuary:island_v24` conservé. Créer un nouveau monde. Aucune migration,
aucune régénération ni intervention dans les sauvegardes personnelles.
Lalpha.27 et ses artefacts restent disponibles. Les vérifications réelles
et leurs limites sont dans [le compte rendu](testing-alpha28.md).
+99
View File
@@ -0,0 +1,99 @@
# Alpha.28 — vérifications
## Protocole
Tests sur macOS avec Java 25 et Minecraft 26.3-pre-2. Aucun essai Windows
natif ni jugement de rendu client nest revendiqué. Le créateur a validé
visuellement la 27 ; les coupes de densité servent ici à contrôler cette
nouvelle passe, sans représenter leau, la végétation ou les structures.
Commande de livraison :
```sh
./gradlew check build -PsanctuaryDensityMaps=false -PsanctuaryAerial24Tests=true
```
`sanctuaryDensityMaps=false` coupe la production des anciennes images,
pas les assertions. Le monde GameTest est recréé dans le dossier de développement
ignoré. Les sauvegardes personnelles ne sont pas utilisées par les tests.
Les smokes de placement aérien examinent 512 / 724 / 1 024, graines 0 / 42 /
2026. Le témoin natif chargé en chunks FULL est Moyen 724, graine 0 ; ceci
ne prétend pas être neuf îles entièrement explorées. Les comparaisons de
terrain analysent deux coupes espacées de 2 blocs et une grille de hauteurs
espacée de 32 blocs. Une expansion naturelle de 512 est contrôlée par son
vrai champ de densité sans être réservée ni ouverte dans le serveur.
## Résultats
`check build` passe en **4 min 15 s**, dont **21/21 GameTests natifs**
en 57,06 s. `assemblePack` passe ensuite sans recompilation des binaires vérifiés.
Les smokes couvrent aussi le Blocodex, les formats de journal, les priorités
et les réservations dexpansion, ainsi que les anciennes géométries pures.
Le terrain de référence conserve **6 098 des 6 860** anciens échantillons
rocheux intérieurs (88,9 %), avec **762** désormais creusés. Cette comparaison
inclut les raccords du nouveau cisaillement, pas seulement les grottes.
Lexpansion de 512 conserve 1 299 échantillons contre 136 creusés.
Sur la grille de hauteurs, 54 colonnes hautes montent et 84 sabaissent.
La hausse maximale relevée est de 32 blocs et le sommet échantillonné atteint
Y280. La baisse maximale de sommet, 132 blocs en (0,160), correspond à un
lobe supérieur qui quitte cette colonne : le sol observé passe de Y248 à
Y116. Elle ne désigne pas un déplacement vertical de 132 blocs ; le déplacement
3D est borné à ±40 en Y. Des surplombs et étranglements subsistent et restent
à apprécier visuellement dans le jeu.
Les **9 chunks FULL** inspectés contiennent les huit minerais Overworld :
| Minerai | Blocs observés, variantes deepslate comprises |
|---|---:|
| Charbon | 1408 |
| Fer | 325 |
| Cuivre | 302 |
| Or | 87 |
| Lapis | 53 |
| Redstone | 77 |
| Diamant | 16 |
| Émeraude | 4 |
Il sagit dun relevé après génération, pas de quotas imposés à chaque chunk.
La relecture finale des fichiers de région confirme **trois donjons vanilla**
avec spawners zombies et coffres `minecraft:chests/simple_dungeon` :
`-101 170 100`, `-92 175 -92` et `-85 178 11`. Une vraie mine est présente
vers `152 176 -120`, avec **143 pièces sauvegardées**. Dans les 14 chunks FULL
du contrôle des structures : 91 planches, 88 barrières, 13 rails, 29 toiles
et un spawner ont été observés dans les pièces chargées. Ce relevé sur les
fichiers MCA après fermeture du serveur ne génère aucun chunk supplémentaire.
Les quatre lieux aériens ont de vrais départs et pièces sauvegardées,
quatre coffres et deux marchands adultes au total, puis passent les contrôles
après ouverture, extraction et modification dans le protocole natif. Il y a
exactement une source de cascade et deux îlots survolant le terrain. Aucun
nouveau site ni habitant nest recréé pour remplacer ce qui a été utilisé.
Preuves locales ignorées : `build/alpha28-check-build-final.log`,
`build/alpha28-preview/geology-final.json`,
`build/alpha28-preview/geology-final-comparison.png`,
`build/alpha28-evidence/final/` et reçus de distribution `build/alpha28-*`.
## Limites et essai du créateur
- La hutte de sorcière est conditionnelle aux berges 3D, à lespace et aux quatre
appuis natifs. Le premier petit échantillon nen contient aucune : son audit
de placement ne constitue pas une preuve de présence en jeu.
- La source de la cascade est contrôlée ; son écoulement complet jusquau vide
nest pas mesuré par ce protocole. La propagation reste celle de Minecraft.
- Les témoins historiques purs restent vérifiés ; les comparaisons exactes de
lancien champ utilisent explicitement `legacy27()`. Elles ne prétendent pas
que le nouveau relief serait identique à celui de la 27.
- Il ne sagit pas dun benchmark client ni dune simulation physique tectonique.
Les performances pourront être étudiées après ce retour visuel.
Créer un **nouveau monde Sanctuary**, taille **Moyen**, graine **0** pour
retrouver le témoin. Garder ses autres sauvegardes ; aucune migration ni
régénération nest réalisée. Valider en priorité les masses soulevées et
abaissées, les cavités à explorer, les îlots au-dessus du terrain et les
navires proches des berges.
+2 -2
View File
@@ -8,6 +8,6 @@ loader_version=0.19.5
loom_version=1.17.20
fabric_api_version=0.160.0+26.3
mod_version=0.1.0-alpha.27
pack_version=0.1.0-alpha.27
mod_version=0.1.0-alpha.28
pack_version=0.1.0-alpha.28
maven_group=fr.koka.sanctuary
+8
View File
@@ -623,3 +623,11 @@ tasks.register('swamp27VolumesSmoke', JavaExec) {
mainClass = 'fr.koka.sanctuary.worldgen.Swamp27VolumesSmoke'
}
tasks.named('check') { dependsOn('swamp27VolumesSmoke') }
tasks.register('cavesRelief28Smoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.CavesRelief28Smoke'
}
tasks.named('check') { dependsOn('cavesRelief28Smoke') }
@@ -44,7 +44,9 @@ public final class Aerial24FlowAdmissionGameTests {
var admitted = new ArrayList<ExpansionIsland>();
try {
report.put("stage", "real admission toward an unseen satellite");
admitted.add(admit(helper, level, generator, session, plan, true, report));
if (!requests(level.getSeed(), session.islands(), plan, true).isEmpty())
admitted.add(admit(helper, level, generator, session, plan, true, report));
else report.put("collision_admission", "No raw expansion candidate reaches the close sites; initial island ownership already excludes them");
report.put("stage", "real admission through an unclaimed interval");
admitted.add(admit(helper, level, generator, session, plan, false, report));
} catch (Exception error) { failure(report, error); throw error; }
@@ -88,15 +90,17 @@ public final class Aerial24FlowAdmissionGameTests {
if (!tickets[0]) {
requested.forEach(chunk -> level.getChunkSource().addTicketWithRadius(TicketType.DRAGON, chunk, 2)); tickets[0] = true;
}
flowTicks[0] += 20; report.put("flow_wait_ticks", flowTicks[0]); report.put("stage", "native fluid ticks toward the void");
flowTicks[0] += 20; report.put("flow_wait_ticks", flowTicks[0]); report.put("stage", "native fluid ticks beneath the suspended island");
var source = AerialGeometry24.waterfallSource(waterfall);
helper.assertTrue(level.getFluidState(source).is(FluidTags.WATER) && level.getFluidState(source).isSource(), "The authored source remains a single native water source");
var below = waterAt(level, waterfall, waterfall.minY() - 16);
var low = waterAt(level, waterfall, 8);
// Alpha28 islets stand above real terrain: a cascade may land on Sanctuary.
// Require native flow through the free air gap, not an impossible route through rock.
var below = waterAt(level, waterfall, waterfall.minY() - 1);
var low = waterAt(level, waterfall, waterfall.minY() - AerialPlanner24.MIN_ISLET_GAP);
report.put("water_below_islet", below == null ? List.of() : coords(below));
report.put("water_near_y0", low == null ? List.of() : coords(low));
report.put("water_across_air_gap", low == null ? List.of() : coords(low));
if (below == null || low == null) {
helper.assertTrue(flowTicks[0] <= 4000, "Scheduled vanilla fluid ticks carry the waterfall below the island and down to y=8");
helper.assertTrue(flowTicks[0] <= 4000, "Scheduled vanilla fluid ticks carry the waterfall through the sixteen-block air gap");
helper.runAtTickTime(helper.getTick() + 20, () -> step[0].run()); return;
}
helper.assertTrue(!level.getFluidState(low).isSource(), "Low waterfall water is flowing, not a fabricated source column");
@@ -128,7 +132,7 @@ public final class Aerial24FlowAdmissionGameTests {
helper.assertTrue(elapsed <= 31_000, "Actual Session.create keeps its production thirty-second deadline");
helper.assertTrue(!plan.conflicts(reserved), "The admitted expansion cannot claim any satellite chunk");
helper.assertTrue(!ExpansionVoidReuse.requiresReload(level, reserved.id()), "Untouched candidate prepares in the same development session");
if (!collision) helper.assertTrue(inAerialBand(reserved, plan), "The admitted free interval lies among the nearby satellites, not beyond a blanket exclusion ring");
helper.assertTrue(session.create(request.id(), "sanctuary", request.direction(), "temperate", request.diameter(), "natural").equals(reserved),
"Reservation retry is idempotent");
attempts.add(Map.of("request", request, "reserved", reserved, "elapsed_ms", elapsed));
@@ -149,7 +153,7 @@ public final class Aerial24FlowAdmissionGameTests {
var raw = ExpansionPlacement.candidates(seed, islands, id, "sanctuary", direction, "temperate", 128, "natural", 0);
if (raw.isEmpty() || plan.conflicts(raw.getFirst()) != collision) continue;
var filtered = raw.stream().filter(candidate -> !plan.conflicts(candidate)).findFirst().orElse(null);
if (filtered == null || !collision && !inAerialBand(filtered, plan)) continue;
if (filtered == null) continue;
result.add(new Request(id, direction, 128, raw.getFirst(), filtered));
}
return result;
@@ -112,6 +112,12 @@ public final class Aerial24WorldGameTests {
level.getChunk(chunk.x(), chunk.z(), ChunkStatus.FULL, true);
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
}
if (!report.containsKey("native_departures_ready")) {
// Fail on a missing native start immediately instead of waiting for entities
// which could never have been placed (for example, an obsolete void-only gate).
report.put("stage", "verify saved aerial departures before entity loading");
report.put("native_departures_ready", starts(helper, level, plan).size());
}
if (!tickets[0]) {
entityChunks.forEach(pos -> level.getChunkSource().addTicketWithRadius(TicketType.DRAGON, pos, 2)); tickets[0] = true;
}
@@ -170,14 +176,25 @@ public final class Aerial24WorldGameTests {
var root = generator.source().islands().stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var views = new LinkedHashMap<String, Object>();
for (var site : plan.sites()) {
helper.assertTrue(site.claimedChunks().stream().noneMatch(chunk -> root.ownsChunk(chunk.x(), chunk.z())), "No aerial claim replaces natural starter chunks");
if (site.merchant()) {
double gap = AerialPlanner24.shoreDistance(site);
helper.assertTrue(gap >= 16 && gap <= 48, "Native ship is just off its verified shore");
helper.assertTrue(Math.abs(AerialGeometry24.entry(site).getY() - site.shoreY()) <= 4, "Boarding deck follows the nearby shore height");
} else {
helper.assertTrue(root.ownsChunk(site.startChunkX(), site.startChunkZ()), "Mineral islet floats above Sanctuary terrain");
var column = generator.aerialBaseColumn24(site.shoreX(), site.shoreZ());
helper.assertTrue(column.getBlock(site.shoreY() - 1).isSolid(), "Overhead islet retains a real ground witness below it");
int highest = 0;
for (int y = 382; y > 0; y--) if (!column.getBlock(y).isAir()) { highest = y; break; }
helper.assertTrue(site.minY() - highest - 1 >= AerialPlanner24.MIN_ISLET_GAP, "At least sixteen air blocks separate ground and suspended rock");
}
var overlap = new ExpansionIsland("aerial_overlap", root.id(), "east", "temperate", 64,
site.startChunkX() * 16, site.startChunkZ() * 16, 42, "natural", "reserved", 24);
helper.assertTrue(plan.conflicts(overlap), "An unseen site already prevents overlapping expansion ownership");
double distance = Math.hypot(site.locator().getX() - site.shoreX(), site.locator().getZ() - site.shoreZ());
helper.assertTrue(distance <= 512, "Aerial site remains within 512 blocks of a real shore witness");
views.put(site.id(), Map.of("shore", List.of(site.shoreX(), site.shoreY() + 2, site.shoreZ()),
"entry", coordinates(AerialGeometry24.entry(site)), "centre", coordinates(site.locator()), "shore_distance", distance,
"entry", coordinates(AerialGeometry24.entry(site)), "centre", coordinates(site.locator()), "shore_distance", distance, "hull_to_shore", AerialPlanner24.shoreDistance(site),
"exterior_view", List.of(site.minX() - 24, site.minY() + site.height() / 2, site.minZ() - 24)));
}
helper.assertTrue(!plan.conflicts(new ExpansionIsland("aerial_far", root.id(), "east", "temperate", 64, 4096, 4096, 42, "natural", "reserved", 24)),
@@ -223,8 +240,17 @@ public final class Aerial24WorldGameTests {
for (int dy = -2; dy <= 20; dy++) column.put(shore.getY() + dy, level.getBlockState(shore.above(dy)).toString());
shores.put(site.id(), Map.of("position", coordinates(shore), "column", column));
helper.assertTrue(level.getBlockState(shore.below()).isSolid(), "Saved shore witness has a real FULL terrain support: " + site.id() + " " + shore);
helper.assertTrue(level.getBlockState(shore.above()).getCollisionShape(level, shore.above()).isEmpty(),
"Shore viewpoint is not buried inside the island: " + site.id() + " " + shore.above() + " " + level.getBlockState(shore.above()));
if (site.merchant()) {
helper.assertTrue(level.getBlockState(shore.above()).getCollisionShape(level, shore.above()).isEmpty(),
"Shore viewpoint is not buried inside the island: " + site.id() + " " + shore.above() + " " + level.getBlockState(shore.above()));
} else {
// Natural trees may occupy the mathematical ground reference below an islet.
// Observe a real nearby walking position without removing that vegetation.
var viewpoint = nearbyGroundView(level, shore);
helper.assertTrue(viewpoint != null, "A walkable ground viewpoint exists within twelve horizontal blocks below the suspended islet: " + site.id());
shores.put(site.id(), Map.of("position", coordinates(shore), "column", column,
"walkable_viewpoint", coordinates(viewpoint), "reference_obstruction", level.getBlockState(shore.above()).toString()));
}
var chestAt = AerialGeometry24.chest(site);
helper.assertTrue(level.getBlockEntity(chestAt) instanceof RandomizableContainerBlockEntity, "Real chest at " + site.id()); chestCount++;
var chest = (RandomizableContainerBlockEntity) level.getBlockEntity(chestAt);
@@ -246,12 +272,15 @@ public final class Aerial24WorldGameTests {
var source = AerialGeometry24.waterfallSource(site);
helper.assertTrue(level.getFluidState(source).isSource(), "The one real source drives the native waterfall"); waterSources++;
}
// These columns are beyond the authored box and well outside initial terrain ownership.
for (var pos : List.of(new BlockPos(site.minX() - 2, site.minY(), site.minZ() - 2), new BlockPos(site.maxX() + 2, site.minY(), site.maxZ() + 2)))
for (int y = site.minY(); y <= site.maxY() + 2; y++) {
var state = level.getBlockState(pos.atY(y));
helper.assertTrue(state.isAir() || site.waterfall() && !state.getFluidState().isEmpty(), "No manufactured structure leaks outside its authored footprint");
}
// Root terrain and its own trees may now exist outside these aerial envelopes.
// Chunk clipping is checked against every authored position; do not confuse legal
// surrounding terrain with a structure leak by requiring a whole exterior air column.
helper.assertTrue(model.keySet().stream().allMatch(site.bounds()::isInside), "Every proposed structure block is inside its finite envelope");
var exterior = new ArrayList<Map<String, Object>>();
for (var at : List.of(new BlockPos(site.minX() - 2, site.minY(), site.minZ() - 2),
new BlockPos(site.maxX() + 2, site.minY(), site.maxZ() + 2)))
exterior.add(Map.of("position", coordinates(at), "observed_block", level.getBlockState(at).toString()));
report.put(site.id() + "_exterior_observations", exterior);
}
helper.assertTrue(chestCount == 4 && waterSources == 1, "Four physical chests and exactly one planned source");
report.put("chests", chestCount); report.put("waterfall_sources", waterSources); report.put("blocks_before", countSites(level, plan));
@@ -259,6 +288,21 @@ public final class Aerial24WorldGameTests {
return expected;
}
private static BlockPos nearbyGroundView(ServerLevel level, BlockPos reference) {
for (int radius = 0; radius <= 12; radius++) for (int dx = -radius; dx <= radius; dx++) for (int dz = -radius; dz <= radius; dz++) {
if (Math.max(Math.abs(dx), Math.abs(dz)) != radius || Math.hypot(dx, dz) > 12) continue;
for (int dy = 8; dy >= -8; dy--) {
var at = reference.offset(dx, dy, dz);
var floor = level.getBlockState(at.below());
if (!floor.isSolid() || floor.is(net.minecraft.tags.BlockTags.LEAVES) || floor.is(net.minecraft.tags.BlockTags.LOGS)) continue;
if (level.getBlockState(at).getCollisionShape(level, at).isEmpty()
&& level.getBlockState(at.above()).getCollisionShape(level, at.above()).isEmpty()
&& level.getFluidState(at).isEmpty() && level.getFluidState(at.above()).isEmpty()) return at;
}
}
return null;
}
private static Map<String, Object> vegetation(GameTestHelper helper, ServerLevel level, AerialPlan24.Site site) {
helper.assertTrue(site.rockHeight() >= 20 && site.rockHeight() <= 35
&& site.height() == site.rockHeight() + AerialPlan24.VEGETATION_HEADROOM,
@@ -0,0 +1,148 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.expansion.ExpansionStructures23;
import fr.koka.sanctuary.expansion.SwampHutPlacement28;
import fr.koka.sanctuary.knowledge.structures.StructureCensusMemory;
import fr.koka.sanctuary.knowledge.structures.StructureCensusService;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.SpawnerBlockEntity;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
/** Observes normal root generation only. No start injection, fake room, expansion or seed search. */
public final class Alpha28NativeStructuresGameTests {
private static final com.google.gson.Gson JSON = new GsonBuilder().setPrettyPrinting().create();
private static final Path OUT = Path.of("diagnostics/native28");
@GameTest(maxTicks = 3600)
public void actualNativeStructures(GameTestHelper helper) throws Exception {
var report = new LinkedHashMap<String, Object>();
try {
var level = helper.getLevel(); var host = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
report.put("seed", level.getSeed()); report.put("diameter", host.initialDiameter());
report.put("process_id", ProcessHandle.current().pid()); report.put("generate_structures", level.structureManager().shouldGenerateStructures());
if (!level.structureManager().shouldGenerateStructures()) {
report.put("scope", "Native structures globally disabled"); report.put("passed", true); write(report); helper.succeed(); return;
}
var root = host.source().islands().stream().filter(island -> island.origin()).findFirst().orElseThrow();
var candidates = ExpansionStructures23.candidates(root).stream().limit(12).toList();
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
var found = new LinkedHashMap<String, StructureStart>(); var observed = new TreeMap<String, Integer>();
for (var candidate : candidates) {
var chunk = level.getChunk(candidate.chunk().x(), candidate.chunk().z(), ChunkStatus.STRUCTURE_STARTS, true);
for (var start : chunk.getAllStarts().values()) {
if (!start.isValid()) continue;
String id = registry.getKey(start.getStructure()).toString(); observed.merge(id, 1, Integer::sum);
if (List.of("minecraft:mineshaft", "minecraft:mineshaft_mesa", "minecraft:swamp_hut").contains(id)) found.putIfAbsent(id, start);
}
}
report.put("candidate_chunks", candidates.size()); report.put("observed_starts", observed);
var full = new LinkedHashSet<ChunkPos>();
// Only a few actual pieces are visited: this is not full-map pregeneration.
for (var entry : found.entrySet()) {
full.add(entry.getValue().getChunkPos());
int remaining = entry.getKey().equals("minecraft:swamp_hut") ? 4 : 4;
for (var piece : entry.getValue().getPieces()) {
if (remaining-- <= 0) break;
var b = piece.getBoundingBox(); full.add(new ChunkPos(b.getCenter().getX() >> 4, b.getCenter().getZ() >> 4));
if (entry.getKey().equals("minecraft:swamp_hut")) b.intersectingChunks().forEach(full::add);
}
}
// A few additional real generation chunks can contain native monster_room attempts.
for (var candidate : candidates) {
if (full.size() >= 16) break;
full.add(candidate.chunk());
}
helper.assertTrue(full.size() <= 24, "Bounded native structure witness requests at most24 FULL chunks");
var iterator = full.iterator(); Runnable[] step = new Runnable[1];
step[0] = () -> {
try {
if (iterator.hasNext()) {
var at = iterator.next(); report.put("stage", "FULL " + at);
level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true);
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
}
inspect(helper, level, found, full, report);
report.put("passed", true); write(report); helper.succeed();
} catch (Exception error) { failure(report, error); throw new IllegalStateException("Native28 witness failed", error); }
};
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run());
} catch (Exception error) { failure(report, error); throw error; }
}
private static void inspect(GameTestHelper helper, ServerLevel level, Map<String, StructureStart> found,
Set<ChunkPos> full, Map<String, Object> report) {
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
var context = StructurePieceSerializationContext.fromLevel(level);
var proofs = new LinkedHashMap<String, Object>();
for (var entry : found.entrySet()) {
var expected = entry.getValue(); var cp = expected.getChunkPos();
var actual = level.getChunk(cp.x(), cp.z(), ChunkStatus.FULL, true)
.getStartForStructure(registry.getValue(Identifier.parse(entry.getKey())));
helper.assertTrue(actual != null && actual.isValid(), "The real selected native start survives FULL: " + entry.getKey());
var tag = actual.createTag(context, cp);
var restored = StructureStart.loadStaticStart(context, tag, level.getSeed());
helper.assertTrue(restored != null && tag.equals(restored.createTag(context, cp)), "Native piece NBT roundtrip: " + entry.getKey());
var counts = new TreeMap<String, Integer>(); var positions = new HashSet<BlockPos>();
for (var piece : actual.getPieces()) {
var b = piece.getBoundingBox();
for (int x = b.minX(); x <= b.maxX(); x++) for (int z = b.minZ(); z <= b.maxZ(); z++) {
if (!full.contains(new ChunkPos(x >> 4, z >> 4))) continue;
for (int y = b.minY(); y <= b.maxY(); y++) {
BlockPos pos = new BlockPos(x, y, z); if (!positions.add(pos)) continue;
var state = level.getBlockState(pos);
if (!state.isAir()) counts.merge(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(), 1, Integer::sum);
}
}
if (entry.getKey().equals("minecraft:swamp_hut")) for (var pier : SwampHutPlacement28.piers(piece)) {
var state = level.getBlockState(pier);
helper.assertTrue(state.is(Blocks.OAK_LOG) || state.isSolid(), "A native hut pier has wood or a real solid foot: " + pier);
}
}
if (entry.getKey().contains("mineshaft")) helper.assertTrue(counts.entrySet().stream()
.filter(e -> e.getKey().endsWith("_planks") || e.getKey().endsWith("_fence") || e.getKey().equals("minecraft:rail"))
.mapToInt(Map.Entry::getValue).sum() > 0, "A selected mine creates actual native timber/rail geometry");
proofs.put(entry.getKey(), Map.of("start_chunk", List.of(cp.x(), cp.z()), "pieces", actual.getPieces().size(),
"blocks_in_loaded_pieces", counts, "native_tag", tag.toString(), "visit", actual.getPieces().getFirst().getLocatorPosition().toShortString()));
}
var rooms = new ArrayList<Object>();
for (var site : StructureCensusService.snapshot(level.getServer()).evidence().sites().values()) {
var key = site.key();
if (!key.id().equals(StructureCensusMemory.MONSTER_ROOM) || !key.dimension().equals(level.dimension().identifier().toString())) continue;
var cp = new ChunkPos(key.x() >> 4, key.z() >> 4);
if (!full.contains(cp)) continue;
var at = new BlockPos(key.x(), key.y(), key.z());
helper.assertTrue(level.getBlockEntity(at) instanceof SpawnerBlockEntity, "Successful vanilla monster_room has its real spawner at " + at);
rooms.add(Map.of("position", List.of(key.x(), key.y(), key.z()), "spawner", true));
}
report.put("structures", proofs); report.put("monster_rooms", rooms); report.put("full_chunks", full.size());
report.put("mineshaft_found", found.keySet().stream().anyMatch(id -> id.contains("mineshaft")));
report.put("swamp_hut_found", found.containsKey("minecraft:swamp_hut"));
report.put("monster_room_found", !rooms.isEmpty());
report.put("scope", "At most12 normal start candidates and24 FULL chunks; absence is reported, never replaced by synthetic fixtures");
}
private static void write(Map<String, Object> report) throws Exception {
Files.createDirectories(OUT); Files.writeString(OUT.resolve("native.json"), JSON.toJson(report) + "\n");
}
private static void failure(Map<String, Object> report, Exception error) {
report.put("passed", false); report.put("error", error.toString());
try { Files.createDirectories(OUT); Files.writeString(OUT.resolve("failure.json"), JSON.toJson(report) + "\n"); }
catch (Exception io) { error.addSuppressed(io); }
SanctuaryMod.LOGGER.error("Native28 witness failed", error);
}
}
@@ -0,0 +1,117 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.worldgen.PopulationIslandDensity;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Bounded comparison of the real registered density, followed by inspection of ordinary FULL chunks. */
public final class Caves28WorldGameTests {
@GameTest(maxTicks = 3600)
public void geologyAndNativeOres(GameTestHelper helper) throws Exception {
var level = helper.getLevel();
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
var binding = generator.starterContext();
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.geology28(), "Current world uses the native-cave adaptation");
var current = binding.random().getSampler(field);
var previous = binding.random().getSampler(field.legacy27());
var context = SamplerContext.builder().enableCaches().build();
var report = new LinkedHashMap<String, Object>();
report.put("seed", level.getSeed()); report.put("diameter", generator.initialDiameter());
report.put("scope", "Registered density comparison; no synthetic terrain, water, ore or room is placed by the test");
var heights = new ArrayList<List<Integer>>();
long oldSolid = 0, remainingSolid = 0, excavated = 0, restored = 0;
int min = -Math.min(320, generator.initialDiameter() / 2), max = -min, raised = 0, lowered = 0, largestUplift = 0, largestSubsidence = 0;
for (int z = min; z <= max; z += 32) for (int x = min; x <= max; x += 32) {
int oldTop = 0, newTop = 0;
for (int y = 2; y <= 382; y += 2) {
boolean old = previous.sampleValue(context, x, y, z) > 0;
boolean now = current.sampleValue(context, x, y, z) > 0;
if (old) oldTop = y; if (now) newTop = y;
if (y >= 48 && y <= 202) {
if (old) { oldSolid++; if (now) remainingSolid++; else excavated++; }
else if (now) restored++;
}
if (Math.hypot(x, z) < 40 && y >= 240)
helper.assertTrue(old == now, "High arrival neighbourhood remains unchanged");
}
if (newTop > oldTop && oldTop > 208) { raised++; largestUplift = Math.max(largestUplift, newTop - oldTop); }
if (newTop < oldTop && oldTop > 240) { lowered++; largestSubsidence = Math.max(largestSubsidence, oldTop - newTop); }
heights.add(List.of(x, z, oldTop, newTop));
}
report.put("heights", heights); report.put("old_solid_samples", oldSolid);
report.put("remaining_solid_samples", remainingSolid); report.put("excavated_samples", excavated);
report.put("restored_rift_samples", restored); report.put("raised_columns", raised);
report.put("largest_uplift", largestUplift);
report.put("lowered_columns", lowered); report.put("largest_subsidence", largestSubsidence);
helper.assertTrue(excavated > 0 && remainingSolid > excavated, "New passages exist while most sampled former rock remains");
helper.assertTrue(raised > 0, "Native volumetric deformation raises some uplands");
var childField = (PopulationIslandDensity) level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION)
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, SanctuaryMod.id("population_5"))).value();
var child = new fr.koka.sanctuary.expansion.ExpansionIsland("geology_witness", "sanctuary", "east", "temperate",
512, 2048, 0, level.getSeed(), "natural", "ready", 24);
var childSettings = level.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS)
.getOrThrow(ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("population_5"))).value();
var childState = net.minecraft.world.level.levelgen.RandomState.create(level.registryAccess().lookupOrThrow(Registries.NOISE), child.seed(), childSettings);
var childNew = childState.getSampler(new fr.koka.sanctuary.expansion.ExpansionDensity(childField, child));
var childOld = childState.getSampler(new fr.koka.sanctuary.expansion.ExpansionDensity(childField.legacy27(), child));
long childExcavated = 0, childRemaining = 0;
for (int z = -160; z <= 160; z += 32) for (int dx = -160; dx <= 160; dx += 32)
for (int y = 48; y <= 202; y += 4) {
if (childOld.sampleValue(context, 2048 + dx, y, z) > 0) {
if (childNew.sampleValue(context, 2048 + dx, y, z) <= 0) childExcavated++;
else childRemaining++;
}
}
report.put("expansion_density", Map.of("diameter", 512, "excavated", childExcavated,
"remaining", childRemaining, "reserved_or_opened", false));
helper.assertTrue(childExcavated > 0 && childRemaining > childExcavated, "The actual expansion wrapper uses the same finite cave field");
var sections = new LinkedHashMap<String, Object>();
for (int z : new int[]{0, 96}) {
var before = new ArrayList<String>(); var after = new ArrayList<String>();
for (int y = 382; y >= 0; y -= 2) {
var a = new StringBuilder(); var b = new StringBuilder();
for (int x = min; x <= max; x += 2) {
a.append(previous.sampleValue(context, x, y, z) > 0 ? '#' : '.');
b.append(current.sampleValue(context, x, y, z) > 0 ? '#' : '.');
}
before.add(a.toString()); after.add(b.toString());
}
sections.put("z" + z, Map.of("before", before, "after", after, "min_x", min, "step", 2));
}
report.put("sections", sections);
var ores = new TreeMap<String, Integer>(); var oreDepths = new TreeMap<String, List<Integer>>();
for (int cx : new int[]{-6, 0, 6}) for (int cz : new int[]{-6, 0, 6}) {
var chunk = level.getChunk(cx, cz, ChunkStatus.FULL, true);
for (int x = 0; x < 16; x++) for (int z = 0; z < 16; z++) for (int y = 1; y < 383; y++) {
var state = chunk.getBlockState(new BlockPos(cx * 16 + x, y, cz * 16 + z));
String id = BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString();
if (id.endsWith("_ore")) {
ores.merge(id, 1, Integer::sum);
var band = oreDepths.computeIfAbsent(id, ignored -> new ArrayList<>(List.of(384, 0)));
band.set(0, Math.min(band.get(0), y)); band.set(1, Math.max(band.get(1), y));
}
}
}
report.put("full_chunks_inspected", 9); report.put("ores", ores); report.put("ore_y_ranges", oreDepths);
helper.assertTrue(!ores.isEmpty(), "Actual generated rock contains native ore deposits");
report.put("passed", true);
Path path = Path.of("diagnostics/alpha28/geology.json"); Files.createDirectories(path.getParent());
Files.writeString(path, new GsonBuilder().setPrettyPrinting().create().toJson(report));
helper.succeed();
}
}
@@ -23,7 +23,7 @@ import net.minecraft.world.level.levelgen.densityfunction.DensitySampler;
import net.minecraft.world.level.levelgen.densityfunction.DensityVolume;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Compares production against the frozen alpha.10 implementation; no chunks are created. */
/** Compares the explicit legacy27 mode against frozen alpha10, not the intentional alpha28 terrain changes. */
final class PopulationDensityEquivalenceDiagnostics {
private record Pair(int players, long seed, DensitySampler reference, DensitySampler actual) {}
@@ -39,7 +39,7 @@ final class PopulationDensityEquivalenceDiagnostics {
var key = SanctuaryMod.id("population_" + players);
var registered = densities.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, key)).value();
helper.assertTrue(registered instanceof PopulationIslandDensity, "Compare the real registered Population density");
var density = (PopulationIslandDensity) registered;
var density = ((PopulationIslandDensity) registered).legacy27();
helper.assertTrue(ExactInterpolationSampler.supportsTerrain(density.terrain()),
"The shipped coordinate-only terrain must actually use the exact cache, not the fallback");
var reference = new Alpha10PopulationDensityReference(density.terrain(), density.distortion(), density.sculpt(),
@@ -113,7 +113,7 @@ final class PopulationDensityEquivalenceDiagnostics {
"reference_commit", "a26b72f", "reference_source_sha256", "fcc5a8a39b7c4d181d74ce4f9183a1ca93646137955ac64bab9d22e6db0752a1",
"comparison", "Float.floatToRawIntBits; no tolerance", "profiles", reports, "adversarial", adversarial,
"concurrent_comparisons", concurrentComparisons, "concurrent_workers", 3, "passed", true,
"scope", "Real compiled scalar/bulk densities, 4 capacities x 3 seeds (5/20/100 compatibility and new10 using the same parameterized algorithm), signed coordinates, region/coast/height boundaries, context switches and concurrency; no block changes")) + "\n");
"scope", "Explicit legacy27 mode versus frozen alpha10: real compiled scalar/bulk densities, 4 capacities x 3 seeds, signed coordinates, region/coast/height boundaries, context switches and concurrency. Geology28 is intentionally disabled only for this historical equivalence proof; no alpha28 generation claim.")) + "\n");
}
private static List<DensityVolume> volumes(IslandCapacity capacity) {
@@ -87,6 +87,7 @@ public final class PopulationDiagnostics {
ResourceKey.create(Registries.DENSITY_FUNCTION, SanctuaryMod.id("population_" + capacity.players()))).value();
helper.assertTrue(density instanceof PopulationIslandDensity, "Inspect the actual compiled population density");
var island = (PopulationIslandDensity) density;
var legacyEnvelope = island.legacy27();
helper.assertTrue(island.capacity() == capacity, "Noise settings and density must agree on saved capacity");
var random = binding.random();
var replay = RandomState.create(level.registryAccess().lookupOrThrow(Registries.NOISE), random.seed(), generator.generatorSettings().value());
@@ -102,7 +103,10 @@ public final class PopulationDiagnostics {
float value = random.sampleBlockValueUncached(density, x, y, z);
float raw = random.sampleBlockValueUncached(island.terrain(), x, y - 64, z);
helper.assertTrue(Float.isFinite(value), "Density remains finite at every sampled position");
helper.assertTrue(value <= 0 || raw > 0, "Enlarging the envelope must never invent terrain inside natural holes");
float historical = island.geology28()
? random.sampleBlockValueUncached(legacyEnvelope, x, y, z) : value;
helper.assertTrue(historical <= 0 || raw > 0,
"The explicit legacy27 envelope never fills its raw natural holes; alpha28 uplands change sample Y");
if (value > 0) { if (bottom < 0) bottom = y; top = y; }
if (x % (step * 8) == 0 && z % (step * 8) == 0)
helper.assertTrue(Float.floatToIntBits(value) == Float.floatToIntBits(replay.sampleBlockValueUncached(density, x, y, z)),
@@ -6,7 +6,7 @@
"environment": "*",
"license": "GPL-3.0-or-later",
"entrypoints": {
"fabric-gametest": ["${gametest_entrypoint}", "fr.koka.sanctuary.gametest.Swamp27WorldGameTests", "fr.koka.sanctuary.gametest.BlockKnowledgeGameTests", "fr.koka.sanctuary.gametest.CensusGameTestsTerrain", "fr.koka.sanctuary.gametest.CensusStockGameTests", "fr.koka.sanctuary.gametest.MaterialActivityGameTests"],
"fabric-gametest": ["${gametest_entrypoint}", "fr.koka.sanctuary.gametest.Swamp27WorldGameTests", "fr.koka.sanctuary.gametest.Caves28WorldGameTests", "fr.koka.sanctuary.gametest.Alpha28NativeStructuresGameTests", "fr.koka.sanctuary.gametest.BlockKnowledgeGameTests", "fr.koka.sanctuary.gametest.CensusGameTestsTerrain", "fr.koka.sanctuary.gametest.CensusStockGameTests", "fr.koka.sanctuary.gametest.MaterialActivityGameTests"],
"fabric-client-gametest": ["fr.koka.sanctuary.gametest.SanctuaryClientRenderTests"]
},
"mixins": ["sanctuary-gametest.mixins.json"],
@@ -146,9 +146,17 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
}
@Override public void applyBiomeDecoration(net.minecraft.world.level.WorldGenLevel level, ChunkAccess chunk, StructureManager structures) {
var island = islandAt(chunk.getPos().getMiddleBlockX(), chunk.getPos().getMiddleBlockZ());
if (!ExpansionOcean23.active(island)) { super.applyBiomeDecoration(level, chunk, structures); return; }
fr.koka.sanctuary.worldgen.IslandOres28.place(level, this, chunk, island);
if (!ExpansionOcean23.active(island)) {
super.applyBiomeDecoration(level, chunk, structures);
fr.koka.sanctuary.worldgen.CavityMonsterRoom28.decorate(level, this, chunk);
return;
}
Integer previous = ExpansionOcean23.enterDecoration(ExpansionOcean23.seaLevel(island));
try { super.applyBiomeDecoration(level, chunk, structures); }
try {
super.applyBiomeDecoration(level, chunk, structures);
fr.koka.sanctuary.worldgen.CavityMonsterRoom28.decorate(level, this, chunk);
}
finally { ExpansionOcean23.restoreDecoration(previous); }
}
@Override public int getSpawnHeight(LevelHeightAccessor level) { return 220; }
@@ -49,7 +49,11 @@ public final class ExpansionRelief {
public static float sculpt(ExpansionIsland island, Column column, double y, float source) {
if (source <= 0) return source;
double reference = referenceY(island, y);
double canyon = 2.1 * column.canyonWeight() * smoothstep((reference - 120.0) / 34.0);
double canyonHeight = island.generation() == 24
? smoothstep((reference - 126 - column.uplift() * .35) / 16)
* smoothstep((226 + column.uplift() * .35 - reference) / 18)
: smoothstep((reference - 120.0) / 34.0);
double canyon = 2.1 * column.canyonWeight() * canyonHeight;
double crater = 2.1 * column.craterWeight() * smoothstep((reference - CRATER_FLOOR) / 14.0);
return (float) Math.clamp(source - Math.max(canyon, crater), -1.0, 1.0);
}
@@ -169,6 +169,15 @@ public final class ExpansionStructures23 {
if (centre.top < 32) return null;
var sampler = random.createClimateSampler(SamplerContext.builder().enableCaches().build());
var resolver = view.getBiomeSource().createResolver(sampler);
var hutHeights = new ArrayList<Integer>();
// The biome is genuinely three dimensional: a swamp bank can be below another island floor.
for (int y = Math.min(centre.top + 1, 280); y >= 48 && hutHeights.size() < 4; y--) {
var floor = centre.column.getBlock(y - 1);
if ((!floor.isSolid() && !floor.getFluidState().is(net.minecraft.tags.FluidTags.WATER))
|| !centre.column.getBlock(y).isAir() || !centre.column.getBlock(y + 1).isAir()) continue;
if (!host.structureBiome(resolver.getNoiseBiome(x >> 2, y >> 2, z >> 2)).is(net.minecraft.world.level.biome.Biomes.SWAMP)) continue;
if (hutHeights.isEmpty() || hutHeights.getLast() - y >= 8) hutHeights.add(y);
}
int undergroundY = Math.clamp(centre.top - 60, 50, 210);
Holder<Biome> surfaceBiome = host.structureBiome(resolver.getNoiseBiome(x >> 2, centre.top >> 2, z >> 2));
Holder<Biome> undergroundBiome = host.structureBiome(resolver.getNoiseBiome(x >> 2, undergroundY >> 2, z >> 2));
@@ -182,7 +191,8 @@ public final class ExpansionStructures23 {
if (holder == null) continue;
Holder<Biome> biome = entry.habitat() == Habitat.UNDERGROUND ? undergroundBiome
: entry.habitat() == Habitat.OCEAN ? waterBiome : surfaceBiome;
if (!holder.value().biomes().contains(biome)) continue;
if (entry.family().equals("swamp_hut")) { if (hutHeights.isEmpty()) continue; }
else if (!holder.value().biomes().contains(biome)) continue;
if (entry.habitat() == Habitat.OCEAN && !ExpansionOcean23.column(island, x, z).water()) continue;
long salt = mix(candidate.roll() ^ entry.id().hashCode() * 0x9e3779b97f4a7c15L);
double unit = ((salt >>> 11) + 1.0) * 0x1.0p-53;
@@ -196,10 +206,14 @@ public final class ExpansionStructures23 {
checkDeadline(deadlineNanos);
Structure original = choice.holder().value();
Entry entry = choice.entry();
int variants = entry.habitat() == Habitat.UNDERGROUND || entry.family().equals("ruined_portal") ? 3 : 1;
boolean hut = entry.family().equals("swamp_hut");
int variants = hut ? hutHeights.size() : entry.habitat() == Habitat.UNDERGROUND || entry.family().equals("ruined_portal") ? 3 : 1;
for (int vertical = 0; vertical < variants; vertical++) {
checkDeadline(deadlineNanos);
int targetY = undergroundY - vertical * 20;
int targetY = hut ? hutHeights.get(vertical) : undergroundY - vertical * 20;
ExpansionChunkGenerator nativeView = hut && view instanceof NativeView local
? local.atHeight(targetY + 1) : view;
GroundSampler nativeGround = hut ? new GroundSampler(nativeView, chunk, random, deadlineNanos, targetY + 1) : ground;
Structure generated = original;
if (entry.family().equals("trial_chambers")) generated = withStartHeight(original, registries, targetY);
Predicate<Holder<Biome>> valid = b -> original.biomes().contains(host.structureBiome(b));
@@ -207,18 +221,18 @@ public final class ExpansionStructures23 {
if (original instanceof StrongholdStructure) {
// Vanilla stronghold piece weights are static mutable state.
synchronized (StrongholdStructure.class) {
start = generated.generate(choice.holder(), dimension, registries, view, view.getBiomeSource(), sampler,
start = generated.generate(choice.holder(), dimension, registries, nativeView, view.getBiomeSource(), sampler,
random, templates, island.seed(), candidate.chunk(), 0, chunk, valid);
}
} else {
start = generated.generate(choice.holder(), dimension, registries, view, view.getBiomeSource(), sampler,
start = generated.generate(choice.holder(), dimension, registries, nativeView, view.getBiomeSource(), sampler,
random, templates, island.seed(), candidate.chunk(), 0, chunk, valid);
}
if (!start.isValid()) { diagnostic(island, candidate, "native-refused", entry.id()); continue; }
// Only buried portal variants need the alternative underground elevations.
if (vertical > 0 && entry.family().equals("ruined_portal") && !undergroundPortal(start)) break;
start = adjustNativeStart(original, start, entry, island, targetY, ground);
String rejection = supportRejection(host, island, entry, start, ground);
start = adjustNativeStart(original, start, entry, island, targetY, nativeGround);
String rejection = supportRejection(host, island, entry, start, nativeGround);
if (rejection != null) { diagnostic(island, candidate, rejection, entry.id()); continue; }
((NativeStructureMetadata23) (Object) start).sanctuary$native23(new NativeStructureMetadata23.Data(island.seed(), seaLevel));
return new Accepted(choice.holder(), start);
@@ -333,17 +347,35 @@ public final class ExpansionStructures23 {
|| city.intersectsCityConstruction15(total.minX() - 2, total.minY() - 12, total.minZ() - 2, total.maxX() + 2, total.maxY() + 4, total.maxZ() + 2)
|| city.intersectsCityConstruction16(total.minX() - 2, total.minY() - 12, total.minZ() - 2, total.maxX() + 2, total.maxY() + 4, total.maxZ() + 2)
|| city.intersectsExperimentalStructures(total.minX() - 2, total.minY() - 12, total.minZ() - 2, total.maxX() + 2, total.maxY() + 4, total.maxZ() + 2)) return "reserved-secret";
if (island.origin()) {
if (island.origin() && !entry.family().equals("swamp_hut")) {
var binding = city.starterContext();
if (fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime.protects(binding.generator(), binding.random(),
total.getCenter().atY(total.minY()), Math.max(total.getXSpan(), total.getZSpan()) / 2 + 3, total.getYSpan() + 16, 26)) return "protected-hydrology";
}
}
if (entry.family().equals("swamp_hut")) {
if (host instanceof SanctuaryChunkGenerator sanctuary && island.origin()) {
var binding = sanctuary.starterContext();
var box = new BoundingBox(total.minX(), total.minY() - SwampHutPlacement28.MAX_PIER_DROP - 2,
total.minZ(), total.maxX(), total.maxY(), total.maxZ());
for (var region : fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime.plansIntersecting(
binding.generator(), binding.random(), total.minX() - 16, total.minZ() - 16, total.maxX() + 16, total.maxZ() + 16)) {
String refusal = SwampHutPlacement28.hydrologyRejection(region, box);
if (refusal != null) return refusal;
}
}
return SwampHutPlacement28.rejection(start, (x, z) -> ground.at(x, z).column);
}
boolean underground = entry.habitat() == Habitat.UNDERGROUND || undergroundPortal(start);
boolean aquatic = entry.habitat() == Habitat.OCEAN;
int siteMin = Integer.MAX_VALUE, siteMax = Integer.MIN_VALUE;
for (StructurePiece piece : start.getPieces()) {
BoundingBox box = piece.getBoundingBox();
if (entry.family().equals("mineshaft")
&& piece instanceof net.minecraft.world.level.levelgen.structure.structures.MineshaftPieces.MineShaftCorridor) {
if (!MineshaftSupport28.anchored(piece, (x, z) -> ground.at(x, z).column)) return "mineshaft-no-native-support";
continue;
}
int low = Integer.MAX_VALUE, high = Integer.MIN_VALUE, supported = 0, sampled = 0;
boolean buriedPiece = underground || !aquatic && box.maxY() < ground.at(box.getCenter().getX(), box.getCenter().getZ()).top - 6;
int step = buriedPiece ? 8 : 4;
@@ -474,7 +506,7 @@ public final class ExpansionStructures23 {
}
private static int weight(String family) {
return switch (family) { case "camp" -> 6; case "ruined_portal", "mineshaft", "ocean_ruin" -> 3;
case "village", "shipwreck", "trail_ruins", "buried_treasure" -> 2; default -> 1; };
case "village", "shipwreck", "trail_ruins", "buried_treasure", "swamp_hut" -> 2; default -> 1; };
}
private static int[] samples(int low, int high, int step) {
var values = new java.util.TreeSet<Integer>(); values.add(low); values.add(high); values.add((low + high) / 2);
@@ -493,9 +525,11 @@ public final class ExpansionStructures23 {
private record Choice(Entry entry, Holder<Structure> holder, double score) {}
private static final class Ground {
final NoiseColumn column; final int top; final int thickness;
Ground(NoiseColumn column, int min, int max) {
Ground(NoiseColumn column, int min, int max) { this(column, min, max, false); }
Ground(NoiseColumn column, int min, int max, boolean waterSurface) {
this.column = column; int y = max;
while (y >= min && !column.getBlock(y).isSolid()) y--;
while (y >= min && !column.getBlock(y).isSolid()
&& !(waterSurface && column.getBlock(y).getFluidState().is(net.minecraft.tags.FluidTags.WATER))) y--;
top = y;
while (y >= min && top - y < 24 && solid(y)) y--;
thickness = top - y;
@@ -505,17 +539,23 @@ public final class ExpansionStructures23 {
}
private static final class GroundSampler {
final ExpansionChunkGenerator generator; final LevelHeightAccessor level; final RandomState random; final long deadline;
final Integer localCeiling;
final Map<Long, Ground> columns = new HashMap<>();
fr.koka.sanctuary.worldgen.city.LostCityProtection16 localCity;
GroundSampler(ExpansionChunkGenerator generator, LevelHeightAccessor level, RandomState random, long deadline) {
this(generator, level, random, deadline, null);
}
GroundSampler(ExpansionChunkGenerator generator, LevelHeightAccessor level, RandomState random, long deadline, Integer localCeiling) {
this.generator = generator; this.level = level; this.random = random; this.deadline = deadline;
this.localCeiling = localCeiling;
}
Ground at(int x, int z) {
long key = ((long) x << 32) ^ (z & 0xffffffffL);
Ground old = columns.get(key); if (old != null) return old;
checkDeadline(deadline);
if (columns.size() >= MAX_COLUMN_SAMPLES) throw new SampleBudgetExceeded();
Ground result = new Ground(generator.getBaseColumn(x, z, level, random), level.getMinY(), level.getMaxY());
Ground result = new Ground(generator.getBaseColumn(x, z, level, random), level.getMinY(),
localCeiling == null ? level.getMaxY() : localCeiling, localCeiling != null);
columns.put(key, result); return result;
}
}
@@ -536,14 +576,23 @@ public final class ExpansionStructures23 {
/** Isolated biome snapshot during preflight, per-island sea level during native generation. */
private static final class NativeView extends ExpansionChunkGenerator {
private final ExpansionChunkGenerator host; private final ExpansionIsland island;
private Integer localCeiling;
NativeView(ExpansionChunkGenerator host, ExpansionIsland island, ExpansionBiomeSource source, RegistryAccess registries) {
super(source, host.initialDiameter(), true); this.host = host; this.island = island; this.registries = registries;
}
NativeView atHeight(int ceiling) {
var result = new NativeView(host, island, source(), registries); result.localCeiling = ceiling; return result;
}
@Override public int rootGeneration() { return island.generation(); }
@Override public Holder<Biome> structureBiome(Holder<Biome> biome) { return host.structureBiome(biome); }
@Override public int getSeaLevel() { return ExpansionOcean23.active(island) ? ExpansionOcean23.seaLevel(island) : 192; }
@Override public RandomState randomState(ExpansionIsland candidate) { return island.origin() ? host.randomState(candidate) : super.randomState(candidate); }
@Override public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor level, RandomState random) {
if (localCeiling != null) {
var column = getBaseColumn(x, z, level, random);
for (int y = localCeiling; y >= level.getMinY(); y--) if (type.isOpaque().test(column.getBlock(y))) return y + 1;
return level.getMinY();
}
return island.origin() ? host.getBaseHeight(x, z, type, level, random) : super.getBaseHeight(x, z, type, level, random);
}
@Override public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor level, RandomState random) {
@@ -0,0 +1,32 @@
package fr.koka.sanctuary.expansion;
import java.util.function.BiFunction;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
/** Certifies the support positions actually used by vanilla MineShaftCorridor, not a percentage of nearby rock. */
public final class MineshaftSupport28 {
private MineshaftSupport28() {}
public static boolean anchored(StructurePiece piece, BiFunction<Integer, Integer, NoiseColumn> columns) {
var box = piece.getBoundingBox();
int length = piece.getOrientation().getAxis() == net.minecraft.core.Direction.Axis.Z ? box.getZSpan() : box.getXSpan();
for (int z : length > 5 ? new int[]{2, length - 3} : new int[]{2}) for (int x : new int[]{0, 2}) {
var pier = SwampHutPlacement28.worldPosition(piece, x, -1, z);
var column = columns.apply(pier.getX(), pier.getZ()); boolean anchor = false;
// Native wooden support searches at most20 below its floor plank.
for (int y = pier.getY() - 1; y >= pier.getY() - 20; y--) {
var state = column.getBlock(y);
if (!state.getFluidState().isEmpty()) break;
if (state.isSolid()) { anchor = true; break; }
}
// Its alternative is a chain up to50 blocks, anchored above the excavated corridor.
if (!anchor) for (int y = box.maxY() + 1; y <= pier.getY() + 50; y++) {
var state = column.getBlock(y);
if (!state.getFluidState().isEmpty()) break;
if (state.isSolid()) { anchor = true; break; }
}
if (!anchor) return false;
}
return true;
}
}
@@ -7,10 +7,34 @@ import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPiec
/** Scoped native placement parameters; never change the sea level of another dimension or start. */
public final class NativeStructurePlacement23 {
private static final ThreadLocal<Integer> SEA_LEVEL = new ThreadLocal<>();
public record HutScope(java.util.List<net.minecraft.core.BlockPos> piers,
java.util.List<net.minecraft.world.level.levelgen.structure.BoundingBox> bodies) {}
private static final ThreadLocal<HutScope> HUT = new ThreadLocal<>();
private NativeStructurePlacement23() {}
public static Integer seaLevel() { return SEA_LEVEL.get(); }
public static Integer enter(int seaLevel) { Integer previous = SEA_LEVEL.get(); SEA_LEVEL.set(seaLevel); return previous; }
public static void restore(Integer previous) { if (previous == null) SEA_LEVEL.remove(); else SEA_LEVEL.set(previous); }
public static HutScope enterHut(net.minecraft.world.level.levelgen.structure.StructureStart start) {
var previous = HUT.get();
var pieces = start.getPieces().stream().filter(net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece.class::isInstance).toList();
HUT.set(new HutScope(pieces.stream().flatMap(piece -> SwampHutPlacement28.piers(piece).stream()).toList(),
pieces.stream().map(StructurePiece::getBoundingBox).toList()));
return previous;
}
public static void restoreHut(HutScope previous) {
if (previous == null) HUT.remove(); else HUT.set(previous);
}
public static boolean allowsHutWrite(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state,
net.minecraft.world.level.block.state.BlockState replaced) {
var scope = HUT.get(); if (scope == null) return false;
// Native construction in already empty air above a certified bank does not cut its retaining bed.
if (replaced.isAir() && scope.bodies().stream().anyMatch(box -> box.isInside(pos))) return true;
if (!state.is(net.minecraft.world.level.block.Blocks.OAK_LOG)
|| !(replaced.isAir() || replaced.getFluidState().isSource()
&& replaced.getFluidState().is(net.minecraft.tags.FluidTags.WATER))) return false;
return scope.piers().stream().anyMatch(pier -> pier.getX() == pos.getX() && pier.getZ() == pos.getZ()
&& pos.getY() <= pier.getY() && pos.getY() > pier.getY() - SwampHutPlacement28.MAX_PIER_DROP);
}
public static void move(StructurePiece piece, int dy) {
if (piece instanceof OceanMonumentPieces.MonumentBuilding monument) {
for (StructurePiece child : ((MonumentChildren23Accessor) monument).sanctuary$children()) child.move(0, dy, 0);
@@ -0,0 +1,87 @@
package fr.koka.sanctuary.expansion;
import fr.koka.sanctuary.worldgen.PopulationHydrology;
import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
import net.minecraft.world.level.material.Fluids;
/** Native hut geometry on a real shallow bank, including banks below another island floor. */
public final class SwampHutPlacement28 {
public static final int MAX_PIER_DROP = 8;
private SwampHutPlacement28() {}
public static String rejection(StructureStart start, BiFunction<Integer, Integer, NoiseColumn> columns) {
for (var piece : start.getPieces()) {
if (!(piece instanceof SwampHutPiece)) return "hut-unexpected-piece";
var box = piece.getBoundingBox();
for (int x = box.minX(); x <= box.maxX(); x++) for (int z = box.minZ(); z <= box.maxZ(); z++) {
var column = columns.apply(x, z);
int waterTop = Integer.MIN_VALUE, waterBottom = Integer.MAX_VALUE;
for (int y = box.minY() - MAX_PIER_DROP; y <= box.maxY(); y++) {
var state = column.getBlock(y); var fluid = state.getFluidState();
if (y >= box.minY() + 2 && !state.isAir()) return "hut-natural-headroom";
if (fluid.isEmpty()) continue;
if (fluid.getType() != Fluids.WATER || !fluid.isSource()) return "hut-flowing-or-nonwater";
waterTop = Math.max(waterTop, y); waterBottom = Math.min(waterBottom, y);
}
if (waterTop != Integer.MIN_VALUE) {
if (waterTop > box.minY() || waterTop - waterBottom + 1 > 4) return "hut-deep-water";
if (!solid(column.getBlock(waterBottom - 1)) || !solid(column.getBlock(waterBottom - 2))) return "hut-unsealed-bed";
}
}
for (var pier : piers(piece)) {
var column = columns.apply(pier.getX(), pier.getZ()); int y = pier.getY(), depth = 0;
while (replaceable(column.getBlock(y)) && depth <= MAX_PIER_DROP) { y--; depth++; }
if (depth > MAX_PIER_DROP || !solid(column.getBlock(y)) || !solid(column.getBlock(y - 1))) return "hut-unsupported-pier";
}
}
return null;
}
/** Still water is compatible; springs, spills, channels and lava keep their protection. */
public static String hydrologyRejection(PopulationHydrologyRuntime.RegionPlan region, BoundingBox box) {
for (var spring : region.water().springs()) if (intersects(box, spring.flowBounds(), 3)) return "hut-protected-spring";
for (var spill : region.water().spills()) if (intersects(box, spill.flowBounds(), 3)) return "hut-protected-spill";
for (var flow : region.lava().flowBounds()) if (intersects(box, flow, 4)) return "hut-protected-lava";
var rivers = region.water().features().stream().filter(f -> f.kind() == PopulationHydrology.Kind.RIVER)
.map(PopulationHydrology.Feature::id).collect(java.util.stream.Collectors.toSet());
for (int x = box.minX(); x <= box.maxX(); x++) for (int z = box.minZ(); z <= box.maxZ(); z++)
for (var cell : region.water().cellsAt(x, z)) if (rivers.contains(cell.featureId())
&& box.minY() <= cell.carveTop() && box.maxY() >= cell.bedY() - cell.sedimentDepth() - 1) return "hut-protected-river";
return null;
}
/** Exact four support origins used by SwampHutPiece.postProcess. */
public static List<BlockPos> piers(StructurePiece piece) {
var result = new ArrayList<BlockPos>(4);
for (int z : new int[]{2, 7}) for (int x : new int[]{1, 5}) result.add(worldPosition(piece, x, -1, z));
return List.copyOf(result);
}
public static BlockPos worldPosition(StructurePiece piece, int x, int y, int z) {
var b = piece.getBoundingBox(); Direction facing = piece.getOrientation();
if (facing == null || facing.getAxis().isVertical()) throw new IllegalArgumentException("A hut requires horizontal orientation");
int wx = switch (facing) { case WEST -> b.maxX() - z; case EAST -> b.minX() + z; default -> b.minX() + x; };
int wz = switch (facing) { case NORTH -> b.maxZ() - z; case SOUTH -> b.minZ() + z; default -> b.minZ() + x; };
return new BlockPos(wx, b.minY() + y, wz);
}
private static boolean solid(BlockState state) { return state.isSolid() && state.getFluidState().isEmpty(); }
private static boolean replaceable(BlockState state) {
return state.isAir() || state.liquid() || state.is(Blocks.GLOW_LICHEN) || state.is(Blocks.SEAGRASS) || state.is(Blocks.TALL_SEAGRASS);
}
private static boolean intersects(BoundingBox b, PopulationHydrology.FlowBounds flow, int margin) {
return b.minX() <= flow.maxX() + margin && b.maxX() >= flow.minX() - margin
&& b.minY() <= flow.maxY() + margin && b.maxY() >= flow.minY() - margin
&& b.minZ() <= flow.maxZ() + margin && b.maxZ() >= flow.minZ() - margin;
}
}
@@ -67,8 +67,9 @@ public abstract class NativeStructureStart23Mixin implements NativeStructureMeta
RandomSource random, BoundingBox bounds, ChunkPos pos, Operation<Void> original) {
if (sanctuary$native23 == null) { original.call(level, structures, generator, random, bounds, pos); return; }
Integer previous = NativeStructurePlacement23.enter(sanctuary$native23.seaLevel());
var previousHut = NativeStructurePlacement23.enterHut((StructureStart) (Object) this);
try { original.call(level, structures, generator, random, bounds, pos); }
finally { NativeStructurePlacement23.restore(previous); }
finally { NativeStructurePlacement23.restoreHut(previousHut); NativeStructurePlacement23.restore(previous); }
}
@Unique private static Data sanctuary$read(CompoundTag tag) {
@@ -62,6 +62,10 @@ public abstract class UnifiedDecorationProtectionMixin {
}
// The dedicated shoreline feature still grows cane on its naturally valid dry banks.
if (state.is(Blocks.SUGAR_CANE)) return;
// Admitted native huts use existing air and four bounded wooden piers in still water.
// The intact basin bed, all other water cells, island ownership and secrets remain protected.
if (!generator.protectsCityDecoration(pos)
&& fr.koka.sanctuary.expansion.NativeStructurePlacement23.allowsHutWrite(pos, state, region.getBlockState(pos))) return;
// Generation16 reserves its architectural volume before decoration. Only its own
// native pieces may replace water there; published generations retain the old rule.
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))
@@ -0,0 +1,37 @@
package fr.koka.sanctuary.worldgen;
/** Bounded domain deformation around native rock noise, followed by native cave cuts. */
public final class CavesRelief28 {
public static final int CAVE_OFFSET = 96;
public static final int DEFORMATION_START = 180;
public static final double MAX_HORIZONTAL_SHIFT = 16;
public static final double MAX_VERTICAL_SHIFT = 40;
private CavesRelief28() {}
public record Warp(double x, double y, double z) {}
public static double deformationWeight(int x, int y, int z, double radius) {
double distance = Math.hypot((double) x, z);
double arrival = smooth((distance - 48) / 48);
double coast = 1 - smooth((distance / radius - .66) / .26);
return smooth((y - DEFORMATION_START) / 48.0) * arrival * coast;
}
/** Signed smooth displacements: no folded ridge, plateau saturation or height-map uplift. */
public static Warp warp(double weight, float across, float vertical, float along) {
return new Warp(MAX_HORIZONTAL_SHIFT * Math.tanh(across * 1.6) * weight,
MAX_VERTICAL_SHIFT * Math.tanh(vertical * 1.6) * weight,
MAX_HORIZONTAL_SHIFT * Math.tanh(along * 1.6) * weight);
}
public static float carve(float rock, float nativeCaves, int y) {
if (rock <= 0) return rock;
double weight = smooth((y - 40) / 28.0) * (1 - smooth((y - 202) / 30.0));
if (weight <= 0) return rock;
return Math.min(rock, (float) Math.clamp(nativeCaves + (1 - weight) * 2, -1, 1));
}
private static double smooth(double value) {
double t = Math.clamp(value, 0, 1);
return t * t * (3 - 2 * t);
}
}
@@ -0,0 +1,65 @@
package fr.koka.sanctuary.worldgen;
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
import java.util.ArrayList;
import java.util.Comparator;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.feature.MonsterRoomFeature;
/** A few attempts at real cave walls; the vanilla feature owns its shell, loot and spawner. */
public final class CavityMonsterRoom28 {
private static final MonsterRoomFeature NATIVE = new MonsterRoomFeature();
private static final int MAX_ATTEMPTS = 8;
private CavityMonsterRoom28() {}
public static void decorate(WorldGenLevel level, ChunkGenerator generator, ChunkAccess chunk) {
if (!(generator instanceof ExpansionChunkGenerator host) || host.rootGeneration() != 24
|| !level.getLevel().structureManager().shouldGenerateStructures()) return;
var pos = chunk.getPos(); var island = host.islandForChunk(pos.x(), pos.z());
if (island == null || island.generation() != 24) return;
long seed = WoodlandGroveGeometry.seed(island.seed(), pos.x(), 28, pos.z(), 0xD086E028L);
if (Math.floorMod(seed, 3) != 0) return;
var candidates = new ArrayList<BlockPos>();
int bottom = Math.max(level.getMinY() + 2, 32), top = Math.min(level.getMaxY() - 6, 280);
for (int dx : new int[]{4, 11}) for (int dz : new int[]{4, 11}) {
int x = pos.getMinBlockX() + dx, z = pos.getMinBlockZ() + dz;
for (int y = bottom; y <= top; y++) {
BlockPos at = new BlockPos(x, y, z);
if (!level.getBlockState(at.below()).isSolid() || !level.getBlockState(at.above(4)).isSolid()) continue;
boolean caveEdge = false;
for (Direction side : Direction.Plane.HORIZONTAL) {
BlockPos opening = at.relative(side, 4);
if (level.getBlockState(opening).isAir() && level.getBlockState(opening.above()).isAir()) { caveEdge = true; break; }
}
if (caveEdge) candidates.add(at);
}
}
candidates.sort(Comparator.<BlockPos>comparingLong(at -> WoodlandGroveGeometry.seed(seed, at.getX(), at.getY(), at.getZ(), 28)));
int attempts = 0;
for (BlockPos at : candidates) {
if (attempts++ >= MAX_ATTEMPTS) break;
if (!safe(level, host, at, island.id())) continue;
// No virtual solid floor/roof and no prebuilt room. Native rejection remains authoritative.
if (NATIVE.place(level, generator, RandomSource.create(WoodlandGroveGeometry.seed(seed, at.getX(), at.getY(), at.getZ(), 0xC0128)), at)) return;
}
}
private static boolean safe(WorldGenLevel level, ExpansionChunkGenerator host, BlockPos at, String owner) {
for (int dx = -4; dx <= 4; dx++) for (int dz = -4; dz <= 4; dz++) for (int dy = -1; dy <= 4; dy++) {
BlockPos block = at.offset(dx, dy, dz);
var destination = host.islandForChunk(block.getX() >> 4, block.getZ() >> 4);
if (destination == null || !destination.id().equals(owner)) return false;
var state = level.getBlockState(block);
if (!state.getFluidState().isEmpty() || state.hasBlockEntity() || state.is(Blocks.BEDROCK)) return false;
if (host instanceof SanctuaryChunkGenerator sanctuary
&& (sanctuary.protectsDecoration(block) || sanctuary.protectsCityDecoration(block))) return false;
}
return true;
}
}
@@ -0,0 +1,53 @@
package fr.koka.sanctuary.worldgen;
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.tags.BlockTags;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.chunk.ChunkAccess;
/** Native finite ore veins, positioned within the whole floating rock column rather than vanilla sea-level bands. */
public final class IslandOres28 {
private IslandOres28() {}
private record Deposit(String feature, int attempts, double minDepth, double maxDepth) {}
// Small supplementary populations shared by all climates. Existing biome ores remain in place.
private static final Deposit[] DEPOSITS = {
new Deposit("ore_coal", 8, .05, .65), new Deposit("ore_iron", 7, .12, .88),
new Deposit("ore_copper_small", 6, .10, .66), new Deposit("ore_gold", 2, .52, .90),
new Deposit("ore_redstone", 3, .56, .92), new Deposit("ore_lapis", 2, .30, .82),
new Deposit("ore_diamond_small", 2, .70, .94), new Deposit("ore_emerald", 1, .25, .85)
};
public static void place(WorldGenLevel level, ExpansionChunkGenerator generator, ChunkAccess chunk, ExpansionIsland island) {
if (island == null || generator.rootGeneration() != 24) return;
int minX = chunk.getPos().getMinBlockX(), minZ = chunk.getPos().getMinBlockZ();
// Density columns precede structures and vegetation. Cavities do not reset mineral depth:
// retain the full top-to-bottom envelope rather than using the nearest cave ceiling.
var column = generator.getBaseColumn(minX + 8, minZ + 8, level, generator.randomState(island));
int top = -1, bottom = -1;
for (int y = 1; y < 383; y++) if (column.getBlock(y).is(BlockTags.BASE_STONE_OVERWORLD)) {
if (bottom < 0) bottom = y;
top = y;
}
if (top - bottom < 24) return;
var registry = level.registryAccess().lookupOrThrow(Registries.FEATURE);
for (int index = 0; index < DEPOSITS.length; index++) {
Deposit deposit = DEPOSITS[index];
var feature = registry.getValue(Identifier.withDefaultNamespace(deposit.feature()));
if (feature == null) throw new IllegalStateException("Missing native ore " + deposit.feature());
var random = RandomSource.create(WoodlandGroveGeometry.seed(island.seed(), minX, index, minZ, 0x0280AE5L));
for (int attempt = 0; attempt < deposit.attempts(); attempt++) {
double depth = deposit.minDepth() + random.nextDouble() * (deposit.maxDepth() - deposit.minDepth());
int y = (int) Math.round(top - depth * (top - bottom));
// Largest selected native vein is 17 blocks (radius bound five). Its writes stay
// in the decorating chunk, including where neighbours are generated in another order.
var at = new BlockPos(minX + 5 + random.nextInt(6), y, minZ + 5 + random.nextInt(6));
if (level.getBlockState(at).is(BlockTags.BASE_STONE_OVERWORLD)) feature.place(level, generator, random, at);
}
}
}
}
@@ -1,6 +1,7 @@
package fr.koka.sanctuary.worldgen;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import fr.koka.sanctuary.SanctuaryMod;
import java.util.concurrent.ConcurrentHashMap;
@@ -10,7 +11,8 @@ import net.minecraft.world.level.levelgen.densityfunction.*;
/** Capacity changes the envelope; all natural noise is still sampled at world coordinates. */
public record PopulationIslandDensity(DensityFunction terrain, DensityFunction distortion,
DensityFunction sculpt, DensityFunction detail, DensityFunction underside,
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity) implements DensityFunction {
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity,
DensityFunction caves28, DensityFunction ridges28, DensityFunction erosion28, boolean geology28) implements DensityFunction {
public static final MapCodec<PopulationIslandDensity> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
DensityFunction.CODEC.fieldOf("terrain").forGetter(PopulationIslandDensity::terrain),
DensityFunction.CODEC.fieldOf("distortion").forGetter(PopulationIslandDensity::distortion),
@@ -19,36 +21,85 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
DensityFunction.CODEC.fieldOf("underside").forGetter(PopulationIslandDensity::underside),
DensityFunction.CODEC.fieldOf("rift_warp").forGetter(PopulationIslandDensity::riftWarp),
DensityFunction.CODEC.fieldOf("rift_detail").forGetter(PopulationIslandDensity::riftDetail),
IslandCapacity.CODEC.optionalFieldOf("players", IslandCapacity.TWENTY).forGetter(PopulationIslandDensity::capacity)
IslandCapacity.CODEC.optionalFieldOf("players", IslandCapacity.TWENTY).forGetter(PopulationIslandDensity::capacity),
DensityFunction.CODEC.optionalFieldOf("caves_28", DensityFunctions.zero()).forGetter(PopulationIslandDensity::caves28),
DensityFunction.CODEC.optionalFieldOf("ridges_28", DensityFunctions.zero()).forGetter(PopulationIslandDensity::ridges28),
DensityFunction.CODEC.optionalFieldOf("erosion_28", DensityFunctions.zero()).forGetter(PopulationIslandDensity::erosion28),
Codec.BOOL.optionalFieldOf("geology_28", false).forGetter(PopulationIslandDensity::geology28)
).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);
}
@Override public DensitySampler compileSampler(CompileContext context) {
var raw = ExactInterpolationSampler.compileTerrain(terrain, context); var edge = distortion.compileSampler(context);
var coarse = sculpt.compileSampler(context); var fine = detail.compileSampler(context);
var bottom = underside.compileSampler(context); var warp = riftWarp.compileSampler(context);
var fractureDetail = riftDetail.compileSampler(context);
var caves = geology28 ? caves28.compileSampler(context) : null;
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 layouts = new ConcurrentHashMap<IslandCapacity.Region, RiftShape.Layout>();
return new DensitySampler() {
private float terrainAt(SamplerContext sample, double x, double y, double z) {
int ix = (int) Math.floor(x), iy = (int) Math.floor(y), iz = (int) Math.floor(z);
double fx = x - ix, fy = y - iy, fz = z - iz;
double sum = 0;
// Trilinear sampling avoids integer snapping of the displaced native field.
for (int dz = 0; dz <= (fz == 0 ? 0 : 1); dz++)
for (int dy = 0; dy <= (fy == 0 ? 0 : 1); dy++)
for (int dx = 0; dx <= (fx == 0 ? 0 : 1); dx++) {
double w = (dx == 0 ? 1 - fx : fx) * (dy == 0 ? 1 - fy : fy)
* (dz == 0 ? 1 - fz : fz);
sum += w * raw.sampleValue(sample, ix + dx, iy + dy, iz + dz);
}
return (float) sum;
}
@Override public float sampleValue(SamplerContext sample, int x, int y, int z) {
if (Math.hypot((double) x, z) >= capacity.terrainLimit() || y <= 0 || y >= 384) return -1;
float terrainValue;
double weight = geology28 ? CavesRelief28.deformationWeight(x, y, z, capacity.radius()) : 0;
if (weight > 0) {
var shift = 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));
terrainValue = terrainAt(sample, x - shift.x(), y - 64 - shift.y(), z - shift.z());
} else terrainValue = raw.sampleValue(sample, x, y - 64, z);
// Keep the island envelope at actual coordinates, independent from displaced rock.
int localY = y - 64;
float natural = PopulationIslandShape.raisedDensity(x, y, z, raw.sampleValue(sample, x, localY, z),
float natural = PopulationIslandShape.raisedDensity(x, y, z, terrainValue,
edge.sampleValue(sample, x, 0, z), coarse.sampleValue(sample, x, localY, z),
fine.sampleValue(sample, x, localY, z), bottom.sampleValue(sample, x, localY, z), 64, 384, capacity);
if (natural <= 0) return natural;
var region = capacity.regionAt(x, z);
var layout = layouts.computeIfAbsent(region, r -> RiftShape.layout(regionSeed(seed, r)));
int rx = x - region.originX(), rz = z - region.originZ();
if (!RiftShape.mayAffect(layout, rx, rz)) return natural;
return RiftShape.density(layout, rx, y, rz, natural,
warp.sampleValue(sample, x, y, z), fractureDetail.sampleValue(sample, x, y, z));
boolean affects = geology28 ? RiftShape.mayAffect28(layout, rx, rz) : RiftShape.mayAffect(layout, rx, rz);
if (affects) natural = geology28
? RiftShape.density28(layout, rx, y, rz, natural,
warp.sampleValue(sample, x, y, z), fractureDetail.sampleValue(sample, x, y, z))
: RiftShape.density(layout, rx, y, rz, natural,
warp.sampleValue(sample, x, y, z), fractureDetail.sampleValue(sample, x, y, z));
if (geology28 && natural > 0 && y > 40 && y < 232)
natural = CavesRelief28.carve(natural, caves.sampleValue(sample, x, y - CavesRelief28.CAVE_OFFSET, z), y);
return natural;
}
@Override public void sampleVolume(SamplerContext sample, DensityBuffer buffer, DensityVolume volume) {
DensitySampler.sampleVolumeNaive(sample, buffer, volume, this);
}
};
}
/** Native diagnostics can compile the exact accepted27 field without new adaptations. */
public PopulationIslandDensity legacy27() {
return new PopulationIslandDensity(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail,
capacity, DensityFunctions.zero(), DensityFunctions.zero(), DensityFunctions.zero(), false);
}
public static long regionSeed(long seed, IslandCapacity.Region region) {
long value = seed ^ ((long) region.x() << 32) ^ (region.z() & 0xffffffffL) ^ 0xA10CA7E1L;
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
@@ -57,7 +108,8 @@ 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(detail), rule.rewrite(underside), rule.rewrite(riftWarp), rule.rewrite(riftDetail), capacity,
rule.rewrite(caves28), rule.rewrite(ridges28), rule.rewrite(erosion28), geology28);
}
@Override public Interval range() { return Interval.of(-1, 1); }
@Override public int domainAxes() { return ALL_AXES; }
@@ -91,6 +91,55 @@ public final class RiftShape {
return (float) Math.max(-1.0, terrain - erosion);
}
/** Alpha28 fractures lean sideways, end within the mass and retain interrupted rock spans. */
public static boolean mayAffect28(Layout layout, int x, int z) {
double radius = Math.hypot((double) x, z);
if (radius <= INNER_UNTOUCHED_RADIUS || radius >= OUTER_UNTOUCHED_RADIUS) return false;
for (Rift rift : layout.rifts()) {
if (near28(rift.main(), x, z)) return true;
for (Segment branch : rift.branches()) if (near28(branch, x, z)) return true;
}
return false;
}
public static float density28(Layout layout, int x, int y, int z, float terrain, float warped, float detail) {
if (terrain <= 0 || y < 88 || y > 244 || !mayAffect28(layout, x, z)) return terrain;
double coarse = Math.clamp(warped, -1, 1), fine = Math.clamp(detail, -1, 1), cut = 0;
for (Rift rift : layout.rifts()) {
cut = Math.max(cut, erosion28(rift.main(), rift.id(), x, y, z, coarse, fine));
for (Segment branch : rift.branches())
cut = Math.max(cut, erosion28(branch, rift.id(), x, y, z, coarse, fine));
}
double radius = Math.hypot((double) x, z);
cut *= smoothstep((radius - INNER_UNTOUCHED_RADIUS) / 16)
* smoothstep((OUTER_UNTOUCHED_RADIUS - radius) / 20);
return (float) Math.max(-1, terrain - cut);
}
private static boolean near28(Segment segment, int x, int z) {
double rx = (double) x - segment.x(), rz = (double) z - segment.z();
return Math.abs(rx * segment.dx() + rz * segment.dz()) <= segment.halfLength() + 16
&& Math.abs(-rx * segment.dz() + rz * segment.dx()) <= segment.bend() * 1.3 + 20;
}
private static double erosion28(Segment segment, long id, int x, int y, int z, double coarse, double fine) {
if (!near28(segment, x, z)) return 0;
double rx = x - segment.x(), rz = z - segment.z();
double along = rx * segment.dx() + rz * segment.dz();
double end = Math.clamp(along, -segment.halfLength(), segment.halfLength());
double centerY = 150 + unit(id + 41) * 32 + end * .12 + Math.sin(end / 29 + segment.phase()) * 7;
double halfHeight = 22 + unit(id + 42) * 14;
double vertical = smoothstep((halfHeight - Math.abs(y - centerY)) / 10);
if (vertical <= 0) return 0;
double lean = (y - centerY) * (.13 + unit(id + 43) * .07);
double sideways = -rx * segment.dz() + rz * segment.dx() - curve(segment, end) - lean - coarse * 1.05;
double width = Math.clamp(segment.halfWidth() + fine * .5 + coarse * .45, 2.2, 5.7);
double distance = Math.hypot(sideways, along - end);
double bridgeAt = (unit(id + 44) - .5) * segment.halfLength();
double bridge = smoothstep((Math.abs(end - bridgeAt) - 3) / 5);
return 2.1 * smoothstep((width + 1.6 - distance) / 2.6) * vertical * bridge;
}
private static boolean near(Segment segment, int x, int z) {
double rx = (double) x - segment.x(), rz = (double) z - segment.z();
return Math.abs(rx * segment.dx() + rz * segment.dz()) <= segment.halfLength() + 10
@@ -183,7 +183,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
public boolean intersectsExperimentalStructures(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
if (generation == 24) for (var site : aerialPlan24.sites()) {
if (minX <= site.maxX() + 1 && maxX >= site.minX() - 1 && minZ <= site.maxZ() + 1 && maxZ >= site.minZ() - 1
&& minY <= site.maxY() + 1 && maxY >= (site.waterfall() ? 0 : site.minY() - 1)) return true;
&& minY <= site.maxY() + 1 && maxY >= site.minY() - 1) return true;
if (site.waterfall()) {
var flow = site.waterfallClaimBounds();
if (minX <= flow.maxX() && maxX >= flow.minX() && minZ <= flow.maxZ() && maxZ >= flow.minZ()
&& minY <= flow.maxY() && maxY >= flow.minY()) return true;
}
}
return (generation == 23 || generation == 24) && mineralSanctuary23.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
@@ -210,6 +215,10 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
case "sanctuary:population_rocky_heath" -> "meadow";
case "sanctuary:population_dark_grove" -> "dark_forest";
case "sanctuary:population_bamboo_grove" -> "bamboo_jungle";
case "sanctuary:swamp27_caves" -> "swamp";
case "sanctuary:population_lush_caves" -> "lush_caves";
case "sanctuary:population_dripstone_caves" -> "dripstone_caves";
case "sanctuary:population_sulfur_depths" -> "sulfur_caves";
default -> null;
};
return equivalent == null ? biome : registries.lookupOrThrow(Registries.BIOME).getOrThrow(
@@ -14,7 +14,7 @@ public final class Swamp27Volumes {
// Broad, connected sectors change with height; finer noise softens their borders.
double broad = noise(seed, x / 94.0, y / 48.0, z / 94.0);
double detail = noise(seed ^ 0x7757A27L, x / 37.0, y / 31.0, z / 37.0);
return broad + detail * .22 > .04 + (1 - verticalEdge) * .7;
return broad + detail * .22 > .25 + (1 - verticalEdge) * .7;
}
private static double noise(long seed, double x, double y, double z) {
@@ -15,6 +15,8 @@ import fr.koka.sanctuary.worldgen.aerial.AerialPlan24.Site;
public final class AerialPlanner24 {
public static final int MAX_ATTEMPTS_PER_SITE = 64;
public static final int MAX_COLUMNS = 8192;
public static final int ARRIVAL_RADIUS = 96;
public static final int MIN_ISLET_GAP = 16;
private AerialPlanner24() {}
public interface Terrain {
NoiseColumn column(int x, int z);
@@ -56,26 +58,67 @@ public final class AerialPlanner24 {
// 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;
Shore shore = shore(probe, angle, radius);
if (shore == null) continue;
int facing = (int) Math.floorMod(mix(draw), 4);
int worldWidth = facing % 2 == 0 ? width : depth, worldDepth = facing % 2 == 0 ? depth : width;
// The full claim remains outside initial terrain ownership, including corner chunks.
double distance = radius + 32 + Math.hypot(worldWidth, worldDepth) / 2 + 64 + (attempt % 4) * 20;
int cx = (int) Math.round(Math.cos(angle) * distance), cz = (int) Math.round(Math.sin(angle) * distance);
if (Math.hypot(cx - shore.x, cz - shore.z) > 500) continue;
int bottom = merchant ? Math.clamp(shore.y - 4 + (int) Math.floorMod(draw, 13), 180, 300)
: Math.clamp(shore.y + 20 - bodyHeight / 2, 210, 320 - bodyHeight);
bottom = Math.min(bottom, 350 - height);
Shore shore;
int cx, cz, bottom;
if (merchant) {
shore = shore(probe, angle, radius);
if (shore == null) continue;
// Start from the real edge, not the nominal island radius. The projected half-box
// puts the complete hull 1632 blocks out, even when the ship is rotated.
double dx = Math.cos(angle), dz = Math.sin(angle);
double offset = Math.abs(dx) * worldWidth / 2.0 + Math.abs(dz) * worldDepth / 2.0
+ 16 + (attempt % 3) * 8;
cx = shore.x + (int) Math.round(dx * offset);
cz = shore.z + (int) Math.round(dz * offset);
// Native boarding deck is five blocks above the envelope's bottom.
bottom = shore.y - 5 + (int) Math.floorMod(draw, 5);
} else {
// Both mineral islands hover over Sanctuary, away from its natural arrival.
double distance = radius * (.48 + .15 * unit(mix(draw ^ 0x28A1L)));
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;
shore = new Shore(cx, top + 1, cz);
int highest = top;
for (int ox = -worldWidth / 2; ox <= worldWidth / 2; ox += 4)
for (int oz = -worldDepth / 2; oz <= worldDepth / 2; oz += 4)
highest = Math.max(highest, probe.highest(cx + ox, cz + oz));
bottom = highest + MIN_ISLET_GAP + 1;
}
if (bottom < 32 || bottom + height > 350) continue;
var site = new Site(ids[ordinal], kinds[ordinal], cx - worldWidth / 2, bottom, cz - worldDepth / 2,
width, height, depth, facing, siteSeed, shore.x, shore.y, shore.z, ordinal == 2);
if (site.claimedChunks().stream().anyMatch(c -> claimed.contains(c.pack()))) continue;
boolean rootOverlap = site.claimedChunks().stream().anyMatch(c -> {
int nearestX = Math.clamp(0, c.getMinBlockX(), c.getMaxBlockX());
int nearestZ = Math.clamp(0, c.getMinBlockZ(), c.getMaxBlockZ());
return Math.hypot(nearestX, nearestZ) <= radius + 32;
});
if (rootOverlap || !visible(probe, shore, cx, bottom + bodyHeight - 4, cz)) continue;
if (!outsideArrival(site) || site.claimedChunks().stream().anyMatch(c -> claimed.contains(c.pack()))) continue;
if (merchant) {
double gap = shoreDistance(site);
if (gap < 16 || gap > 48 || !clearShip(probe, site)) continue;
} 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.
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 (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));
}
bottom = Math.max(bottom, highest + MIN_ISLET_GAP + 1);
if (bottom + height > 350) continue;
site = new Site(site.id(), site.kind(), site.minX(), bottom, site.minZ(), width, height, depth,
facing, siteSeed, shore.x, shore.y, shore.z, site.waterfall());
if (!clearReservations(probe, site)) continue;
}
if (!visible(probe, shore, cx, bottom + bodyHeight - 4, cz)) continue;
accepted = site; break;
}
if (accepted == null) throw new IllegalStateException("aerial_sites_unavailable: could not place " + ids[ordinal] + " within the bounded search");
@@ -89,6 +132,39 @@ public final class AerialPlanner24 {
return result;
}
/** Horizontal distance from the saved land witness to the nearest point of the hull envelope. */
public static double shoreDistance(Site site) {
return Math.hypot(site.shoreX() - Math.clamp(site.shoreX(), site.minX(), site.maxX()),
site.shoreZ() - Math.clamp(site.shoreZ(), site.minZ(), site.maxZ()));
}
private static boolean outsideArrival(Site site) {
if (Math.hypot(Math.clamp(0, site.minX(), site.maxX()), Math.clamp(0, site.minZ(), site.maxZ())) < ARRIVAL_RADIUS) return false;
if (site.waterfall()) {
var box = site.waterfallClaimBounds();
if (Math.hypot(Math.clamp(0, box.minX(), box.maxX()), Math.clamp(0, box.minZ(), box.maxZ())) < ARRIVAL_RADIUS) return false;
}
return true;
}
private static boolean isletColumn(Site site, int x, int z) {
return AerialGeometry24.radial(site, x, z) <= 1
|| site.waterfall() && Math.abs(x - site.width() / 2) <= 2 && z <= 6;
}
private static boolean clearShip(Probe probe, Site site) {
for (int x = site.minX(); x <= site.maxX(); x++) for (int z = site.minZ(); z <= site.maxZ(); z++)
if (probe.highest(x, z) >= site.minY() - 3 || probe.terrain.reserved(x, z, site.minY() - 1, site.maxY() + 1)) return false;
return true;
}
private static boolean clearReservations(Probe probe, Site site) {
for (int x = site.minX(); x <= site.maxX(); x++) for (int z = site.minZ(); z <= site.maxZ(); z++)
if (probe.terrain.reserved(x, z, site.minY() - 1, site.maxY() + 1)) return false;
if (site.waterfall()) {
var box = site.waterfallClaimBounds();
for (int x = box.minX(); x <= box.maxX(); x++) for (int z = box.minZ(); z <= box.maxZ(); z++)
if (probe.terrain.reserved(x, z, box.minY(), box.maxY())) return false;
}
return true;
}
private static Shore shore(Probe probe, double angle, int radius) {
double dx = Math.cos(angle), dz = Math.sin(angle);
// An outside-in scan finds the outer shore, never an internal pond. Refine its edge to a block.
@@ -138,6 +214,11 @@ public final class AerialPlanner24 {
for (int y = 382; y > 0; y--) if (column.getBlock(y).isSolid() && column.getBlock(y).getFluidState().isEmpty()) return y;
return 0;
}
int highest(int x, int z) {
var column = column(x, z); if (column == null) return 0;
for (int y = 382; y > 0; y--) if (!column.getBlock(y).isAir()) return y;
return 0;
}
boolean solid(int x, int y, int z) {
var column = column(x, z); return column != null && !column.getBlock(y).isAir();
}
@@ -95,13 +95,9 @@ public final class AerialStore24 {
private static void validateExtent(AerialPlan24 plan, int diameter) throws IOException {
if (diameter != 512 && diameter != 724 && diameter != 1024) throw new IOException("Unsupported aerial root size");
int radius = (diameter == 724 ? 395 : diameter / 2 + 32) + 32;
for (long packed : plan.claimsChunks()) {
var c = net.minecraft.world.level.ChunkPos.unpack(packed);
int x = Math.clamp(0, c.getMinBlockX(), c.getMaxBlockX());
int z = Math.clamp(0, c.getMinBlockZ(), c.getMaxBlockZ());
if (Math.hypot(x, z) <= radius) throw new IOException("Aerial claim overlaps initial terrain ownership");
}
// Aerial bodies may share X/Z with the initial island. Their vertical clearance is
// certified at creation; the immutable manifest preserves it on subsequent loads.
// Site/Plan constructors still validate finite height, dimensions, count and disjoint claims.
}
private static JsonObject encode(AerialPlan24 plan, long seed, int diameter) {
var root = new JsonObject();
@@ -22,7 +22,7 @@ import net.minecraft.world.level.levelgen.structure.StructureType;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceType;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
/** Native starts for four immutable initial sites. Their void chunks need no terrain island owner. */
/** Native starts for four immutable initial sites, in the void or above initial terrain. */
public final class AerialStructures24 {
public static final ResourceKey<Structure> MERCHANT_SHIP = key("aerial_merchant_ship");
public static final ResourceKey<Structure> MINERAL_ISLET = key("aerial_mineral_islet");
@@ -67,7 +67,10 @@ public final class AerialStructures24 {
if (previous != null && previous.isValid()) continue;
var start = structure.generate(holder, level, registries, generator, generator.getBiomeSource(), sampler,
random, templates, origin.seed(), chunk.getPos(), previous == null ? 0 : previous.getReferences(),
chunk, structure.biomes()::contains);
// Only this explicit saved departure bypasses biome selection. Its complete
// geometry and clearance were certified by the aerial planner; the biome below
// may be forest, marsh or void. findGenerationPoint still requires this exact plan.
chunk, ignoredBiome -> sites.stream().anyMatch(site -> key(site).equals(key)));
if (start.isValid()) manager.setStartForStructure(structure, start, chunk);
}
}
@@ -0,0 +1,58 @@
{
"type": "minecraft:min",
"left": {
"type": "minecraft:max",
"left": {
"type": "minecraft:min",
"left": {
"type": "minecraft:min",
"left": {
"type": "minecraft:add",
"left": {
"type": "minecraft:mul",
"left": {
"type": "minecraft:square",
"input": {
"type": "minecraft:noise",
"noise": "minecraft:cave_layer",
"xz_scale": 1.0,
"y_scale": 8.0
}
},
"right": 4.0
},
"right": {
"type": "minecraft:clamp",
"input": {
"type": "minecraft:add",
"left": {
"type": "minecraft:noise",
"noise": "minecraft:cave_cheese",
"xz_scale": 1.0,
"y_scale": 0.6666666666666666
},
"right": 0.27
},
"max": 1.0,
"min": -1.0
}
},
"right": "minecraft:overworld/caves/entrances"
},
"right": {
"type": "minecraft:add",
"left": "minecraft:overworld/caves/spaghetti_2d",
"right": "minecraft:overworld/caves/spaghetti_roughness_function"
}
},
"right": {
"type": "minecraft:range_choice",
"input": "minecraft:overworld/caves/pillars",
"max_exclusive": 0.03,
"min_inclusive": -1000000.0,
"when_in_range": -1000000.0,
"when_out_of_range": "minecraft:overworld/caves/pillars"
}
},
"right": "minecraft:overworld/caves/noodle"
}
@@ -49,5 +49,19 @@
"noise": "sanctuary:rift_detail",
"xz_scale": 1.0,
"y_scale": 1.0
},
"geology_28": true,
"caves_28": "sanctuary:caves28_native",
"ridges_28": {
"type": "minecraft:noise",
"noise": "minecraft:ridge",
"xz_scale": 1.0,
"y_scale": 1.0
},
"erosion_28": {
"type": "minecraft:noise",
"noise": "minecraft:erosion",
"xz_scale": 2.0,
"y_scale": 2.0
}
}
@@ -49,5 +49,19 @@
"noise": "sanctuary:rift_detail",
"xz_scale": 1.0,
"y_scale": 1.0
},
"geology_28": true,
"caves_28": "sanctuary:caves28_native",
"ridges_28": {
"type": "minecraft:noise",
"noise": "minecraft:ridge",
"xz_scale": 1.0,
"y_scale": 1.0
},
"erosion_28": {
"type": "minecraft:noise",
"noise": "minecraft:erosion",
"xz_scale": 2.0,
"y_scale": 2.0
}
}
@@ -49,5 +49,19 @@
"noise": "sanctuary:rift_detail",
"xz_scale": 1.0,
"y_scale": 1.0
},
"geology_28": true,
"caves_28": "sanctuary:caves28_native",
"ridges_28": {
"type": "minecraft:noise",
"noise": "minecraft:ridge",
"xz_scale": 1.0,
"y_scale": 1.0
},
"erosion_28": {
"type": "minecraft:noise",
"noise": "minecraft:erosion",
"xz_scale": 2.0,
"y_scale": 2.0
}
}
@@ -0,0 +1,54 @@
package fr.koka.sanctuary.worldgen;
/** Adapters and finite fractures only. Native noise presence is checked separately in Minecraft. */
public final class CavesRelief28Smoke {
public static void main(String[] args) {
for (int y = 0; y < 384; y++) {
require(CavesRelief28.deformationWeight(0, y, 0, 256) == 0, "arrival must not deform");
require(CavesRelief28.deformationWeight(256, y, 0, 256) == 0, "outer coast must not deform");
if (y <= 180) require(CavesRelief28.deformationWeight(128, y, 0, 256) == 0,
"accepted lower mass must not deform");
}
require(CavesRelief28.deformationWeight(128, 220, 0, 256)
< CavesRelief28.deformationWeight(128, 228, 0, 256), "3D deformation must have a smooth vertical envelope");
for (float v : new float[] {-10, -1, -.5F, 0, .5F, 1, 10}) {
var warp = CavesRelief28.warp(1, v, v, v);
var opposite = CavesRelief28.warp(1, -v, -v, -v);
require(Math.abs(warp.x()) <= 16 && Math.abs(warp.z()) <= 16 && Math.abs(warp.y()) <= 40,
"deformation must stay bounded in all three axes");
require(warp.x() == -opposite.x() && warp.y() == -opposite.y() && warp.z() == -opposite.z(),
"noise must permit equivalent uplift and subsidence, without positive bias");
var fixed = CavesRelief28.warp(0, v, v, v);
require(fixed.x() == 0 && fixed.y() == 0 && fixed.z() == 0, "zero envelope leaves coordinates unchanged");
}
require(CavesRelief28.warp(1, 0, .5F, 0).y() < CavesRelief28.warp(1, 0, .6F, 0).y(),
"deformation must not saturate into broad flat plateaus");
for (int y = 0; y < 384; y++) for (float cave : new float[] {-1, -.1F, .1F, 1, 64}) {
require(CavesRelief28.carve(-.3F, cave, y) == -.3F, "caves must never fill existing void");
require(CavesRelief28.carve(.7F, cave, y) <= .7F, "caves must only remove rock");
if (y <= 40 || y >= 232) require(CavesRelief28.carve(.7F, cave, y) == .7F, "bounded cave heights");
}
require(CavesRelief28.carve(.7F, -.1F, 140) < 0, "negative native cave must open solid interior");
for (long seed : new long[] {0, 42, 2026}) {
var layout = RiftShape.layout(seed);
int removed = 0, retained = 0;
for (int x = -216; x <= 216; x += 4) for (int z = -216; z <= 216; z += 4) {
require(RiftShape.density28(layout, x, 60, z, .7F, 0, 0) == .7F, "rift cannot cut lower boundary");
require(RiftShape.density28(layout, x, 260, z, .7F, 0, 0) == .7F, "rift cannot traverse upper surface");
int columnCut = 0;
for (int y = 88; y <= 244; y += 4) {
float value = RiftShape.density28(layout, x, y, z, .7F, .2F, -.3F);
require(value <= .7F, "rift cannot add material");
if (value < 0) { removed++; columnCut++; } else retained++;
}
require(columnCut < 22, "fracture cannot cut the entire vertical island mass");
}
require(removed > 0 && retained > removed, "finite ravines must open passages and retain rock");
System.out.println("caves28 rifts seed=" + seed + " removed=" + removed + " retained=" + retained);
}
System.out.println("CavesRelief28Smoke: signed 3D deformation, native cut adapter and bounded ravines passed");
}
private static void require(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
}
@@ -48,12 +48,24 @@ public final class Aerial24Smoke {
&& site.height() == site.rockHeight() + AerialPlan24.VEGETATION_HEADROOM,
"Vegetation adds eight blocks of headroom without enlarging the stone body");
double distance = Math.hypot(site.locator().getX() - site.shoreX(), site.locator().getZ() - site.shoreZ());
require(distance <= 512, "Every site has a nearby shore witness");
require(distance <= 512, "Every site has a nearby ground witness");
if (site.merchant()) {
double gap = AerialPlanner24.shoreDistance(site);
require(gap >= 16 && gap <= 48, "Complete ship envelope is 1648 blocks from its actual shore");
require(Math.abs(AerialGeometry24.entry(site).getY() - site.shoreY()) <= 4,
"Boarding deck remains near the shore's walking height");
} else {
require(root.ownsChunk(site.startChunkX(), site.startChunkZ()), "Mineral islets hover over the initial island");
require(site.minY() - 240 - 1 >= AerialPlanner24.MIN_ISLET_GAP,
"The unchanged rocky body has at least sixteen air blocks above the terrain");
}
require(Math.hypot(Math.clamp(0, site.minX(), site.maxX()), Math.clamp(0, site.minZ(), site.maxZ())) >= AerialPlanner24.ARRIVAL_RADIUS,
"Aerial bodies preserve the natural arrival zone");
require(plan.atStartChunk(site.startChunkX(), site.startChunkZ()).contains(site), "Native departure belongs to its saved site");
for (var chunk : site.claimedChunks()) {
require(allClaims.add(chunk.pack()), "Site claims do not overlap");
require(site.ownsChunk(chunk.x(), chunk.z()) && plan.ownsChunk(chunk.x(), chunk.z()), "Claim query agrees with the immutable mask");
require(!root.ownsChunk(chunk.x(), chunk.z()), "Claim does not replace initial island ownership");
}
var overlapping = new ExpansionIsland("overlap", "sanctuary", "east", "temperate", 64,
site.startChunkX() * 16, site.startChunkZ() * 16, 42, "natural", "reserved", 24);
@@ -109,7 +121,7 @@ public final class Aerial24Smoke {
require(columns[0] <= AerialPlanner24.MAX_COLUMNS, "Reservations preserve the bounded terrain sampling budget");
for (var site : replacement.sites())
require(!terrain.reserved(site.shoreX(), site.shoreZ(), site.shoreY() - 1, site.shoreY() + 2),
"Every accepted shore stays outside planned root structures");
"Every accepted ground witness stays outside planned root structures");
}
}
private static AerialPlan24.Site rotate(AerialPlan24.Site site, int facing) {
@@ -51,6 +51,9 @@ public final class AerialPersistence24Smoke {
require(draws.get() == 1 && stored.plan().sites().size() == 4, "One complete initial plan is recorded");
var journal = new ExpansionJournal(directory, SEED, 724, 24, stored.token());
var root = ExpansionIsland.origin(724, SEED, 24);
require(stored.plan().sites().stream().filter(site -> site.kind() == Kind.MINERAL_ISLET)
.allMatch(site -> root.ownsChunk(site.startChunkX(), site.startChunkZ())),
"An immutable elevated islet may share initial terrain chunks without replacing root ownership");
Path journalFile = directory.resolve(ExpansionJournal.FILE_NAME);
byte[] pristineJournal = Files.readAllBytes(journalFile);
require(JsonParser.parseString(Files.readString(journalFile)).getAsJsonObject().get("schema").getAsInt() == 2,
@@ -143,8 +146,8 @@ public final class AerialPersistence24Smoke {
return new AerialPlan24(List.of(
new Site("merchant_provisions", Kind.MERCHANT_PROVISIONS, 0, 235, 660, 11, 18, 28, 0, 2, 0, 240, 390, false),
new Site("merchant_tools", Kind.MERCHANT_TOOLS, -740, 242, 0, 11, 18, 28, 0, 3, -390, 240, 0, false),
new Site("mineral_islet_water", Kind.MINERAL_ISLET, -450, 255, -650, 48, 36, 48, 0, 4, -200, 240, -330, true),
new Site("mineral_islet_dry", Kind.MINERAL_ISLET, 460, 260, -600, 56, 40, 56, 0, 5, 250, 240, -300, false)));
new Site("mineral_islet_water", Kind.MINERAL_ISLET, -180, 270, -160, 48, 36, 48, 0, 4, -156, 240, -136, true),
new Site("mineral_islet_dry", Kind.MINERAL_ISLET, 125, 270, -170, 56, 40, 56, 0, 5, 153, 240, -142, false)));
}
private static String digest(byte[] bytes) throws Exception { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes)); }
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "0.1.0-alpha.27"
version = "0.1.0-alpha.28"
pack-format = "packwiz:1.1.0"
[index]