Finish alpha.30.7 with natural island treasures, direct rails and dry ruins
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-12 20:29:32 +02:00
parent 0b7e0c6717
commit d5cd895403
34 changed files with 715 additions and 45 deletions
+11
View File
@@ -21,6 +21,17 @@ le relief et les souterrains de la 24, retire la montgolfière et travaille les
marais souterrains et la restauration de vrais modèles d’épaves vanilla. Voir le
[chantier et ses limites](docs/generation-alpha27.md).
## Alpha.30.7 — trésors aériens et vestiges asséchés
Le relief de 30.6 est conservé. Les nouveaux morceaux de terrain aérien reçoivent
les minerais et trésors des anciens îlots artificiels, retirés de la planification.
Les rails privilégient les lignes directes. Une seconde traversée souterraine et
quelques petites ruines océaniques asséchées sont cherchées lorsque le terrain
les accueille.
Voir [la génération](docs/generation-alpha30.7.md) et [les essais](docs/testing-alpha30.7.md).
**Nouveau monde requis.**
## Alpha.30.6 — village souterrain et voies ramifiées
La déformation du terrain 30.5 est légèrement renforcée. Un village vanilla
+66
View File
@@ -0,0 +1,66 @@
# Alpha.30.7 — dernières installations de la génération
Cette passe conserve le relief et les morceaux de terrain naturel de lalpha.30.6.
Les fonctions de densité, cavités, rivières, biomes et tailles ne changent pas.
Minecraft reste en 26.3-pre-2 ; plafond à Y640, Moyen 724 par défaut.
Créer un nouveau monde pour voir la nouvelle répartition des installations.
## Trésors dans le terrain aérien
Les deux anciens îlots minéraux construits comme des structures ne sont plus
planifiés dans les nouveaux mondes. Les deux morceaux naturels de Petit et Moyen,
ou les trois de Grand, portent désormais chacun un coffre enterré et des filons.
Le coffre utilise la table de richesses aériennes existante, uniquement vanilla.
Aucun objet de quête nest ajouté.
Lenrichissement remplace seulement la roche existante entre Y300 et Y399 :
charbon, cuivre et fer dans les couches supérieures ; or, lapis, redstone, diamant
et émeraude plus à lintérieur. La profondeur est mesurée dans chaque colonne de
l’îlot. Les réserves sont finies, sans renouvellement après minage ou pillage.
Le profil est volontairement très riche ; les comptages par minerai figurent
dans les rapports aériens de latlas. La forme, le sol et la végétation de ces morceaux naturels restent ceux de 30.6.
Lancienne cascade artificielle disparaît avec son ancien îlot.
Les deux navires restaurés restent près de leurs côtes. Les nouveaux trésors
possèdent des départs natifs, des pièces sauvegardées et des réservations limitées
à leurs emprises. Ils utilisent lidentifiant de localisation
`sanctuary:aerial_mineral_islet`. Le terrain aérien naturel existe également quand
les structures sont désactivées ; ses coffres et son enrichissement expérimental
suivent les options de structures.
## Rails et traversées
Chaque prolongement ferroviaire garde son axe. Un terrain incompatible termine
le parcours au lieu de provoquer une succession de virages. Les embranchements
partent des longues lignes principales ; les extensions successives ne produisent
plus de branches tournantes récursives. Une séparation évite les lignes presque
parallèles autour dune même voie. Cela peut réduire la longueur totale du réseau :
les 68 % anciennement évoqués ne constituent pas un quota.
Après la première traversée souterraine, une seconde recherche est effectuée dans
lespace restant. Elle respecte les mêmes règles de sol, couverture et accès,
avec une séparation de 24 blocs autour des modules de la première. Si elle ne
tient pas dans le terrain et le budget de recherche, la première reste conservée.
Chaque traversée possède ses deux accès ; aucun croisement aveugle nest creusé
pour forcer la seconde.
## Ruines dun ancien fond marin
`sanctuary:drained_ocean_ruin` place quelques petits templates vanilla de ruines
océaniques chaudes dans les cavités sèches existantes entre Y60 et Y174. Trois
emplacements sont recherchés, quatre en Grand, sans garantie de quantité. Les
refus de sol, dégagement ou réservation sont journalisés. Aucun nouveau grand
volume souterrain nest creusé pour les accueillir.
Les modèles vanilla habités (`warm_2`, `warm_3`, `warm_4`, `warm_5`, `warm_8`),
leur dégradation et leurs tables de butin sont conservés.
Les marqueurs dhabitants placent des zombies vanilla persistants ; des coraux
morts complètent les vestiges et sont posés avec `waterlogged=false`. Les blocs
gorgés deau sont asséchés. La protection contre la décoration ultérieure reste
limitée à la petite emprise de chaque ruine. Les pièces
sauvegardées conservent leur altitude souterraine au lieu de recalculer un fond
océanique lors du rechargement. Ces ruines Sanctuary ont leur propre identifiant ;
elles ne remplacent pas les ruines aquatiques des expansions océaniques.
Voir [les vérifications](testing-alpha30.7.md). La structure envisagée à Y600 reste
une idée ; cette livraison najoute rien à cette hauteur.
+26
View File
@@ -100,3 +100,29 @@ build/atlas-venv/bin/python scripts/structure_atlas.py \
mods/sanctuary/build/run/gameTest/diagnostics/atlas306/terrain-724-0.json \
--output build/atlas306/structures
```
## Finition 30.7
Le terrain de 30.6 est conservé. Lexport natif est répété sur les neuf couples
512 / 724 / 1 024 × 0 / 42 / -7228211907433324401 pour comparer exactement les
colonnes et les coupes avant décoration. Lenrichissement des minerais et les
petites structures nappartiennent pas à ce champ de densité.
`scripts/finishing_atlas.py` superpose les rails planifiés, les axes des traversées
parcourues dans les blocs, les ruines asséchées et les réservations aériennes.
Les rectangles représentent les emprises ; ils ne représentent pas des îlots
rectangulaires. Le script refuse des rapports de graines ou tailles différentes
et des témoins natifs non réussis.
```sh
build/atlas-venv/bin/python scripts/finishing_atlas.py \
--terrain build/atlas307/raw/terrain-724-0.json \
--village CHEMIN_DU_RAPPORT_VILLAGE \
--transit CHEMIN_DU_RAPPORT_TRANSIT \
--aerial CHEMIN_DU_RAPPORT_AERIEN \
--ruins CHEMIN_DU_RAPPORT_RUINES --output build/atlas307/installations
```
Les preuves, cartes PNG/PDF et leurs empreintes sont ajoutées à larchive de
livraison. Ces cartes sont des vues de diagnostic opérateur, pas des cartes
automatiquement révélées aux joueurs.
+91
View File
@@ -0,0 +1,91 @@
# Vérifications alpha.30.7
Essais automatisés sur macOS, Java 25, Minecraft 26.3-pre-2. Aucun essai Windows
ni visite visuelle interactive de lalpha.30.7 na été réalisé. Les mondes des
fixtures restent dans le dossier de développement ignoré.
## Témoins dans de vrais chunks
| Témoin | Terrain aérien enrichi | Navires / villageois | Traversées praticables | Village abandonné | Mines |
|---|---:|---:|---|---|---:|
| Moyen 724, graine 0 | 2 corps, 2 coffres | 2 / 2 | 2, portées 272 et 204 blocs | 15 maisons, 99 pièces, 10 zombie-villageois | 1 |
| Grand 1 024, graine -7228211907433324401 | 3 corps, 3 coffres | 2 / 2 | 1, portée 374 blocs | 12 maisons, 70 pièces, 9 zombie-villageois | 2 |
La seconde traversée du Grand est refusée : aucun accès ne satisfait le contrôle
civil. La première reste générée et parcourable. « Deux si la place le permet »
ne signifie donc pas deux dans chaque monde.
Les fixtures aériennes contrôlent les départs natifs, les coffres enterrés,
les huit minerais dans chaque corps, la présence des deux villageois, puis
pillent les coffres, minent un témoin et déplacent les habitants dans leur monde
jetable. Les fixtures de traversée vérifient les blocs de circulation de chaque
réseau séparément, ses accès et ses parois. Le village conserve ses pièces
Jigsaw, ses coffres et ses habitants. Les rails sont mesurés comme plans et
ballast : aucune circulation de wagonnet nest certifiée sur les voies abandonnées.
## Ruines asséchées
Sur Moyen / 0 : trois ruines réellement posées, trois coffres, trois zombies
persistants et neuf coraux morts. Le contrôle après ticks vérifie labsence
deau dans les emprises, les blocs vanilla et le roundtrip NBT des départs.
Sur Grand / -7228211907433324401 : quatre ruines réellement posées, quatre
coffres, cinq zombies persistants et douze coraux morts. La passe dédiée réussit
en 3 min 21 s, dont 15 s dexécution des deux tests après préparation du monde.
Le test a révélé que les coraux morts ont un état `waterlogged=true` par défaut.
Ils sont désormais explicitement posés secs. La correction est locale à la ruine ;
aucun filtre étendu ne supprime les eaux ou la décoration des grottes voisines.
Les sauvegardes de pièces et les marqueurs dhabitants sont contrôlés par
sérialisation native. Une reprise complète dans un nouveau processus JVM après
pillage et démolition na pas été rejouée pour cette livraison ; elle ne doit pas
être confondue avec ce roundtrip NBT.
## Commandes et portée
Une première exécution complète de `check build` a réussi avec 26 GameTests.
Après la correction finale des coraux, les contrôles purs et les cinq GameTests
ciblant les comportements modifiés ont tous réussi (5 min 47 s) :
```sh
./gradlew check build \
-PsanctuaryFocusedTests=aerial,transit,village,drained \
-PsanctuaryAerial24Tests=true -PsanctuaryTransit29Tests=true
```
Le sélecteur `sanctuaryFocusedTests` limite seulement les fixtures natives ;
les contrôles de `check` restent exécutés. Sans ce paramètre, la suite native
complète conserve son fonctionnement habituel.
Les témoins Grand pour les îlots, navires, traversée et village proviennent de
la passe précédente de cette même livraison. Seuls la pose sèche des coraux et
les contrôles locaux des ruines ont ensuite changé ; la validation dédiée des
ruines Grand a réussi séparément avec la correction finale.
Les durées ci-dessus sont celles des fixtures (préparation et lecture de milliers
de blocs incluses), pas une mesure de FPS ni une promesse de durée de chargement.
Les neuf graines/tailles de latlas ne constituent pas neuf visites complètes de
structures. Les plafonds, reliefs et corps flottants restent ceux de 30.6.
## Atlas et intégrité du terrain
Lexport natif final (3 min 42 s) a réussi pour les neuf couples
512 / 724 / 1 024 × graines 0 / 42 / -7228211907433324401.
Toutes les colonnes et coupes des quatre variantes sont **exactement égales** à
lexport 30.6, avec les mêmes coordonnées et résolutions. La comparaison porte
sur les tableaux de densité avant décoration, pas sur les matériaux enrichis ni
sur les installations. Les empreintes des deux jeux de données sont archivées
avec le rapport `density-306-307-comparison.json`.
`./gradlew assemblePack -x :sanctuary:check` a réussi après le `check build` final.
Le MRpack a été vérifié : version, dépendance exacte, tailles, empreintes, CRC,
absence de sauvegardes personnelles et absence de second mécanisme de mise à jour.
## Livraison
Créer un **nouveau monde Sanctuary**. Le codec de prototype `sanctuary:island_v24`
est conservé ; aucune migration ni régénération des sauvegardes nest exécutée.
Les réservations aériennes déjà enregistrées dans les anciens mondes sont gardées.
Le pack reste sur Minecraft 26.3-pre-2, Fabric Loader 0.19.5 et Fabric API
0.160.0+26.3. La réception publique et Prism est consignée dans `docs/packwiz.md`.
+2 -2
View File
@@ -8,6 +8,6 @@ loader_version=0.19.5
loom_version=1.17.20
fabric_api_version=0.160.0+26.3
mod_version=0.1.0-alpha.30.6
pack_version=0.1.0-alpha.30.6
mod_version=0.1.0-alpha.30.7
pack_version=0.1.0-alpha.30.7
maven_group=fr.koka.sanctuary
+9
View File
@@ -89,6 +89,11 @@ tasks.named('runGameTest') {
tasks.named('processGametestResources') {
def villageOnly = providers.gradleProperty('sanctuaryVillageOnly').map { it.toBoolean() }.getOrElse(false)
def focused = providers.gradleProperty('sanctuaryFocusedTests').getOrElse('').split(',').findAll { !it.isEmpty() }
def focusClasses = [aerial: 'Aerial24WorldGameTests', transit: 'Transit29WorldGameTests', village: 'Village306GameTests', drained: 'Drained307GameTests']
focused.each { if (!focusClasses.containsKey(it)) throw new GradleException('Unknown focused native test: ' + it) }
inputs.property('focusedNative', focused)
inputs.property('villageOnly', villageOnly)
def atlasOnly = providers.gradleProperty('sanctuaryAtlasOnly').map { it.toBoolean() }.getOrElse(false)
inputs.property('atlasOnly', atlasOnly)
@@ -140,6 +145,10 @@ tasks.named('processGametestResources') {
: unified ? 'fr.koka.sanctuary.gametest.UnifiedWorldGameTests'
: 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests')
if (atlasOnly || villageOnly) filter { line -> line.replaceAll(/("fabric-gametest": \["[^"]+")[^\]]*/, '$1') }
if (!focused.isEmpty()) filter { line ->
line.contains('"fabric-gametest":') ? ' "fabric-gametest": [' + focused.collect { '"fr.koka.sanctuary.gametest.' + focusClasses[it] + '"' }.join(', ') + '],' : line
}
}
}
@@ -69,9 +69,9 @@ public final class Aerial24WorldGameTests {
helper.assertTrue(StructureCensusService.aerialSites(level).isEmpty(), "Disabled aerial sites are not presented as planned");
report.put("passed", true); write(Path.of("diagnostics/aerial24/off.json"), report); helper.succeed(); return;
}
helper.assertTrue(plan.sites().size() == 4, "All four initial aerial sites have a finite saved plan");
helper.assertTrue(plan.sites().size() == (generator.initialDiameter()==1024?5:4), "Both ships and all natural floating bodies have a saved plan");
helper.assertTrue(plan.sites().stream().filter(AerialPlan24.Site::merchant).count() == 2, "Two merchant ships");
helper.assertTrue(plan.sites().stream().filter(AerialPlan24.Site::waterfall).count() == 1, "One waterfall site");
helper.assertTrue(plan.sites().stream().filter(AerialPlan24.Site::waterfall).count() == 0, "No artificial mineral-islet waterfall remains");
if (generator.initialDiameter() == 724 && (level.getSeed() == 0 || level.getSeed() == 42))
AerialRenderer24.writeDiagnostic(Path.of("diagnostics/aerial24/blocks.json"), plan.sites());
report.put("geometry", JSON.toJsonTree(plan.sites())); report.put("claim_count", plan.claimsChunks().size());
@@ -121,6 +121,9 @@ public final class Aerial24WorldGameTests {
if (!tickets[0]) {
entityChunks.forEach(pos -> level.getChunkSource().addTicketWithRadius(TicketType.DRAGON, pos, 2)); tickets[0] = true;
}
// Other simultaneous fixtures may release a shared vanilla DRAGON ticket.
// Refresh our own still-required chunks until the lifecycle inspection completes.
entityChunks.forEach(pos -> level.getChunkSource().addTicketWithRadius(TicketType.DRAGON, pos, 2));
var occupants = occupants(level, plan);
var readiness = new TreeMap<String, Object>(); boolean entitiesReady = true;
for (var pos : entityChunks) {
@@ -171,7 +174,7 @@ public final class Aerial24WorldGameTests {
report.put("native_tags", tags); report.put("full_chunks", chunks.size());
report.put("blocks_after", countSites(level, plan));
var census = StructureCensusService.aerialSites(level);
helper.assertTrue(census.size() == 4 && census.stream().allMatch(site -> site.status() == StructureCensusService.AerialStatus.GENERATED),
helper.assertTrue(census.size() == plan.sites().size() && census.stream().allMatch(site -> site.status() == StructureCensusService.AerialStatus.GENERATED),
"Four historical generated sites remain indexed after loot, mining and demolition");
report.put("census_after", census);
helper.assertTrue(equalFiles(journalBefore, files(world.resolve("data").resolve(generator.journalDirectory()))), "Aerial tests never reserve or activate an expansion");
@@ -197,7 +200,7 @@ public final class Aerial24WorldGameTests {
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 {
} else if(site.kind()==AerialPlan24.Kind.MINERAL_ISLET) {
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");
@@ -275,12 +278,12 @@ public final class Aerial24WorldGameTests {
helper.assertTrue(AerialRenderer24.loot(site).equals(chest.getLootTable()), "Aerial chest retains its unopened site-specific loot table");
for (var entry : model.entrySet()) if (!entry.getValue().isAir() && !entry.getValue().is(Blocks.WATER))
helper.assertTrue(level.getBlockState(entry.getKey()).is(entry.getValue().getBlock()), "Native FULL keeps authored material at " + site.id() + " " + entry.getKey());
for (var pos : AerialGeometry24.interiorChecks(site)) {
for (var pos : site.kind()==AerialPlan24.Kind.NATURAL_ISLET ? List.<BlockPos>of() : AerialGeometry24.interiorChecks(site)) {
helper.assertTrue(level.getBlockState(pos).getCollisionShape(level, pos).isEmpty()
&& level.getBlockState(pos.above()).getCollisionShape(level, pos.above()).isEmpty(), "Usable two-block headroom at " + site.id());
helper.assertTrue(!level.getBlockState(pos.below()).getCollisionShape(level, pos.below()).isEmpty(), "Interior stands on a real floor");
}
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET) for (int dy = 1; dy <= 3; dy++)
if (!site.merchant()) for (int dy = 1; dy <= 3; dy++)
helper.assertTrue(level.getBlockState(chestAt.above(dy)).isSolid(), "Mineral chest needs excavation through three layers");
} else helper.assertTrue(chest.getLootTable() == null && chest.isEmpty(), "Looted chest never refills on reload");
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET)
@@ -299,9 +302,14 @@ public final class Aerial24WorldGameTests {
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");
helper.assertTrue(chestCount == plan.sites().size() && waterSources == 0, "One physical chest per site and no artificial waterfall");
report.put("chests", chestCount); report.put("waterfall_sources", waterSources); report.put("blocks_before", countSites(level, plan));
report.put("vegetation", vegetation);
if(fresh) for(var site:plan.sites()) if(site.kind()==AerialPlan24.Kind.NATURAL_ISLET) {
var blocks=countSites(level,plan).get(site.id());
for(String id:List.of("coal","iron","copper","gold","lapis","redstone","diamond","emerald"))
helper.assertTrue(blocks.getOrDefault("minecraft:"+id+"_ore",0)>0,"Finite "+id+" veins in "+site.id());
}
return expected;
}
@@ -359,8 +367,12 @@ public final class Aerial24WorldGameTests {
for (int slot = 0; slot < chest.getContainerSize(); slot++) quantity += chest.getItem(slot).getCount();
helper.assertTrue(quantity > 0, "Site-specific chest produces usable vanilla loot"); loot.put(site.id(), quantity);
chest.clearContent(); chest.setChanged(); emptied.add(coordinates(at));
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET) {
var ores = models.get(site.id()).entrySet().stream().filter(entry -> ore(entry.getValue())).sorted(Map.Entry.comparingByKey(
if (!site.merchant()) {
var body = new HashMap<>(models.get(site.id()));
if(site.kind()==AerialPlan24.Kind.NATURAL_ISLET) for(var pos:BlockPos.betweenClosed(site.minX(),site.minY(),site.minZ(),site.maxX(),site.maxY(),site.maxZ())) {
var state=level.getBlockState(pos); if(ore(state)) body.put(pos.immutable(),state);
}
var ores = body.entrySet().stream().filter(entry -> ore(entry.getValue())).sorted(Map.Entry.comparingByKey(
Comparator.<BlockPos>comparingInt(BlockPos::getX).thenComparingInt(BlockPos::getY).thenComparingInt(BlockPos::getZ))).limit(16).toList();
helper.assertTrue(!ores.isEmpty(), "Each mineral island contains actual finite veins");
for (var entry : ores) { level.setBlock(entry.getKey(), Blocks.AIR.defaultBlockState(), 2); removed.add(coordinates(entry.getKey())); }
@@ -0,0 +1,62 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.nio.file.*;
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.gametest.framework.GameTestHelper;
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;
public final class Drained307GameTests {
@GameTest(maxTicks=12000)
public void nativeRuinsRemainDryAndKeepMarkers(GameTestHelper helper) throws Exception {
var level=helper.getLevel();var host=(SanctuaryChunkGenerator)level.getChunkSource().getGenerator();
if(host.rootGeneration()!=24){helper.succeed();return;}
var plans=host.dryRuins307();
if(!level.structureManager().shouldGenerateStructures()||!host.experimentalStructures()) {
helper.assertTrue(plans.isEmpty(),"Disabled structures reserve no dry ruins");helper.succeed();return;
}
var report=new LinkedHashMap<String,Object>();report.put("seed",Long.toString(level.getSeed()));report.put("diameter",host.initialDiameter());
report.put("planned",plans.size());report.put("bounds",plans.stream().map(s->s.getBoundingBox().toString()).toList());
Path out=Path.of("diagnostics/drained307",host.initialDiameter()+"-"+level.getSeed()+".json");Files.createDirectories(out.getParent());
Files.writeString(out,new GsonBuilder().setPrettyPrinting().create().toJson(report));
if(Boolean.getBoolean("sanctuary.test.village306.planOnly")) {helper.succeed();return;}
var chunks=new LinkedHashSet<net.minecraft.world.level.ChunkPos>();plans.forEach(s->s.getBoundingBox().intersectingChunks().forEach(chunks::add));
var iterator=chunks.iterator();Runnable[] step=new Runnable[1];boolean[] tickets={false};int[] wait={0};
step[0]=()->{try {
if(iterator.hasNext()) {var cp=iterator.next();level.getChunk(cp.x(),cp.z(),ChunkStatus.FULL,true);helper.runAtTickTime(helper.getTick()+1,()->step[0].run());return;}
if(!tickets[0]){chunks.forEach(cp->level.getChunkSource().addTicketWithRadius(net.minecraft.server.level.TicketType.DRAGON,cp,2));tickets[0]=true;}
if(wait[0]++<40){helper.runAtTickTime(helper.getTick()+1,()->step[0].run());return;}
var counts=new TreeMap<String,Integer>();var saved=new ArrayList<String>();int zombies=0,chests=0,corals=0;
var ctx=StructurePieceSerializationContext.fromLevel(level);
for(var planned:plans) {
var cp=planned.getChunkPos();var actual=level.getChunk(cp.x(),cp.z()).getStartForStructure(planned.getStructure());
helper.assertTrue(actual!=null&&actual.isValid(),"Dry ruin has a native start");
var tag=actual.createTag(ctx,cp);var restored=StructureStart.loadStaticStart(ctx,tag,level.getSeed());
helper.assertTrue(restored!=null&&tag.equals(restored.createTag(ctx,cp)),"Ruin floor, orientation, seed and occupant flags survive native serialization");
saved.add(tag.toString());var box=planned.getBoundingBox();
for(var at:BlockPos.betweenClosed(box.minX(),box.minY(),box.minZ(),box.maxX(),box.maxY(),box.maxZ())) {
var state=level.getBlockState(at);var id=BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString();
if(!state.isAir())counts.merge(id,1,Integer::sum);
helper.assertTrue(state.getFluidState().isEmpty(),"Drained ruin has no water or waterlogged block at "+at);
if(id.contains("dead_")&&id.contains("coral"))corals++;
if(level.getBlockEntity(at) instanceof net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity chest) {
chests++;helper.assertTrue(net.minecraft.world.level.storage.loot.BuiltInLootTables.UNDERWATER_RUIN_SMALL.equals(chest.getLootTable()),"Native ocean loot retained");
}
}
var inhabitants=level.getEntitiesOfClass(net.minecraft.world.entity.monster.zombie.Zombie.class,net.minecraft.world.phys.AABB.of(box).inflate(8));
zombies+=inhabitants.stream().filter(z->z.isPersistenceRequired()).count();
}
report.put("blocks",counts);report.put("chests",chests);report.put("dead_corals",corals);report.put("persistent_zombies",zombies);report.put("saved_starts",saved);
if(!plans.isEmpty()) {helper.assertTrue(chests>0,"Native treasure marker placed");helper.assertTrue(corals>0,"Dead coral accents present");helper.assertTrue(zombies>0,"Persistent vanilla zombie occupants present");}
report.put("passed",true);Files.writeString(out,new GsonBuilder().setPrettyPrinting().create().toJson(report));
chunks.forEach(cp->level.getChunkSource().removeTicketWithRadius(net.minecraft.server.level.TicketType.DRAGON,cp,2));helper.succeed();
}catch(Exception e){throw new IllegalStateException(e);}};
helper.runAtTickTime(helper.getTick()+1,()->step[0].run());
}
}
@@ -39,9 +39,9 @@ public final class Transit29WorldGameTests {
}
helper.assertFalse(plan.isEmpty(), "A structures-enabled Sanctuary world must retain its main traversal: " + plan.diagnostic());
var accesses = plan.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.ACCESS).toList();
helper.assertTrue(accesses.size() == 2, "The complete traversal has two independent surface accesses");
int span = Math.max(Math.abs(accesses.getFirst().centerX() - accesses.getLast().centerX()),
Math.abs(accesses.getFirst().centerZ() - accesses.getLast().centerZ()));
helper.assertTrue(List.of(2,4).contains(accesses.size()), "The complete traversal has two independent surface accesses");
int span = Math.max(Math.abs(accesses.getFirst().centerX() - accesses.get(1).centerX()),
Math.abs(accesses.getFirst().centerZ() - accesses.get(1).centerZ()));
helper.assertTrue(span >= generator.initialDiameter() / 4, "The main route spans at least a quarter of the island diameter");
report.put("span", span);
var holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(TransitStructures21.KEY);
@@ -72,7 +72,7 @@ public final class Transit29WorldGameTests {
for (int z = module.minZ() - 3; z < module.minZ(); z++) chunks.add(new ChunkPos(x >> 4, z >> 4));
}
}
helper.assertTrue(chunks.size() <= 160, "The native traversal witness stays inside its bounded footprint");
helper.assertTrue(chunks.size() <= 320, "The native traversal witness stays inside its bounded footprint");
report.put("local_starts", starts.size()); report.put("planned_footprint_chunks", chunks.size());
boolean planOnly = Boolean.getBoolean("sanctuary.test.transit29.planOnly");
report.put("plan_only", planOnly);
@@ -107,7 +107,18 @@ public final class Transit29WorldGameTests {
}
report.put("closed_wall_checks", closedWallChecks);
report.put("module_biomes", biomes);
report.put("walk", Transit21WorldGameTests.verifyAccess(helper, plan));
var walks=new ArrayList<Object>();
var remaining=new ArrayList<>(plan.modules());
while(!remaining.isEmpty()) {
var group=new ArrayList<TransitPlan21.Module>();group.add(remaining.removeFirst());
boolean grew;
do {grew=false;for(var candidate:List.copyOf(remaining)) if(group.stream().anyMatch(m->
m.minX()<=candidate.maxX()+1&&m.maxX()+1>=candidate.minX()&&m.minZ()<=candidate.maxZ()+1&&m.maxZ()+1>=candidate.minZ())) {
group.add(candidate);remaining.remove(candidate);grew=true;
}}while(grew);
walks.add(Transit21WorldGameTests.verifyAccess(helper,new TransitPlan21(group,0,"accepted29 native component")));
}
report.put("walk",walks);
var nearest = TransitStructures21.findNearest(level, HolderSet.direct(holder), net.minecraft.core.BlockPos.ZERO, 128, false);
helper.assertTrue(nearest != null && accesses.stream().anyMatch(m -> TransitPiece21.locator(m).equals(nearest.getFirst())),
"The real structure locator returns one of the walkable surface gates");
@@ -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.Caves28WorldGameTests", "fr.koka.sanctuary.gametest.Terraces29WorldGameTests", "fr.koka.sanctuary.gametest.River30WorldGameTests", "fr.koka.sanctuary.gametest.Transit29WorldGameTests", "fr.koka.sanctuary.gametest.Alpha28NativeStructuresGameTests", "fr.koka.sanctuary.gametest.Village306GameTests", "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.Terraces29WorldGameTests", "fr.koka.sanctuary.gametest.River30WorldGameTests", "fr.koka.sanctuary.gametest.Transit29WorldGameTests", "fr.koka.sanctuary.gametest.Alpha28NativeStructuresGameTests", "fr.koka.sanctuary.gametest.Village306GameTests", "fr.koka.sanctuary.gametest.Drained307GameTests", "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"],
@@ -80,6 +80,7 @@ public final class SanctuaryMod implements ModInitializer {
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v24"), SanctuaryBiomeSource.AERIAL24_CODEC);
fr.koka.sanctuary.worldgen.aerial.AerialStructures24.register();
fr.koka.sanctuary.worldgen.underground306.Village306.register();
fr.koka.sanctuary.worldgen.drained307.DryOcean307.register();
fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.register();
fr.koka.sanctuary.worldgen.transit.TransitStructures21.register();
fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.register();
@@ -138,6 +138,12 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
if (existing == null || !existing.isValid()) structures.setStartForStructure(planned.getStructure(), planned, chunk);
}
}
if (this instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator host && structures.shouldGenerateStructures() && Level.OVERWORLD.equals(dimension)) {
for (var planned : host.dryRuins307()) if (planned.getChunkPos().equals(chunk.getPos())) {
var existing = structures.getStartForStructure(planned.getStructure(), chunk);
if (existing == null || !existing.isValid()) structures.setStartForStructure(planned.getStructure(), planned, chunk);
}
}
ExpansionStructures.generate(this, registries, state, structures, chunk, templates, dimension);
}
@Override public void spawnOriginalMobs(WorldGenRegion region) {
@@ -554,6 +554,7 @@ public final class ExpansionStructures23 {
if (generator.village306().start() != null) {
var village = generator.village306().start().getChunkPos(); addPosition(positions, village.x(), village.z());
}
generator.dryRuins307().forEach(s -> addPosition(positions, s.getChunkPos().x(), s.getChunkPos().z()));
var mineral = generator.mineralSanctuary23();
if (!mineral.isEmpty()) addPosition(positions, mineral.chamber().startChunkX(), mineral.chamber().startChunkZ());
if (generator.rootGeneration() == 24) generator.aerialPlan24().sites()
@@ -21,6 +21,10 @@ public abstract class UnifiedDecorationProtectionMixin {
if (!(region.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| !fr.koka.sanctuary.expansion.ExpansionIsland.spatialGeneration(generator.rootGeneration())) return;
// Aerial pieces write in genuine void chunks, within their own immutable site and current chunk only.
if (fr.koka.sanctuary.worldgen.drained307.DryOcean307.isPlacing()) {
if (!fr.koka.sanctuary.worldgen.drained307.DryOcean307.mayWrite(pos)) callback.setReturnValue(false);
return;
}
if (fr.koka.sanctuary.worldgen.underground306.Village306.isPlacing()) {
if (!fr.koka.sanctuary.worldgen.underground306.Village306.mayWrite(pos)) callback.setReturnValue(false);
return;
@@ -48,6 +52,10 @@ public abstract class UnifiedDecorationProtectionMixin {
if (!(region.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)) return;
// Vanilla rejects writes outside its region before reading the destination chunk.
if (!region.isWithinWriteZone(pos)) return;
if (fr.koka.sanctuary.worldgen.drained307.DryOcean307.isPlacing()) {
if (!fr.koka.sanctuary.worldgen.drained307.DryOcean307.mayWrite(pos)) callback.setReturnValue(false);
return;
}
if (fr.koka.sanctuary.worldgen.underground306.Village306.isPlacing()) {
if (!fr.koka.sanctuary.worldgen.underground306.Village306.mayWrite(pos)) callback.setReturnValue(false);
return;
@@ -56,6 +64,11 @@ public abstract class UnifiedDecorationProtectionMixin {
if (!fr.koka.sanctuary.worldgen.aerial.AerialStructures24.mayWrite(pos)) callback.setReturnValue(false);
return;
}
// Native vegetation patches may call setBlock without ensureCanWrite, including
// from a neighbouring chunk after this ruin was placed. Preserve its dry footprint.
if (generator.protectsDryRuin307(pos)) {
callback.setReturnValue(false); return;
}
if ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) {
if (fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.isPlacing()) return;
var island = generator.islandAt(pos.getX(), pos.getZ());
@@ -104,6 +104,16 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
prepareHeritage(root);
prepareTransit(root);
}
public java.util.List<FloatingTerrain306.Mass> naturalMasses306(ServerLevel level) {
int players=initialDiameter()==512?5:initialDiameter()==724?10:20;
var field=level.registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.DENSITY_FUNCTION)
.getOrThrow(net.minecraft.resources.ResourceKey.create(net.minecraft.core.registries.Registries.DENSITY_FUNCTION,
SanctuaryMod.id("population_"+players))).value();
var sampler=originContext.random().getSampler(field);
if(sampler instanceof DetachedRock302 detached) sampler=detached.input();
if(!(sampler instanceof FloatingTerrain306 floating)) throw new IllegalStateException("Missing natural upper terrain sampler");
return floating.masses(net.minecraft.world.level.levelgen.densityfunction.SamplerContext.builder().enableCaches().build());
}
public net.minecraft.world.level.NoiseColumn aerialBaseColumn24(int x, int z) {
var current = originContext;
if (current == null || heightAccessor == null) throw new IllegalStateException("Aerial sampling before world attachment");
@@ -140,6 +150,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
private volatile fr.koka.sanctuary.worldgen.underground306.Village306.Plan village306 = fr.koka.sanctuary.worldgen.underground306.Village306.Plan.EMPTY;
private boolean villagePrepared306;
private boolean minesPrepared306;
private boolean dryRuinsPrepared307;
private volatile java.util.List<net.minecraft.world.level.levelgen.structure.StructureStart> dryRuins307 = java.util.List.of();
public java.util.List<net.minecraft.world.level.levelgen.structure.StructureStart> dryRuins307() { return dryRuins307; }
public boolean protectsDryRuin307(net.minecraft.core.BlockPos pos) {
return dryRuins307.stream().anyMatch(s -> s.getBoundingBox().isInside(pos));
}
private volatile java.util.List<fr.koka.sanctuary.expansion.ExpansionStructures23.Accepted> starterMines306 = java.util.List.of();
public java.util.List<fr.koka.sanctuary.expansion.ExpansionStructures23.Accepted> starterMines306() { return starterMines306; }
public fr.koka.sanctuary.worldgen.underground306.Village306.Plan village306() { return village306; }
@@ -196,6 +212,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
public fr.koka.sanctuary.worldgen.heritage.HeritagePlan19 heritagePlan19() { return heritagePlan19; }
public boolean intersectsExperimentalStructures(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
if (village306.intersects(minX,minY,minZ,maxX,maxY,maxZ)) return true;
for (var ruin : dryRuins307) if (ruin.getBoundingBox().intersects(new net.minecraft.world.level.levelgen.structure.BoundingBox(minX,minY,minZ,maxX,maxY,maxZ))) return true;
for (var mine : starterMines306) for(var piece : mine.start().getPieces())
if(piece.getBoundingBox().intersects(new net.minecraft.world.level.levelgen.structure.BoundingBox(minX,minY,minZ,maxX,maxY,maxZ))) return true;
if (generation == 24) for (var site : aerialPlan24.sites()) {
@@ -263,6 +280,10 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
minesPrepared306 = true;
starterMines306 = fr.koka.sanctuary.expansion.ExpansionStructures23.starterMines306(level,this,islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow());
}
if (!dryRuinsPrepared307 && generation == 24 && cityStructuresEnabled && experimentalStructures) {
dryRuinsPrepared307 = true;
dryRuins307 = fr.koka.sanctuary.worldgen.drained307.DryOcean307.plan(level, this);
}
}
@Override public void setIslands(List<ExpansionIsland> islands) {
// Complete and publish city geometry before workers can observe the new owner list.
@@ -372,7 +393,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()) || heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ()));
long start = System.nanoTime();
transitPlan21 = generation == 24
? fr.koka.sanctuary.worldgen.transit.TransitPlanner29.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
? fr.koka.sanctuary.worldgen.transit.TransitPlanner29.plan307(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
: generation >= 22
? fr.koka.sanctuary.worldgen.transit.TransitPlanner22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
: fr.koka.sanctuary.worldgen.transit.TransitPlanner21.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
@@ -570,6 +591,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
}
public boolean protectsCityDecoration(net.minecraft.core.BlockPos pos) {
if (village306.cavern() != null && village306.cavern().getBoundingBox().isInside(pos)) return true;
for (var ruin : dryRuins307) if (ruin.getBoundingBox().isInside(pos)) return true;
if (generation == 24 && aerialPlan24.protects(pos)) return true;
if ((generation == 23 || generation == 24) && mineralSanctuary23.protects(pos.getX(), pos.getY(), pos.getZ())) return true;
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
@@ -15,6 +15,7 @@ public final class AerialGeometry24 {
public static BlockPos chest(AerialPlan24.Site s) {
return switch (s.kind()) {
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> world(s, 7, 4, 24);
case NATURAL_ISLET -> world(s, s.width()/2, s.height()/2, s.depth()/2);
case MINERAL_ISLET -> {
int x = s.width() / 2 + 4, z = s.depth() / 2 + 2;
yield world(s, x, terrainTop(s, x, z) - 3, z);
@@ -37,6 +38,7 @@ public final class AerialGeometry24 {
public static BlockPos entry(AerialPlan24.Site s) {
return switch (s.kind()) {
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> world(s, 3, 5, 13);
case NATURAL_ISLET -> chest(s);
case MINERAL_ISLET -> world(s, s.width() / 2, terrainTop(s, s.width() / 2, s.depth() / 2) + 1, s.depth() / 2);
};
}
@@ -75,6 +75,7 @@ public final class AerialPiece24 extends StructurePiece {
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
AerialStructures24.placing(site, () -> {
if(site.kind()==AerialPlan24.Kind.NATURAL_ISLET) NaturalIslet307.enrich(level,site,clip);
AerialRenderer24.render(site, new LostCityRenderer.BlockWriter() {
private BlockPos position(int x, int y, int z) { return new BlockPos(site.minX() + x, site.minY() + y, site.minZ() + z); }
@Override public void set(int x, int y, int z, BlockState state) {
@@ -19,16 +19,19 @@ public final class AerialPlan24 {
private final List<Site> sites;
private final Set<Long> claims;
public enum Kind { MERCHANT_PROVISIONS, MERCHANT_TOOLS, MINERAL_ISLET }
public enum Kind { MERCHANT_PROVISIONS, MERCHANT_TOOLS, MINERAL_ISLET, NATURAL_ISLET }
public AerialPlan24(List<Site> sites) {
this.sites = List.copyOf(sites);
if (!sites.isEmpty() && (sites.size() != 4
|| sites.stream().filter(s -> s.kind() == Kind.MERCHANT_PROVISIONS).count() != 1
|| sites.stream().filter(s -> s.kind() == Kind.MERCHANT_TOOLS).count() != 1
|| sites.stream().filter(s -> s.kind() == Kind.MINERAL_ISLET).count() != 2
|| sites.stream().filter(Site::waterfall).count() != 1))
throw new IllegalArgumentException("An aerial plan contains all four sites and exactly one waterfall, or is disabled");
boolean legacy = sites.size()==4 && sites.stream().filter(s -> s.kind()==Kind.MINERAL_ISLET).count()==2
&& sites.stream().filter(Site::waterfall).count()==1;
boolean natural = (sites.size()==4 || sites.size()==5)
&& sites.stream().filter(s -> s.kind()==Kind.NATURAL_ISLET).count()==sites.size()-2
&& sites.stream().noneMatch(Site::waterfall);
if (!sites.isEmpty() && (!(legacy || natural)
|| sites.stream().filter(s -> s.kind()==Kind.MERCHANT_PROVISIONS).count()!=1
|| sites.stream().filter(s -> s.kind()==Kind.MERCHANT_TOOLS).count()!=1))
throw new IllegalArgumentException("Incomplete aerial sites");
var ids = new HashSet<String>();
var columns = new HashSet<Long>();
for (Site site : sites) {
@@ -55,7 +58,8 @@ public final class AerialPlan24 {
}
return false;
}
public boolean protects(BlockPos pos) { return sites.stream().anyMatch(s -> s.mayWrite(pos)); }
public boolean protects(BlockPos pos) { return sites.stream().anyMatch(s -> s.kind()==Kind.NATURAL_ISLET
? AerialGeometry24.chest(s).distManhattan(pos)<=2 : s.mayWrite(pos)); }
/** Width and depth are local dimensions; facing rotates them around the world-space bounding-box minimum. */
public record Site(String id, Kind kind, int minX, int minY, int minZ, int width, int height, int depth,
@@ -64,8 +68,8 @@ public final class AerialPlan24 {
if (id == null || !id.matches("[a-z][a-z0-9_]{0,47}") || kind == null || facing < 0 || facing > 3
|| Math.abs((long) minX) > 2048 || Math.abs((long) minZ) > 2048
|| Math.abs((long) shoreX) > 1024 || Math.abs((long) shoreZ) > 1024
|| shoreY < 1 || shoreY > 383 || minY < 32 || height < 8 || height > 35 + VEGETATION_HEADROOM || minY + height > 350
|| width < 7 || width > 64 || depth < 7 || depth > 64
|| shoreY < 1 || shoreY > 383 || minY < 32 || height < 8 || height > (kind==Kind.NATURAL_ISLET ? 110 : 35 + VEGETATION_HEADROOM) || minY + height > (kind==Kind.NATURAL_ISLET ? 430 : 350)
|| width < 7 || width > (kind==Kind.NATURAL_ISLET ? 136 : 64) || depth < 7 || depth > (kind==Kind.NATURAL_ISLET ? 136 : 64)
|| waterfall && kind != Kind.MINERAL_ISLET)
throw new IllegalArgumentException("Invalid aerial site geometry");
if (kind == Kind.MINERAL_ISLET && (width < 40 || depth < 40 || height < 20 + VEGETATION_HEADROOM)
@@ -38,10 +38,14 @@ public final class AerialPlanner24 {
@Override public boolean reserved(int x, int z, int minY, int maxY) {
return generator.intersectsExperimentalStructures(x, minY, z, x, maxY, z);
}
});
}, generator.naturalMasses306(level));
}
public static AerialPlan24 plan(long seed, int diameter, Terrain terrain) {
return plan(seed,diameter,terrain,null);
}
private static AerialPlan24 plan(long seed,int diameter,Terrain terrain,
java.util.List<fr.koka.sanctuary.worldgen.FloatingTerrain306.Mass> natural) {
if (diameter != 512 && diameter != 724 && diameter != 1024) throw new IllegalArgumentException("Unsupported aerial root size");
int radius = diameter == 724 ? 395 : diameter / 2 + 32;
var probe = new Probe(terrain, radius);
@@ -50,7 +54,7 @@ public final class AerialPlanner24 {
Kind[] kinds = {Kind.MERCHANT_PROVISIONS, Kind.MERCHANT_TOOLS, Kind.MINERAL_ISLET, Kind.MINERAL_ISLET};
String[] ids = {"merchant_provisions", "merchant_tools", "mineral_islet_water", "mineral_islet_dry"};
double initialAngle = unit(mix(seed ^ 0xA24AE21AL)) * Math.PI * 2;
for (int ordinal = 0; ordinal < kinds.length; ordinal++) {
for (int ordinal = 0; ordinal < (natural==null ? kinds.length : 2); ordinal++) {
long siteSeed = ExpansionIsland.deriveSeed(seed ^ 0xA24AE21AL, ids[ordinal]);
boolean merchant = ordinal < 2;
int width = merchant ? 11 : 40 + 4 * (int) Math.floorMod(siteSeed, 7);
@@ -153,6 +157,26 @@ public final class AerialPlanner24 {
sites.add(accepted);
accepted.claimedChunks().forEach(c -> claimed.add(c.pack()));
}
if(natural!=null) {
if(natural.size()!=(diameter==1024?3:2)) throw new IllegalStateException("Missing natural upper terrain");
for(int i=0;i<natural.size();i++) {
var m=natural.get(i);
var coast=shore(probe,Math.atan2(m.z(),m.x()),radius);
if(coast==null) throw new IllegalStateException("No shore witness for natural islet");
int rx=(int)Math.ceil(m.rx()*1.8),rz=(int)Math.ceil(m.rz()*1.8),half=(int)Math.ceil(m.depth()*1.5);
var site=new Site("natural_islet_"+i,Kind.NATURAL_ISLET,m.x()-rx,m.y()-half,m.z()-rz,
rx*2+1,half*2+1,rz*2+1,0,ExpansionIsland.deriveSeed(seed,"natural_islet_"+i),
coast.x,coast.y,coast.z,false);
// A chest replaces one existing rock block; the surrounding body must already exist.
var chest=AerialGeometry24.chest(site);
for(int dx=-1;dx<=1;dx++)for(int dz=-1;dz<=1;dz++)for(int dy=-1;dy<=2;dy++)
if(!probe.column(chest.getX()+dx,chest.getZ()+dz).getBlock(chest.getY()+dy).isSolid())
throw new IllegalStateException("Natural treasure has no stable rock cover: "+site.id());
if(site.claimedChunks().stream().anyMatch(c->claimed.contains(c.pack())))
throw new IllegalStateException("Natural treasure reservation overlaps another aerial site");
sites.add(site);site.claimedChunks().forEach(c->claimed.add(c.pack()));
}
}
var result = new AerialPlan24(sites);
System.getLogger(AerialPlanner24.class.getName()).log(System.Logger.Level.INFO,
"Aerial24 seed=" + seed + " diameter=" + diameter + " sites=" + result.sites().size()
@@ -60,7 +60,7 @@ public final class AerialRenderer24 {
return Map.copyOf(blocks);
}
public static ResourceKey<LootTable> loot(AerialPlan24.Site s) {
return ResourceKey.create(Registries.LOOT_TABLE, SanctuaryMod.id("chests/aerial24/" + s.kind().name().toLowerCase(java.util.Locale.ROOT)));
return ResourceKey.create(Registries.LOOT_TABLE, SanctuaryMod.id("chests/aerial24/" + (s.kind()==AerialPlan24.Kind.NATURAL_ISLET ? "mineral_islet" : s.kind().name().toLowerCase(java.util.Locale.ROOT))));
}
public static void render(AerialPlan24.Site s, LostCityRenderer.BlockWriter out) { render(s, out, s.bounds()); }
public static void render(AerialPlan24.Site s, LostCityRenderer.BlockWriter out, BoundingBox clip) {
@@ -74,12 +74,13 @@ public final class AerialRenderer24 {
if (clip.isInside(s.minX() + x, s.minY() + y, s.minZ() + z)) out.chest(x, y, z, table, seed);
}
};
var connected = s.kind() == AerialPlan24.Kind.MINERAL_ISLET ? null : ConnectedBlocks20.buffer(clipped);
var connected = !s.merchant() ? null : ConnectedBlocks20.buffer(clipped);
var destination = connected == null ? clipped : connected;
var b = new Builder(s, destination, connected == null ? clip : s.bounds());
switch (s.kind()) {
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> b.ship();
case MINERAL_ISLET -> b.islet();
case NATURAL_ISLET -> { /* Existing density owns the whole natural silhouette. */ }
}
var chest = AerialGeometry24.chest(s);
destination.chest(chest.getX() - s.minX(), chest.getY() - s.minY(), chest.getZ() - s.minZ(), loot(s), s.seed() ^ 0x24C4E57L);
@@ -36,10 +36,10 @@ public final class AerialStructures24 {
private static ResourceKey<Structure> key(String id) { return ResourceKey.create(Registries.STRUCTURE, SanctuaryMod.id(id)); }
public static void register() {}
public static String family(AerialPlan24.Site site) {
return switch (site.kind()) { case MINERAL_ISLET -> "mineral_islet"; default -> "merchant_ship"; };
return switch (site.kind()) { case MINERAL_ISLET, NATURAL_ISLET -> "mineral_islet"; default -> "merchant_ship"; };
}
public static ResourceKey<Structure> key(AerialPlan24.Site site) {
return switch (site.kind()) { case MINERAL_ISLET -> MINERAL_ISLET; default -> MERCHANT_SHIP; };
return switch (site.kind()) { case MINERAL_ISLET, NATURAL_ISLET -> MINERAL_ISLET; default -> MERCHANT_SHIP; };
}
public static String structureId(AerialPlan24.Site site) { return key(site).identifier().toString(); }
public static boolean isPlacing() { return PLACING.get() != null; }
@@ -0,0 +1,39 @@
package fr.koka.sanctuary.worldgen.aerial;
import net.minecraft.core.BlockPos;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
/** Finite seams replace only the existing rock. The accepted30.6 density is untouched. */
public final class NaturalIslet307 {
private NaturalIslet307() {}
public static void enrich(WorldGenLevel level,AerialPlan24.Site site,BoundingBox clip) {
for(int x=Math.max(site.minX(),clip.minX());x<=Math.min(site.maxX(),clip.maxX());x++)
for(int z=Math.max(site.minZ(),clip.minZ());z<=Math.min(site.maxZ(),clip.maxZ());z++) {
int top=-1,bottom=-1;
for(int y=Math.min(399,site.maxY());y>=Math.max(300,site.minY());y--) {
var state=level.getBlockState(new BlockPos(x,y,z));
if(state.is(BlockTags.BASE_STONE_OVERWORLD)) {if(top<0)top=y;bottom=y;}
}
if(top<0)continue;
for(int y=bottom;y<=top;y++) {
var at=new BlockPos(x,y,z);var state=level.getBlockState(at);
if(!state.is(BlockTags.BASE_STONE_OVERWORLD))continue;
var ore=ore(site.seed(),x,y,z,(top-y)/(double)Math.max(1,top-bottom));
if(ore!=null)level.setBlock(at,ore.defaultBlockState(),2);
}
}
}
static Block ore(long seed,int x,int y,int z,double depth) {
long cell=AerialGeometry24.mix(seed ^ ((long)Math.floorDiv(x,4)<<42)
^ ((long)Math.floorDiv(z,4)<<21) ^ Math.floorDiv(y,3));
int roll=(int)Math.floorMod(cell,100);
if(roll>=37)return null;
if(depth<.27)return roll<15?Blocks.COAL_ORE:roll<27?Blocks.COPPER_ORE:Blocks.IRON_ORE;
if(depth<.60)return roll<13?Blocks.IRON_ORE:roll<21?Blocks.GOLD_ORE:roll<29?Blocks.LAPIS_ORE:Blocks.COAL_ORE;
return roll<10?Blocks.IRON_ORE:roll<19?Blocks.REDSTONE_ORE:roll<27?Blocks.GOLD_ORE:roll<34?Blocks.DIAMOND_ORE:Blocks.EMERALD_ORE;
}
}
@@ -0,0 +1,71 @@
package fr.koka.sanctuary.worldgen.drained307;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.util.*;
import net.minecraft.core.*;
import net.minecraft.core.registries.*;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.*;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.levelgen.structure.*;
import net.minecraft.world.level.levelgen.structure.pieces.*;
/** Sparse dry remnants on existing lower cave floors; no new cavern or ocean density. */
public final class DryOcean307 {
public static final ResourceKey<Structure> KEY=ResourceKey.create(Registries.STRUCTURE,SanctuaryMod.id("drained_ocean_ruin"));
public static final StructureType<DryOceanStructure307> TYPE=Registry.register(BuiltInRegistries.STRUCTURE_TYPE,
SanctuaryMod.id("drained_ocean_v307"),()->DryOceanStructure307.CODEC);
public static final StructurePieceType PIECE=Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
SanctuaryMod.id("drained_ocean_piece_v307"),DryOceanPiece307::new);
private static final ThreadLocal<BoundingBox> PLACING=new ThreadLocal<>();
private DryOcean307(){}
public static void register(){}
public static boolean isPlacing(){return PLACING.get()!=null;}
public static boolean mayWrite(BlockPos pos){return PLACING.get()!=null&&PLACING.get().isInside(pos);}
static void placing(BoundingBox box,Runnable action){var old=PLACING.get();PLACING.set(box);try{action.run();}finally{if(old==null)PLACING.remove();else PLACING.set(old);}}
public static List<StructureStart> plan(ServerLevel level,SanctuaryChunkGenerator host) {
var result=new ArrayList<StructureStart>();var columns=new HashMap<Long,NoiseColumn>();var reasons=new TreeMap<String,Integer>();
var random=new Random(level.getSeed() ^ 0x3070CEA1L);
java.util.function.BiFunction<Integer,Integer,NoiseColumn> column=(x,z)->{
long key=BlockPos.asLong(x,0,z);var c=columns.get(key);
if(c==null){if(columns.size()>=8000)throw new Budget();c=host.aerialBaseColumn24(x,z);columns.put(key,c);}return c;
};
int target=host.initialDiameter()==1024?4:3;
try {
outer:for(int attempt=0;attempt<512 && result.size()<target;attempt++) {
double a=random.nextDouble()*Math.PI*2,r=host.initialDiameter()*(.10+random.nextDouble()*.27);
int x=(int)(Math.cos(a)*r),z=(int)(Math.sin(a)*r);
if(result.stream().anyMatch(s->Math.hypot(s.getBoundingBox().getCenter().getX()-x,s.getBoundingBox().getCenter().getZ()-z)<48))continue;
var center=column.apply(x,z);
for(int floor=60;floor<=174;floor++) {
if(!center.getBlock(floor-1).isSolid()||!center.getBlock(floor).isAir()||!center.getBlock(floor+6).isAir())continue;
String template="underwater_ruin/warm_"+new int[]{2,3,4,5,8}[random.nextInt(5)];
var piece=new DryOceanPiece307(level.getStructureTemplateManager(),template,new BlockPos(x,floor,z),
Rotation.values()[random.nextInt(4)],random.nextLong());
var box=piece.getBoundingBox();
if(host.intersectsExperimentalStructures(box.minX()-1,box.minY()-2,box.minZ()-1,box.maxX()+1,box.maxY()+2,box.maxZ()+1)) {
reasons.merge("reserved",1,Integer::sum);continue;
}
int support=0,total=0;boolean fits=true;
for(int px=box.minX();fits&&px<=box.maxX();px++)for(int pz=box.minZ();fits&&pz<=box.maxZ();pz++) {
var c=column.apply(px,pz);total++;
if(c.getBlock(floor-1).isSolid()&&c.getBlock(floor-2).isSolid())support++;
for(int py=floor;py<=box.maxY()+1;py++)
if(!c.getBlock(py).isAir()||host.protectsDecoration(new BlockPos(px,py,pz))) {fits=false;break;}
}
if(!fits||support*100<total*90){reasons.merge("cave-floor-or-clearance",1,Integer::sum);continue;}
var cp=new ChunkPos(x>>4,z>>4);
if(result.stream().anyMatch(s->s.getChunkPos().equals(cp)))continue;
result.add(new StructureStart(level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY).value(),
cp,0,new PiecesContainer(List.of(piece))));
continue outer;
}
}
}catch(Budget exhausted){reasons.merge("column-budget",1,Integer::sum);}
SanctuaryMod.LOGGER.info("Drained307 seed {}: {} / {} dry ruins, {} columns; {}",level.getSeed(),result.size(),target,columns.size(),reasons);
return List.copyOf(result);
}
private static final class Budget extends RuntimeException {}
}
@@ -0,0 +1,102 @@
package fr.koka.sanctuary.worldgen.drained307;
import java.nio.charset.StandardCharsets;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.Identifier;
import net.minecraft.util.RandomSource;
import net.minecraft.util.valueproviders.ConstantInt;
import net.minecraft.world.entity.EntitySpawnReason;
import net.minecraft.world.entity.EntityTypes;
import net.minecraft.world.level.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.entity.ChestBlockEntity;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.structure.*;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.levelgen.structure.templatesystem.*;
import net.minecraft.world.level.levelgen.structure.templatesystem.rule.blockentity.AppendLoot;
import net.minecraft.world.level.storage.loot.BuiltInLootTables;
/** Native ocean-ruin templates at a certified cave floor, with dry markers and native loot. */
public final class DryOceanPiece307 extends TemplateStructurePiece {
private final long seed;
private final Set<Long> occupants=new HashSet<>();
public DryOceanPiece307(StructureTemplateManager manager,String template,BlockPos at,Rotation rotation,long seed) {
super(DryOcean307.PIECE,0,manager,Identifier.withDefaultNamespace(template),template,settings(rotation),at);
this.seed=seed;
}
public DryOceanPiece307(StructurePieceSerializationContext context,CompoundTag tag) {
super(DryOcean307.PIECE,tag,context.structureTemplateManager(),ignored->settings(tag.read("Rot",Rotation.LEGACY_CODEC).orElseThrow()));
seed=tag.getLong("Seed").orElseThrow();
for(long pos:tag.getLongArray("Occupants").orElseThrow())occupants.add(pos);
if(!templateName.matches("underwater_ruin/warm_[1-8]") || occupants.size()>16)
throw new IllegalArgumentException("Invalid drained ruin");
}
private static StructurePlaceSettings settings(Rotation rotation) {
// Same warm-ruin archaeology recipe: up to five native suspicious sand blocks.
var archaeology=new CappedProcessor(new RuleProcessor(List.of(new ProcessorRule(
new BlockMatchTest(Blocks.SAND),AlwaysTrueTest.INSTANCE,PosAlwaysTrueTest.INSTANCE,
Blocks.SUSPICIOUS_SAND.defaultBlockState(),new AppendLoot(BuiltInLootTables.OCEAN_RUIN_WARM_ARCHAEOLOGY)))),ConstantInt.of(5));
return new StructurePlaceSettings().setRotation(rotation).addProcessor(new BlockRotProcessor(.8F))
.addProcessor(BlockIgnoreProcessor.STRUCTURE_AND_AIR).addProcessor(archaeology);
}
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context,CompoundTag tag) {
super.addAdditionalSaveData(context,tag);tag.store("Rot",Rotation.LEGACY_CODEC,placeSettings.getRotation());
tag.putLong("Seed",seed);tag.putLongArray("Occupants",occupants.stream().sorted().mapToLong(Long::longValue).toArray());
}
@Override protected synchronized void handleDataMarker(String marker,BlockPos pos,ServerLevelAccessor level,RandomSource random,BoundingBox clip) {
if(!clip.isInside(pos))return;
if(marker.equals("chest")) {
if(level.getBlockState(pos).is(Blocks.CHEST))return;
level.setBlock(pos,Blocks.CHEST.defaultBlockState(),2);
if(level.getBlockEntity(pos) instanceof ChestBlockEntity chest)
chest.setLootTable(BuiltInLootTables.UNDERWATER_RUIN_SMALL,seed ^ pos.asLong());
} else if(marker.equals("drowned")) {
level.setBlock(pos,Blocks.AIR.defaultBlockState(),2);
if(!occupants.add(pos.asLong()))return;
var zombie=EntityTypes.ZOMBIE.create(level.getLevel(),EntitySpawnReason.STRUCTURE);
if(zombie==null)throw new IllegalStateException("Missing ruin zombie");
zombie.snapTo(pos,0,0);zombie.finalizeSpawn(level,level.getCurrentDifficultyAt(pos),EntitySpawnReason.STRUCTURE,null);
zombie.setPersistenceRequired();zombie.setUUID(UUID.nameUUIDFromBytes(("sanctuary:drained307:"+seed+":"+pos.asLong()).getBytes(StandardCharsets.UTF_8)));
level.addFreshEntityWithPassengers(zombie);
}
}
@Override public synchronized void postProcess(WorldGenLevel level,StructureManager manager,ChunkGenerator generator,
RandomSource random,BoundingBox clip,ChunkPos chunk,BlockPos pivot) {
DryOcean307.placing(getBoundingBox(),()->{
super.postProcess(level,manager,generator,RandomSource.create(seed ^ chunk.pack()),clip,chunk,pivot);
var box=getBoundingBox();
for(int x=Math.max(box.minX(),clip.minX());x<=Math.min(box.maxX(),clip.maxX());x++)
for(int z=Math.max(box.minZ(),clip.minZ());z<=Math.min(box.maxZ(),clip.maxZ());z++) {
for(int y=box.minY();y<=box.maxY();y++) {
var pos=new BlockPos(x,y,z);var state=level.getBlockState(pos);
if(state.is(Blocks.WATER))level.setBlock(pos,Blocks.AIR.defaultBlockState(),2);
else if(state.hasProperty(BlockStateProperties.WATERLOGGED)&&state.getValue(BlockStateProperties.WATERLOGGED))
level.setBlock(pos,state.setValue(BlockStateProperties.WATERLOGGED,false),2);
}
long roll=mix(seed ^ BlockPos.asLong(x,0,z));
if(Math.floorMod(roll,7)!=0)continue;
for(int y=box.minY()+1;y<box.maxY();y++) {
var pos=new BlockPos(x,y,z);var below=level.getBlockState(pos.below());
if(level.getBlockState(pos).isAir() && below.isFaceSturdy(level,pos.below(),net.minecraft.core.Direction.UP)
&& !below.is(Blocks.CHEST)) {
Block coral=switch((int)Math.floorMod(roll>>>8,4)) {
case 0->Blocks.DEAD_BRAIN_CORAL_BLOCK;case 1->Blocks.DEAD_TUBE_CORAL;
case 2->Blocks.DEAD_FIRE_CORAL;default->Blocks.DEAD_BUBBLE_CORAL;
};
var dry=coral.defaultBlockState();
// Coral plants default to waterlogged even when already dead.
// A broken support would otherwise release a new water source here.
if(dry.hasProperty(BlockStateProperties.WATERLOGGED)) dry=dry.setValue(BlockStateProperties.WATERLOGGED,false);
if(dry.canSurvive(level,pos))level.setBlock(pos,dry,2);
break;
}
}
}
});
}
private static long mix(long n){n=(n^n>>>30)*0xbf58476d1ce4e5b9L;n=(n^n>>>27)*0x94d049bb133111ebL;return n^n>>>31;}
}
@@ -0,0 +1,11 @@
package fr.koka.sanctuary.worldgen.drained307;
import com.mojang.serialization.MapCodec;
import java.util.Optional;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureType;
public final class DryOceanStructure307 extends Structure {
public static final MapCodec<DryOceanStructure307> CODEC=simpleCodec(DryOceanStructure307::new);
public DryOceanStructure307(StructureSettings settings){super(settings);}
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context){return Optional.empty();}
@Override public StructureType<?> type(){return DryOcean307.TYPE;}
}
@@ -240,13 +240,14 @@ public final class HeritagePlanner23 {
for(int step=0;step<target/8;step++) {
RouteDraft chosen=null; List<XZ> chosenPath=null; int chosenFace=face;
var last=path.getLast();
for(int turn:new int[]{0,1,-1}) {
for(int turn:new int[]{0}) {
int direction=(face+turn+4)&3;
// Give long straight runs priority, with curves only where the grade needs them.
// A branch stays on its initial axis. An obstruction ends it instead of curling back.
var end=last.move(direction,8);int top=p.surface(end.x(),end.z());
if(top<14||top>350)continue;
if(path.stream().anyMatch(at->Math.abs(at.x()-end.x())+Math.abs(at.z()-end.z())<5))continue;
if(cells.containsKey(key(end.x(),end.z())))continue;
// Keep a real separation from existing parallel routes, outside the shared junction.
if(proposedNearRoute307(path.getFirst(), end, cells)) continue;
var proposed=new ArrayList<>(path);line(proposed,end);
var endEntry=new Entry(end.x(),top+1,end.z(),(direction+2)&3);
var historical=landmarks.stream().filter(l->!l.id().startsWith("natural_rail_")&&!l.id().startsWith("rail_branch_")).toList();
@@ -260,7 +261,7 @@ public final class HeritagePlanner23 {
var end=best.path().getLast();
var terminal=new Landmark(id,LandmarkKind.MINE,new Entry(end.x(),end.floorY()+1,end.z(),face));
landmarks.add(terminal);
if(best.path().size() >= 49 && landmarks.size() < 15 && links.size() < 19) {
if(arm.depth()==0 && best.path().size() >= 65 && landmarks.size() < 15 && links.size() < 19) {
int middle=best.path().size()/2;var at=best.path().get(middle);var after=best.path().get(middle+1);
int tangent=direction(at.x(),at.z(),after.x(),after.z());
var fork=new Landmark(id+"_fork",LandmarkKind.MINE,new Entry(at.x(),at.floorY()+1,at.z(),tangent));
@@ -279,11 +280,18 @@ public final class HeritagePlanner23 {
}
serial++;
// Forward continuation and a lateral branch share their ballast; the switch is repairable.
if(arm.depth()<2){queue.addFirst(new Arm(terminal,(face+(serial%2==0?1:3))&3,arm.depth()+1));queue.addLast(new Arm(terminal,face,arm.depth()+1));}
if(arm.depth()==0) queue.addLast(new Arm(terminal,face,1));
}
} catch(BudgetExceeded exhausted){p.reject("branched-rail-budget");}
}
private static boolean proposedNearRoute307(XZ start, XZ end, Map<Long, OwnedCell> cells) {
if(Math.abs(start.x()-end.x())+Math.abs(start.z()-end.z()) <= 16) return false;
for(int dx=-10;dx<=10;dx++) for(int dz=-10;dz<=10;dz++)
if(cells.containsKey(key(end.x()+dx,end.z()+dz))) return true;
return false;
}
private static List<Candidate> candidates(long seed, Kind kind, int cx, int cz, int diameter,
List<Site> sites, List<Landmark> landmarks, Probe probe) {
double angle = unit(mix(seed ^ 0x19ACEL)) * Math.PI * 2 + kind.ordinal() * 2.399963229728653;
@@ -64,8 +64,8 @@ public record TransitPlan21(List<Module> modules, int samples, String diagnostic
modules = List.copyOf(modules);
if (modules.size() > 64 || samples < 0 || samples > MAX_SAMPLES || diagnostic == null || diagnostic.length() > 1024)
throw new IllegalArgumentException("Invalid transit planning result");
if (!modules.isEmpty() && modules.stream().filter(m -> m.kind() == Kind.ACCESS).count() != 2)
throw new IllegalArgumentException("A through-gallery needs two surface accesses");
if (!modules.isEmpty() && !List.of(2L,4L).contains(modules.stream().filter(m -> m.kind() == Kind.ACCESS).count()))
throw new IllegalArgumentException("Each through-gallery needs two surface accesses");
for (int i = 0; i < modules.size(); i++) for (int j = i + 1; j < modules.size(); j++) {
var a = modules.get(i); var b = modules.get(j);
if (a.minX() <= b.maxX() && a.maxX() >= b.minX() && a.minZ() <= b.maxZ() && a.maxZ() >= b.minZ())
@@ -12,6 +12,27 @@ public final class TransitPlanner29 {
public static final int MAX_COLUMNS = 12_000;
private TransitPlanner29() {}
public static TransitPlan21 plan307(long seed, int cx, int cz, int diameter, Terrain terrain) {
var first=plan(seed,cx,cz,diameter,terrain);
if(first.isEmpty()) return first;
Terrain remaining=new Terrain() {
@Override public boolean owns(int x,int z) {return terrain.owns(x,z);}
@Override public boolean solid(int x,int y,int z) {return terrain.solid(x,y,z);}
@Override public int surfaceY(int x,int z) {return terrain.surfaceY(x,z);}
@Override public boolean protectedAt(int x,int y,int z) {
// Separate corridors and their entrances, with untouched rock between them.
for(var m:first.modules()) if(x>=m.minX()-24&&x<=m.maxX()+24&&z>=m.minZ()-24&&z<=m.maxZ()+24) return true;
return terrain.protectedAt(x,y,z);
}
};
var second=plan(seed ^ 0x3077A115L,cx,cz,diameter,remaining);
if(second.isEmpty() || first.modules().size()+second.modules().size()>64
|| first.samples()+second.samples()>TransitPlan21.MAX_SAMPLES)
return new TransitPlan21(first.modules(),first.samples(),first.diagnostic()+"; second-refused="+second.diagnostic());
var modules=new ArrayList<>(first.modules());modules.addAll(second.modules());
return new TransitPlan21(modules,first.samples()+second.samples(),"two-traversals; "+first.diagnostic()+"; "+second.diagnostic());
}
public static TransitPlan21 plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
if (!List.of(512, 724, 1024).contains(diameter)) throw new IllegalArgumentException("Unsupported transit diameter");
var p = new Probe(terrain); int layouts = 0; String reason = "no natural entrance pair";
@@ -238,5 +238,6 @@
"sanctuary.structures.census.aerial.status.unknown": "unknown",
"structure.sanctuary.aerial_merchant_ship": "Merchant ship",
"structure.sanctuary.aerial_mineral_islet": "Mineral islet",
"biome.sanctuary.swamp27_caves": "Underground swamp"
"biome.sanctuary.swamp27_caves": "Underground swamp",
"structure.sanctuary.drained_ocean_ruin": "Drained Ocean Ruins"
}
@@ -238,5 +238,6 @@
"sanctuary.structures.census.aerial.status.unknown": "inconnu",
"structure.sanctuary.aerial_merchant_ship": "Bateau marchand",
"structure.sanctuary.aerial_mineral_islet": "Îlot minéral",
"biome.sanctuary.swamp27_caves": "Marais souterrain"
"biome.sanctuary.swamp27_caves": "Marais souterrain",
"structure.sanctuary.drained_ocean_ruin": "Ruines océaniques asséchées"
}
@@ -0,0 +1,7 @@
{
"type": "sanctuary:drained_ocean_v307",
"biomes": "#minecraft:is_overworld",
"step": "top_layer_modification",
"terrain_adaptation": "none",
"spawn_overrides": {}
}
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "0.1.0-alpha.30.6"
version = "0.1.0-alpha.30.7"
pack-format = "packwiz:1.1.0"
[index]
+1 -1
View File
@@ -57,7 +57,7 @@ def main():
raise SystemExit("Empty atlas or duplicate seed/size")
for name in ("scripts/terrain_atlas.py", "scripts/terrain-atlas-requirements.txt",
"scripts/archive_terrain_atlas.py", "docs/terrain-atlas.md",
"scripts/structure_atlas.py", f"docs/generation-alpha{version.split('alpha.')[-1]}.md", f"docs/testing-alpha{version.split('alpha.')[-1]}.md"):
"scripts/structure_atlas.py", "scripts/finishing_atlas.py", f"docs/generation-alpha{version.split('alpha.')[-1]}.md", f"docs/testing-alpha{version.split('alpha.')[-1]}.md"):
files[name] = (ROOT / name).read_bytes()
for path in args.extra:
key = "structures/" + path.name
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""Archive native 30.7 structure witnesses over the unchanged 30.6 terrain field."""
import argparse, hashlib, json, re
from pathlib import Path
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
import numpy as np
p=argparse.ArgumentParser(description=__doc__)
for name in ('terrain','village','transit','aerial','ruins','output'): p.add_argument('--'+name,type=Path,required=True)
a=p.parse_args()
data={name:json.loads(getattr(a,name).read_text()) for name in ('terrain','village','transit','aerial','ruins')}
t=data['terrain']; seed=str(t['seed']); size=t['diameter']
for name in ('village','transit','aerial','ruins'):
d=data[name]
if str(d['seed'])!=seed or d['diameter']!=size or not d.get('passed'): raise ValueError('Unmatched/failed '+name)
cols=np.asarray(t['variants']['current306']['columns']);xs,zs=np.unique(cols[:,0]),np.unique(cols[:,1]);g=cols.reshape(len(zs),len(xs),5)
fig,ax=plt.subplots(figsize=(11,10),layout='constrained')
step=t['map_step'];extent=(xs[0]-step/2,xs[-1]+step/2,zs[-1]+step/2,zs[0]-step/2)
im=ax.imshow(np.ma.masked_where(g[:,:,2]==0,g[:,:,2]),extent=extent,cmap='terrain',vmin=0,vmax=400,interpolation='nearest')
for link in data['village']['rail_links']:
if link['kind']=='FREIGHT': ax.plot([v['x'] for v in link['points']],[v['z'] for v in link['points']],color='#962427',lw=2)
walks=data['transit'].get('walk',[])
if isinstance(walks,dict): walks=[walks]
for walk in walks:
gates=[v['gate'] for v in walk['surface_accesses']]
ax.plot([v[0] for v in gates],[v[2] for v in gates],color='#223494',lw=2,ls='--',marker='s',ms=4)
for b in data['ruins']['bounds']:
v={k:int(n) for k,n in re.findall(r'(minX|minZ|maxX|maxZ)=(-?\d+)',b)}
ax.add_patch(Rectangle((v['minX'],v['minZ']),v['maxX']-v['minX']+1,v['maxZ']-v['minZ']+1,fill=False,edgecolor='#02abb2',linewidth=2))
for site in data['aerial']['geometry']:
width,depth=(site['width'],site['depth']) if site['facing']%2==0 else (site['depth'],site['width'])
colour='#8a4196' if site['kind']=='NATURAL_ISLET' else '#41301f'
ax.add_patch(Rectangle((site['minX'],site['minZ']),width,depth,fill=False,edgecolor=colour,linewidth=1.3))
ax.scatter([site['minX']+width//2],[site['minZ']+depth//2],color=colour,s=20,marker='x')
ax.scatter([0],[0],marker='+',s=70,color='black')
for label,color,style in [('Rails prévus — lacunes réparables','#962427','-'),('Axes des traversées parcourues','#223494','--'),('Ruines asséchées — emprises','#02abb2','-'),('Trésors du terrain aérien — emprises','#8a4196','-'),('Navires restaurés — emprises','#41301f','-')]:ax.plot([],[],color=color,ls=style,label=label)
ax.legend(loc='upper left',fontsize=8);ax.set(xlabel='X (blocs)',ylabel='Z (blocs) · nord en haut',aspect='equal')
ax.set_title(f'Sanctuary alpha.30.7 · {size} · graine {seed}\nInstallations sur le relief natif inchangé de 30.6')
fig.colorbar(im,ax=ax,shrink=.7,label='Altitude du terrain Y')
fig.supxlabel('Relief échantillonné ; rails planifiés. Traversées, coffres et ruines contrôlés dans les chunks FULL.\nLes rectangles indiquent des réservations, pas la forme construite ni les frontières des biomes.',fontsize=9)
a.output.mkdir(parents=True,exist_ok=True)
for ext in ('png','pdf'):fig.savefig(a.output/f'installations-{size}-{seed}.{ext}',dpi=160)
(a.output/f'installations-{size}-{seed}-provenance.json').write_text(json.dumps({k:{'path':str(getattr(a,k)),'sha256':hashlib.sha256(getattr(a,k).read_bytes()).hexdigest()} for k in data},indent=2)+'\n')