Add alpha.17 experimental forgotten structure networks
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-09 19:14:09 +02:00
parent 1a64fbe5b9
commit 0969ed52da
37 changed files with 2203 additions and 44 deletions
+19
View File
@@ -1,5 +1,24 @@
# Changelog
## 0.1.0-alpha.17 — 2026-09-09
- Prototype de trois réseaux oubliés sur l’île initiale : mines et ateliers,
salles étranges, infrastructures abandonnées. Accès depuis la surface,
couloirs et embranchements, outils vanilla utilisables, mobilier et traces de
chantier. Placement borné, hors ville et eaux protégées, avec appuis vérifiés.
- Option FR/EN « Structures expérimentales » dans Personnaliser, enregistrée
avec la taille du nouveau monde. Les connexions entre réseaux, le réacteur,
la progression et les reliques ne sont pas encore implémentés.
- Codec `sanctuary:island_v17`, pièces natives indépendantes, journal 17.
Les mondes 16 et antérieurs conservent leurs plans et futurs chunks.
Loption ne supprime ni najoute rétroactivement des structures.
`check build assemblePack` passe. Création et réouverture natives : 15 pièces,
7 639 blocs contrôlés, 49 cibles accessibles, 12 appuis et 3 coffres conservés.
Le cas désactivé et la sauvegarde des trois tailles passent. La génération 16
conserve les empreintes natives et le journal des témoins publiés. Artefacts
vérifiés, publication en cours ; voir [le protocole](docs/testing-alpha17.md).
## 0.1.0-alpha.16 — 2026-09-09
- Ville compacte obligatoire sur la nouvelle île initiale, structures activées.
+25 -2
View File
@@ -19,6 +19,29 @@ décrits dans la vision ne sont pas encore implémentés. **Lalpha.16 est pub
piscines, des parkings profonds et des égouts ouverts. Le canal packwiz et la
même instance Prism sont à jour ; voir [Distribution](docs/packwiz.md).
## Alpha.17 — prototype des réseaux oubliés
La nouvelle génération expérimentale recherche trois petits réseaux sur l’île
initiale : mines et ateliers, salles étranges, installations techniques
abandonnées. Chaque réseau possède une entrée de surface et des salles reliées,
avec mobilier, outils vanilla et traces de travaux interrompus. Les continents
conservent leur génération urbaine sans recevoir ces réseaux supplémentaires.
Dans **Personnaliser**, loption **Structures expérimentales** accompagne les
trois tailles habituelles. Elle est enregistrée avec chaque nouveau monde ;
elle permet de comparer un monde avec les nouveaux lieux et un autre avec le
socle urbain seul. La présence des réseaux dépend des emplacements admissibles
trouvés dans un budget borné. Les connexions entre familles restent à construire.
Le prototype demande un **nouveau monde**. Les mondes 16 et antérieurs gardent
leur génération. Revenir au binaire 16 ne permet pas douvrir un monde 17 : les
mondes dessai restent séparés. Voir [le contrat alpha.17](docs/testing-alpha17.md).
Le réacteur, la progression et les reliques restent prévus pour la suite.
`check build assemblePack`, création/réouverture et option désactivée passent.
Les empreintes natives de la génération 16 restent identiques aux témoins publiés.
Les artefacts 17 sont vérifiés ; publication et mise à jour du canal en cours.
## Alpha.16 — Sanctuary habitée avant nous
Une ville compacte apparaît sur la nouvelle île de départ lorsque les structures
@@ -381,7 +404,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.16 |
| Sanctuary / pack | 0.1.0-alpha.17 |
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
@@ -410,7 +433,7 @@ décrits dans [Validation](docs/testing.md).
Résultats :
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.16.jar` : mod à installer avec
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.17.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).
+29
View File
@@ -668,6 +668,35 @@ Release et canal packwiz publiés ; même instance Prism synchronisée deux fois
avec les 767 fichiers personnels et réglages suivis conservés. Voir [le reçu](packwiz.md).
Les choix des futurs réseaux et donjons sont consignés dans la vision.
### WG-20 — Réseaux oubliés, prototype réversible — alpha.17
**Branche :** `codex/structures-experiment-alpha17`.
**Contrat :** rendre explorables les trois familles validées : mines et ateliers,
lieux étranges physiques dans l’île, infrastructures abandonnées. Les traces
privilégient des installations utilisables et des chantiers interrompus.
- Nouveau générateur 17 et option « Structures expérimentales » dans le bouton
natif Personnaliser. La taille et loption sont enregistrées avec le nouveau
monde ; aucune activation rétroactive ou suppression de structures existantes.
- Conserver les codecs, pièces, plans et futurs chunks des générations publiées,
dont 16. La ville 16 reste le socle de 17 ; les nouveaux réseaux disposent de
leurs pièces natives, emprises protégées et calculs bornés propres.
- Trois petits réseaux recherchés sur l’île initiale, avec accès de surface,
salles, galeries et mobilier. Les continents ne reçoivent pas ce prototype.
Les connexions entre réseaux seront évaluées après les premières implantations.
- Ne pas implémenter de réacteur, progression, reliques ou règles de récompenses
personnelles. Les coffres éventuels utilisent des tables vanilla différées.
- Tester terrain/supports, connexions internes, accès, réglage sauvegardé,
placement natif et réouverture. Livrer le pack et synchroniser linstance
existante selon le protocole packwiz. Donner graines et coordonnées dessai.
**État : code et artefacts vérifiés, publication en cours.** Création/réouverture,
option désactivée, compatibilité 16 et `check build assemblePack` passent.
Retour arrière par branche/artefact et option désactivée
pour un autre nouveau monde. Aucun retour binaire vers 16 dans une sauvegarde 17
nest déclaré compatible ; les mondes dessai restent séparés.
## Réserve de thèmes futurs
Ces thèmes servent à retrouver la vision, pas à demander leur implémentation immédiate. On en extrait un ticket seulement lorsqu'il devient utile au prochain incrément jouable.
+95
View File
@@ -0,0 +1,95 @@
# Alpha.17 — réseaux oubliés, prototype réversible
**État : code et artefacts vérifiés ; publication en cours.** Ticket WG-20, branche
`codex/structures-experiment-alpha17`. Minecraft 26.3-pre-2, mêmes dépendances
que lalpha.16. Cet essai porte sur les lieux et leur génération.
## Contrat
Créer un **nouveau monde Sanctuary**, puis **Personnaliser** : Petit 512,
Moyen 724 par défaut, Grand 1 024 et **Structures expérimentales** (activées par
défaut). Le choix est enregistré dans le générateur 17, avec la taille. Désactiver
loption pour un autre nouveau monde permet de comparer avec le socle urbain 16.
Ce bouton ne supprime ni ne rajoute de structures dans une sauvegarde existante.
Les anciens mondes gardent leurs codecs, rendus, pièces et futurs chunks. Le
preset 16 historique est conservé à lidentique et caché à la création. Le 17
possède son propre journal ; aucune conversion ni rétrogradation de sauvegarde
nest proposée. Le retour au binaire 16 concerne seulement les mondes 16 et
antérieurs, jamais un monde 17.
Les nouvelles structures sont recherchées sur l’île initiale uniquement, hors
ville et eaux protégées. Trois petits réseaux distincts : mines et ateliers,
salles étranges, installations techniques abandonnées. Salles et couloirs se
raccordent à des accès de surface. Les fondations reposent sur le terrain ou sur
des appuis courts certifiés. La présence dépend des emplacements trouvés dans
un budget borné. Les jonctions entre familles restent une étape suivante.
Les établis, fourneaux et outils vanilla donnent des usages aux installations ;
rails, soutènements et chantiers interrompus suggèrent les anciens occupants.
Les lieux étranges sont physiques dans l’île, distincts de la future dimension
Backrooms. Aucun réacteur, progression, relique collective, récompense personnelle
ou nouveau spawner nest ajouté par ce prototype. Les coffres éventuels utilisent
les tables de butin vanilla.
## Vérification et essai
Le monde **Moyen, graine 0**, option expérimentale activée, reçoit trois réseaux
avec cinq pièces chacun. Les contrôles natifs portent sur **15 pièces**, **7 639
blocs**, **13 chunks FULL**, **12 appuis**, **49 cibles de parcours** et **3 coffres
à butin différé**. Les empreintes des starts, le plan et un bloc dor témoin sont
conservés après réouverture dans une seconde JVM. Les trappes sont ouvertes pour
vérifier le parcours réel dun joueur, puis refermées avant la sauvegarde.
| Réseau | Entrée X / Y / Z |
| --- | --- |
| Mine et atelier | **78 / 252 / 179** |
| Salles étranges et archives | **194 / 254 / 22** |
| Locaux techniques | **116 / 253 / 157** |
Avec les commandes autorisées :
```text
/locate structure sanctuary:forgotten_networks
```
La commande donne une entrée de surface. Le prototype recherche les trois
familles, sans les garantir sur chaque graine. Les neuf types de salles et
couloirs plus laccès sont testés sur des dimensions et ouvertures variées ;
les plafonds, appuis de mobilier, orientations et chemins sont contrôlés.
Les trois smokes dédiés passent. Les huit contrôles moteur du socle passent,
avec conservation des cavités et eaux naturelles. Les trois tailles et les deux
valeurs de loption passent le stockage réel des paramètres sur disque.
La planification des réseaux prend environ une seconde après celle de la ville
sur le témoin 0 de ce Mac : 915 colonnes lues et 11 974 sondages, pour des plafonds
respectifs de 5 000 et 250 000. Cela ne mesure ni le rendu client ni le chargement
complet sur une autre machine. Rapports locaux ignorés :
`build/alpha17-evidence/ruins17-create.json` et `ruins17-reload.json`.
Logs : `build/alpha17-native-create-final.log`, `alpha17-native-reload.log`,
`alpha17-first-native-base.log`, `alpha17-planner-piece-smokes.log`.
Lessai natif avec **Structures expérimentales désactivées** passe : plan vide,
zéro sondage expérimental et booléen conservé. Rapport :
`build/alpha17-evidence/ruins17-disabled.json`, log `build/alpha17-native-disabled.log`.
La création et la réouverture actives utilisent les processus 32029 et 32123 ; le
cas désactivé utilise 32176. Aucune sauvegarde personnelle na été ouverte.
La création et la réouverture des mondes de génération **16** passent sous le
binaire 17 : les 71 pièces de la ville initiale et du continent témoin gardent
leurs empreintes natives, ainsi que le journal, les paramètres et le bloc dor
témoin de la version publiée. Rapports :
`build/alpha17-evidence/legacy16-create.json` et `legacy16-reload.json`.
Le preset 16 historique est également identique octet par octet. La comparaison
avec les reçus publiés est consignée dans `legacy16-comparison.json` :
création sous 17 dans le processus 32244, réouverture dans 32429.
`./gradlew check build assemblePack` passe : 52 tâches, huit GameTests moteur
et les trois smokes du prototype. Le JAR de production exclut les classes de
test ; les versions, les hashes du pack, le MRpack et le ZIP damorçage Prism
sont vérifiés. Journal final : `build/alpha17-final-check-build-pack.log`.
La publication et la synchronisation de linstance sont en cours. Aucun essai
Windows natif ni revue visuelle dans le client nest revendiqué ; le prochain
essai de Koka validera lambiance.
+5
View File
@@ -231,6 +231,11 @@ L'ambition est de reprendre puis d'étendre la centaine de biomes du catalogue T
spawners pourront rester exploitables après exploration sans régénérer les
donjons. Les règles précises restent à concevoir. Le réacteur et ces nouveaux
systèmes ne font pas partie de lalpha.16.
Le prototype alpha.17 commence par trois petits réseaux indépendants, avec
accès de surface, ateliers, galeries, salles étranges et locaux techniques.
Loption « Structures expérimentales » se choisit pour un nouveau monde.
Les connexions entre familles et les systèmes de butin spécifiques restent
à construire ; voir [le contrat du prototype](testing-alpha17.md).
- **Secret rooms et challenge rooms** : des accès présents dans le monde mènent à des indoors aménagés, avec des équipements, des épreuves ou des gisements de fer, d'or, d'argent ou de titane.
### Dimensions
+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.16
pack_version=0.1.0-alpha.16
mod_version=0.1.0-alpha.17
pack_version=0.1.0-alpha.17
maven_group=fr.koka.sanctuary
+17 -2
View File
@@ -40,12 +40,14 @@ tasks.named('runGameTest') {
systemProperty('sanctuary.test.unified', providers.gradleProperty('sanctuaryUnifiedTests').getOrElse('true'))
systemProperty('sanctuary.test.diameter', providers.gradleProperty('sanctuaryTestDiameter').getOrElse('724'))
systemProperty('sanctuary.test.generation', providers.gradleProperty('sanctuaryTestGeneration').getOrElse(
providers.gradleProperty('sanctuaryLostCityTests').map { it.toBoolean() }.getOrElse(false) ? '14' : providers.gradleProperty('sanctuaryLostCity15Tests').map { it.toBoolean() }.getOrElse(false) ? '15' : '16'))
providers.gradleProperty('sanctuaryLostCityTests').map { it.toBoolean() }.getOrElse(false) ? '14' : providers.gradleProperty('sanctuaryLostCity15Tests').map { it.toBoolean() }.getOrElse(false) ? '15' : providers.gradleProperty('sanctuaryLostCity16Tests').map { it.toBoolean() }.getOrElse(false) ? '16' : '17'))
systemProperty('sanctuary.test.expansion', providers.gradleProperty('sanctuaryExpansionTests').getOrElse('false'))
systemProperty('sanctuary.test.expansionReload', providers.gradleProperty('sanctuaryExpansionReload').getOrElse('false'))
systemProperty('sanctuary.test.lostCity', providers.gradleProperty('sanctuaryLostCityTests').getOrElse('false'))
systemProperty('sanctuary.test.lostCity15', providers.gradleProperty('sanctuaryLostCity15Tests').getOrElse('false'))
systemProperty('sanctuary.test.lostCity16', providers.gradleProperty('sanctuaryLostCity16Tests').getOrElse('false'))
systemProperty('sanctuary.test.ruins17', providers.gradleProperty('sanctuaryRuins17Tests').getOrElse('false'))
systemProperty('sanctuary.test.experimentalStructures', providers.gradleProperty('sanctuaryExperimentalStructures').getOrElse('true'))
systemProperty('sanctuary.test.cityMatrix', providers.gradleProperty('sanctuaryCityMatrix').getOrElse('false'))
systemProperty('sanctuary.test.cityMatrixOnly', providers.gradleProperty('sanctuaryCityMatrixOnly').getOrElse('false'))
systemProperty('sanctuary.test.voidReuse', providers.gradleProperty('sanctuaryVoidReuseTests').getOrElse('false'))
@@ -65,10 +67,12 @@ tasks.named('processGametestResources') {
inputs.property('lostCity15', lostCity15)
def lostCity16 = providers.gradleProperty('sanctuaryLostCity16Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('lostCity16', lostCity16)
def ruins17 = providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('ruins17', ruins17)
def voidReuse = providers.gradleProperty('sanctuaryVoidReuseTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('voidReuse', voidReuse)
filesMatching('fabric.mod.json') {
expand(gametest_entrypoint: voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark
expand(gametest_entrypoint: ruins17 ? 'fr.koka.sanctuary.gametest.Ruins17WorldGameTests' : voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark
? 'fr.koka.sanctuary.gametest.WorldgenPerformanceGameTests'
: unified ? 'fr.koka.sanctuary.gametest.UnifiedWorldGameTests'
: 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests')
@@ -400,3 +404,14 @@ tasks.register('lostCityFoundation16Smoke', JavaExec) {
mainClass = 'fr.koka.sanctuary.worldgen.city.LostCityFoundation16Smoke'
}
tasks.named('check') { dependsOn('lostCityFoundation16Smoke') }
// The experimental pieces and algorithms are additive; historical city suites remain unchanged.
['Planner', 'Renderer', 'Piece'].each { component ->
def smoke = tasks.register("ruins17${component}Smoke", JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = "fr.koka.sanctuary.worldgen.ruins.Ruins${component}17Smoke"
}
tasks.named('check') { dependsOn(smoke) }
}
@@ -0,0 +1,351 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mojang.serialization.JsonOps;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import fr.koka.sanctuary.worldgen.ruins.RuinsPiece17;
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17;
import fr.koka.sanctuary.worldgen.ruins.RuinsRenderer17;
import fr.koka.sanctuary.worldgen.ruins.RuinsStructures17;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Predicate;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderSet;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.LevelChunk;
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;
import net.minecraft.world.level.storage.LevelResource;
import net.minecraft.world.level.storage.loot.LootTable;
import net.minecraft.world.phys.AABB;
/** Opt-in native proof confined to the new generation17 starter island in a disposable development world. */
public final class Ruins17WorldGameTests {
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
@GameTest(maxTicks = 12000)
public void forgottenNetworksPersistNatively(GameTestHelper helper) {
helper.assertTrue(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator,
"The forgotten-networks proof requires a Sanctuary generator");
var generator = (SanctuaryChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
helper.assertTrue(generator.rootGeneration() == 17, "Use a fresh generation17 test world; do not convert old saves");
new Scenario(helper, generator, Boolean.getBoolean("sanctuary.test.expansionReload")).advance();
}
private static final class Scenario {
final GameTestHelper helper;
final ServerLevel level;
final SanctuaryChunkGenerator generator;
final RuinsPlan17 plan;
final boolean reload;
final Map<String, Object> report = new LinkedHashMap<>();
final List<ChunkPos> chunks = new ArrayList<>();
final List<StructureStart> starts = new ArrayList<>();
JsonObject previous;
BlockPos gold;
int nextChunk;
boolean prepared;
Scenario(GameTestHelper helper, SanctuaryChunkGenerator generator, boolean reload) {
this.helper = helper; this.level = helper.getLevel(); this.generator = generator; this.reload = reload;
this.plan = generator.ruinsPlan17();
report.put("seed", level.getSeed()); report.put("size", generator.initialDiameter());
report.put("process_id", ProcessHandle.current().pid()); report.put("world_directory", world(level).toString());
report.put("experimental_structures", generator.experimentalStructures());
report.put("structures_enabled", level.structureManager().shouldGenerateStructures());
report.put("scope", "Native starts, references, room blocks, player-sized routes and unopened loot on the starter island only. "
+ "No expansion is created and no personal save is opened. A single outer-wall gold edit in this disposable world "
+ "is saved and checked in a second JVM. Open hatches are restored after traversal; no free-fluid simulation is claimed.");
}
void advance() {
try {
if (!prepared) {
prepare(); prepared = true;
if (plan.isEmpty()) { finishDisabled(); return; }
}
if (nextChunk < chunks.size()) {
var pos = chunks.get(nextChunk++);
var loaded = level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true);
helper.assertTrue(loaded instanceof LevelChunk, "Every room footprint must reach native FULL: " + pos);
if (nextChunk % 10 == 0) SanctuaryMod.LOGGER.info("Forgotten networks17: loaded {}/{} footprint chunks", nextChunk, chunks.size());
helper.runAtTickTime(helper.getTick() + 1, this::advance); return;
}
verifyPlaced();
report.put("passed", true);
if (!reload) {
helper.assertTrue(gold != null && level.getBlockState(gold).isCollisionShapeFullBlock(level, gold),
"Gameplay witness must replace a solid outer wall away from every doorway");
level.setBlock(gold, Blocks.GOLD_BLOCK.defaultBlockState(), 3);
report.put("gold_witness", coords(gold));
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush native structures, deferred loot and the gold edit");
report.put("world_flushed_before_shutdown", true);
} else {
helper.assertTrue(level.getBlockState(gold).is(Blocks.GOLD_BLOCK), "Reload replaced the existing wall edit");
report.put("gold_witness", coords(gold)); report.put("gold_witness_preserved", true);
}
write(level, report, reload); helper.succeed();
} catch (Exception failure) {
report.put("passed", false); report.put("failure", failure.toString());
try { write(level, report, reload); } catch (IOException ignored) { }
throw new IllegalStateException("Forgotten networks17 native " + (reload ? "reload" : "creation") + " failed", failure);
}
}
void prepare() throws IOException {
var ops = RegistryOps.create(JsonOps.INSTANCE, level.registryAccess());
var encoded = ChunkGenerator.CODEC.encodeStart(ops, generator).getOrThrow();
var decoded = ChunkGenerator.CODEC.parse(ops, encoded).getOrThrow();
helper.assertTrue(decoded instanceof SanctuaryChunkGenerator copy && copy.rootGeneration() == 17
&& copy.experimentalStructures() == generator.experimentalStructures(), "The generation17 codec must preserve its experimental switch");
report.put("generator_codec", encoded); report.put("plan_sha256", planHash(plan)); report.put("planning_samples", plan.samples());
helper.assertTrue(plan.samples() <= RuinsPlan17.MAX_SAMPLES, "Forgotten-network planning exceeded its budget");
if (reload) {
previous = JsonParser.parseString(Files.readString(proof(level))).getAsJsonObject();
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("world_flushed_before_shutdown").getAsBoolean(),
"Reload requires the successful, flushed creation proof");
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload must run in a separate JVM");
helper.assertTrue(previous.get("seed").getAsLong() == level.getSeed()
&& previous.get("size").getAsInt() == generator.initialDiameter()
&& previous.get("world_directory").getAsString().equals(world(level).toString()), "Reload used another world, seed or size");
helper.assertTrue(previous.get("generator_codec").equals(encoded) && previous.get("plan_sha256").getAsString().equals(planHash(plan)),
"Saved switch or planned generation17 room identities changed on reload");
if (previous.has("gold_witness")) gold = readPos(previous.getAsJsonArray("gold_witness"));
}
boolean enabled = generator.experimentalStructures() && level.structureManager().shouldGenerateStructures();
if (!enabled) {
helper.assertTrue(plan.isEmpty(), "Experimental=false or generateStructures=false must publish no forgotten network");
return;
}
helper.assertTrue(!plan.isEmpty(), "Enabled reference island has no experimental network");
var families = EnumSet.noneOf(RuinsPlan17.Family.class);
plan.networks().forEach(n -> families.add(n.family()));
report.put("families", families.stream().map(Enum::name).toList()); report.put("networks", plan.networks().size());
if (level.getSeed() == 0) helper.assertTrue(families.equals(EnumSet.allOf(RuinsPlan17.Family.class)),
"Seed0 reference must demonstrate all three experimental families; found " + families);
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(RuinsStructures17.KEY);
var origins = new HashSet<Long>(); var uniqueChunks = new HashSet<ChunkPos>();
for (var network : plan.networks()) {
long origin = new ChunkPos(network.startChunkX(), network.startChunkZ()).pack();
if (!origins.add(origin)) continue;
var chunk = level.getChunk(network.startChunkX(), network.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
var start = chunk.getStartForStructure(structure.value());
helper.assertTrue(start != null && start.isValid(), "Accepted experimental network has no native start: " + network.id());
starts.add(start);
for (var piece : start.getPieces()) {
helper.assertTrue(piece instanceof RuinsPiece17, "Forgotten network loaded a foreign or historical piece type");
piece.getBoundingBox().intersectingChunks().forEach(uniqueChunks::add);
}
}
helper.assertTrue(uniqueChunks.size() <= 225, "The bounded root-only proof unexpectedly covers more than225 chunks");
chunks.addAll(uniqueChunks.stream().sorted(java.util.Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList());
}
void finishDisabled() throws IOException {
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(RuinsStructures17.KEY);
var chunk = level.getChunk(0, 0, ChunkStatus.STRUCTURE_STARTS, true);
var start = chunk.getStartForStructure(structure.value());
helper.assertTrue(start == null || !start.isValid(), "Disabled experimental generation emitted a native start");
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
helper.assertTrue(located == null, "Disabled experimental structures must not be located or prepared");
report.put("empty_plan_verified", true); report.put("passed", true);
if (!reload) {
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save the disabled experiment setting");
report.put("world_flushed_before_shutdown", true);
}
write(level, report, reload); helper.succeed();
}
void verifyPlaced() {
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(RuinsStructures17.KEY);
var context = StructurePieceSerializationContext.fromLevel(level);
var sites = new ArrayList<Map<String, Object>>();
int blocks = 0, supports = 0, chests = 0, pieces = 0;
for (var start : starts) {
var origin = start.getChunkPos(); var saved = start.createTag(context, origin);
var copy = StructureStart.loadStaticStart(context, saved, level.getSeed());
helper.assertTrue(copy != null && copy.isValid() && copy.createTag(context, origin).equals(saved), "The native start changed on an NBT roundtrip");
var expectedRooms = plan.networks().stream().filter(n -> n.startChunkX() == origin.x() && n.startChunkZ() == origin.z())
.flatMap(n -> n.rooms().stream()).toList();
var actualRooms = start.getPieces().stream().map(RuinsPiece17.class::cast).map(RuinsPiece17::room).toList();
helper.assertTrue(actualRooms.size() == expectedRooms.size() && new HashSet<>(actualRooms).equals(new HashSet<>(expectedRooms)),
"Native start did not persist every planned room exactly once");
String nativeHash = hash(saved.toString());
if (reload) {
JsonObject prior = null;
for (var value : previous.getAsJsonArray("starts")) {
var site = value.getAsJsonObject(); var at = site.getAsJsonArray("start_chunk");
if (at.get(0).getAsInt() == origin.x() && at.get(1).getAsInt() == origin.z()) prior = site;
}
helper.assertTrue(prior != null && prior.get("native_start_sha256").getAsString().equals(nativeHash),
"Reload altered native room parameters, supports or reference count");
}
var loot = new ArrayList<Map<String, Object>>();
for (var piece : start.getPieces()) {
var roomPiece = (RuinsPiece17) piece; var room = roomPiece.room(); pieces++;
piece.getBoundingBox().intersectingChunks().forEach(at -> {
var full = level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true);
helper.assertTrue(full.getReferencesForStructure(structure.value()).contains(origin.pack()), "A saved FULL chunk lost the native structure reference");
});
var roomTag = roomPiece.createTag(context);
var roundtrip = new RuinsPiece17(context, roomTag);
helper.assertTrue(roundtrip.room().equals(room) && roundtrip.createTag(context).equals(roomTag), "The native room failed its exact saved-field roundtrip");
var corrupt = roomTag.copy(); var payload = corrupt.getCompound("SanctuaryRoom").orElseThrow(); payload.putInt("Schema", 99);
boolean rejected = false;
try { new RuinsPiece17(context, corrupt); } catch (IllegalArgumentException expected) { rejected = true; }
helper.assertTrue(rejected, "An unknown generation17 piece schema must be refused");
var expected = rendered(room); blocks += expected.blocks().size();
for (var entry : expected.blocks().entrySet()) {
var at = entry.getKey(); var wanted = entry.getValue();
helper.assertTrue(RuinsPiece17.bounds(room).isInside(at), "Renderer escaped its saved room/support bounds");
if (reload && at.equals(gold)) continue;
helper.assertTrue(level.getBlockState(at).is(wanted.getBlock()),
"Native clipping or later decoration changed " + room.kind() + " at " + at + ": expected " + wanted + " got " + level.getBlockState(at));
helper.assertTrue(!level.getBlockState(at).is(Blocks.SPAWNER), "Experimental architecture must not create a spawner");
}
for (var support : room.supports()) {
supports++;
var below = new BlockPos(support.x(), support.minY() - 1, support.z());
helper.assertTrue(level.getBlockState(below).isSolid() && level.getFluidState(below).isEmpty(), "Room footing has lost its native rock anchor");
}
for (var chest : expected.chests()) {
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Deferred native chest block entity is missing");
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
helper.assertTrue(chest.table().equals(entity.getLootTable()) && entity.getLootTableSeed() == chest.seed(),
"Native chest loot must remain deferred with its exact table and seed");
loot.add(Map.of("position", coords(chest.position()), "table", chest.table().identifier().toString(), "seed", chest.seed())); chests++;
}
if (!reload && gold == null && room.kind() != RuinsPlan17.Kind.ACCESS && room.kind() != RuinsPlan17.Kind.CORRIDOR)
gold = new BlockPos(room.minX(), room.floorY() + 1, room.minZ() + 1);
}
sites.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", nativeHash,
"pieces", start.getPieces().size(), "unopened_chests", loot));
}
var traversal = new ArrayList<Map<String, Object>>();
for (var network : plan.networks()) traversal.add(verifyAccess(helper, network));
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
helper.assertTrue(located != null && located.getSecond().is(RuinsStructures17.KEY), "Native locate cannot find a saved forgotten network");
helper.assertTrue(plan.networks().stream().flatMap(n -> n.rooms().stream()).filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS)
.anyMatch(r -> RuinsPiece17.locator(r).equals(located.getFirst())), "Locate must lead to a surface entrance rather than an underground floor");
report.put("starts", sites); report.put("native_pieces", pieces); report.put("native_blocks_checked", blocks);
report.put("full_chunks_checked", chunks.size()); report.put("rock_supports_checked", supports);
report.put("unopened_deferred_chests", chests); report.put("native_routes", traversal); report.put("locate", coords(located.getFirst()));
}
}
private record Chest(BlockPos position, ResourceKey<LootTable> table, long seed) {}
private record Rendered(Map<BlockPos, BlockState> blocks, List<Chest> chests) {}
private static Rendered rendered(RuinsPlan17.Room room) {
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
RuinsRenderer17.render(room, new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(at(room, x, y, z), state); }
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
var position = at(room, x, y, z); blocks.put(position, Blocks.CHEST.defaultBlockState()); chests.add(new Chest(position, table, RandomSource.create(seed).nextLong()));
}
});
return new Rendered(blocks, chests);
}
private static Map<String, Object> verifyAccess(GameTestHelper helper, RuinsPlan17.Network network) {
var level = helper.getLevel(); var access = network.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS).findFirst().orElseThrow();
var hatches = new HashMap<BlockPos, BlockState>();
rendered(access).blocks().forEach((at, state) -> { if (state.is(Blocks.DARK_OAK_TRAPDOOR)) hatches.put(at, level.getBlockState(at)); });
helper.assertTrue(hatches.size() == 1, "Each network must have one usable wooden surface hatch");
hatches.forEach((at, state) -> {
helper.assertTrue(state.is(Blocks.DARK_OAK_TRAPDOOR) && state.canSurvive(level, at), "Surface hatch is missing its native support");
level.setBlock(at, state.setValue(BlockStateProperties.OPEN, true), 2);
});
try {
Predicate<BlockPos> inside = pos -> network.rooms().stream().anyMatch(r -> r.contains(pos.getX(), pos.getY(), pos.getZ()));
var start = RuinsPiece17.locator(access); var reached = walk(helper, start, inside);
int targets = 0;
for (var room : network.rooms()) for (var target : RuinsRenderer17.targets(room)) {
var point = at(room, target.x(), target.y(), target.z());
helper.assertTrue(reached.contains(point), "Native network " + network.id() + " cannot reach " + room.kind() + "/" + target.name() + " at " + point);
targets++;
}
for (var hatch : hatches.keySet()) helper.assertTrue(reached.contains(hatch), "Surface approach cannot enter its ladder shaft");
return Map.of("id", network.id(), "family", network.family().name(), "rooms", network.rooms().size(),
"entrance", coords(start), "walkable_targets", targets, "standing_cells", reached.size());
} finally { hatches.forEach((at, state) -> level.setBlock(at, state, 2)); }
}
private static Set<BlockPos> walk(GameTestHelper helper, BlockPos start, Predicate<BlockPos> inside) {
var level = helper.getLevel(); helper.assertTrue(inside.test(start) && standable(level, start), "Blocked native surface approach: " + start);
var seen = new HashSet<BlockPos>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
while (!queue.isEmpty()) {
var pos = queue.removeFirst();
for (var direction : HORIZONTAL) for (int dy = -1; dy <= 1; dy++) {
var next = pos.relative(direction).offset(0, dy, 0);
if (!inside.test(next) || dy > 0 && !clearBody(level, pos.above())) continue;
if (standable(level, next) && seen.add(next)) queue.add(next);
}
for (int dy : new int[] {-1, 1}) {
var next = pos.offset(0, dy, 0); if (!inside.test(next)) continue;
if ((level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(next).is(Blocks.LADDER)
|| level.getBlockState(pos.below()).is(Blocks.LADDER)) && standable(level, next) && seen.add(next)) queue.add(next);
}
helper.assertTrue(seen.size() <= 50_000, "Native traversal exceeded its room bounds");
}
return seen;
}
private static boolean standable(ServerLevel level, BlockPos pos) {
if (!clearBody(level, pos)) return false;
if (level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(pos.below()).is(Blocks.LADDER)) return true;
var below = pos.below();
return level.getBlockState(below).getCollisionShape(level, below).toAabbs().stream()
.anyMatch(b -> b.maxY >= .5 && b.minX <= .5 && b.maxX >= .5 && b.minZ <= .5 && b.maxZ >= .5);
}
private static boolean clearBody(ServerLevel level, BlockPos pos) {
var body = new AABB(pos.getX() + .2, pos.getY(), pos.getZ() + .2, pos.getX() + .8, pos.getY() + 1.8, pos.getZ() + .8);
for (var at : new BlockPos[] {pos, pos.above()}) {
if (!level.getFluidState(at).isEmpty()) return false;
for (var shape : level.getBlockState(at).getCollisionShape(level, at).toAabbs())
if (shape.move(at.getX(), at.getY(), at.getZ()).intersects(body)) return false;
}
return true;
}
private static BlockPos at(RuinsPlan17.Room room, int x, int y, int z) { return new BlockPos(room.minX() + x, room.minY() + y, room.minZ() + z); }
private static List<Integer> coords(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
private static BlockPos readPos(com.google.gson.JsonArray value) { return new BlockPos(value.get(0).getAsInt(), value.get(1).getAsInt(), value.get(2).getAsInt()); }
private static Path world(ServerLevel level) { return level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize(); }
private static Path proof(ServerLevel level) { return world(level).resolve("ruins17-create-proof.json"); }
private static String planHash(RuinsPlan17 plan) { return hash(plan.networks().toString()); }
private static void write(ServerLevel level, Map<String, Object> report, boolean reload) throws IOException {
var path = Path.of("diagnostics", "ruins17-" + (reload ? "reload" : "create") + ".json"); Files.createDirectories(path.getParent());
var value = new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n";
Files.writeString(path, value);
if (!reload) Files.writeString(proof(level), value);
}
private static String hash(String value) {
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8))); }
catch (NoSuchAlgorithmException impossible) { throw new AssertionError(impossible); }
}
}
@@ -42,8 +42,11 @@ final class UnifiedWorldSettingsDiagnostics {
var historical15 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v15"))).value().createWorldDimensions();
helper.assertTrue(historical15.overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 15 && old.source().generation() == 15,
"Historical alpha.15 keeps its own biome codec and city generator");
var historical16 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v16"))).value().createWorldDimensions();
helper.assertTrue(historical16.overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 16
&& old.source().generation() == 16 && !old.experimentalStructures(), "Historical16 never gains experimental structures");
var generator = (SanctuaryChunkGenerator) standard.overworld();
helper.assertTrue(generator.rootGeneration() == 16 && generator.source().generation() == 16, "The public preset uses generation16");
helper.assertTrue(generator.rootGeneration() == 17 && generator.source().generation() == 17 && generator.experimentalStructures(), "The public preset uses generation17 with its new-world option");
helper.assertTrue(generator.initialDiameter() == 724, "The public preset defaults to Medium / 724 blocks");
for (var tag : List.of(WorldPresetTags.NORMAL, WorldPresetTags.EXTENDED)) {
var visible = presets.getOrThrow(tag).stream().map(holder -> holder.getRegisteredName())
@@ -53,8 +56,13 @@ final class UnifiedWorldSettingsDiagnostics {
}
var ops = RegistryOps.create(JsonOps.INSTANCE, access);
var activeCodec = generator.rootGeneration() == 16 ? SanctuaryChunkGenerator.CITY16_CODEC : generator.rootGeneration() == 15 ? SanctuaryChunkGenerator.CITY15_CODEC : generator.rootGeneration() == 14 ? SanctuaryChunkGenerator.CITY_CODEC : SanctuaryChunkGenerator.CODEC;
var activeCodec = generator.rootGeneration() == 17 ? SanctuaryChunkGenerator.RUINS17_CODEC : generator.rootGeneration() == 16 ? SanctuaryChunkGenerator.CITY16_CODEC : generator.rootGeneration() == 15 ? SanctuaryChunkGenerator.CITY15_CODEC : generator.rootGeneration() == 14 ? SanctuaryChunkGenerator.CITY_CODEC : SanctuaryChunkGenerator.CODEC;
var serialized = activeCodec.codec().encodeStart(ops, generator).getOrThrow().getAsJsonObject();
serialized.remove("experimental_structures");
helper.assertTrue(activeCodec.codec().parse(ops, serialized).getOrThrow().experimentalStructures(),
"Generation17 omitted experimental option defaults to enabled");
helper.assertTrue(!((SanctuaryChunkGenerator) historical16.overworld()).withExperimentalStructures(true).experimentalStructures(),
"The new-world switch cannot activate experimental generation in a historical16 generator");
serialized.remove("initial_diameter");
helper.assertTrue(activeCodec.codec().parse(ops, serialized).getOrThrow().initialDiameter() == 724,
"An omitted new-generation size defaults to Medium");
@@ -67,8 +75,8 @@ final class UnifiedWorldSettingsDiagnostics {
Files.createDirectories(directory);
var storage = LevelStorageSource.createDefault(directory);
List<Map<String, Object>> results = new ArrayList<>();
for (int diameter : new int[]{512, 724, 1024}) {
var chosen = generator.withInitialDiameter(diameter);
for (int diameter : new int[]{512, 724, 1024}) for (boolean experimental : new boolean[]{false, true}) {
var chosen = generator.withInitialDiameter(diameter).withExperimentalStructures(experimental);
helper.assertTrue(generator.initialDiameter() == 724 && chosen.source() != generator.source(),
"Customizing a new world must copy the generator and its mutable biome source");
var dimensions = standard.replaceOverworldGenerator(access, chosen);
@@ -77,17 +85,17 @@ final class UnifiedWorldSettingsDiagnostics {
"Customizing the island preserves the other dimensions");
}
var original = new WorldGenSettings(new WorldOptions(helper.getLevel().getSeed(), false, true), dimensions);
String name = "diameter-" + diameter;
String name = "diameter-" + diameter + "-experimental-" + experimental;
try (var save = storage.createAccess(name)) {
LevelStorageSource.writeWorldGenSettings(access, storage.getLevelPath(name), original);
var restored = LevelStorageSource.readExistingSavedData(save, access, WorldGenSettings.TYPE).getOrThrow();
helper.assertTrue(restored.dimensions().overworld() instanceof SanctuaryChunkGenerator reloaded
&& reloaded.initialDiameter() == diameter && reloaded.rootGeneration() == generator.rootGeneration(),
&& reloaded.initialDiameter() == diameter && reloaded.rootGeneration() == generator.rootGeneration() && reloaded.experimentalStructures() == experimental,
"The selected diameter must survive real world-settings storage");
helper.assertTrue(WorldGenSettings.CODEC.encodeStart(ops, restored).getOrThrow()
.equals(WorldGenSettings.CODEC.encodeStart(ops, original).getOrThrow()),
"Seed, generation options and all dimensions survive the disk round trip");
results.add(Map.of("diameter", diameter, "disk_round_trip", true));
results.add(Map.of("diameter", diameter, "experimental_structures", experimental, "disk_round_trip", true));
}
}
Files.writeString(directory.resolve("round-trip.json"), new GsonBuilder().setPrettyPrinting().create().toJson(Map.of(
@@ -21,7 +21,7 @@ public abstract class SanctuaryGameTestServerMixin {
// generation test's deadline before its IO/workers can complete. This is test-only pacing.
@Inject(method = "waitUntilNextTick", at = @At("RETURN"))
private void sanctuary$paceExpansionWorkers(CallbackInfo ci) {
if (Boolean.getBoolean("sanctuary.test.expansion") || Boolean.getBoolean("sanctuary.test.lostCity")
if (Boolean.getBoolean("sanctuary.test.ruins17") || Boolean.getBoolean("sanctuary.test.expansion") || Boolean.getBoolean("sanctuary.test.lostCity")
|| Boolean.getBoolean("sanctuary.test.lostCity16") || Boolean.getBoolean("sanctuary.test.lostCity15") || Boolean.getBoolean("sanctuary.test.voidReuse")) {
long until = System.nanoTime() + 20_000_000L;
long remaining;
@@ -46,9 +46,9 @@ public abstract class SanctuaryGameTestServerMixin {
"Lnet/minecraft/world/level/levelgen/presets/WorldPresets;FLAT_ALL_DIMENSIONS:Lnet/minecraft/resources/ResourceKey;"))
private static ResourceKey<WorldPreset> sanctuary$selectActualPreset() {
if (Boolean.getBoolean("sanctuary.test.unified")) {
int generation = Integer.getInteger("sanctuary.test.generation", 16);
if (generation != 13 && generation != 14 && generation != 15 && generation != 16) throw new IllegalArgumentException("Unsupported test generation");
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 16 ? "sanctuary" : "sanctuary_v" + generation));
int generation = Integer.getInteger("sanctuary.test.generation", 17);
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17) throw new IllegalArgumentException("Unsupported test generation");
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 17 ? "sanctuary" : "sanctuary_v" + generation));
}
if (Boolean.getBoolean("sanctuary.test.expansion")) return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("expansion_lab"));
int players = Integer.getInteger("sanctuary.test.players", 10);
@@ -70,7 +70,8 @@ public abstract class SanctuaryGameTestServerMixin {
if (Boolean.getBoolean("sanctuary.test.unified")) {
var generator = (fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator) dimensions.overworld();
return dimensions.replaceOverworldGenerator(context.datapackWorldRegistries(),
generator.withInitialDiameter(Integer.getInteger("sanctuary.test.diameter", 724)));
generator.withInitialDiameter(Integer.getInteger("sanctuary.test.diameter", 724))
.withExperimentalStructures(Boolean.parseBoolean(System.getProperty("sanctuary.test.experimentalStructures", "true"))));
}
return dimensions;
}
@@ -57,7 +57,10 @@ public final class SanctuaryMod implements ModInitializer {
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v15"), SanctuaryBiomeSource.CITY15_CODEC);
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v16"), SanctuaryChunkGenerator.CITY16_CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v16"), SanctuaryBiomeSource.CITY16_CODEC);
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v17"), SanctuaryChunkGenerator.RUINS17_CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v17"), SanctuaryBiomeSource.RUINS17_CODEC);
fr.koka.sanctuary.worldgen.city.LostCityStructures.register();
fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.register();
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("population_island"), PopulationIslandDensity.CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("population_island"), PopulationIslandBiomeSource.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("population_groves"), PopulationGrovesFeature.CODEC);
@@ -12,12 +12,13 @@ import net.minecraft.client.gui.screens.worldselection.WorldCreationContext;
import net.minecraft.network.chat.CommonComponents;
import net.minecraft.network.chat.Component;
/** Edits only the pending new world's island size. Cancel never changes its dimensions. */
/** Edits size and the saved experiment switch of a pending new world. Cancel applies neither. */
public final class SanctuaryWorldOptionsScreen extends Screen {
private final CreateWorldScreen parent;
private final SanctuaryChunkGenerator generator;
private final HeaderAndFooterLayout layout = new HeaderAndFooterLayout(this);
private IslandSize selectedSize;
private boolean experimental;
private MultiLineTextWidget diameterHint;
public SanctuaryWorldOptionsScreen(CreateWorldScreen parent, WorldCreationContext context) {
@@ -28,6 +29,7 @@ public final class SanctuaryWorldOptionsScreen extends Screen {
}
generator = sanctuary;
selectedSize = IslandSize.fromDiameter(generator.initialDiameter());
experimental = generator.experimentalStructures();
}
@Override
@@ -49,6 +51,13 @@ public final class SanctuaryWorldOptionsScreen extends Screen {
contents.addChild(sizeButton);
diameterHint = contents.addChild(new MultiLineTextWidget(diameterHint(), font)
.setMaxWidth(310).setCentered(true));
if (generator.rootGeneration() == 17) {
contents.addChild(CycleButton.builder((Boolean enabled) -> enabled ? CommonComponents.OPTION_ON : CommonComponents.OPTION_OFF, experimental)
.withValues(true, false).create(0, 0, 280, 20, Component.translatable("sanctuary.world_options.experimental"),
(button, value) -> experimental = value));
contents.addChild(new MultiLineTextWidget(Component.translatable("sanctuary.world_options.experimental_hint"), font)
.setMaxWidth(310).setCentered(true));
}
layout.addToContents(contents);
LinearLayout actions = LinearLayout.horizontal().spacing(8);
@@ -65,7 +74,7 @@ public final class SanctuaryWorldOptionsScreen extends Screen {
private void applyAndClose() {
parent.getUiState().updateDimensions((registries, dimensions) -> dimensions.replaceOverworldGenerator(
registries, generator.withInitialDiameter(selectedSize.diameter)));
registries, generator.withInitialDiameter(selectedSize.diameter).withExperimentalStructures(experimental)));
onClose();
}
@@ -121,6 +121,7 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
@Override public void createStructures(RegistryAccess registries, ChunkGeneratorStructureState state, StructureManager structures,
ChunkAccess chunk, StructureTemplateManager templates, ResourceKey<Level> dimension) {
fr.koka.sanctuary.worldgen.city.LostCityStructures.generate(this, registries, state, structures, chunk, templates, dimension);
fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.generate(this, registries, state, structures, chunk, templates, dimension);
ExpansionStructures.generate(this, registries, state, structures, chunk, templates, dimension);
}
@Override public void spawnOriginalMobs(WorldGenRegion region) {
@@ -41,7 +41,7 @@ public record ExpansionIsland(String id, String parent, String direction, String
if (!((spatialGeneration(generation) ? DIAMETERS : LEGACY_DIAMETERS).contains(diameter) || (id.equals("sanctuary") && (diameter == 724 || (generation == 13 || spatialGeneration(generation)) && diameter == 1024)))) throw new IllegalArgumentException("Unsupported nominal diameter");
if (Math.abs((long) centerX) > 100_000 || Math.abs((long) centerZ) > 100_000 || (centerX & 15) != 0 || (centerZ & 15) != 0) throw new IllegalArgumentException("Invalid island centre");
if (!List.of("reserved", "ready").contains(status) || (generation != GENERATION && !spatialGeneration(generation) && !(generation == 13 && id.equals("sanctuary")))) throw new IllegalArgumentException("Unsupported island state or generation");
if (climate.equals("lost_city") && (!spatialGeneration(generation) || id.equals("sanctuary"))) throw new IllegalArgumentException("Lost City requires a generation 14, 15 or 16 expansion");
if (climate.equals("lost_city") && (!spatialGeneration(generation) || id.equals("sanctuary"))) throw new IllegalArgumentException("Lost City requires a generation 14, 15, 16 or 17 expansion");
if (reuseToken == null || !reuseToken.isEmpty() && (!spatialGeneration(generation) || id.equals("sanctuary") || !reuseToken.matches("[0-9a-f]{64}")))
throw new IllegalArgumentException("Invalid void reuse certificate token");
}
@@ -53,7 +53,7 @@ public record ExpansionIsland(String id, String parent, String direction, String
return new ExpansionIsland("sanctuary", "sanctuary", "north", "temperate", diameter, 0, 0, seed, "natural", "ready", generation);
}
public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16; }
public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16 || generation == 17; }
public boolean origin() { return id.equals("sanctuary"); }
public double referenceDiameter() { return origin() && diameter == 724 ? Math.sqrt(2) * 512 : 512; }
public double horizontalScale() { return origin() ? 1.0 : diameter / referenceDiameter(); }
@@ -139,6 +139,9 @@ public final class ExpansionStructures {
if (fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime.protects(binding.generator(), binding.random(),
new BlockPos(centreX, total.minY(), centreZ), radius, total.getYSpan() + 16, 26)) return "protected-hydrology";
}
if (generator instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator city
&& city.intersectsExperimentalStructures(total.minX() - 1, total.minY() - (temple ? 24 : 12), total.minZ() - 1,
total.maxX() + 1, total.maxY() + 4, total.maxZ() + 1)) return "experimental-structure";
if (!inside(island, total.minX(), total.minZ(), 4) || !inside(island, total.maxX(), total.minZ(), 4)
|| !inside(island, total.minX(), total.maxZ(), 4) || !inside(island, total.maxX(), total.maxZ(), 4)) return "outside-island";
if (total.minY() < chunk.getMinY() || total.maxY() > chunk.getMaxY() - 4) return "outside-height";
@@ -24,7 +24,8 @@ public abstract class UnifiedDecorationProtectionMixin {
var owner = generator.islandForChunk(center.x(), center.z());
var destination = generator.islandForChunk(pos.getX() >> 4, pos.getZ() >> 4);
if (owner == null || destination == null || !owner.id().equals(destination.id())
|| !fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing() && generator.protectsCityDecoration(pos))
|| !fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
&& !fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.isPlacing() && generator.protectsCityDecoration(pos))
callback.setReturnValue(false);
}
@@ -39,7 +40,7 @@ public abstract class UnifiedDecorationProtectionMixin {
if (state.is(Blocks.SUGAR_CANE)) 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
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17)
&& fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
&& generator.protectsCityDecoration(pos)) return;
if (generator.protectsDecoration(pos) && !region.getBlockState(pos).equals(state)) {
@@ -38,6 +38,10 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 16)));
public static final MapCodec<SanctuaryBiomeSource> RUINS17_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 17)));
private final List<Holder<Biome>> allBiomes;
private final PopulationIslandBiomeSource population;
private final boolean cities;
@@ -71,7 +75,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
cityPlans = java.util.Map.copyOf(plans);
}
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; }
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; }
@Override public BiomeResolver createResolver(Climate.Sampler sampler) {
BiomeResolver woodlands = population.createResolver(sampler);
@@ -39,6 +39,15 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
DIAMETER_CODEC.optionalFieldOf("initial_diameter", 724).forGetter(SanctuaryChunkGenerator::initialDiameter)
).apply(i, (source, diameter) -> new SanctuaryChunkGenerator(source, diameter, 16)));
public static final MapCodec<SanctuaryChunkGenerator> RUINS17_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
SanctuaryBiomeSource.RUINS17_CODEC.fieldOf("biome_source").forGetter(SanctuaryChunkGenerator::source),
DIAMETER_CODEC.optionalFieldOf("initial_diameter", 724).forGetter(SanctuaryChunkGenerator::initialDiameter),
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 17, experimental)));
private final boolean experimentalStructures;
private volatile fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17 = fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.EMPTY;
private boolean ruinsPrepared;
private volatile Context originContext;
private final int generation;
private net.minecraft.world.level.LevelHeightAccessor heightAccessor;
@@ -70,14 +79,26 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
this(source, initialDiameter, 13);
}
private SanctuaryChunkGenerator(SanctuaryBiomeSource source, int initialDiameter, int generation) {
this(source, initialDiameter, generation, false);
}
private SanctuaryChunkGenerator(SanctuaryBiomeSource source, int initialDiameter, int generation, boolean experimental) {
super(source, initialDiameter, true);
this.experimentalStructures = generation == 17 && experimental;
this.generation = generation;
if (generation != source.generation()) throw new IllegalArgumentException("Sanctuary generator and biome source versions must agree");
}
@Override public SanctuaryBiomeSource source() { return (SanctuaryBiomeSource) super.source(); }
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; }
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; }
public SanctuaryChunkGenerator withInitialDiameter(int diameter) {
return new SanctuaryChunkGenerator(source().copy(), diameter, generation);
return new SanctuaryChunkGenerator(source().copy(), diameter, generation, experimentalStructures);
}
public boolean experimentalStructures() { return experimentalStructures; }
public SanctuaryChunkGenerator withExperimentalStructures(boolean enabled) {
return new SanctuaryChunkGenerator(source().copy(), initialDiameter(), generation, enabled);
}
public fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17() { return ruinsPlan17; }
public boolean intersectsExperimentalStructures(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
return generation == 17 && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ);
}
public IslandCapacity capacity() {
return switch (initialDiameter()) {
@@ -112,10 +133,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
// Publish one root context before any generation workers use it.
context(islands.getFirst());
prepareCities(islands);
prepareRuins(islands);
}
@Override public void setIslands(List<ExpansionIsland> islands) {
// Complete and publish city geometry before workers can observe the new owner list.
prepareCities(islands);
prepareRuins(islands);
super.setIslands(islands);
}
private void prepareCities(List<ExpansionIsland> islands) {
@@ -126,7 +149,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
source().setCityPlans(java.util.Map.of());
return;
}
if (generation == 16 && !cityStructuresEnabled) {
if ((generation == 16 || generation == 17) && !cityStructuresEnabled) {
cityProtection16 = fr.koka.sanctuary.worldgen.city.LostCityProtection16.EMPTY;
activeCityPlans = java.util.Map.of();
source().setCityPlans(java.util.Map.of());
@@ -142,7 +165,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
}
cityProtection15 = protection.build();
}
if (generation == 16) {
if (generation == 16 || generation == 17) {
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
for (var island : islands) {
protection.add(published.get(island.id()), undergroundPlan16(island));
@@ -156,6 +179,19 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
activeCityPlans = java.util.Map.copyOf(published);
source().setCityPlans(published);
}
private void prepareRuins(List<ExpansionIsland> islands) {
if (generation != 17 || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var terrain = context(root);
var sampled = new fr.koka.sanctuary.worldgen.ruins.RuinsTerrain17(root, terrain.generator(), terrain.random(),
heightAccessor, pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()));
long start = System.nanoTime();
var plan = fr.koka.sanctuary.worldgen.ruins.RuinsPlanner17.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
ruinsPlan17 = plan;
ruinsPrepared = true;
SanctuaryMod.LOGGER.info("Experimental ruins17 seed {}: {} networks, {} probes, {} noise columns in {} ms",
root.seed(), plan.networks().size(), plan.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000);
}
private static ExpansionIsland cityIdentity(ExpansionIsland island) {
// State and receipt do not change the terrain geometry used by a cached plan.
var ready = island.status().equals("ready") ? island : island.ready();
@@ -181,7 +217,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
return cityPlans.computeIfAbsent(cityIdentity(island), ignored -> {
long start = System.nanoTime();
var terrain = context(island);
if (generation == 16) {
if (generation == 16 || generation == 17) {
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain16(island, terrain.generator(), terrain.random(),
heightAccessor, pos -> island.origin() && protectsDecoration(pos));
var placement = LostCityPlanner16.plan(island.seed(), island.centerX(), island.centerZ(),
@@ -252,7 +288,8 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
});
}
public boolean protectsCityDecoration(net.minecraft.core.BlockPos pos) {
if (generation == 16) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ());
if (generation == 16 || generation == 17) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| generation == 17 && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ());
if (generation == 15) return cityProtection15.contains(pos.getX(), pos.getY(), pos.getZ());
if (generation != 14) return false;
var island = islandAt(pos.getX(), pos.getZ());
@@ -274,20 +311,20 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
return citySupports.getOrDefault(cityIdentity(island), List.of());
}
public boolean intersectsCityConstruction16(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
return generation == 16 && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
return (generation == 16 || generation == 17) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
}
public fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan undergroundPlan16(ExpansionIsland island) {
if (generation != 16) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
if (generation != 16 && generation != 17) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
lostCityPlan(island);
return undergroundPlans16.getOrDefault(cityIdentity(island), new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0));
}
public List<LostCityPlanner16.Support> citySupports16(ExpansionIsland island) {
if (generation != 16) return List.of();
if (generation != 16 && generation != 17) return List.of();
lostCityPlan(island);
return citySupports16.getOrDefault(cityIdentity(island), List.of());
}
public LostCityPlanner16.Fallback cityReservation16(ExpansionIsland island) {
if (generation != 16) return null;
if (generation != 16 && generation != 17) return null;
lostCityPlan(island);
return cityReservations16.get(cityIdentity(island));
}
@@ -299,6 +336,8 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
net.minecraft.core.BlockPos origin, int radius, boolean skipReferenced) {
if (structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.city.LostCityStructures.KEY))
return fr.koka.sanctuary.worldgen.city.LostCityStructures.findNearest(level, structures, origin, radius, skipReferenced);
if (generation == 17 && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.KEY))
return fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.findNearest(level, structures, origin, radius, skipReferenced);
return super.findNearestMapStructure(level, structures, origin, radius, skipReferenced);
}
@Override protected Context context(ExpansionIsland island) {
@@ -326,7 +365,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
@Override protected void afterTerrain(ExpansionIsland island, ChunkAccess chunk) {
// Root water/lava are already applied by the Population hook on the named delegate.
if (!island.origin()) super.afterTerrain(island, chunk);
if (generation == 16 && cityStructuresEnabled) {
if ((generation == 16 || generation == 17) && cityStructuresEnabled) {
var reserved = cityReservations16.get(cityIdentity(island));
if (reserved != null) {
var mutable = new net.minecraft.core.BlockPos.MutableBlockPos();
@@ -29,17 +29,17 @@ public final class LostCityStructure extends Structure {
throw new IllegalStateException("Lost City district exceeds native structure reference range");
}
return Optional.of(new GenerationStub(LostCityStructures.locator(district), pieces -> {
if (generator.rootGeneration() == 16 && generator.cityReservation16(island) != null)
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17) && generator.cityReservation16(island) != null)
pieces.addPiece(new LostCityFoundationPiece16(generator.cityReservation16(island), district.seed()));
if (generator.rootGeneration() == 15) for (var support : generator.citySupports(island)) {
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
pieces.addPiece(new LostCitySupportPiece15(support));
}
if (generator.rootGeneration() == 16) for (var support : generator.citySupports16(island)) {
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17)) for (var support : generator.citySupports16(island)) {
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
pieces.addPiece(new LostCitySupportPiece16(support));
}
district.parcels().forEach(parcel -> pieces.addPiece(generator.rootGeneration() == 16 ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
district.parcels().forEach(parcel -> pieces.addPiece((generator.rootGeneration() == 16 || generator.rootGeneration() == 17) ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
? new LostCityPiece15(parcel) : new LostCityPiece(parcel)));
if (generator.rootGeneration() == 15) for (var network : generator.undergroundPlan(island).networks()) {
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
@@ -49,7 +49,7 @@ public final class LostCityStructure extends Structure {
network.modules().stream().filter(m -> m.kind() == LostCityUnderground15.Kind.ACCESS)
.forEach(m -> pieces.addPiece(new LostCityUndergroundPiece15(m)));
}
if (generator.rootGeneration() == 16) for (var network : generator.undergroundPlan16(island).networks()) {
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17)) for (var network : generator.undergroundPlan16(island).networks()) {
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
// Surface comes first, rooms/corridors next, shafts last so every entrance remains open.
network.modules().stream().filter(m -> m.kind() != LostCityUnderground16.Kind.ACCESS)
@@ -0,0 +1,114 @@
package fr.koka.sanctuary.worldgen.ruins;
import java.util.ArrayList;
import java.util.Set;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.ResourceKey;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.loot.LootTable;
/** Saved generation17 room geometry is rendered without replanning or reading neighboring chunks. */
public final class RuinsPiece17 extends StructurePiece {
private static final Set<String> ROOM_KEYS = Set.of("Schema", "Kind", "MinX", "MinY", "MinZ", "Width",
"Height", "Depth", "Openings", "RoomSeed", "Supports");
private static final Set<String> SUPPORT_KEYS = Set.of("X", "MinY", "Z", "MaxY");
private final RuinsPlan17.Room room;
public RuinsPiece17(RuinsPlan17.Room room) {
super(RuinsStructures17.PIECE, 0, bounds(room));
this.room = room;
setOrientation(null);
}
public RuinsPiece17(StructurePieceSerializationContext context, CompoundTag tag) {
super(RuinsStructures17.PIECE, tag);
room = decodeRoom(tag.getCompound("SanctuaryRoom")
.orElseThrow(() -> new IllegalArgumentException("Missing saved forgotten room")));
if (!getBoundingBox().equals(bounds(room)))
throw new IllegalArgumentException("Forgotten room disagrees with its saved native bounding box");
setOrientation(null);
}
public RuinsPlan17.Room room() { return room; }
@Override public BlockPos getLocatorPosition() { return locator(room); }
public static BlockPos locator(RuinsPlan17.Room room) {
return new BlockPos(room.minX() + room.width() / 2,
room.kind() == RuinsPlan17.Kind.ACCESS ? room.minY() + room.height() - 2 : room.floorY() + 1,
room.minZ() + room.depth() / 2);
}
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
tag.put("SanctuaryRoom", encodeRoom(room));
}
public static CompoundTag encodeRoom(RuinsPlan17.Room room) {
var tag = new CompoundTag(); tag.putInt("Schema", 1); tag.putString("Kind", room.kind().name());
tag.putInt("MinX", room.minX()); tag.putInt("MinY", room.minY()); tag.putInt("MinZ", room.minZ());
tag.putInt("Width", room.width()); tag.putInt("Height", room.height()); tag.putInt("Depth", room.depth());
tag.putInt("Openings", room.openings()); tag.putLong("RoomSeed", room.seed());
var supports = new ListTag();
for (var support : room.supports()) {
var value = new CompoundTag(); value.putInt("X", support.x()); value.putInt("MinY", support.minY());
value.putInt("Z", support.z()); value.putInt("MaxY", support.maxY()); supports.add(value);
}
tag.put("Supports", supports);
return tag;
}
public static RuinsPlan17.Room decodeRoom(CompoundTag tag) {
if (!tag.keySet().equals(ROOM_KEYS)) throw new IllegalArgumentException("Incomplete or unknown forgotten room fields");
if (integer(tag, "Schema") != 1) throw new IllegalArgumentException("Unsupported forgotten room schema");
var kind = RuinsPlan17.Kind.valueOf(tag.getString("Kind").orElseThrow(() -> new IllegalArgumentException("Missing room kind")));
type(tag, "RoomSeed", Tag.TAG_LONG); type(tag, "Supports", Tag.TAG_LIST);
var saved = tag.getList("Supports").orElseThrow();
if (saved.size() > 64) throw new IllegalArgumentException("Too many saved forgotten room supports");
var supports = new ArrayList<RuinsPlan17.Support>();
for (var entry : saved) {
if (!(entry instanceof CompoundTag value) || !value.keySet().equals(SUPPORT_KEYS))
throw new IllegalArgumentException("Invalid forgotten room support fields");
supports.add(new RuinsPlan17.Support(integer(value, "X"), integer(value, "MinY"), integer(value, "Z"), integer(value, "MaxY")));
}
return new RuinsPlan17.Room(kind, integer(tag, "MinX"), integer(tag, "MinY"), integer(tag, "MinZ"),
integer(tag, "Width"), integer(tag, "Height"), integer(tag, "Depth"), integer(tag, "Openings"),
tag.getLong("RoomSeed").orElseThrow(), supports);
}
private static int integer(CompoundTag tag, String key) {
type(tag, key, Tag.TAG_INT);
return tag.getInt(key).orElseThrow();
}
private static void type(CompoundTag tag, String key, byte id) {
if (tag.get(key) == null || tag.get(key).getId() != id)
throw new IllegalArgumentException("Invalid saved forgotten room field: " + key);
}
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
RuinsStructures17.placing(() -> RuinsRenderer17.render(room, new LostCityRenderer.BlockWriter() {
private BlockPos world(int x, int y, int z) { return new BlockPos(room.minX() + x, room.minY() + y, room.minZ() + z); }
@Override public void set(int x, int y, int z, BlockState block) {
var position = world(x, y, z);
if (getBoundingBox().isInside(position) && clip.isInside(position))
placeBlock(level, block, position.getX(), position.getY(), position.getZ(), clip);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
var position = world(x, y, z);
if (getBoundingBox().isInside(position) && clip.isInside(position))
createChest(level, clip, RandomSource.create(seed), position, loot, null);
}
}));
}
public static BoundingBox bounds(RuinsPlan17.Room room) {
int minY = room.minY(), maxY = Math.addExact(room.minY(), room.height() - 1);
for (var support : room.supports()) {
minY = Math.min(minY, support.minY()); maxY = Math.max(maxY, support.maxY());
}
return new BoundingBox(room.minX(), minY, room.minZ(), Math.addExact(room.minX(), room.width() - 1),
maxY, Math.addExact(room.minZ(), room.depth() - 1));
}
}
@@ -0,0 +1,123 @@
package fr.koka.sanctuary.worldgen.ruins;
import java.util.HashSet;
import java.util.List;
/** Immutable generation17 experiment. Native pieces persist every room and its bounded footing. */
public record RuinsPlan17(List<Network> networks, int samples) {
public static final int MAX_SAMPLES = 250_000;
public static final RuinsPlan17 EMPTY = new RuinsPlan17(List.of(), 0);
public enum Family { MINE, STRANGE, INFRASTRUCTURE }
public enum Kind { ACCESS, CORRIDOR, MINE_GALLERY, WORKSHOP, STORE,
TILED_HALL, ARCHIVE, SERVICE_HALL, PUMP_HALL, CONTROL_ROOM }
/** One-block architectural pier. Its immediately lower block remains native solid rock. */
public record Support(int x, int minY, int z, int maxY) {
public Support {
if (minY < 9 || maxY >= 380 || minY > maxY || maxY - minY >= 12)
throw new IllegalArgumentException("Invalid generation17 ruins support");
}
}
/** Openings N=1,E=2,S=4,W=8: centered three wide, local y2..4. Floor is local y1.
* Adjacent bodies never overlap, including their opened boundary walls. */
public record Room(Kind kind, int minX, int minY, int minZ, int width, int height, int depth,
int openings, long seed, List<Support> supports) {
public Room {
supports = List.copyOf(supports);
if (kind == null || width < 5 || width > 17 || depth < 5 || depth > 17
|| (width & 1) == 0 || (depth & 1) == 0 || height < 6 || height > 40
|| minY < 10 || minY + height > 383 || openings < 0 || openings > 15 || supports.size() > 9)
throw new IllegalArgumentException("Invalid generation17 ruins room");
if (kind == Kind.ACCESS && (width != 5 || depth != 5))
throw new IllegalArgumentException("Ruins access must be five by five");
if (kind != Kind.ACCESS && kind != Kind.CORRIDOR && (width < 9 || depth < 9 || height < 7))
throw new IllegalArgumentException("Thematic ruins rooms need space for their central passage and furnishings");
var positions = new HashSet<Long>();
for (var support : supports) if (support.x() < minX || support.x() >= minX + width
|| support.z() < minZ || support.z() >= minZ + depth || support.maxY() != minY - 1
|| !positions.add(key(support.x(), support.z())))
throw new IllegalArgumentException("Ruins support does not meet its own room floor");
}
public int maxX() { return minX + width - 1; }
public int maxY() { return minY + height - 1; }
public int maxZ() { return minZ + depth - 1; }
public int centerX() { return minX + width / 2; }
public int centerZ() { return minZ + depth / 2; }
public int floorY() { return minY + 1; }
public int boundsMinY() { return supports.stream().mapToInt(Support::minY).min().orElse(minY); }
public int boundsMaxY() { return maxY(); }
public boolean contains(int x, int y, int z) {
if (x < minX || x > maxX() || z < minZ || z > maxZ()) return false;
if (y >= minY && y <= maxY()) return true;
for (var support : supports) if (support.x() == x && support.z() == z && y >= support.minY() && y <= support.maxY()) return true;
return false;
}
public boolean intersects(int x0, int y0, int z0, int x1, int y1, int z1) {
if (x0 > maxX() || x1 < minX || z0 > maxZ() || z1 < minZ) return false;
if (y0 <= maxY() && y1 >= minY) return true;
for (var support : supports) if (support.x() >= x0 && support.x() <= x1 && support.z() >= z0 && support.z() <= z1
&& support.minY() <= y1 && support.maxY() >= y0) return true;
return false;
}
public Room withOpening(int direction) {
if (direction < 0 || direction > 3) throw new IllegalArgumentException("Invalid opening direction");
return new Room(kind, minX, minY, minZ, width, height, depth, openings | 1 << direction, seed, supports);
}
public Room withSupports(List<Support> values) {
return new Room(kind, minX, minY, minZ, width, height, depth, openings, seed, values);
}
}
public record Network(String id, Family family, int startChunkX, int startChunkZ, List<Room> rooms) {
public Network {
rooms = List.copyOf(rooms);
if (id == null || !id.matches("[a-z][a-z0-9_]{0,31}") || family == null || rooms.size() < 3 || rooms.size() > 6
|| rooms.stream().filter(room -> room.kind() == Kind.ACCESS).count() != 1)
throw new IllegalArgumentException("Invalid generation17 ruins network");
for (var room : rooms) if (Math.abs((room.minX() >> 4) - startChunkX) > 7
|| Math.abs((room.maxX() >> 4) - startChunkX) > 7
|| Math.abs((room.minZ() >> 4) - startChunkZ) > 7 || Math.abs((room.maxZ() >> 4) - startChunkZ) > 7)
throw new IllegalArgumentException("Ruins network exceeds native reference range");
}
}
public RuinsPlan17 {
networks = List.copyOf(networks);
if (networks.size() > 3 || samples < 0 || samples > MAX_SAMPLES)
throw new IllegalArgumentException("Invalid generation17 ruins planning budget");
var ids = new HashSet<String>();
for (var network : networks) if (!ids.add(network.id())) throw new IllegalArgumentException("Duplicate ruins network id");
}
public boolean isEmpty() { return networks.isEmpty(); }
public boolean contains(int x, int y, int z) {
for (var network : networks) for (var room : network.rooms()) if (room.contains(x, y, z)) return true;
return false;
}
public boolean intersects(int x0, int y0, int z0, int x1, int y1, int z1) {
if (x0 > x1 || y0 > y1 || z0 > z1) throw new IllegalArgumentException("Reversed ruins bounds");
for (var network : networks) for (var room : network.rooms()) if (room.intersects(x0, y0, z0, x1, y1, z1)) return true;
return false;
}
/** Construction plus the unpainted native rock which carries its floors and short piers. */
public boolean protects(int x, int y, int z) {
if (contains(x, y, z)) return true;
for (var network : networks) for (var room : network.rooms()) {
if (x >= room.minX() && x <= room.maxX() && z >= room.minZ() && z <= room.maxZ()
&& y >= room.minY() - 2 && y < room.minY()) return true;
for (var support : room.supports()) if (x == support.x() && z == support.z() && y == support.minY() - 1) return true;
}
return false;
}
public boolean intersectsProtection(int x0, int y0, int z0, int x1, int y1, int z1) {
if (intersects(x0, y0, z0, x1, y1, z1)) return true;
for (var network : networks) for (var room : network.rooms()) {
if (x0 <= room.maxX() && x1 >= room.minX() && z0 <= room.maxZ() && z1 >= room.minZ()
&& y0 < room.minY() && y1 >= room.minY() - 2) return true;
for (var support : room.supports()) if (support.x() >= x0 && support.x() <= x1 && support.z() >= z0 && support.z() <= z1
&& support.minY() - 1 >= y0 && support.minY() - 1 <= y1) return true;
}
return false;
}
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
}
@@ -0,0 +1,217 @@
package fr.koka.sanctuary.worldgen.ruins;
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Family;
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Kind;
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Network;
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Room;
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Support;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/** Opt-in generation17 prototype. Three independently budgeted families have discoverable
* surface shafts and compact, connected interiors. No existing chunk or world state is read.
* Families remain separate in this prototype; no speculative cross-network door is published. */
public final class RuinsPlanner17 {
public static final int MAX_COLUMNS = 5_000;
public static final int MAX_SAMPLES = RuinsPlan17.MAX_SAMPLES;
private static final int INVALID = Integer.MIN_VALUE;
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
private RuinsPlanner17() {}
public interface Terrain {
boolean owns(int x, int z);
boolean solid(int x, int y, int z);
boolean protectedAt(int x, int y, int z);
/** Highest dry solid block; -1 when the column has no accessible surface. */
int surfaceY(int x, int z);
}
public static RuinsPlan17 plan(long seed, int centerX, int centerZ, int diameter, Terrain terrain) {
if (diameter < 64 || diameter > 1024) throw new IllegalArgumentException("Unsupported ruins island size");
var probe = new Probe(terrain, centerX, centerZ, diameter);
var networks = new ArrayList<Network>();
for (var family : Family.values()) {
int remaining = Family.values().length - family.ordinal() - 1;
probe.columnLimit = MAX_COLUMNS - remaining * (MAX_COLUMNS / 3);
probe.sampleLimit = MAX_SAMPLES - remaining * (MAX_SAMPLES / 3);
Network best = null;
try {
search: for (int attempt = 0; attempt < 6; attempt++) {
Site site = site(seed, family, attempt, centerX, centerZ, diameter, probe);
if (site == null) continue;
for (int drop : new int[] {14, 22, 30}) {
Network candidate = network(seed, family, site, drop, networks, probe);
if (candidate != null && (best == null || candidate.rooms().size() > best.rooms().size())) best = candidate;
if (best != null && best.rooms().size() == 5) break search;
}
}
} catch (BudgetExceeded exhausted) { probe.reject(family.name().toLowerCase(Locale.ROOT) + "-budget"); }
if (best != null) networks.add(best);
}
if (Boolean.getBoolean("sanctuary.test.ruins17") || Boolean.getBoolean("sanctuary.ruins.plannerDiagnostics"))
System.getLogger(RuinsPlanner17.class.getName()).log(System.Logger.Level.INFO,
"Ruins17 seed=" + seed + " diameter=" + diameter + " networks="
+ networks.stream().map(n -> n.family() + ":" + n.rooms().size()).toList()
+ " columns=" + probe.columns.size() + " samples=" + probe.samples + " rejected=" + probe.rejections);
return new RuinsPlan17(networks, probe.samples);
}
private static Site site(long seed, Family family, int attempt, int cx, int cz, int diameter, Probe probe) {
long salt = mix(seed ^ family.ordinal() * 0x9E3779B97F4A7C15L ^ attempt * 0xD1342543DE82EF95L ^ 0x1700L);
double base = unit(mix(seed ^ 0x17BA5EL)) * Math.PI * 2 + family.ordinal() * Math.PI * 2 / 3;
double angle = base + new double[] {0, .30, -.30, .48, -.48, .72}[attempt];
double radius = Math.min(diameter * .27, 216) * (attempt == 3 || attempt == 4 ? .72 : 1);
int x = cx + (int) Math.round(Math.cos(angle) * radius), z = cz + (int) Math.round(Math.sin(angle) * radius);
int low = 384, high = -1;
// Five coarse witnesses reject cliffs, water and unowned corners before a volume scan.
for (int[] offset : new int[][] {{-2, -2}, {-2, 2}, {2, -2}, {2, 2}, {0, 0}}) {
int px = x + offset[0], pz = z + offset[1], top = probe.surface(px, pz);
if (top < 54 || top > 376 || !probe.solid(px, top, pz) || probe.protectedAt(px, top, pz)) {
probe.reject("coarse-entrance"); return null;
}
low = Math.min(low, top); high = Math.max(high, top);
}
if (high - low > 4) { probe.reject("entrance-cliff"); return null; }
int direction = Math.abs(x - cx) >= Math.abs(z - cz) ? x < cx ? 1 : 3 : z < cz ? 2 : 0;
if ((salt & 1) != 0) direction = (direction + (attempt % 2 == 0 ? 1 : 3)) & 3;
return new Site(x, z, high, direction, salt);
}
private static Network network(long seed, Family family, Site site, int drop, List<Network> previous, Probe probe) {
int top = site.top();
for (int z = site.z() - 2; z <= site.z() + 2; z++) for (int x = site.x() - 2; x <= site.x() + 2; x++) {
int height = probe.surface(x, z);
if (height < site.top() - 4 || height > site.top() + 4) { probe.reject("entrance-surface"); return null; }
top = Math.max(top, height);
}
int floor = top - drop;
Room access = new Room(Kind.ACCESS, site.x() - 2, floor - 1, site.z() - 2,
5, drop + 4, 5, 0, mix(seed ^ site.seed() ^ drop), List.of());
var rooms = new ArrayList<Room>();
access = certify(access, rooms, previous, probe);
if (access == null) return null;
rooms.add(access);
if (!attach(rooms, 0, Kind.CORRIDOR, site.direction(), mix(site.seed() ^ drop ^ 0xC0L), previous, probe)) return null;
Kind[] kinds = switch (family) {
case MINE -> new Kind[] {Kind.MINE_GALLERY, Kind.WORKSHOP, Kind.STORE};
case STRANGE -> new Kind[] {Kind.TILED_HALL, Kind.ARCHIVE, Kind.STORE};
case INFRASTRUCTURE -> new Kind[] {Kind.SERVICE_HALL, Kind.PUMP_HALL, Kind.CONTROL_ROOM};
};
if (!attach(rooms, 1, kinds[0], site.direction(), mix(site.seed() ^ drop ^ 0xA1L), previous, probe)) return null;
// Keep already connected rooms when an optional branch encounters protected terrain.
int side = (site.direction() + ((site.seed() & 2) == 0 ? 1 : 3)) & 3;
try {
attach(rooms, 2, kinds[1], side, mix(site.seed() ^ drop ^ 0xB2L), previous, probe);
attach(rooms, 2, kinds[2], site.direction(), mix(site.seed() ^ drop ^ 0xC3L), previous, probe);
} catch (BudgetExceeded exhausted) { probe.reject("optional-branch-budget"); }
return new Network(family.name().toLowerCase(Locale.ROOT), family, site.x() >> 4, site.z() >> 4, rooms);
}
private static boolean attach(List<Room> rooms, int parentIndex, Kind kind, int direction, long seed,
List<Network> previous, Probe probe) {
Room parent = rooms.get(parentIndex);
int width = kind == Kind.CORRIDOR ? 5 : kind == Kind.MINE_GALLERY ? 13 : 9;
int depth = kind == Kind.CORRIDOR ? 5 : 9, height = kind == Kind.CORRIDOR ? 6 : 7;
int x = parent.centerX() - width / 2, z = parent.centerZ() - depth / 2;
if (direction == 0) z = parent.minZ() - depth;
if (direction == 1) x = parent.maxX() + 1;
if (direction == 2) z = parent.maxZ() + 1;
if (direction == 3) x = parent.minX() - width;
Room room = new Room(kind, x, parent.minY(), z, width, height, depth, 1 << ((direction + 2) & 3), seed, List.of());
room = certify(room, rooms, previous, probe);
if (room == null) return false;
rooms.set(parentIndex, parent.withOpening(direction)); rooms.add(room);
return true;
}
private static Room certify(Room draft, List<Room> siblings, List<Network> previous, Probe probe) {
for (Room room : siblings) if (conflicts(room, draft)) { probe.reject("sibling-overlap"); return null; }
for (Network network : previous) for (Room room : network.rooms()) if (conflicts(room, draft)) {
probe.reject("network-overlap"); return null;
}
for (int z = draft.minZ(); z <= draft.maxZ(); z++) for (int x = draft.minX(); x <= draft.maxX(); x++) {
int top = probe.surface(x, z);
if (top == INVALID || top < draft.minY() + 5 || draft.kind() != Kind.ACCESS && top < draft.maxY() + 1) {
probe.reject("room-exposed"); return null;
}
for (int y = draft.minY(); y <= draft.maxY(); y++) if (probe.protectedAt(x, y, z)) {
probe.reject("room-protected"); return null;
}
}
var supports = new ArrayList<Support>();
for (int z : new int[] {draft.minZ() + 1, draft.centerZ(), draft.maxZ() - 1})
for (int x : new int[] {draft.minX() + 1, draft.centerX(), draft.maxX() - 1}) {
int bottom = draft.minY() - 1;
if (probe.solid(x, bottom, z) && probe.solid(x, bottom - 1, z)) {
if (probe.protectedAt(x, bottom, z) || probe.protectedAt(x, bottom - 1, z)) { probe.reject("footing-protected"); return null; }
continue;
}
int rock = -1;
for (int y = bottom - 1; y >= Math.max(9, draft.minY() - 13); y--)
if (probe.solid(x, y, z) && probe.solid(x, y - 1, z)) { rock = y; break; }
if (rock < 0) { probe.reject("footing-void"); return null; }
for (int y = rock; y <= bottom; y++) if (probe.protectedAt(x, y, z)) { probe.reject("pier-protected"); return null; }
supports.add(new Support(x, rock + 1, z, bottom));
}
Room result = draft.withSupports(supports);
for (Room sibling : siblings) if (conflicts(sibling, result)) { probe.reject("pier-sibling"); return null; }
for (Network network : previous) for (Room room : network.rooms()) if (conflicts(room, result)) {
probe.reject("pier-network"); return null;
}
return result;
}
private static boolean conflicts(Room a, Room b) {
if (protectsBounds(a, b.minX(), b.minY(), b.minZ(), b.maxX(), b.maxY(), b.maxZ())
|| protectsBounds(b, a.minX(), a.minY(), a.minZ(), a.maxX(), a.maxY(), a.maxZ())) return true;
for (var support : b.supports()) if (protectsBounds(a, support.x(), support.minY(), support.z(), support.x(), support.maxY(), support.z())) return true;
for (var support : a.supports()) if (protectsBounds(b, support.x(), support.minY(), support.z(), support.x(), support.maxY(), support.z())) return true;
return false;
}
private static boolean protectsBounds(Room room, int x0, int y0, int z0, int x1, int y1, int z1) {
if (room.intersects(x0, y0, z0, x1, y1, z1)) return true;
if (x0 <= room.maxX() && x1 >= room.minX() && z0 <= room.maxZ() && z1 >= room.minZ()
&& y0 < room.minY() && y1 >= room.minY() - 2) return true;
for (var support : room.supports()) if (support.x() >= x0 && support.x() <= x1 && support.z() >= z0 && support.z() <= z1
&& support.minY() - 1 >= y0 && support.minY() - 1 <= y1) return true;
return false;
}
private record Site(int x, int z, int top, int direction, long seed) {}
private static final class BudgetExceeded extends RuntimeException {}
private static final class Probe {
final Terrain terrain; final int centerX, centerZ; final double radius;
final Map<Long, Integer> columns = new HashMap<>();
final Map<String, Integer> rejections = new java.util.TreeMap<>();
int samples, columnLimit, sampleLimit;
Probe(Terrain terrain, int centerX, int centerZ, int diameter) {
this.terrain = java.util.Objects.requireNonNull(terrain); this.centerX = centerX; this.centerZ = centerZ;
radius = diameter / 2.0 - 3;
}
int surface(int x, int z) {
if (Math.hypot((double) x - centerX, (double) z - centerZ) > radius || !terrain.owns(x, z)) return INVALID;
long key = (long) x << 32 ^ z & 0xffffffffL;
var cached = columns.get(key); if (cached != null) return cached;
if (columns.size() >= columnLimit) throw new BudgetExceeded();
count(); int top = terrain.surfaceY(x, z); columns.put(key, top); return top;
}
boolean solid(int x, int y, int z) {
if (y < 0 || y >= 384 || surface(x, z) == INVALID) return false;
count(); return terrain.solid(x, y, z);
}
boolean protectedAt(int x, int y, int z) {
if (y < 0 || y >= 384 || surface(x, z) == INVALID) return true;
count(); return terrain.protectedAt(x, y, z);
}
void count() { if (samples >= sampleLimit) throw new BudgetExceeded(); samples++; }
void reject(String reason) { rejections.merge(reason, 1, Integer::sum); }
}
private static double unit(long value) { return (value >>> 11) * 0x1.0p-53; }
private static long mix(long value) {
value = (value ^ value >>> 30) * 0xbf58476d1ce4e5b9L;
value = (value ^ value >>> 27) * 0x94d049bb133111ebL;
return value ^ value >>> 31;
}
}
@@ -0,0 +1,277 @@
package fr.koka.sanctuary.worldgen.ruins;
import fr.koka.sanctuary.worldgen.city.LostCityPalette;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.core.Direction;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.AttachFace;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.storage.loot.BuiltInLootTables;
/** Generation17 experiments: original, physical rooms reconstructed solely from their saved inputs.
* Coordinates given to the writer are local to the saved room. There are no terrain reads, entities,
* active machines or free fluids. Openings are restored last so connected pieces share dry thresholds. */
public final class RuinsRenderer17 {
private static final Direction[] DIRECTIONS = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
private RuinsRenderer17() {}
/** Local standing positions for native path checks, including approaches to useful fixtures. */
public record Target(String name, int x, int y, int z) {}
public static List<Target> targets(RuinsPlan17.Room room) {
var targets = new ArrayList<Target>();
if (room.kind() == RuinsPlan17.Kind.ACCESS) {
targets.add(new Target("surface_hatch_approach", room.width() / 2, room.height() - 2, room.depth() / 2));
targets.add(new Target("shaft_bottom", room.width() / 2, 2, room.depth() / 2));
} else {
targets.add(new Target("central_crossing", room.width() / 2, 2, room.depth() / 2));
switch (room.kind()) {
case WORKSHOP -> {
targets.add(decorTarget(room, "workbench", 2, 2, 2));
targets.add(decorTarget(room, "unfinished_work_platform", room.width() - 3, 4, room.depth() - 3));
}
case STORE -> targets.add(decorTarget(room, "supply_chest", room.width() - 3, 2, room.depth() - 2));
case ARCHIVE -> targets.add(decorTarget(room, "reading_desk", 2, 2, 2));
case PUMP_HALL, CONTROL_ROOM -> targets.add(decorTarget(room, "controls", 2, 2, 2));
default -> { }
}
}
for (int direction = 0; direction < 4; direction++) if ((room.openings() & 1 << direction) != 0) {
int x = direction == 1 ? room.width() - 1 : direction == 3 ? 0 : room.width() / 2;
int z = direction == 2 ? room.depth() - 1 : direction == 0 ? 0 : room.depth() / 2;
targets.add(new Target("opening_" + DIRECTIONS[direction].getName(), x, 2, z));
}
return List.copyOf(targets);
}
public static void render(RuinsPlan17.Room room, LostCityRenderer.BlockWriter output) {
var b = new Builder(room, output);
b.shell();
switch (room.kind()) {
case ACCESS -> b.access();
case CORRIDOR -> b.corridor();
case MINE_GALLERY -> b.mine();
case WORKSHOP -> b.workshop();
case STORE -> b.store();
case TILED_HALL -> b.tiledHall();
case ARCHIVE -> b.archive();
case SERVICE_HALL -> b.service();
case PUMP_HALL -> b.pumps();
case CONTROL_ROOM -> b.controls();
}
if (room.kind() != RuinsPlan17.Kind.ACCESS) b.lights();
b.openings();
for (var support : room.supports()) for (int y = support.minY(); y <= support.maxY(); y++)
output.set(support.x() - room.minX(), y - room.minY(), support.z() - room.minZ(),
y == support.minY() ? Blocks.SMOOTH_STONE.defaultBlockState() : Blocks.STONE_BRICKS.defaultBlockState());
}
private static boolean turn(RuinsPlan17.Room room) { return (room.seed() & 1L) != 0; }
private static Target decorTarget(RuinsPlan17.Room room, String name, int x, int y, int z) {
return new Target(name, turn(room) ? room.width() - 1 - x : x, y, turn(room) ? room.depth() - 1 - z : z);
}
private static final class Builder {
final RuinsPlan17.Room room;
final LostCityRenderer.BlockWriter out;
final int w, h, d, cx, cz;
final boolean reverse;
Builder(RuinsPlan17.Room room, LostCityRenderer.BlockWriter out) {
this.room = room; this.out = out; w = room.width(); h = room.height(); d = room.depth();
cx = w / 2; cz = d / 2; reverse = turn(room);
}
void set(int x, int y, int z, BlockState state) { out.set(x, y, z, state); }
void set(int x, int y, int z, net.minecraft.world.level.block.Block block) { set(x, y, z, block.defaultBlockState()); }
void decor(int x, int y, int z, BlockState state) {
out.set(reverse ? w - 1 - x : x, y, reverse ? d - 1 - z : z,
reverse ? state.rotate(Rotation.CLOCKWISE_180) : state);
}
void decor(int x, int y, int z, net.minecraft.world.level.block.Block block) { decor(x, y, z, block.defaultBlockState()); }
boolean crossing(int x, int z) { return Math.abs(x - cx) <= 1 || Math.abs(z - cz) <= 1; }
void shell() {
for (int y = 0; y < h; y++) for (int z = 0; z < d; z++) for (int x = 0; x < w; x++) {
BlockState state;
boolean wall = x == 0 || x == w - 1 || z == 0 || z == d - 1;
if (y == 0) state = Blocks.STONE.defaultBlockState();
else if (y == 1) state = floor(x, z);
else if (wall || y == h - 1) state = LostCityPalette.masonry(room.seed(), x, y, z);
else state = Blocks.AIR.defaultBlockState();
set(x, y, z, state);
}
// A continuous ceiling cornice and stone thresholds remain intact despite the weathering.
for (int x = 0; x < w; x++) for (int z = 0; z < d; z++) if (x == 0 || x == w - 1 || z == 0 || z == d - 1)
set(x, h - 2, z, Blocks.SMOOTH_STONE);
}
BlockState floor(int x, int z) {
return switch (room.kind()) {
case MINE_GALLERY -> (x == cx ? Blocks.DARK_OAK_PLANKS : (x + z) % 5 == 0 ? Blocks.ANDESITE : Blocks.STONE).defaultBlockState();
case WORKSHOP, STORE -> (x == 1 || x == w - 2 || z == 1 || z == d - 2
? Blocks.DARK_OAK_PLANKS : Blocks.SMOOTH_STONE).defaultBlockState();
case TILED_HALL, ARCHIVE -> ((x / 2 + z / 2) % 2 == 0 ? Blocks.CALCITE : Blocks.POLISHED_ANDESITE).defaultBlockState();
case SERVICE_HALL, PUMP_HALL, CONTROL_ROOM -> (x == cx || z == cz ? Blocks.POLISHED_ANDESITE : Blocks.SMOOTH_STONE).defaultBlockState();
default -> LostCityPalette.pavement(room.seed(), x, z);
};
}
void access() {
int exit = Integer.numberOfTrailingZeros(room.openings() == 0 ? 1 : room.openings());
int wall = (exit + 2) & 3;
int x = wall == 1 ? w - 2 : wall == 3 ? 1 : cx;
int z = wall == 2 ? d - 2 : wall == 0 ? 1 : cz;
Direction facing = DIRECTIONS[wall].getOpposite();
if ((room.openings() & 1 << wall) != 0) {
// A branching shaft cannot attach its lowest ladder to another open doorway.
// This corner post leaves all three doorway cells and the central crossing usable.
x = 1; z = 2; facing = Direction.SOUTH;
for (int y = 2; y <= h - 3; y++) set(1, y, 1, Blocks.STONE_BRICKS);
}
BlockState ladder = Blocks.LADDER.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing);
for (int y = 2; y <= h - 3; y++) set(x, y, z, ladder);
for (int zz = 0; zz < d; zz++) for (int xx = 0; xx < w; xx++) {
set(xx, h - 3, zz, xx == x && zz == z ? ladder : Blocks.SMOOTH_STONE.defaultBlockState());
set(xx, h - 2, zz, Blocks.AIR); set(xx, h - 1, zz, Blocks.AIR);
}
set(x, h - 2, z, Blocks.DARK_OAK_TRAPDOOR.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, facing));
}
void corridor() {
boolean alongX = w > d;
int length = alongX ? w : d;
for (int n = 2; n < length - 1; n += 4) {
if (alongX) {
for (int y = 2; y <= h - 1; y++) { set(n, y, 0, Blocks.POLISHED_ANDESITE); set(n, y, d - 1, Blocks.POLISHED_ANDESITE); }
} else for (int y = 2; y <= h - 1; y++) { set(0, y, n, Blocks.POLISHED_ANDESITE); set(w - 1, y, n, Blocks.POLISHED_ANDESITE); }
}
}
void mine() {
for (int z : new int[] {2, d - 3}) {
for (int x : new int[] {2, w - 3}) for (int y = 2; y <= h - 2; y++) decor(x, y, z, Blocks.STRIPPED_OAK_LOG);
for (int x = 2; x <= w - 3; x++) decor(x, h - 2, z, Blocks.STRIPPED_OAK_LOG.defaultBlockState()
.setValue(BlockStateProperties.AXIS, Direction.Axis.X));
}
for (int z = 1; z < d - 1; z++) set(cx, 2, z, Blocks.RAIL);
// A short second siding ends at a buffer; the main track and cross passage remain open.
if (w >= 13) {
for (int z = 3; z < d - 2; z++) decor(w - 5, 2, z, Blocks.RAIL);
decor(w - 5, 2, d - 2, Blocks.DARK_OAK_FENCE);
}
decor(1, 2, 1, Blocks.BARREL);
decor(w - 2, 2, d - 2, Blocks.ANDESITE);
if (LostCityPalette.roll(room.seed(), 1, 0, 0, 2) == 0) decor(w - 2, 3, d - 2, Blocks.COBBLESTONE);
}
void workshop() {
decor(1, 2, 1, Blocks.CRAFTING_TABLE);
decor(2, 2, 1, Blocks.FURNACE.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
decor(1, 2, d - 3, Blocks.GRINDSTONE.defaultBlockState()
.setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR)
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST));
decor(w - 2, 2, 1, Blocks.STONECUTTER.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST));
decor(1, 2, d - 2, Blocks.CAULDRON);
decor(2, 2, d - 2, Blocks.ANVIL);
// Two grounded posts and a small planked deck preserve the trace of unfinished repairs.
for (int x = w - 3; x <= w - 2; x++) {
decor(x, 2, d - 3, Blocks.STRIPPED_OAK_LOG);
for (int z = d - 3; z <= d - 2; z++) decor(x, 3, z, Blocks.DARK_OAK_PLANKS);
}
for (int y = 2; y <= 3; y++) decor(w - 3, y, d - 4, Blocks.LADDER.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
decor(w - 2, 2, d - 2, Blocks.BARREL);
}
void store() {
for (int z = 1; z < d - 1; z++) if (!crossing(1, z)) {
decor(1, 2, z, Blocks.BARREL);
if ((z & 1) == 0) decor(1, 3, z, Blocks.BARREL);
decor(1, 4, z, Blocks.DARK_OAK_SLAB);
}
for (int x = cx + 2; x <= w - 2; x++) decor(x, 2, 1, Blocks.DARK_OAK_PLANKS);
decor(w - 2, 3, 1, Blocks.BARREL);
loot(w - 2, 2, d - 2, BuiltInLootTables.VILLAGE_PLAINS_HOUSE);
}
void tiledHall() {
// Repeated columns are interrupted by complete, centered cross passages.
for (int x : new int[] {2, w - 3}) for (int z : new int[] {2, d - 3})
for (int y = 2; y < h - 1; y++) decor(x, y, z, y == 2 || y == h - 2 ? Blocks.SMOOTH_STONE : Blocks.CALCITE);
if (w >= 13) for (int x : new int[] {3, w - 4}) for (int z = 1; z <= d - 2; z++)
if (Math.abs(z - cz) > 1) for (int y = 2; y < h - 1; y++)
decor(x, y, z, y == h - 2 ? Blocks.POLISHED_ANDESITE : Blocks.SMOOTH_STONE);
for (int x = cx - 1; x <= cx + 1; x++) {
decor(x, h - 2, 1, Blocks.POLISHED_ANDESITE);
decor(x, h - 2, d - 2, Blocks.POLISHED_ANDESITE);
}
// Mismatched surviving benches give these physically reachable side niches a former use.
decor(1, 2, 1, seat(Direction.WEST));
decor(w - 2, 2, 1, seat(Direction.EAST));
}
void archive() {
for (int z = 1; z <= d - 2; z++) if (Math.abs(z - cz) > 1)
for (int y = 2; y <= 4; y++) decor(w - 2, y, z,
y == 4 && (z & 1) == 0 ? Blocks.DARK_OAK_PLANKS : Blocks.BOOKSHELF);
decor(1, 2, 1, Blocks.BOOKSHELF);
decor(1, 2, 2, Blocks.LECTERN.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST));
decor(1, 2, d - 2, Blocks.CARTOGRAPHY_TABLE);
decor(2, 2, d - 3, Blocks.BARREL);
loot(1, 2, d - 3, BuiltInLootTables.VILLAGE_CARTOGRAPHER);
}
void service() {
for (int z = 1; z < d - 1; z++) if (Math.abs(z - cz) > 1) {
decor(1, 2, z, Blocks.POLISHED_ANDESITE);
decor(1, 3, z, Blocks.IRON_BARS);
}
for (int z : new int[] {1, d - 2}) for (int x = 1; x < w - 1; x++)
decor(x, h - 2, z, Blocks.STRIPPED_OAK_LOG.defaultBlockState().setValue(BlockStateProperties.AXIS, Direction.Axis.X));
decor(w - 2, 2, 1, Blocks.BARREL);
decor(w - 2, 2, d - 2, Blocks.CRAFTING_TABLE);
decor(2, 2, d - 2, seat(Direction.SOUTH));
}
void pumps() {
for (int z : new int[] {1, d - 2}) {
decor(1, 2, z, Blocks.DISPENSER.defaultBlockState().setValue(BlockStateProperties.FACING, Direction.EAST));
decor(2, 2, z, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 3));
decor(1, 3, z, Blocks.POLISHED_ANDESITE);
decor(1, 4, z, Blocks.IRON_BARS);
decor(w - 2, 2, z, Blocks.CAULDRON);
}
for (int z = 1; z <= d - 2; z++) decor(1, h - 2, z, Blocks.POLISHED_ANDESITE);
decor(w - 2, 2, 2, Blocks.SMOOTH_STONE);
decor(w - 2, 3, 2, Blocks.LEVER.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR)
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST));
}
void controls() {
for (int x = 1; x <= cx - 2; x++) {
decor(x, 2, 1, Blocks.SMOOTH_STONE);
decor(x, 3, 1, Blocks.LEVER.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR)
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
decor(x, 3, 0, Blocks.REDSTONE_LAMP);
}
decor(1, 2, 2, seat(Direction.WEST));
decor(w - 2, 2, 1, Blocks.CRAFTING_TABLE);
decor(w - 2, 2, d - 2, Blocks.BARREL);
// An inspection window remains backed by a complete wall: there is no opening into unverified rock or water.
for (int z = cz + 2; z < d - 1; z++) decor(0, 3, z, Blocks.GLASS);
decor(1, 2, d - 2, Blocks.CARTOGRAPHY_TABLE);
}
BlockState seat(Direction back) { return Blocks.DARK_OAK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, back); }
void lights() {
// Every lantern hangs directly from the full masonry ceiling; never from a bottom slab.
for (int[] p : new int[][] {{1, 1}, {w - 2, d - 2}})
set(p[0], h - 2, p[1], Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
}
void openings() {
for (int direction = 0; direction < 4; direction++) if ((room.openings() & 1 << direction) != 0)
for (int side = -1; side <= 1; side++) {
int x = direction == 1 ? w - 1 : direction == 3 ? 0 : cx + side;
int z = direction == 2 ? d - 1 : direction == 0 ? 0 : cz + side;
set(x, 1, z, Blocks.SMOOTH_STONE);
for (int y = 2; y <= 4; y++) set(x, y, z, Blocks.AIR);
}
}
void loot(int x, int y, int z, net.minecraft.resources.ResourceKey<net.minecraft.world.level.storage.loot.LootTable> table) {
out.chest(reverse ? w - 1 - x : x, y, reverse ? d - 1 - z : z, table, room.seed() ^ 0x17A6C41L);
}
}
}
@@ -0,0 +1,39 @@
package fr.koka.sanctuary.worldgen.ruins;
import com.mojang.serialization.MapCodec;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.util.Optional;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureType;
/** One start contains every planned experimental network sharing its origin chunk. */
public final class RuinsStructure17 extends Structure {
public static final MapCodec<RuinsStructure17> CODEC = simpleCodec(RuinsStructure17::new);
public RuinsStructure17(StructureSettings settings) { super(settings); }
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 17 || !generator.experimentalStructures()) return Optional.empty();
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
if (island == null || !island.origin() || island.generation() != 17) return Optional.empty();
var networks = generator.ruinsPlan17().networks().stream()
.filter(n -> n.startChunkX() == context.chunkPos().x() && n.startChunkZ() == context.chunkPos().z()).toList();
if (networks.isEmpty()) return Optional.empty();
for (var network : networks) for (var room : network.rooms()) {
var bounds = RuinsPiece17.bounds(room);
if (Math.abs((long) Math.floorDiv(bounds.minX(), 16) - network.startChunkX()) > 8
|| Math.abs((long) Math.floorDiv(bounds.maxX(), 16) - network.startChunkX()) > 8
|| Math.abs((long) Math.floorDiv(bounds.minZ(), 16) - network.startChunkZ()) > 8
|| Math.abs((long) Math.floorDiv(bounds.maxZ(), 16) - network.startChunkZ()) > 8)
throw new IllegalStateException("Forgotten network exceeds native reference range: " + network.id());
}
return Optional.of(new GenerationStub(RuinsStructures17.locator(networks.getFirst()), pieces -> {
// Shafts come last so their surface hatches cannot be filled by a connected room.
networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() != RuinsPlan17.Kind.ACCESS)
.forEach(r -> pieces.addPiece(new RuinsPiece17(r))));
networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS)
.forEach(r -> pieces.addPiece(new RuinsPiece17(r))));
}));
}
@Override public StructureType<?> type() { return RuinsStructures17.TYPE; }
}
@@ -0,0 +1,111 @@
package fr.koka.sanctuary.worldgen.ruins;
import com.mojang.datafixers.util.Pair;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
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;
/** Experimental generation17 uses independent native starts, references and saved room pieces. */
public final class RuinsStructures17 {
public static final ResourceKey<Structure> KEY = ResourceKey.create(Registries.STRUCTURE, SanctuaryMod.id("forgotten_networks"));
public static final StructureType<RuinsStructure17> TYPE = Registry.register(BuiltInRegistries.STRUCTURE_TYPE,
SanctuaryMod.id("forgotten_networks_v17"), () -> RuinsStructure17.CODEC);
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
SanctuaryMod.id("forgotten_room_v17"), RuinsPiece17::new);
private static final ThreadLocal<Integer> PLACEMENT_DEPTH = ThreadLocal.withInitial(() -> 0);
private RuinsStructures17() {}
public static void register() { /* Initialize serializers before the dynamic structure registry is read. */ }
public static boolean isPlacing() { return PLACEMENT_DEPTH.get() > 0; }
static void placing(Runnable renderer) {
int previous = PLACEMENT_DEPTH.get();
PLACEMENT_DEPTH.set(previous + 1);
try { renderer.run(); }
finally {
if (previous == 0) PLACEMENT_DEPTH.remove();
else PLACEMENT_DEPTH.set(previous);
}
}
public static void generate(ExpansionChunkGenerator generator, RegistryAccess registries,
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> level) {
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || generator.rootGeneration() != 17
|| !sanctuary.experimentalStructures()) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || island.generation() != 17) return;
var plan = sanctuary.ruinsPlan17();
if (plan.networks().stream().noneMatch(n -> n.startChunkX() == chunk.getPos().x()
&& n.startChunkZ() == chunk.getPos().z())) return;
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
Structure structure = holder.value();
StructureStart previous = manager.getStartForStructure(structure, chunk);
if (previous != null && previous.isValid()) return;
var random = generator.randomState(island);
var sampler = random.createClimateSampler(SamplerContext.builder().enableCaches().build());
var start = structure.generate(holder, level, registries, generator, generator.getBiomeSource(), sampler,
random, templates, island.seed(), chunk.getPos(), previous == null ? 0 : previous.getReferences(),
chunk, structure.biomes()::contains);
if (start.isValid()) manager.setStartForStructure(structure, start, chunk);
}
/** Starts are loaded at STRUCTURE_STARTS; locating never creates an expansion or rebuilds an existing start. */
public static Pair<BlockPos, Holder<Structure>> findNearest(ServerLevel level, HolderSet<Structure> structures,
BlockPos origin, int radius, boolean skipReferenced) {
if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 17 || !generator.experimentalStructures()
|| !level.structureManager().shouldGenerateStructures()) return null;
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
if (!structures.contains(holder)) return null;
var candidates = new ArrayList<RuinsPlan17.Network>();
int originChunkX = origin.getX() >> 4, originChunkZ = origin.getZ() >> 4;
for (var network : generator.ruinsPlan17().networks()) {
var island = generator.islandForChunk(network.startChunkX(), network.startChunkZ());
if (island == null || !island.origin() || island.generation() != 17 || !island.status().equals("ready")) continue;
if (Math.abs((long) network.startChunkX() - originChunkX) <= radius
&& Math.abs((long) network.startChunkZ() - originChunkZ) <= radius) candidates.add(network);
}
candidates.sort(Comparator.comparingDouble(n -> origin.distSqr(locator(n))));
var visited = new HashSet<Long>();
for (var network : candidates) {
long key = ((long) network.startChunkX() << 32) ^ (network.startChunkZ() & 0xffffffffL);
if (!visited.add(key)) continue;
var chunk = level.getChunk(network.startChunkX(), network.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
StructureStart start = level.structureManager().getStartForStructure(holder.value(), chunk);
if (start == null || !start.isValid() || skipReferenced && !start.canBeReferenced()) continue;
if (skipReferenced) level.structureManager().addReference(start);
var entrance = start.getPieces().stream().filter(p -> p instanceof RuinsPiece17 room
&& room.room().kind() == RuinsPlan17.Kind.ACCESS).findFirst().orElse(start.getPieces().getFirst());
return Pair.of(entrance.getLocatorPosition(), holder);
}
return null;
}
static BlockPos locator(RuinsPlan17.Network network) {
var room = network.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS).findFirst()
.orElseGet(() -> network.rooms().getFirst());
return RuinsPiece17.locator(room);
}
}
@@ -0,0 +1,60 @@
package fr.koka.sanctuary.worldgen.ruins;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Predicate;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.RandomState;
/** Read-only, bounded native-noise cache. Unknown columns always reject construction. */
public final class RuinsTerrain17 implements RuinsPlanner17.Terrain {
public static final int MAX_COLUMNS = 5_000;
private final ExpansionIsland island;
private final NoiseBasedChunkGenerator generator;
private final RandomState random;
private final LevelHeightAccessor height;
private final Predicate<BlockPos> protection;
private final Map<Long, NoiseColumn> columns = new HashMap<>();
public RuinsTerrain17(ExpansionIsland island, NoiseBasedChunkGenerator generator, RandomState random,
LevelHeightAccessor height, Predicate<BlockPos> protection) {
this.island = island; this.generator = generator; this.random = random;
this.height = height; this.protection = protection;
}
private NoiseColumn column(int x, int z) {
if (!owns(x, z)) return null;
long key = ((long) x << 32) ^ (z & 0xffffffffL);
var found = columns.get(key);
if (found != null) return found;
if (columns.size() >= MAX_COLUMNS) return null;
var result = generator.getBaseColumn(x, z, height, random);
columns.put(key, result);
return result;
}
@Override public boolean owns(int x, int z) { return island.contains(x, z); }
@Override public boolean solid(int x, int y, int z) {
if (y < 0 || y >= 384) return false;
var column = column(x, z);
return column != null && column.getBlock(y).isSolid() && column.getBlock(y).getFluidState().isEmpty();
}
@Override public boolean protectedAt(int x, int y, int z) {
if (y < 0 || y >= 384) return true;
var column = column(x, z);
return column == null || !column.getBlock(y).getFluidState().isEmpty()
|| protection.test(new BlockPos(x, y, z));
}
@Override public int surfaceY(int x, int z) {
var column = column(x, z);
if (column == null) return -1;
for (int y = 380; y >= 1; y--) {
var state = column.getBlock(y);
if (!state.isAir()) return state.isSolid() && state.getFluidState().isEmpty() ? y : -1;
}
return -1;
}
public int columnsUsed() { return columns.size(); }
}
@@ -124,5 +124,8 @@
"sanctuary.expansion.error.city_generation_required": "This profile or size requires a generation 14 or 15 Sanctuary world. This save keeps its original generation.",
"sanctuary.expansion.reuse_reload": "Expansion %s reserved in previously generated void. Close and reopen the world to begin preparation.",
"sanctuary.expansion.error.reload_required": "This expansion is waiting for the world to reopen before reusing verified void chunks.",
"sanctuary.expansion.error.void_reuse_incomplete": "Void reuse is incomplete. The reservation is retained; check the server log."
"sanctuary.expansion.error.void_reuse_incomplete": "Void reuse is incomplete. The reservation is retained; check the server log.",
"sanctuary.world_options.experimental": "Experimental structures",
"sanctuary.world_options.experimental_hint": "Mines, strange rooms and abandoned facilities. This choice is saved with the new world.",
"structure.sanctuary.forgotten_networks": "Forgotten networks"
}
@@ -124,5 +124,8 @@
"sanctuary.expansion.error.city_generation_required": "Ce profil ou cette taille demande un monde Sanctuary de génération 14 ou 15. Cette sauvegarde conserve sa génération.",
"sanctuary.expansion.reuse_reload": "Expansion %s réservée dans du vide déjà généré. Fermez puis rouvrez le monde pour démarrer sa préparation.",
"sanctuary.expansion.error.reload_required": "Cette expansion attend la réouverture du monde pour réutiliser les chunks de vide vérifiés.",
"sanctuary.expansion.error.void_reuse_incomplete": "La réutilisation du vide est incomplète. La réservation est conservée ; consultez le journal du serveur."
"sanctuary.expansion.error.void_reuse_incomplete": "La réutilisation du vide est incomplète. La réservation est conservée ; consultez le journal du serveur.",
"sanctuary.world_options.experimental": "Structures expérimentales",
"sanctuary.world_options.experimental_hint": "Mines, salles étranges et installations abandonnées. Ce choix reste attaché au nouveau monde.",
"structure.sanctuary.forgotten_networks": "Réseaux oubliés"
}
@@ -0,0 +1,55 @@
{
"replace": false,
"values": [
"#minecraft:is_overworld",
"sanctuary:cavern_bamboo_grove",
"sanctuary:cavern_birch_forest",
"sanctuary:cavern_clearing",
"sanctuary:cavern_dark_grove",
"sanctuary:cavern_dripstone_caves",
"sanctuary:cavern_dry_woodland",
"sanctuary:cavern_lush_caves",
"sanctuary:cavern_oak_forest",
"sanctuary:cavern_rocky_heath",
"sanctuary:cavern_sulfur_depths",
"sanctuary:layered_flower_meadow",
"sanctuary:layered_lower_grove",
"sanctuary:layered_meadow",
"sanctuary:layered_mossy_terraces",
"sanctuary:layered_rocky_terraces",
"sanctuary:lost_city",
"sanctuary:population_bamboo_grove",
"sanctuary:population_birch_forest",
"sanctuary:population_clearing",
"sanctuary:population_dark_grove",
"sanctuary:population_dripstone_caves",
"sanctuary:population_dry_woodland",
"sanctuary:population_lush_caves",
"sanctuary:population_oak_forest",
"sanctuary:population_rocky_heath",
"sanctuary:population_sulfur_depths",
"sanctuary:rift_bamboo_grove",
"sanctuary:rift_birch_forest",
"sanctuary:rift_clearing",
"sanctuary:rift_dark_grove",
"sanctuary:rift_dry_woodland",
"sanctuary:rift_oak_forest",
"sanctuary:rift_rocky_heath",
"sanctuary:rift_sulfur_depths",
"sanctuary:starter_forest",
"sanctuary:starter_forest_hydrology",
"sanctuary:temperate_birch_forest",
"sanctuary:temperate_dappled_forest",
"sanctuary:temperate_flower_forest",
"sanctuary:temperate_forest",
"sanctuary:temperate_plains",
"sanctuary:woodland_bamboo_grove",
"sanctuary:woodland_birch_forest",
"sanctuary:woodland_clearing",
"sanctuary:woodland_dark_grove",
"sanctuary:woodland_dry_woodland",
"sanctuary:woodland_oak_forest",
"sanctuary:woodland_rocky_heath",
"sanctuary:woodland_sulfur_depths"
]
}
@@ -0,0 +1,7 @@
{
"type": "sanctuary:forgotten_networks_v17",
"biomes": "#sanctuary:has_structure/forgotten_networks",
"step": "underground_structures",
"terrain_adaptation": "none",
"spawn_overrides": {}
}
@@ -3,7 +3,7 @@
"minecraft:overworld": {
"type": "sanctuary:sanctuary_384",
"generator": {
"type": "sanctuary:island_v16",
"type": "sanctuary:island_v17",
"biome_source": {
"biomes": [
"minecraft:the_void",
@@ -0,0 +1,81 @@
{
"dimensions": {
"minecraft:overworld": {
"type": "sanctuary:sanctuary_384",
"generator": {
"type": "sanctuary:island_v16",
"biome_source": {
"biomes": [
"minecraft:the_void",
"minecraft:forest",
"minecraft:birch_forest",
"minecraft:flower_forest",
"minecraft:plains",
"minecraft:meadow",
"minecraft:taiga",
"minecraft:snowy_taiga",
"minecraft:snowy_plains",
"minecraft:grove",
"minecraft:ice_spikes",
"minecraft:frozen_peaks",
"minecraft:old_growth_spruce_taiga",
"minecraft:dark_forest",
"minecraft:swamp",
"minecraft:pale_garden",
"minecraft:jungle",
"minecraft:sparse_jungle",
"minecraft:bamboo_jungle",
"minecraft:mangrove_swamp",
"minecraft:savanna",
"minecraft:savanna_plateau",
"minecraft:desert",
"minecraft:badlands",
"minecraft:eroded_badlands",
"minecraft:wooded_badlands",
"minecraft:windswept_hills",
"minecraft:windswept_gravelly_hills",
"minecraft:stony_peaks",
"minecraft:dappled_forest",
"minecraft:cherry_grove",
"minecraft:lush_caves",
"minecraft:dripstone_caves",
"minecraft:sulfur_caves",
"sanctuary:population_oak_forest",
"sanctuary:population_birch_forest",
"sanctuary:population_clearing",
"sanctuary:population_dry_woodland",
"sanctuary:population_rocky_heath",
"sanctuary:population_dark_grove",
"sanctuary:population_bamboo_grove",
"sanctuary:population_sulfur_depths",
"sanctuary:population_lush_caves",
"sanctuary:population_dripstone_caves",
"sanctuary:lost_city"
]
},
"initial_diameter": 724
}
},
"minecraft:the_end": {
"type": "minecraft:the_end",
"generator": {
"type": "minecraft:noise",
"biome_source": {
"type": "minecraft:the_end"
},
"settings": "minecraft:end"
}
},
"minecraft:the_nether": {
"type": "minecraft:the_nether",
"generator": {
"type": "minecraft:noise",
"biome_source": {
"type": "minecraft:multi_noise",
"preset": "minecraft:nether"
},
"settings": "minecraft:nether"
}
}
}
}
@@ -143,6 +143,21 @@ public final class ExpansionJournalSmoke {
require(Arrays.equals(ruinsBytes, Files.readAllBytes(ruinsDirectory.resolve(ExpansionJournal.FILE_NAME)))
&& Arrays.equals(detailBytes, Files.readAllBytes(detailDirectory.resolve(ExpansionJournal.FILE_NAME))),
"Rejected migrations preserve both published15 and16 bytes");
var experimentRoot = ExpansionIsland.origin(724, SEED, 17);
var experimentDirectory = scratch.resolve("experiment-17");
var experimentJournal = new ExpansionJournal(experimentDirectory, SEED, 724, 17);
var experimentChild = ExpansionPlacement.candidates(SEED, List.of(experimentRoot), "experiment", "sanctuary", "east", "lost_city", 128, "natural", 0).getFirst()
.withReuseToken("e".repeat(64));
experimentJournal.commit(List.of(experimentRoot, experimentChild));
require(new ExpansionJournal(experimentDirectory, SEED, 724, 17).islands().equals(List.of(experimentRoot, experimentChild)),
"Generation17 journal and child preserve their independent identity");
byte[] experimentBytes = Files.readAllBytes(experimentDirectory.resolve(ExpansionJournal.FILE_NAME));
reject(() -> new ExpansionJournal(experimentDirectory, SEED, 724, 16), "No implicit downgrade from17 to16");
reject(() -> new ExpansionJournal(ruinsDirectory, SEED, 724, 17), "No implicit upgrade from16 to17");
reject(() -> experimentJournal.commit(List.of(experimentRoot, ruinsChild)), "No generation16 child in17");
require(Arrays.equals(experimentBytes, Files.readAllBytes(experimentDirectory.resolve(ExpansionJournal.FILE_NAME)))
&& Arrays.equals(ruinsBytes, Files.readAllBytes(ruinsDirectory.resolve(ExpansionJournal.FILE_NAME))),
"Rejected cross-generation changes preserve experiment and published16 bytes");
try {
ExpansionPlacement.candidates(SEED, List.of(ExpansionIsland.origin(724, SEED, 13)), "city", "sanctuary", "east", "lost_city", 256, "natural", 0);
throw new AssertionError("Lost City must not silently activate in a saved alpha.13 world");
@@ -0,0 +1,55 @@
package fr.koka.sanctuary.worldgen.ruins;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
/** Generation17's independent saved payload: exact roundtrips and fail-closed malformed geometry. */
public final class RuinsPiece17Smoke {
public static void main(String[] args) {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); run();
System.out.println("RuinsPiece17Smoke: all room kinds, strict schema/types, supports, bounds and invalid geometry passed.");
}
public static void run() {
for (var kind : RuinsPlan17.Kind.values()) {
int size = kind == RuinsPlan17.Kind.ACCESS ? 5 : 17;
var room = new RuinsPlan17.Room(kind, -32, 120, 40, size, kind == RuinsPlan17.Kind.ACCESS ? 18 : 9, size,
10, Long.MIN_VALUE + kind.ordinal(), List.of(new RuinsPlan17.Support(-31, 114, 41, 119)));
var saved = RuinsPiece17.encodeRoom(room);
var decoded = RuinsPiece17.decodeRoom(saved);
require(room.equals(decoded) && RuinsPiece17.encodeRoom(decoded).equals(saved), "Saved room fields changed for " + kind);
var bounds = RuinsPiece17.bounds(room);
require(bounds.minX() == -32 && bounds.minZ() == 40 && bounds.minY() == 114
&& bounds.maxX() == room.maxX() && bounds.maxZ() == room.maxZ() && bounds.maxY() == room.maxY(), "Bounds excluded a saved footing");
if (kind == RuinsPlan17.Kind.ACCESS) require(RuinsPiece17.locator(room).getY() == room.maxY() - 1,
"Locate points beneath the surface hatch");
var missing = saved.copy(); missing.remove("Openings"); rejects(missing);
var version = saved.copy(); version.putInt("Schema", 2); rejects(version);
var coercion = saved.copy(); coercion.putDouble("MinY", 120.0); rejects(coercion);
var seedType = saved.copy(); seedType.putInt("RoomSeed", 1); rejects(seedType);
var unknown = saved.copy(); unknown.putInt("FutureField", 1); rejects(unknown);
var geometry = saved.copy(); geometry.putInt("Width", 18); rejects(geometry);
var opening = saved.copy(); opening.putInt("Openings", 16); rejects(opening);
var footings = new ListTag(); var bad = new CompoundTag();
bad.putInt("X", room.maxX() + 1); bad.putInt("MinY", 114); bad.putInt("Z", 41); bad.putInt("MaxY", 119);
footings.add(bad); var outside = saved.copy(); outside.put("Supports", footings); rejects(outside);
bad.putInt("X", -31); bad.putInt("MinY", 100); rejects(outside); // Overlong support.
bad.putInt("MinY", 114); bad.putInt("MaxY", 118); rejects(outside); // Detached support.
bad.putInt("MaxY", 119); footings.add(bad.copy()); rejects(outside); // Duplicate anchor column.
}
var supportList = new ArrayList<RuinsPlan17.Support>();
supportList.add(new RuinsPlan17.Support(1, 114, 1, 119));
var room = new RuinsPlan17.Room(RuinsPlan17.Kind.ACCESS, 0, 120, 0, 5, 18, 5, 2, 42, supportList);
supportList.clear();
require(RuinsPiece17.decodeRoom(RuinsPiece17.encodeRoom(room)).supports().size() == 1, "External list mutation changed saved support inputs");
}
private static void rejects(CompoundTag tag) {
try { RuinsPiece17.decodeRoom(tag); }
catch (IllegalArgumentException expected) { return; }
throw new AssertionError("Malformed forgotten room payload was accepted: " + tag);
}
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
}
@@ -0,0 +1,174 @@
package fr.koka.sanctuary.worldgen.ruins;
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Family;
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Kind;
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Room;
import java.util.ArrayDeque;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.IntBinaryOperator;
/** Pure terrain-contract tests: entrances, exact doorway joins, independent budgets and short supports. */
public final class RuinsPlanner17Smoke {
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
public static void main(String[] args) {
Set<Kind> kinds = EnumSet.noneOf(Kind.class);
for (int diameter : new int[] {512, 724, 1024}) for (long seed : new long[] {0, 1, 42, -1}) {
Fixture terrain = new Fixture((x, z) -> 220);
var plan = RuinsPlanner17.plan(seed, 0, 0, diameter, terrain);
check(plan, terrain);
require(plan.networks().size() == 3, "Unobstructed starter does not receive all three families");
require(plan.networks().stream().map(RuinsPlan17.Network::family).distinct().count() == 3, "A family was duplicated");
require(plan.networks().stream().allMatch(n -> n.rooms().size() == 5), "Flat terrain loses a certified branch");
plan.networks().forEach(n -> n.rooms().forEach(r -> kinds.add(r.kind())));
require(plan.equals(RuinsPlanner17.plan(seed, 0, 0, diameter, new Fixture((x, z) -> 220))), "Ruins depend on planning order");
}
require(kinds.equals(EnumSet.allOf(Kind.class)), "A requested thematic kind is never generated");
var flat = RuinsPlanner17.plan(0, 0, 0, 724, new Fixture((x, z) -> 220));
require(!flat.equals(RuinsPlanner17.plan(1, 0, 0, 724, new Fixture((x, z) -> 220))), "Network locations ignore the seed");
Fixture shifted = new Fixture((x, z) -> 220);
var translated = RuinsPlanner17.plan(42, -1024, 512, 724, shifted);
require(translated.networks().size() == 3, "Negative world coordinates lose a network"); check(translated, shifted);
Fixture wet = new Fixture((x, z) -> 220); wet.protection = (x, y, z) -> true;
var refusedWater = RuinsPlanner17.plan(0, 0, 0, 724, wet);
check(refusedWater, wet); require(refusedWater.isEmpty(), "Ruins overwrite protected water or a city");
Fixture absent = new Fixture((x, z) -> -1);
var refusedVoid = RuinsPlanner17.plan(0, 0, 0, 724, absent);
check(refusedVoid, absent); require(refusedVoid.isEmpty(), "Void receives an artificial entry platform");
Fixture thin = new Fixture((x, z) -> 220); thin.rock = (x, y, z) -> y >= 218 && y <= 220;
var refusedThin = RuinsPlanner17.plan(0, 0, 0, 724, thin);
check(refusedThin, thin); require(refusedThin.isEmpty(), "A shaft descends from a shelf with no reachable bearing rock");
Fixture cavity = new Fixture((x, z) -> 220);
cavity.rock = (x, y, z) -> y >= 8 && y <= 220 && (y < 200 || y > 204);
var supported = RuinsPlanner17.plan(0, 0, 0, 724, cavity);
check(supported, cavity);
require(supported.networks().size() == 3 && supported.networks().stream().flatMap(n -> n.rooms().stream())
.anyMatch(r -> !r.supports().isEmpty()), "Short cave gaps cannot receive certified architectural supports");
// The first family's sector is obstructed. Its retries must not eliminate the other two.
var first = flat.networks().getFirst().rooms().getFirst();
double radiusSquared = (double) first.centerX() * first.centerX() + (double) first.centerZ() * first.centerZ();
Fixture obstructed = new Fixture((x, z) -> 220);
obstructed.protection = (x, y, z) -> (double) x * first.centerX() + (double) z * first.centerZ() > radiusSquared * .5;
var partial = RuinsPlanner17.plan(0, 0, 0, 724, obstructed);
check(partial, obstructed);
require(partial.networks().stream().noneMatch(n -> n.family() == Family.MINE), "Obstructed family escaped its own candidate sector");
require(partial.networks().stream().anyMatch(n -> n.family() == Family.STRANGE)
&& partial.networks().stream().anyMatch(n -> n.family() == Family.INFRASTRUCTURE), "One family starved all later families");
for (int diameter : new int[] {64, 128, 256}) {
Fixture small = new Fixture((x, z) -> 220);
check(RuinsPlanner17.plan(42, 0, 0, diameter, small), small);
}
boolean rejected = false;
try { new Room(Kind.WORKSHOP, 0, 100, 0, 5, 7, 5, 0, 0, List.of()); }
catch (IllegalArgumentException expected) { rejected = true; }
require(rejected, "Thematic room accepts dimensions too small for its central passage");
System.out.println("RuinsPlanner17Smoke passed: three independent families, discoverable shafts, exact connected doorways, protected-volume rejection, short native footings and bounded per-family searches");
}
private static void check(RuinsPlan17 plan, Fixture terrain) {
require(plan.samples() == terrain.samples && plan.samples() <= RuinsPlanner17.MAX_SAMPLES, "Probe budget drifted");
require(terrain.columns.size() <= RuinsPlanner17.MAX_COLUMNS, "Column budget exceeded");
Set<Long> starts = new HashSet<>(); Set<Point> written = new HashSet<>();
for (var network : plan.networks()) {
require(starts.add(key(network.startChunkX(), network.startChunkZ())), "Two networks collide at one native start");
require(network.rooms().size() >= 3 && network.rooms().size() <= 6, "Network size is unbounded");
require(network.rooms().stream().filter(r -> r.kind() == Kind.ACCESS).count() == 1, "Network has no unique surface shaft");
Map<Long, Integer> walk = new HashMap<>();
for (int index = 0; index < network.rooms().size(); index++) {
Room room = network.rooms().get(index);
require(Math.abs((room.minX() >> 4) - network.startChunkX()) <= 7
&& Math.abs((room.maxX() >> 4) - network.startChunkX()) <= 7
&& Math.abs((room.minZ() >> 4) - network.startChunkZ()) <= 7
&& Math.abs((room.maxZ() >> 4) - network.startChunkZ()) <= 7, "Native piece reference exceeds seven chunks");
int highest = -1;
for (int z = room.minZ(); z <= room.maxZ(); z++) for (int x = room.minX(); x <= room.maxX(); x++) {
highest = Math.max(highest, terrain.height.applyAsInt(x, z));
for (int y = room.minY(); y <= room.maxY(); y++) {
require(written.add(new Point(x, y, z)), "Independent rooms write the same voxel");
require(!terrain.protection.test(x, y, z), "Room overwrites protected terrain or water");
require(plan.contains(x, y, z) && plan.protects(x, y, z), "Room is absent from the construction mask");
}
require(plan.protects(x, room.minY() - 2, z), "Native floor bearing is exposed to decoration");
if (passage(room, x, z)) require(walk.put(key(x, z), index) == null, "Room walking planes overlap");
}
if (room.kind() == Kind.ACCESS) require(room.maxY() == highest + 2, "Entry is buried below the sampled surface");
for (int z : new int[] {room.minZ() + 1, room.centerZ(), room.maxZ() - 1})
for (int x : new int[] {room.minX() + 1, room.centerX(), room.maxX() - 1}) {
var support = room.supports().stream().filter(s -> s.x() == x && s.z() == z).findFirst();
if (support.isEmpty()) require(terrain.rock.test(x, room.minY() - 1, z) && terrain.rock.test(x, room.minY() - 2, z),
"Floor has neither a native rock footing nor a short pier");
}
for (var support : room.supports()) {
require(support.maxY() == room.minY() - 1 && support.maxY() - support.minY() < 12,
"Support misses the floor or descends too far");
require(terrain.rock.test(support.x(), support.minY() - 1, support.z())
&& terrain.rock.test(support.x(), support.minY() - 2, support.z()), "Support does not end on certified native rock");
require(plan.protects(support.x(), support.minY() - 1, support.z())
&& plan.intersectsProtection(support.x(), support.minY() - 1, support.z(), support.x(), support.minY() - 1, support.z()),
"Support bearing is missing from the decoration/structure protection masks");
for (int y = support.minY(); y <= support.maxY(); y++) {
require(written.add(new Point(support.x(), y, support.z())), "Support collides with another room or support");
require(!terrain.protection.test(support.x(), y, support.z()), "Support overwrites protected water");
require(plan.contains(support.x(), y, support.z()), "Support is missing from the exact volume mask");
}
}
for (int direction = 0; direction < 4; direction++) if ((room.openings() & 1 << direction) != 0) {
Room neighbor = neighbor(room, direction, network.rooms());
require(neighbor != null, "A published doorway opens into unplanned rock");
require(neighbor.floorY() == room.floorY() && (neighbor.openings() & 1 << ((direction + 2) & 3)) != 0,
"Doorways disagree on height or reciprocal opening");
}
}
require(!walk.isEmpty(), "Network has no walkable central passage");
Set<Long> reached = new HashSet<>(); var queue = new ArrayDeque<Long>();
Room access = network.rooms().stream().filter(r -> r.kind() == Kind.ACCESS).findFirst().orElseThrow();
long origin = key(access.centerX(), access.centerZ()); reached.add(origin); queue.add(origin);
while (!queue.isEmpty()) {
long position = queue.removeFirst(); int x = (int) (position >> 32), z = (int) position;
for (int direction = 0; direction < 4; direction++) {
long next = key(x + DX[direction], z + DZ[direction]);
if (walk.containsKey(next) && reached.add(next)) queue.addLast(next);
}
}
require(reached.equals(walk.keySet()), "A thematic room is disconnected from its discoverable entrance");
}
require(!plan.contains(100_000, 200, 100_000) && !plan.intersectsProtection(100_000, 0, 100_000, 100_005, 383, 100_005),
"Ruins protection escapes the accepted networks");
}
private static boolean passage(Room r, int x, int z) {
if (x == r.minX()) return (r.openings() & 8) != 0 && Math.abs(z - r.centerZ()) <= 1;
if (x == r.maxX()) return (r.openings() & 2) != 0 && Math.abs(z - r.centerZ()) <= 1;
if (z == r.minZ()) return (r.openings() & 1) != 0 && Math.abs(x - r.centerX()) <= 1;
if (z == r.maxZ()) return (r.openings() & 4) != 0 && Math.abs(x - r.centerX()) <= 1;
return Math.abs(x - r.centerX()) <= 1 || Math.abs(z - r.centerZ()) <= 1;
}
private static Room neighbor(Room room, int direction, List<Room> rooms) {
int x = direction == 1 ? room.maxX() + 1 : direction == 3 ? room.minX() - 1 : room.centerX();
int z = direction == 2 ? room.maxZ() + 1 : direction == 0 ? room.minZ() - 1 : room.centerZ();
return rooms.stream().filter(r -> r != room && x >= r.minX() && x <= r.maxX() && z >= r.minZ() && z <= r.maxZ()
&& (DX[direction] == 0 ? r.centerX() == room.centerX() : r.centerZ() == room.centerZ())).findFirst().orElse(null);
}
@FunctionalInterface private interface Volume { boolean test(int x, int y, int z); }
private static final class Fixture implements RuinsPlanner17.Terrain {
final IntBinaryOperator height; final Set<Long> columns = new HashSet<>(); int samples;
Volume protection = (x, y, z) -> false;
Volume rock;
Fixture(IntBinaryOperator height) { this.height = height; rock = (x, y, z) -> y >= 8 && y <= height.applyAsInt(x, z); }
@Override public boolean owns(int x, int z) { return true; }
@Override public boolean solid(int x, int y, int z) { samples++; return rock.test(x, y, z); }
@Override public boolean protectedAt(int x, int y, int z) { samples++; return protection.test(x, y, z); }
@Override public int surfaceY(int x, int z) {
samples++; require(columns.add(key(x, z)), "Column noise was sampled more than once"); return height.applyAsInt(x, z);
}
}
private record Point(int x, int y, int z) {}
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
private static void require(boolean valid, String message) { if (!valid) throw new AssertionError(message); }
}
@@ -0,0 +1,219 @@
package fr.koka.sanctuary.worldgen.ruins;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.RailBlock;
import net.minecraft.world.level.block.StairBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.storage.loot.LootTable;
/** Exercises the final native block states, saved bounds and meaningful circulation of all prototypes. */
public final class RuinsRenderer17Smoke {
private RuinsRenderer17Smoke() {}
public static void main(String[] args) {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); run();
System.out.println("RuinsRenderer17Smoke passed: ten physical room types, saved seed variation, dry connected openings, usable work platform, native fixture supports, deferred loot and chunk-order reconstruction");
}
public static void run() {
Set<Map<BlockPos, BlockState>> kinds = new HashSet<>();
for (var kind : RuinsPlan17.Kind.values()) {
for (int size = 0; size < 3; size++) for (int openings : new int[] {1, 2, 4, 8, 5, 10, 15})
for (long seed : new long[] {0, 1}) {
var room = room(kind, size, openings, seed);
var c = render(room); var repeated = render(room);
require(c.blocks.equals(repeated.blocks) && c.loot.equals(repeated.loot), label(room) + " is not deterministic");
if (size == 0 && openings == 15 && seed == 0) kinds.add(c.blocks);
var start = RuinsRenderer17.targets(room).getFirst();
Set<BlockPos> reached = walk(c, position(start));
for (var target : RuinsRenderer17.targets(room)) require(reached.contains(position(target)),
label(room) + " cannot reach " + target.name() + " at " + position(target));
openings(c, reached);
supports(c);
fixtures(c, reached);
identity(c);
for (var chest : c.loot.keySet()) require(approachable(chest, reached), label(room) + " chest is inaccessible at " + chest);
require(c.blocks.values().stream().noneMatch(b -> b.is(Blocks.SPAWNER) || !b.getFluidState().isEmpty()),
label(room) + " introduced a spawner or uncontrolled fluid");
}
var a = render(room(kind, 1, 15, 0)); var b = render(room(kind, 1, 15, 1));
require(!a.blocks.equals(b.blocks), kind + " ignores its saved variation seed");
if (kind != RuinsPlan17.Kind.ACCESS && kind != RuinsPlan17.Kind.CORRIDOR)
require(!furniture(a).equals(furniture(b)), kind + " varies only stone weathering, not physical use or furniture placement");
}
require(kinds.size() == RuinsPlan17.Kind.values().length, "Room names disguise identical rendered interiors");
nativeChunkPartition();
}
private static RuinsPlan17.Room room(RuinsPlan17.Kind kind, int size, int openings, long seed) {
int width = size == 0 ? 9 : size == 1 ? 13 : 17;
int depth = size < 2 ? 9 : 13, height = size < 2 ? 7 : 9;
if (kind == RuinsPlan17.Kind.ACCESS) { width = depth = 5; height = new int[] {14, 27, 40}[size]; }
if (kind == RuinsPlan17.Kind.CORRIDOR) { width = size == 2 ? 17 : 5; depth = size == 1 ? 13 : 5; height = 6; }
int x = -43, y = 150, z = 59;
return new RuinsPlan17.Room(kind, x, y, z, width, height, depth, openings, seed,
List.of(new RuinsPlan17.Support(x, y - 5, z, y - 1),
new RuinsPlan17.Support(x + width - 1, y - 12, z + depth - 1, y - 1)));
}
private static String label(RuinsPlan17.Room r) {
return r.kind() + " " + r.width() + "x" + r.height() + "x" + r.depth() + " openings=" + r.openings() + " seed=" + r.seed();
}
private static BlockPos position(RuinsRenderer17.Target target) { return new BlockPos(target.x(), target.y(), target.z()); }
private static Capture render(RuinsPlan17.Room room) {
var result = new Capture(room); RuinsRenderer17.render(room, result); return result;
}
private static void openings(Capture c, Set<BlockPos> reached) {
var r = c.room;
for (int direction = 0; direction < 4; direction++) for (int side = -1; side <= 1; side++) {
int x = direction == 1 ? r.width() - 1 : direction == 3 ? 0 : r.width() / 2 + side;
int z = direction == 2 ? r.depth() - 1 : direction == 0 ? 0 : r.depth() / 2 + side;
var threshold = new BlockPos(x, 1, z);
require(solid(c.block(threshold)), label(r) + " has no dry threshold at " + threshold);
if ((r.openings() & 1 << direction) == 0) {
require(!c.block(threshold.above()).isAir(), label(r) + " opens an unconnected wall");
continue;
}
for (int y = 2; y <= 4; y++) require(c.block(new BlockPos(x, y, z)).isAir(),
label(r) + " obstructs its three-wide opening at " + x + "/" + z);
require(reached.contains(threshold.above()), label(r) + " opening edge is not connected to the room");
}
}
private static void supports(Capture c) {
var r = c.room;
for (var support : r.supports()) {
for (int y = support.minY(); y <= support.maxY(); y++) require(solid(c.block(new BlockPos(
support.x() - r.minX(), y - r.minY(), support.z() - r.minZ()))), label(r) + " interrupts its saved footing");
require(solid(c.block(new BlockPos(support.x() - r.minX(), 0, support.z() - r.minZ()))),
label(r) + " footing does not meet the structural floor");
}
for (int x = 0; x < r.width(); x++) for (int z = 0; z < r.depth(); z++)
require(solid(c.block(new BlockPos(x, 0, z))), label(r) + " has a perforated structural floor");
}
private static void fixtures(Capture c, Set<BlockPos> reached) {
for (var entry : c.blocks.entrySet()) {
var p = entry.getKey(); var block = entry.getValue();
if (block.is(Blocks.LANTERN)) {
require(block.getValue(BlockStateProperties.HANGING), "Lantern must hang from the ceiling");
require(solid(c.block(p.above())), label(c.room) + " floating lantern at " + p);
}
if (block.is(Blocks.LADDER)) {
Direction facing = block.getValue(BlockStateProperties.HORIZONTAL_FACING);
require(solid(c.block(p.relative(facing.getOpposite()))), label(c.room) + " ladder lacks a back wall at " + p);
}
if (block.is(Blocks.LEVER) || block.is(Blocks.GRINDSTONE)) require(solid(c.block(p.below())),
label(c.room) + " unsupported usable fixture at " + p);
if (block.is(Blocks.DARK_OAK_STAIRS)) {
Direction back = block.getValue(BlockStateProperties.HORIZONTAL_FACING);
require(solid(c.block(p.below())), label(c.room) + " seat has no floor");
require(reached.contains(p.relative(back.getOpposite())), label(c.room) + " seat faces an obstruction at " + p);
}
if (block.is(Blocks.CRAFTING_TABLE) || block.is(Blocks.FURNACE) || block.is(Blocks.GRINDSTONE)
|| block.is(Blocks.STONECUTTER) || block.is(Blocks.CARTOGRAPHY_TABLE) || block.is(Blocks.LECTERN))
require(approachable(p, reached), label(c.room) + " useful furniture cannot be approached at " + p);
}
}
private static void identity(Capture c) {
int expectedLoot = c.room.kind() == RuinsPlan17.Kind.STORE || c.room.kind() == RuinsPlan17.Kind.ARCHIVE ? 1 : 0;
require(c.loot.size() == expectedLoot, label(c.room) + " has unbounded or missing deferred loot");
switch (c.room.kind()) {
case ACCESS -> { has(c, Blocks.LADDER); has(c, Blocks.DARK_OAK_TRAPDOOR); }
case CORRIDOR -> has(c, Blocks.POLISHED_ANDESITE);
case MINE_GALLERY -> { has(c, Blocks.RAIL); has(c, Blocks.STRIPPED_OAK_LOG); has(c, Blocks.BARREL); }
case WORKSHOP -> { has(c, Blocks.CRAFTING_TABLE); has(c, Blocks.FURNACE); has(c, Blocks.GRINDSTONE); has(c, Blocks.STONECUTTER); has(c, Blocks.ANVIL); has(c, Blocks.LADDER); }
case STORE -> { has(c, Blocks.BARREL); has(c, Blocks.DARK_OAK_SLAB); }
case TILED_HALL -> { has(c, Blocks.CALCITE); has(c, Blocks.POLISHED_ANDESITE); has(c, Blocks.DARK_OAK_STAIRS); }
case ARCHIVE -> { has(c, Blocks.BOOKSHELF); has(c, Blocks.LECTERN); has(c, Blocks.CARTOGRAPHY_TABLE); }
case SERVICE_HALL -> { has(c, Blocks.IRON_BARS); has(c, Blocks.CRAFTING_TABLE); }
case PUMP_HALL -> { has(c, Blocks.WATER_CAULDRON); has(c, Blocks.DISPENSER); has(c, Blocks.LEVER); }
case CONTROL_ROOM -> { has(c, Blocks.LEVER); has(c, Blocks.REDSTONE_LAMP); has(c, Blocks.CARTOGRAPHY_TABLE); }
}
}
private static void has(Capture c, Block block) {
require(c.blocks.values().stream().anyMatch(b -> b.is(block)), label(c.room) + " lost its recognizable " + block);
}
private static Map<BlockPos, BlockState> furniture(Capture c) {
var result = new HashMap<BlockPos, BlockState>();
c.blocks.forEach((p, b) -> {
if (p.getY() >= 2 && p.getY() < c.room.height() - 2 && !b.isAir()
&& !b.is(Blocks.STONE_BRICKS) && !b.is(Blocks.CRACKED_STONE_BRICKS)
&& !b.is(Blocks.MOSSY_STONE_BRICKS) && !b.is(Blocks.SMOOTH_STONE)) result.put(p, b);
});
return result;
}
private static boolean approachable(BlockPos p, Set<BlockPos> reached) {
for (Direction d : Direction.Plane.HORIZONTAL) if (reached.contains(p.relative(d))) return true;
return false;
}
private static Set<BlockPos> walk(Capture c, BlockPos start) {
require(stand(c, start), label(c.room) + " starts without two-block standing space at " + start);
var seen = new HashSet<BlockPos>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
while (!queue.isEmpty()) {
var p = queue.removeFirst();
for (Direction direction : Direction.Plane.HORIZONTAL) for (int dy : new int[] {0, 1, -1}) {
var q = p.relative(direction).offset(0, dy, 0);
if (stand(c, q) && seen.add(q)) queue.add(q);
}
for (int dy : new int[] {-1, 1}) {
var q = p.offset(0, dy, 0);
if ((climb(c.block(p)) || climb(c.block(q)) || climb(c.block(p.below())))
&& stand(c, q) && seen.add(q)) queue.add(q);
}
}
return seen;
}
private static boolean stand(Capture c, BlockPos p) {
if (p.getX() < 0 || p.getX() >= c.room.width() || p.getZ() < 0 || p.getZ() >= c.room.depth()
|| p.getY() < 2 || p.getY() >= c.room.height() - 1) return false;
return passable(c.block(p)) && passable(c.block(p.above()))
&& (solid(c.block(p.below())) || c.block(p.below()).getBlock() instanceof StairBlock
|| climb(c.block(p)) || climb(c.block(p.below())));
}
private static boolean climb(BlockState b) { return b.is(Blocks.LADDER) || b.is(Blocks.DARK_OAK_TRAPDOOR); }
private static boolean passable(BlockState b) { return b.isAir() || climb(b) || b.getBlock() instanceof RailBlock || b.is(Blocks.LEVER); }
private static boolean solid(BlockState b) { return b.canOcclude() && b.getFluidState().isEmpty(); }
private static void nativeChunkPartition() {
var r = room(RuinsPlan17.Kind.WORKSHOP, 2, 15, 1); var all = render(r); var chunks = new Capture(r);
for (int cz = r.maxZ() >> 4; cz >= r.minZ() >> 4; cz--) for (int cx = r.maxX() >> 4; cx >= r.minX() >> 4; cx--) {
final int chunkX = cx, chunkZ = cz;
RuinsRenderer17.render(r, new LostCityRenderer.BlockWriter() {
private boolean belongs(int x, int z) { return ((r.minX() + x) >> 4) == chunkX && ((r.minZ() + z) >> 4) == chunkZ; }
@Override public void set(int x, int y, int z, BlockState state) { if (belongs(x, z)) chunks.set(x, y, z, state); }
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { if (belongs(x, z)) chunks.chest(x, y, z, table, seed); }
});
}
require(chunks.blocks.equals(all.blocks) && chunks.loot.equals(all.loot), "Chunk order changes saved room rendering");
}
private record Loot(ResourceKey<LootTable> table, long seed) {}
private static final class Capture implements LostCityRenderer.BlockWriter {
final RuinsPlan17.Room room;
final Map<BlockPos, BlockState> blocks = new HashMap<>();
final Map<BlockPos, Loot> loot = new HashMap<>();
Capture(RuinsPlan17.Room room) { this.room = room; }
@Override public void set(int x, int y, int z, BlockState state) {
int wx = room.minX() + x, wy = room.minY() + y, wz = room.minZ() + z;
require(room.contains(wx, wy, wz), label(room) + " wrote outside its saved exact body/support mask: " + x + "/" + y + "/" + z);
require(wy >= room.boundsMinY() && wy <= room.boundsMaxY(), "Write escapes native vertical bounds");
blocks.put(new BlockPos(x, y, z), state);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
var pos = new BlockPos(x, y, z);
require(loot.put(pos, new Loot(table, seed)) == null, "Duplicate deferred chest");
set(x, y, z, Blocks.CHEST.defaultBlockState());
}
BlockState block(BlockPos p) { return blocks.getOrDefault(p, Blocks.AIR.defaultBlockState()); }
}
private static void require(boolean test, String message) { if (!test) throw new AssertionError(message); }
}
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "0.1.0-alpha.16"
version = "0.1.0-alpha.17"
pack-format = "packwiz:1.1.0"
[index]