feat(worldgen): add reversible alpha19 heritage structures and routes
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-09 22:42:32 +02:00
parent f2da922bad
commit c23e72b8e6
54 changed files with 4021 additions and 51 deletions
+22
View File
@@ -1,5 +1,27 @@
# Changelog
## 0.1.0-alpha.19 — 2026-09-09
- Quatorze nouvelles infrastructures abandonnées sur l’île initiale, avec pièces,
accès, mobilier, secrets et outils vanilla associés. La présence varie selon
la graine et le relief ; les quatorze types ont été rencontrés en tests natifs.
- Voies de fret aux rails incomplets et chemins entre destinations réelles.
Le témoin Moyen, graine 0, contient treize lieux et sept liaisons, dont un
fret de 499 blocs entre dépôt et pompage. Tracés enregistrés par chunk.
- Option FR/EN « Structures expérimentales » dans le bouton natif Personnaliser,
trois tailles conservées. Codec `sanctuary:island_v19` et journal 19 ; les
anciens mondes gardent leurs plans et futurs chunks. Un nouveau monde avec
loption désactivée conserve le socle urbain sans les expériences 17/19.
- Commande opérateur `/sanctuary structures` pour relever les entrées.
La ville et ses sous-sols sont conservés ; les systèmes de la bêta restent
à construire.
`check build assemblePack` passe, dont les huit GameTests généraux. Les trois
formats sont vérifiés nativement ; le témoin moyen se rouvre avec ses 112 pièces,
13 coffres, 68 cibles accessibles et la modification témoin. Les identités 17
restent identiques à la release publiée. JAR, MRpack et ZIP Prism vérifiés ;
publication en préparation. Voir [les preuves et limites](docs/testing-alpha19.md).
## 0.1.0-alpha.17 — 2026-09-09
- Prototype de trois réseaux oubliés sur l’île initiale : mines et ateliers,
+16 -2
View File
@@ -20,6 +20,20 @@ trois familles de réseaux oubliés expérimentaux rejoignent la ville et ses gr
sous-sols sur les nouvelles îles de départ. Le canal packwiz et la même instance
Prism sont à jour ; voir [Distribution](docs/packwiz.md).
## Alpha.19 — anciennes infrastructures, livraison préparée
Le chantier ajoute une première passe des quatorze lieux du
[catalogue](docs/structures-audit.md), avec leurs accès, des petits butins vanilla
et des voies de fret ou chemins abandonnés entre destinations réelles.
Loption **Structures expérimentales** reste enregistrée à la création du monde.
La nouvelle génération garde les anciennes sauvegardes intactes. Aucun système
de progression, mailbox, caméra ou réacteur nest activé.
Les trois tailles sont vérifiées en génération native ; création et réouverture
conservent les structures et les coffres. `check build assemblePack` passe.
Pour voir le grand fret : **Moyen, graine 0**, option activée ;
`/sanctuary structures` liste les entrées. Voir [les preuves et la carte](docs/testing-alpha19.md).
La publication et la synchronisation Prism sont en cours.
## Alpha.17 — prototype des réseaux oubliés
La nouvelle génération expérimentale recherche trois petits réseaux sur l’île
@@ -409,7 +423,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.17 |
| Sanctuary / pack | 0.1.0-alpha.19 |
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
@@ -438,7 +452,7 @@ décrits dans [Validation](docs/testing.md).
Résultats :
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.17.jar` : mod à installer avec
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.19.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).
+33
View File
@@ -717,6 +717,39 @@ Aucune structure, règle de butin, version binaire ou instance Prism modifiée.
La sélection des lieux et la réalisation des axes feront lobjet du chantier
suivant ; ce catalogue nest pas une liste dajouts tous validés.
### WG-22 — Anciennes infrastructures et liaisons — alpha.19
**Branche :** `codex/heritage-worldgen-alpha19`.
**Demande validée :** première passe de toutes les nouvelles structures du
catalogue WG-21 et de leurs liaisons avant la bêta, en conservant la possibilité
de revenir à une génération sans ce chantier.
- Ajouter les quatorze lieux avec architectures, entrées, pièces, mobilier et
petits butins vanilla associés à leur usage. Présence selon le terrain ;
relever les lieux réellement générés et donner les coordonnées des témoins.
- Planifier des voies de fret et chemins entre destinations réelles, avec
sections abandonnées lisibles, appuis et passages vérifiés. Rechercher les
raccords aux anciennes installations dans un budget borné.
- Garder une majorité naturelle à l’île ; conserver le terrain, lhydrologie,
la ville et les réseaux existants comme base. Première île uniquement.
- Nouveau codec 19, option expérimentale sauvegardée à la création du monde.
Option désactivée : socle urbain seul. Les mondes 17 et antérieurs gardent
leur génération, leurs pièces et leurs futurs chunks. Aucune conversion,
suppression de structures ou rétrogradation dune sauvegarde 19.
- Les bâtiments préparent les futurs services ; mailbox, deposit boxes,
caméra, réacteur, progression et récompenses personnelles restent pour la
bêta. Les coffres de cette passe sont partagés et utilisent du butin vanilla.
- Vérifier créations/réouvertures, option désactivée, anciens mondes, supports,
circulations et vrais placements. `check build assemblePack`, artefacts,
publication packwiz et mise à jour de la même instance Prism.
**État : code et artefacts vérifiés, publication en cours.** Voir
[les essais natifs et la carte des lieux](testing-alpha19.md). Version 19 demandée
explicitement ; aucune version 18 publiée
dans ce dépôt à louverture du chantier. Le catalogue est maintenant accepté
pour une première passe, sans imposer sa présence exhaustive sur chaque graine.
## 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.
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

+169
View File
@@ -0,0 +1,169 @@
# Alpha.19 — anciennes infrastructures de Sanctuary
**État : code et artefacts vérifiés, publication en préparation.** Ticket WG-22, branche
`codex/heritage-worldgen-alpha19`. Minecraft 26.3-pre-2, mêmes dépendances que
lalpha.17. La numérotation 19 est demandée explicitement ; aucune 18 nest
intercalée dans ce dépôt.
## Périmètre et réversibilité
Première passe des quatorze lieux du [catalogue](structures-audit.md) : gare de
marchandises, centre postal, dépôt communal, cartothèque, observatoire,
maintenance ferroviaire, pompage, sous-station, serre, quai dexpédition,
relais radio, club photo, vestiaires/salles étranges et refuge de chantier.
Les plans recherchent des sites admissibles et des voies de fret ou chemins
entre destinations réelles ; ils ne garantissent pas tous les lieux sur chaque
graine. Les mesures et coordonnées des essais figureront ci-dessous.
Créer un **nouveau monde Sanctuary** et utiliser **Personnaliser** :
Petit 512, Moyen 724 par défaut ou Grand 1 024, avec **Structures expérimentales**.
Loption est activée par défaut et enregistrée avec le générateur 19. La désactiver
pour un autre nouveau monde conserve le socle urbain sans les réseaux
expérimentaux 17 et les infrastructures 19. Elle ne retire aucun bloc ni ne change
les règles dune sauvegarde existante.
Les mondes 17 et antérieurs gardent leurs codecs, plans, pièces et futurs chunks.
Le preset 17 public est conservé octet par octet sous un identifiant historique
caché à la création. Le générateur 19 et son journal sont distincts. Il nexiste
aucune conversion ou rétrogradation de sauvegarde 19 vers un ancien binaire.
Lancienne release reste disponible pour ses propres mondes, et loption dun
nouveau monde permet de comparer les deux configurations du nouveau binaire.
Les nouveaux lieux sont réservés à l’île initiale. La ville et les anciens
réseaux servent de base ; leurs protections et celles de lhydrologie restent
respectées. Les liaisons vers de futurs continents seront construites après
leur apparition, sans présumer leurs emplacements dans cette génération.
## Objets et systèmes
Les bâtiments suggèrent leurs usages futurs par leurs pièces et aménagements.
Mailbox, deposit boxes, caméra, réacteur, progression et récompenses
personnelles ne sont pas activés. Chaque nouveau lieu dispose dune table
de coffre propre à la génération 19, avec un outil vanilla lié à son usage et
quelques fournitures. Les coffres sont partagés et ne se renouvellent pas.
La valeur des rails, équipements et blocs récupérables du décor sajoute au
butin. Leur distribution doit être constatée sur les témoins, sans transformer
cette première passe en une garantie d’équilibrage économique pour la bêta.
## Vérifications prévues
- Placement des quatorze architectures, ouvertures, circulation, orientation
et appuis du mobilier ; accès réel au coffre et retour possible.
- Plans bornés sur les trois tailles, terrain non occupé, supports, continuité
des tracés et distinction entre ballast praticable et rails retirés.
- Pièces natives et références locales, y compris pour les longues voies ;
géométrie, orientation et rails sauvegardés dans un schéma strict.
- Création et réouverture dun monde 19 avec modification témoin ; conservation
des starts, butins différés, accès, routes et supports.
- Option désactivée, paramètres sauvegardés, génération 17 comparée aux preuves
de la version publiée, refus des migrations implicites de journaux.
- `./gradlew check build assemblePack`, artefacts et index, publication immuable
puis canal packwiz et deux synchronisations de la même instance Prism.
Les smokes des quatorze architectures, de leurs quatre orientations, des
plans sur les trois tailles et des pièces natives passent. Le mode désactivé
est vérifié dans un monde 19 neuf : aucun site ni liaison expérimental.
`./gradlew check build assemblePack` passe, avec les huit GameTests généraux,
125 chunks FULL, 5 923 blocs deau conservés et 1 800 ticks de fluides.
Le JAR, le MRpack, les hashes du pack et le ZIP Prism sont vérifiés.
La publication et la synchronisation de linstance restent en cours. Aucun essai client ou
Windows nest encore effectué.
## Témoin natif Moyen, graine 0
Créer un nouveau monde avec **Structures expérimentales activées**.
`/sanctuary structures` liste les entrées prévues sans charger de chunks ;
`/locate structure sanctuary:heritage_networks` retrouve lentrée dun site.
Le relevé ci-dessous provient des placements et parcours vérifiés par le moteur.
![Entrées et tracés du témoin alpha.19](images/heritage19-seed0.svg)
Treize des quatorze lieux apparaissent. **La cartothèque est absente de cette
graine** : la recherche na pas trouvé demplacement admissible. La ville et les
trois réseaux souterrains 17 sont conservés. Les raccords recherchés vers la
ville et lancienne mine ne sont pas admis sur ce témoin.
Les sept liaisons comprennent un tracé de fret **dépôt → pompage de 499 blocs**,
dune étendue de 245 blocs sur X et 181 sur Z. Son ballast est praticable ;
478 rails sont présents et 21 sont volontairement manquants. Les autres voies
et chemins relient gare, dépôt, atelier, poste, club photo, serre et vestiaires.
Le quai, lobservatoire, le relais, la sous-station et le refuge gardent leurs
propres accès au terrain, même sans liaison avec ce réseau.
La création et la réouverture dans deux JVM distinctes passent chacune les deux
GameTests : **112 pièces natives, 111 chunks FULL, 80 487 blocs contrôlés,
2 882 contrôles de portance, 13 coffres différés et 68 cibles accessibles**.
Trois tirages de chacune des quatorze tables de butin sont vérifiés
séparément, sans ouvrir les coffres persistants. Une modification témoin à
`-204 / 255 / -13` est conservée à la réouverture.
Empreinte stable du plan :
`7f476c2ecca703a05bf88a550c61cc5cbeff2594024c43ccd9f3620bbc2ad3de`.
JVM de création `36366`, de réouverture `36481`. Reçus locaux ignorés :
`build/alpha19-evidence/heritage19-create.json` et `heritage19-reload.json`.
La planification supplémentaire utilise 15 144 colonnes natives, dans un plafond
de 16 000, en 20,7 s sur le Mac de développement. Ce temps concerne ce plan,
pas le chargement total du jeu. Les routes sont découpées par chunk ; aucun
start ne couvre toute leur longueur ni ne charge une destination lointaine.
La prospection 19 consulte les plans hydrologiques de toute lenveloppe de
départ, avant de publier les sites. Le contrôle général utilise donc pour 19
une limite géographique calculée : 5 régions possibles pour Petit, 9 pour
Moyen/Grand, avec les budgets de colonnes et de sondes contrôlés séparément.
Le témoin Moyen en consulte 5 ; les quatre régions périphériques représentent
3,38 s de calcul hydrologique supplémentaire dans le relevé du build.
Ces consultations ne matérialisent aucun chunk. Les générations historiques
conservent le contrôle initial de 4 régions maximum autour du spawn.
```sh
./gradlew :sanctuary:runGameTest -PsanctuaryHeritage19Tests=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false
./gradlew :sanctuary:runGameTest -PsanctuaryHeritage19Tests=true -PsanctuaryExpansionReload=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false
```
## Autres tailles et couverture du catalogue
Les deux GameTests natifs passent aussi sur Petit et Grand, graine 42.
Les quatorze architectures ont ainsi chacune été rencontrées et parcourues
sur le terrain réel, en complément des rotations vérifiées par les smokes.
| Taille et graine | Lieux | Liaisons | Pièces natives | Chunks FULL |
| --- | ---: | ---: | ---: | ---: |
| Petit 512, 42 | 10 | 2 | 35 | 44 |
| Moyen 724, 0 | 13 | 7 | 112 | 111 |
| Grand 1 024, 42 | 13 | 4 | 47 | 61 |
Pour visiter la cartothèque : **Petit, graine 42**, entrée
`-101 / 251 / -25`, ou **Grand, graine 42**, entrée `-252 / 249 / 146`.
Le Petit testé na ni poste, ni observatoire, ni atelier ferroviaire, ni quai.
Le Grand testé na pas de quai. Aucun grand axe nest admis sur ces deux témoins :
les portées maximales de leurs liaisons sont respectivement de 32 et 55 blocs.
Le témoin Moyen, graine 0, reste celui conseillé pour découvrir le grand fret.
Ces résultats rendent explicite la variation de cette première passe ; ils ne
constituent pas une garantie de présence ou de connectivité pour chaque graine.
Reçus locaux ignorés : `build/alpha19-evidence/small42-create.json` (JVM 36591)
et `large42-create.json` (JVM 36669).
## Compatibilité 17 vérifiée
Deux JVM de développement distinctes créent puis rouvrent le témoin 17
**Moyen, graine 0**, avec le nouveau binaire. Les deux GameTests passent dans
chaque exécution. Le codec, les starts natifs, les butins différés et les
parcours restent identiques aux preuves de lalpha.17 publiée. Le bloc dor
placé par le test est conservé à la réouverture.
Empreinte du plan 17 :
`7358ecd7841c03be1977a7f45f37c0c3ef83a32dc27e8d3ec4f0d85d6ee5d3ef`.
Les trois starts restent ceux documentés dans [le témoin 17](testing-alpha17.md).
Reçus locaux ignorés : `build/alpha19-evidence/legacy17-create.json` et
`legacy17-reload.json`.
```sh
./gradlew :sanctuary:runGameTest -PsanctuaryRuins17Tests=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false
./gradlew :sanctuary:runGameTest -PsanctuaryRuins17Tests=true -PsanctuaryExpansionReload=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false
```
Ces commandes visent uniquement le monde de développement ignoré. La première
le recrée ; la seconde conserve ce même monde et lit ses preuves précédentes.
+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.17
pack_version=0.1.0-alpha.17
mod_version=0.1.0-alpha.19
pack_version=0.1.0-alpha.19
maven_group=fr.koka.sanctuary
+16 -2
View File
@@ -40,13 +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' : providers.gradleProperty('sanctuaryLostCity16Tests').map { it.toBoolean() }.getOrElse(false) ? '16' : '17'))
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' : providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false) ? '17' : '19'))
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.heritage19', providers.gradleProperty('sanctuaryHeritage19Tests').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'))
@@ -69,10 +70,12 @@ tasks.named('processGametestResources') {
inputs.property('lostCity16', lostCity16)
def ruins17 = providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('ruins17', ruins17)
def heritage19 = providers.gradleProperty('sanctuaryHeritage19Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('heritage19', heritage19)
def voidReuse = providers.gradleProperty('sanctuaryVoidReuseTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('voidReuse', voidReuse)
filesMatching('fabric.mod.json') {
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
expand(gametest_entrypoint: heritage19 ? 'fr.koka.sanctuary.gametest.Heritage19WorldGameTests' : 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')
@@ -415,3 +418,14 @@ tasks.named('check') { dependsOn('lostCityFoundation16Smoke') }
}
tasks.named('check') { dependsOn(smoke) }
}
// Local native starts keep the long heritage routes bounded by Minecraft's reference range.
['Planner', 'Renderer', 'Piece'].each { component ->
def smoke = tasks.register("heritage19${component}Smoke", JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = "fr.koka.sanctuary.worldgen.heritage.Heritage${component}19Smoke"
}
tasks.named('check') { dependsOn(smoke) }
}
@@ -0,0 +1,452 @@
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.heritage.HeritagePiece19;
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19;
import fr.koka.sanctuary.worldgen.heritage.HeritageRenderer19;
import fr.koka.sanctuary.worldgen.heritage.HeritageStructures19;
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.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
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.item.Item;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.DoorBlock;
import net.minecraft.world.level.block.TrapDoorBlock;
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.LootParams;
import net.minecraft.world.level.storage.loot.LootTable;
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
/** Native creation/reload proof confined to a disposable generation19 starter island. */
public final class Heritage19WorldGameTests {
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
@GameTest(maxTicks = 12000)
public void heritagePersistsWithRealDestinations(GameTestHelper helper) {
helper.assertTrue(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator,
"The heritage proof requires a Sanctuary generator");
var generator = (SanctuaryChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
helper.assertTrue(generator.rootGeneration() == 19, "Use a fresh generation19 test world; do not convert an old save");
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 HeritagePlan19 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.heritagePlan19();
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 local starts, saved sites and routes, player-sized entrances, supports and unopened chests on the starter island. "
+ "All14 tool tables are sampled separately without consuming a saved chest. Door states are restored after traversal. "
+ "No personal save or expansion is opened; one disposable outer-wall edit is checked in a second JVM. "
+ "Missing rails are intentional; this proof checks walking and saved rail shapes, not an uninterrupted minecart journey.");
}
void advance() {
try {
if (!prepared) { report.put("stage", "prepare"); prepare(); prepared = true; if (plan.isEmpty()) { finishDisabled(); return; } }
if (nextChunk < chunks.size()) {
report.put("stage", "load_full_chunks"); var pos = chunks.get(nextChunk++);
helper.assertTrue(level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true) instanceof LevelChunk,
"Heritage footprint must reach native FULL: " + pos);
if (nextChunk % 20 == 0) SanctuaryMod.LOGGER.info("Heritage19 loaded {}/{} footprint chunks", nextChunk, chunks.size());
helper.runAtTickTime(helper.getTick() + 1, this::advance); return;
}
report.put("stage", "verify_native_geometry"); verifyPlaced(); report.put("passed", true);
if (!reload) {
helper.assertTrue(gold != null && level.getBlockState(gold).isCollisionShapeFullBlock(level, gold), "Gold witness must replace a solid wall away from circulation");
level.setBlock(gold, Blocks.GOLD_BLOCK.defaultBlockState(), 3); report.put("gold_witness", coords(gold));
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush native starts, unopened loot and the wall edit");
report.put("world_flushed_before_shutdown", true);
} else {
helper.assertTrue(level.getBlockState(gold).is(Blocks.GOLD_BLOCK), "Reload replaced the player's 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("Heritage19 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() == 19
&& copy.experimentalStructures() == generator.experimentalStructures(), "Generation19 codec lost its experimental switch");
report.put("generator_codec", encoded); report.put("plan_sha256", planHash(plan));
report.put("planned_sites", plan.sites().stream().map(site -> Map.of("id", site.id(), "kind", site.kind().name(),
"entry", coords(HeritagePiece19.locator(site)), "floor_y", site.floorY(), "foundation_depth", site.foundationDepth(), "footings", site.supports().size())).toList());
report.put("planned_links", plan.links().stream().map(link -> Map.of("id", link.id(), "from", link.fromId(), "to", link.toId(), "kind", link.kind().name(), "length", link.path().size(), "path", pathCoords(link))).toList());
report.put("planning_samples", plan.samples()); report.put("planning_columns", plan.columns()); report.put("planning_diagnostics", plan.diagnostics());
helper.assertTrue(plan.samples() <= HeritagePlan19.MAX_SAMPLES && plan.columns() <= HeritagePlan19.MAX_COLUMNS, "Heritage planning exceeded its budget");
verifyLootTables(helper, report);
if (reload) {
previous = JsonParser.parseString(Files.readString(proof(level))).getAsJsonObject();
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("world_flushed_before_shutdown").getAsBoolean(), "Reload needs a successful flushed creation proof");
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload must use a second 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 selected another world");
helper.assertTrue(previous.get("generator_codec").equals(encoded) && previous.get("plan_sha256").getAsString().equals(planHash(plan)), "Saved settings or planned identities changed on reload");
if (previous.has("gold_witness")) gold = readPos(previous.getAsJsonArray("gold_witness"));
}
if (!generator.experimentalStructures() || !level.structureManager().shouldGenerateStructures()) {
helper.assertTrue(plan.isEmpty() && plan.segments().isEmpty() && plan.links().isEmpty(), "Disabled generation published heritage geometry"); return;
}
helper.assertTrue(!plan.isEmpty(), "Enabled reference island has no heritage site");
report.put("present_site_kinds", plan.sites().stream().map(s -> s.kind().name()).distinct().sorted().toList());
report.put("missing_site_kinds", java.util.Arrays.stream(HeritagePlan19.Kind.values()).filter(k -> plan.sites().stream().noneMatch(s -> s.kind() == k)).map(Enum::name).toList());
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
var origins = new HashSet<ChunkPos>(); var uniqueChunks = new HashSet<ChunkPos>();
plan.sites().forEach(s -> origins.add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
plan.segments().forEach(s -> origins.add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
for (var origin : origins.stream().sorted(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList()) {
var chunk = level.getChunk(origin.x(), origin.z(), ChunkStatus.STRUCTURE_STARTS, true);
var start = chunk.getStartForStructure(structure.value());
helper.assertTrue(start != null && start.isValid(), "Accepted site/route has no native start at " + origin); starts.add(start);
for (var piece : start.getPieces()) {
helper.assertTrue(piece instanceof HeritagePiece19, "Heritage loaded a foreign piece type");
var box = piece.getBoundingBox();
helper.assertTrue(Math.abs((long) (box.minX() >> 4) - origin.x()) <= 7 && Math.abs((long) (box.maxX() >> 4) - origin.x()) <= 7
&& Math.abs((long) (box.minZ() >> 4) - origin.z()) <= 7 && Math.abs((long) (box.maxZ() >> 4) - origin.z()) <= 7, "Native start references a distant route endpoint");
box.intersectingChunks().forEach(uniqueChunks::add);
}
}
for (var site : plan.sites()) uniqueChunks.add(ChunkPos.containing(HeritagePiece19.locator(site)));
for (var link : plan.links()) for (var point : link.path()) uniqueChunks.add(new ChunkPos(point.x() >> 4, point.z() >> 4));
helper.assertTrue(uniqueChunks.size() <= 1024, "Bounded proof footprint exceeds1024 chunks");
chunks.addAll(uniqueChunks.stream().sorted(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList());
}
void finishDisabled() throws IOException {
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
var start = level.getChunk(0, 0, ChunkStatus.STRUCTURE_STARTS, true).getStartForStructure(structure.value());
helper.assertTrue(start == null || !start.isValid(), "Disabled heritage emitted a native start");
helper.assertTrue(generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false) == null, "Disabled heritage is locatable");
report.put("empty_plan_verified", true); report.put("passed", true);
if (!reload) { helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save 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(HeritageStructures19.KEY);
var context = StructurePieceSerializationContext.fromLevel(level); var summaries = new ArrayList<Map<String, Object>>();
int blocks = 0, chests = 0, pieces = 0, supports = 0;
for (var start : starts) {
var origin = start.getChunkPos(); var saved = start.createTag(context, origin); String nativeHash = hash(saved.toString());
var copy = StructureStart.loadStaticStart(context, saved, level.getSeed());
helper.assertTrue(copy != null && copy.isValid() && copy.createTag(context, origin).equals(saved), "Native start failed an exact NBT roundtrip");
var expectedPayloads = new HashSet<net.minecraft.nbt.CompoundTag>();
plan.sites().stream().filter(s -> s.startChunkX() == origin.x() && s.startChunkZ() == origin.z()).forEach(s -> expectedPayloads.add(HeritagePiece19.encodeSite(s)));
plan.segments().stream().filter(s -> s.startChunkX() == origin.x() && s.startChunkZ() == origin.z()).forEach(s -> expectedPayloads.add(HeritagePiece19.encodeSegment(s)));
var actualPayloads = new HashSet<net.minecraft.nbt.CompoundTag>();
if (reload) {
JsonObject prior = null;
for (var value : previous.getAsJsonArray("starts")) { var item = value.getAsJsonObject(); var at = item.getAsJsonArray("start_chunk"); if (at.get(0).getAsInt() == origin.x() && at.get(1).getAsInt() == origin.z()) prior = item; }
helper.assertTrue(prior != null && prior.get("native_start_sha256").getAsString().equals(nativeHash), "Reload changed saved geometry or native reference count");
}
var loot = new ArrayList<Map<String, Object>>();
for (var value : start.getPieces()) {
var piece = (HeritagePiece19) value; pieces++;
var tag = piece.createTag(context); var restored = new HeritagePiece19(context, tag);
helper.assertTrue(restored.createTag(context).equals(tag), "Saved piece parameters changed on native load");
actualPayloads.add(tag.getCompound("SanctuaryHeritage").orElseThrow());
var corrupt = tag.copy(); corrupt.getCompound("SanctuaryHeritage").orElseThrow().putInt("Schema", 20);
boolean rejected = false; try { new HeritagePiece19(context, corrupt); } catch (IllegalArgumentException expected) { rejected = true; }
helper.assertTrue(rejected, "Unknown schema must fail closed");
piece.getBoundingBox().intersectingChunks().forEach(at -> helper.assertTrue(level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true)
.getReferencesForStructure(structure.value()).contains(origin.pack()), "FULL chunk lost its native structure reference"));
var expected = rendered(piece); blocks += expected.blocks().size();
for (var entry : expected.blocks().entrySet()) {
var at = entry.getKey(); var wanted = entry.getValue(); helper.assertTrue(piece.getBoundingBox().isInside(at), "Renderer escaped saved bounds");
if (reload && at.equals(gold)) continue;
helper.assertTrue(level.getBlockState(at).is(wanted.getBlock()), "Clipping or decoration changed heritage at " + at + ": expected " + wanted + " got " + level.getBlockState(at));
helper.assertTrue(!wanted.is(Blocks.SPAWNER), "Heritage must not create spawners");
}
if (piece.isSite()) {
supports += verifySiteBearing(helper, generator, plan, piece.site());
if (!reload && gold == null) {
gold = expected.blocks().entrySet().stream().filter(e -> isOuterWall(piece.site(), e.getKey()) && e.getValue().isCollisionShapeFullBlock(level, e.getKey()))
.map(Map.Entry::getKey).sorted(Comparator.<BlockPos>comparingInt(p -> p.getY()).thenComparingInt(p -> p.getX()).thenComparingInt(p -> p.getZ())).findFirst().orElse(null);
}
} else for (var cell : piece.segment().cells()) { supports++; assertRock(helper, new BlockPos(cell.x(), cell.minY() - 1, cell.z())); }
for (var chest : expected.chests()) {
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Deferred chest is missing");
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
helper.assertTrue(entity.getLootTable() != null && entity.getLootTable().equals(chest.table()) && entity.getLootTableSeed() == chest.seed(), "Deferred loot table/seed was lost or consumed");
if (piece.isSite()) helper.assertTrue(chest.table().equals(lootKey(piece.site().kind())), "Site uses another kind's tool table");
loot.add(Map.of("position", coords(chest.position()), "table", chest.table().identifier().toString(), "seed", chest.seed())); chests++;
}
}
helper.assertTrue(actualPayloads.equals(expectedPayloads) && start.getPieces().size() == expectedPayloads.size(), "Native start lost or duplicated planned pieces");
summaries.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", nativeHash, "pieces", start.getPieces().size(), "unopened_chests", loot));
}
report.put("starts", summaries); 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("stage", "verify_site_routes"); var sites = new ArrayList<Map<String, Object>>(); for (var site : plan.sites()) sites.add(verifyAccess(helper, site)); report.put("native_sites", sites);
report.put("stage", "verify_read_only_landings");
report.put("read_only_landmarks", verifyReadOnlyLandings(helper, generator, plan));
report.put("stage", "verify_island_routes"); report.put("native_links", verifyLinks(helper, plan));
int greatestSpan = plan.links().stream().mapToInt(Heritage19WorldGameTests::routeSpan).max().orElse(0);
report.put("greatest_single_route_span", greatestSpan);
if (level.getSeed() == 0 && generator.initialDiameter() == 724) {
int requiredSpan = generator.initialDiameter() / 4;
report.put("reference_minimum_route_span", requiredSpan);
helper.assertTrue(greatestSpan >= requiredSpan, "Seed0 Medium must demonstrate an island-scale route spanning at least "
+ requiredSpan + " blocks on X or Z; got " + greatestSpan);
}
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
helper.assertTrue(located != null && located.getSecond().is(HeritageStructures19.KEY) && plan.sites().stream().anyMatch(s -> HeritagePiece19.locator(s).equals(located.getFirst())), "Locate must lead to a real exterior site entry");
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(HeritagePiece19 piece) {
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
int x0 = piece.isSite() ? piece.site().minX() : piece.segment().minX();
int y0 = piece.isSite() ? piece.site().floorY() : piece.segment().minY();
int z0 = piece.isSite() ? piece.site().minZ() : piece.segment().minZ();
var writer = new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(new BlockPos(x0 + x, y0 + y, z0 + z), state); }
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
var at = new BlockPos(x0 + x, y0 + y, z0 + z); blocks.put(at, Blocks.CHEST.defaultBlockState()); chests.add(new Chest(at, table, RandomSource.create(seed).nextLong()));
}
};
if (piece.isSite()) HeritageRenderer19.render(piece.site(), writer); else HeritageRenderer19.renderSegment(piece.segment(), writer);
return new Rendered(blocks, chests);
}
private static boolean isOuterWall(HeritagePlan19.Site site, BlockPos pos) {
return pos.getY() >= site.floorY() + 2 && pos.getY() <= site.floorY() + 3
&& (pos.getX() == site.minX() + 2 || pos.getX() == site.maxX() - 2 || pos.getZ() == site.minZ() + 2 || pos.getZ() == site.maxZ() - 2)
&& Math.abs(pos.getX() - site.entry().x()) + Math.abs(pos.getZ() - site.entry().z()) > 6;
}
private static Map<String, Object> verifyAccess(GameTestHelper helper, HeritagePlan19.Site site) {
var level = helper.getLevel(); var expected = rendered(new HeritagePiece19(site)); var doors = new HashMap<BlockPos, BlockState>();
expected.blocks().forEach((at, state) -> {
if ((state.getBlock() instanceof DoorBlock || state.getBlock() instanceof TrapDoorBlock) && state.hasProperty(BlockStateProperties.OPEN)) doors.put(at, level.getBlockState(at));
});
doors.forEach((at, state) -> level.setBlock(at, state.setValue(BlockStateProperties.OPEN, true), 2));
try {
var entry = HeritagePiece19.locator(site);
Predicate<BlockPos> inside = p -> p.equals(entry) || p.getX() >= site.minX() && p.getX() <= site.maxX()
&& p.getZ() >= site.minZ() && p.getZ() <= site.maxZ() && p.getY() >= site.floorY() - 4 && p.getY() < site.maxY();
var reached = walk(helper, entry, inside); int targets = 0;
for (var target : HeritageRenderer19.targets(site)) {
var at = new BlockPos(site.minX() + target.x(), site.floorY() + target.y(), site.minZ() + target.z());
helper.assertTrue(reached.contains(at), "Site " + site.id() + " cannot reach " + target.name() + " at " + at); targets++;
}
return Map.of("id", site.id(), "kind", site.kind().name(), "entry", coords(entry), "standing_cells", reached.size(), "walkable_targets", targets);
} finally { doors.forEach((at, state) -> level.setBlock(at, state, 2)); }
}
private static List<Map<String, Object>> verifyLinks(GameTestHelper helper, HeritagePlan19 plan) {
helper.assertTrue(!plan.links().isEmpty(), "Reference island needs a real route between accepted destinations");
var entries = new HashMap<String, HeritagePlan19.Entry>(); plan.sites().forEach(s -> entries.put(s.id(), s.entry())); plan.landmarks().forEach(s -> entries.put(s.id(), s.entry()));
var cells = new HashMap<Long, HeritagePlan19.Cell>(); plan.segments().forEach(s -> s.cells().forEach(c -> cells.put(column(c.x(), c.z()), c)));
var result = new ArrayList<Map<String, Object>>();
for (var link : plan.links()) {
var from = entries.get(link.fromId()); var to = entries.get(link.toId());
helper.assertTrue(from != null && to != null, "Route points to an absent destination");
var first = link.path().getFirst(); var last = link.path().getLast();
helper.assertTrue(first.x() == from.x() && first.z() == from.z() && first.floorY() + 1 == from.y()
&& last.x() == to.x() && last.z() == to.z() && last.floorY() + 1 == to.y(), "Route does not meet its real entrances");
int present = 0, missing = 0;
for (int i = 0; i < link.path().size(); i++) {
var point = link.path().get(i); var at = new BlockPos(point.x(), point.floorY() + 1, point.z());
helper.assertTrue(standable(helper.getLevel(), at), "Ballast route " + link.id() + " is blocked at " + at);
if (i > 0) { var prior = link.path().get(i - 1); if (point.floorY() > prior.floorY()) helper.assertTrue(clearBody(helper.getLevel(), new BlockPos(prior.x(), prior.floorY() + 2, prior.z())), "Route slope cannot be climbed by a player"); }
var cell = cells.get(column(point.x(), point.z()));
if (cell != null && cell.rail() != HeritagePlan19.Rail.NONE) {
var actual = helper.getLevel().getBlockState(at);
if (cell.railPresent()) {
helper.assertTrue(actual.is(Blocks.RAIL), "Saved existing rail disappeared");
helper.assertTrue(actual.getValue(BlockStateProperties.RAIL_SHAPE).name().equals(cell.rail().name()), "Native rail update changed the intended slope or curve at " + at); present++;
} else { helper.assertTrue(!actual.is(Blocks.RAIL), "An intentionally missing rail regenerated"); missing++; }
}
}
int spanX = link.path().stream().mapToInt(HeritagePlan19.Point::x).max().orElseThrow() - link.path().stream().mapToInt(HeritagePlan19.Point::x).min().orElseThrow();
int spanZ = link.path().stream().mapToInt(HeritagePlan19.Point::z).max().orElseThrow() - link.path().stream().mapToInt(HeritagePlan19.Point::z).min().orElseThrow();
result.add(Map.of("id", link.id(), "from", link.fromId(), "to", link.toId(), "kind", link.kind().name(), "length", link.path().size(),
"span_x", spanX, "span_z", spanZ, "existing_rails", present, "deliberately_missing_rails", missing, "path", pathCoords(link)));
}
return result;
}
private static int routeSpan(HeritagePlan19.Link link) {
int spanX = link.path().stream().mapToInt(HeritagePlan19.Point::x).max().orElseThrow()
- link.path().stream().mapToInt(HeritagePlan19.Point::x).min().orElseThrow();
int spanZ = link.path().stream().mapToInt(HeritagePlan19.Point::z).max().orElseThrow()
- link.path().stream().mapToInt(HeritagePlan19.Point::z).min().orElseThrow();
return Math.max(spanX, spanZ);
}
private static List<List<Integer>> pathCoords(HeritagePlan19.Link link) {
return link.path().stream().map(point -> List.of(point.x(), point.floorY(), point.z())).toList();
}
private static int verifySiteBearing(GameTestHelper helper, SanctuaryChunkGenerator generator, HeritagePlan19 plan, HeritagePlan19.Site site) {
int checked = 0;
// The planner certifies a nine-point bearing grid, either directly below its slab or below a saved short footing.
for (int dx : new int[] {1, 8, 15}) for (int dz : new int[] {1, 8, 15}) {
int x = site.minX() + dx, z = site.minZ() + dz;
var support = site.supports().stream().filter(s -> s.x() == x && s.z() == z).findFirst();
int bottom = support.map(HeritagePlan19.Support::minY).orElse(site.minY());
for (int below = 1; below <= 2; below++) {
var at = new BlockPos(x, bottom - below, z); assertRock(helper, at);
helper.assertTrue(plan.protects(at.getX(), at.getY(), at.getZ()) && generator.protectsCityDecoration(at),
"Site's certified bearing layer is not published in the decoration mask: " + site.id() + " at " + at);
helper.assertTrue(!plan.contains(at.getX(), at.getY(), at.getZ()), "Original bearing rock became a writable heritage volume: " + at);
checked++;
}
}
return checked;
}
private static List<Map<String, Object>> verifyReadOnlyLandings(GameTestHelper helper, SanctuaryChunkGenerator generator, HeritagePlan19 plan) {
var used = new HashSet<String>(); plan.links().forEach(link -> { used.add(link.fromId()); used.add(link.toId()); });
var result = new ArrayList<Map<String, Object>>();
for (var landmark : plan.landmarks()) if (used.contains(landmark.id())) {
var e = landmark.entry(); int checked = 0;
for (int side = -1; side <= 1; side++) {
int x = e.x() + ((e.facing() & 1) == 0 ? side : 0), z = e.z() + ((e.facing() & 1) == 1 ? side : 0);
for (int y = e.y() - 2; y <= e.y() + 2; y++) {
var at = new BlockPos(x, y, z);
helper.assertTrue(plan.protects(x, y, z) && generator.protectsCityDecoration(at), "Read-only arrival lost its decoration mask: " + landmark.id() + " at " + at);
helper.assertTrue(!plan.contains(x, y, z), "Heritage gained write ownership of a historical arrival: " + landmark.id() + " at " + at);
if (y == e.y() - 1) assertRock(helper, at);
if (y >= e.y()) helper.assertTrue(helper.getLevel().getBlockState(at).isAir(), "Decoration obstructed a certified historical arrival: " + at);
checked++;
}
}
result.add(Map.of("id", landmark.id(), "entry", List.of(e.x(), e.y(), e.z()), "read_only_cells", checked));
}
return result;
}
private static void assertRock(GameTestHelper helper, BlockPos at) {
helper.assertTrue(helper.getLevel().getBlockState(at).isSolid() && helper.getLevel().getFluidState(at).isEmpty(), "Lost original rock bearing at " + at);
}
private static void verifyLootTables(GameTestHelper helper, Map<String, Object> report) {
var result = new ArrayList<Map<String, Object>>(); var level = helper.getLevel();
var params = new LootParams.Builder(level).withParameter(LootContextParams.ORIGIN, new Vec3(0, 160, 0)).create(LootContextParamSets.CHEST);
for (var kind : HeritagePlan19.Kind.values()) {
var table = level.getServer().reloadableRegistries().getLootTable(lootKey(kind));
helper.assertTrue(table != LootTable.EMPTY, "Heritage tool table did not load: " + kind);
for (long seed : new long[] {0, 42, -1}) helper.assertTrue(table.getRandomItems(params, seed).stream().anyMatch(stack -> stack.is(tool(kind))), "Tool table is empty or lacks its expected primary tool: " + kind);
result.add(Map.of("kind", kind.name(), "table", lootKey(kind).identifier().toString(), "sampled_seeds", List.of(0, 42, -1)));
}
report.put("tool_tables_sampled_separately", result);
}
private static ResourceKey<LootTable> lootKey(HeritagePlan19.Kind kind) { return ResourceKey.create(Registries.LOOT_TABLE, SanctuaryMod.id("chests/heritage19/" + kind.name().toLowerCase(Locale.ROOT))); }
private static Item tool(HeritagePlan19.Kind kind) {
return switch (kind) {
case OBSERVATORY -> Items.SPYGLASS;
case POST_OFFICE, EXPEDITION_OFFICE, RADIO_RELAY -> Items.COMPASS;
case SUBSTATION, IMPOSSIBLE_SERVICE -> Items.CLOCK;
case FREIGHT_STATION -> Items.MINECART;
case COMMUNAL_DEPOT -> Items.IRON_AXE;
case RAIL_WORKSHOP -> Items.IRON_PICKAXE;
case PUMP_STATION -> Items.BUCKET;
case GREENHOUSE -> Items.IRON_HOE;
case VOID_DOCK -> Items.LEAD;
case PHOTO_CLUB -> Items.BOOK;
case CONSTRUCTION_REFUGE -> Items.IRON_SHOVEL;
};
}
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 or unsupported exterior entrance: " + 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 site traversal escaped its 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 long column(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
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("heritage19-create-proof.json"); }
private static String planHash(HeritagePlan19 plan) { return hash(plan.sites().toString() + plan.landmarks() + plan.links() + plan.segments()); }
private static void write(ServerLevel level, Map<String, Object> report, boolean reload) throws IOException {
var path = Path.of("diagnostics", "heritage19-" + (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); }
}
}
@@ -210,8 +210,30 @@ public final class PopulationDiagnostics {
}
void prepare() throws IOException {
helper.assertTrue(regionsAtInitialSpawn > 0 && regionsAtInitialSpawn <= 4,
"Initial spawn must plan only nearby regions, not materialize the large island: " + regionsAtInitialSpawn);
boolean heritage = level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator sanctuary
&& sanctuary.rootGeneration() == 19 && sanctuary.experimentalStructures();
// Generation19 explicitly prospects destinations across the root island before publishing them.
// A consulted hydrology region is a bounded immutable plan, not a loaded/generated chunk.
// Historical generators retain their original near-spawn contract.
int regionLimit = 4;
if (heritage) {
int extent = capacity.terrainLimit();
var first = capacity.regionAt(-extent, -extent); var last = capacity.regionAt(extent, extent);
regionLimit = 0;
for (int x = first.x(); x <= last.x(); x++) for (int z = first.z(); z <= last.z(); z++) {
var region = new IslandCapacity.Region(x, z);
double nearestX = Math.max(0, Math.max(region.minX(), -region.maxX()));
double nearestZ = Math.max(0, Math.max(region.minZ(), -region.maxZ()));
if (Math.hypot(nearestX, nearestZ) < extent) regionLimit++;
}
helper.assertTrue(regionLimit <= 9, "Heritage starter hydrology must fit within nine local regions");
var plan = ((SanctuaryChunkGenerator) level.getChunkSource().getGenerator()).heritagePlan19();
helper.assertTrue(plan.columns() <= fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.MAX_COLUMNS
&& plan.samples() <= fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.MAX_SAMPLES,
"Island-wide heritage prospecting exceeded its independent column/probe budget");
}
helper.assertTrue(regionsAtInitialSpawn > 0 && regionsAtInitialSpawn <= regionLimit,
"Initial hydrology exceeded its geographic planning scope (limit " + regionLimit + "): " + regionsAtInitialSpawn);
List<IslandCapacity.Region> targets = new ArrayList<>();
targets.add(new IslandCapacity.Region(0,0));
if (capacity.players() == 20) targets.add(new IslandCapacity.Region(1,0));
@@ -45,8 +45,11 @@ final class UnifiedWorldSettingsDiagnostics {
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 historical17 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v17"))).value().createWorldDimensions();
helper.assertTrue(historical17.overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 17
&& old.source().generation() == 17 && old.experimentalStructures(), "Historical17 keeps its original experimental generator");
var generator = (SanctuaryChunkGenerator) standard.overworld();
helper.assertTrue(generator.rootGeneration() == 17 && generator.source().generation() == 17 && generator.experimentalStructures(), "The public preset uses generation17 with its new-world option");
helper.assertTrue(generator.rootGeneration() == 19 && generator.source().generation() == 19 && generator.experimentalStructures(), "The public preset uses generation19 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())
@@ -56,11 +59,11 @@ final class UnifiedWorldSettingsDiagnostics {
}
var ops = RegistryOps.create(JsonOps.INSTANCE, access);
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 activeCodec = generator.rootGeneration() == 19 ? SanctuaryChunkGenerator.HERITAGE19_CODEC : 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");
"Generation19 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");
@@ -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.ruins17") || Boolean.getBoolean("sanctuary.test.expansion") || Boolean.getBoolean("sanctuary.test.lostCity")
if (Boolean.getBoolean("sanctuary.test.heritage19") || 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", 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));
int generation = Integer.getInteger("sanctuary.test.generation", 19);
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19) throw new IllegalArgumentException("Unsupported test generation");
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 19 ? "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);
@@ -48,6 +48,7 @@ public final class SanctuaryMod implements ModInitializer {
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("expansion_island"), fr.koka.sanctuary.expansion.ExpansionDensity.CODEC);
fr.koka.sanctuary.expansion.ExpansionRuntime.register();
fr.koka.sanctuary.expansion.ExpansionCommands.register();
fr.koka.sanctuary.worldgen.heritage.HeritageCommands19.register();
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("main_island"), MainIslandDensity.CODEC);
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v13"), SanctuaryChunkGenerator.CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v13"), SanctuaryBiomeSource.CODEC);
@@ -60,6 +61,9 @@ public final class SanctuaryMod implements ModInitializer {
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();
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v19"), SanctuaryChunkGenerator.HERITAGE19_CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v19"), SanctuaryBiomeSource.HERITAGE19_CODEC);
fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.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);
@@ -51,11 +51,11 @@ public final class SanctuaryWorldOptionsScreen extends Screen {
contents.addChild(sizeButton);
diameterHint = contents.addChild(new MultiLineTextWidget(diameterHint(), font)
.setMaxWidth(310).setCentered(true));
if (generator.rootGeneration() == 17) {
if (generator.rootGeneration() == 17 || generator.rootGeneration() == 19) {
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)
contents.addChild(new MultiLineTextWidget(Component.translatable(generator.rootGeneration() == 19 ? "sanctuary.world_options.heritage_hint" : "sanctuary.world_options.experimental_hint"), font)
.setMaxWidth(310).setCentered(true));
}
layout.addToContents(contents);
@@ -122,6 +122,7 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
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);
fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.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, 16 or 17 expansion");
if (climate.equals("lost_city") && (!spatialGeneration(generation) || id.equals("sanctuary"))) throw new IllegalArgumentException("Lost City requires a generation 14, 15, 16, 17 or 19 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 || generation == 17; }
public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16 || generation == 17 || generation == 19; }
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(); }
@@ -25,7 +25,8 @@ public abstract class UnifiedDecorationProtectionMixin {
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()
&& !fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.isPlacing() && generator.protectsCityDecoration(pos))
&& !fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.isPlacing()
&& !fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.isPlacing() && generator.protectsCityDecoration(pos))
callback.setReturnValue(false);
}
@@ -40,7 +41,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 || generator.rootGeneration() == 17)
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19)
&& fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
&& generator.protectsCityDecoration(pos)) return;
if (generator.protectsDecoration(pos) && !region.getBlockState(pos).equals(state)) {
@@ -42,6 +42,10 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 17)));
public static final MapCodec<SanctuaryBiomeSource> HERITAGE19_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 19)));
private final List<Holder<Biome>> allBiomes;
private final PopulationIslandBiomeSource population;
private final boolean cities;
@@ -75,7 +79,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
cityPlans = java.util.Map.copyOf(plans);
}
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; }
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 19 ? HERITAGE19_CODEC : 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);
@@ -45,9 +45,17 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 17, experimental)));
public static final MapCodec<SanctuaryChunkGenerator> HERITAGE19_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
SanctuaryBiomeSource.HERITAGE19_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, 19, 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 fr.koka.sanctuary.worldgen.heritage.HeritagePlan19 heritagePlan19 = fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.EMPTY;
private boolean heritagePrepared;
private volatile Context originContext;
private final int generation;
private net.minecraft.world.level.LevelHeightAccessor heightAccessor;
@@ -83,12 +91,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
}
private SanctuaryChunkGenerator(SanctuaryBiomeSource source, int initialDiameter, int generation, boolean experimental) {
super(source, initialDiameter, true);
this.experimentalStructures = generation == 17 && experimental;
this.experimentalStructures = (generation == 17 || generation == 19) && 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 == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; }
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 19 ? HERITAGE19_CODEC : 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, experimentalStructures);
}
@@ -97,8 +105,10 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
return new SanctuaryChunkGenerator(source().copy(), initialDiameter(), generation, enabled);
}
public fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17() { return ruinsPlan17; }
public fr.koka.sanctuary.worldgen.heritage.HeritagePlan19 heritagePlan19() { return heritagePlan19; }
public boolean intersectsExperimentalStructures(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
return generation == 17 && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ);
return (generation == 17 || generation == 19) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| generation == 19 && heritagePlan19.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ);
}
public IslandCapacity capacity() {
return switch (initialDiameter()) {
@@ -134,11 +144,13 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
context(islands.getFirst());
prepareCities(islands);
prepareRuins(islands);
prepareHeritage(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);
prepareHeritage(islands);
super.setIslands(islands);
}
private void prepareCities(List<ExpansionIsland> islands) {
@@ -149,7 +161,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
source().setCityPlans(java.util.Map.of());
return;
}
if ((generation == 16 || generation == 17) && !cityStructuresEnabled) {
if ((generation == 16 || generation == 17 || generation == 19) && !cityStructuresEnabled) {
cityProtection16 = fr.koka.sanctuary.worldgen.city.LostCityProtection16.EMPTY;
activeCityPlans = java.util.Map.of();
source().setCityPlans(java.util.Map.of());
@@ -165,7 +177,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
}
cityProtection15 = protection.build();
}
if (generation == 16 || generation == 17) {
if (generation == 16 || generation == 17 || generation == 19) {
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
for (var island : islands) {
protection.add(published.get(island.id()), undergroundPlan16(island));
@@ -180,7 +192,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
source().setCityPlans(published);
}
private void prepareRuins(List<ExpansionIsland> islands) {
if (generation != 17 || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return;
if ((generation != 17 && generation != 19) || !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(),
@@ -192,6 +204,22 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
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 void prepareHeritage(List<ExpansionIsland> islands) {
if (generation != 19 || !experimentalStructures || !cityStructuresEnabled || heritagePrepared) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var terrain = context(root);
var sampled = new fr.koka.sanctuary.worldgen.heritage.HeritageTerrain19(root, terrain.generator(), terrain.random(),
heightAccessor, pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()));
long start = System.nanoTime();
sampled.discoverLandmarks(lostCityPlan(root), ruinsPlan17);
var plan = fr.koka.sanctuary.worldgen.heritage.HeritagePlanner19.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
heritagePlan19 = plan;
heritagePrepared = true;
SanctuaryMod.LOGGER.info("Heritage19 seed {}: {} sites, {} routes, {} segments, {} probes, {} native columns in {} ms; {}",
root.seed(), plan.sites().size(), plan.links().size(), plan.segments().size(), plan.samples(), sampled.columnsUsed(),
(System.nanoTime() - start) / 1_000_000, plan.diagnostics());
}
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();
@@ -217,7 +245,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
return cityPlans.computeIfAbsent(cityIdentity(island), ignored -> {
long start = System.nanoTime();
var terrain = context(island);
if (generation == 16 || generation == 17) {
if (generation == 16 || generation == 17 || generation == 19) {
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(),
@@ -288,8 +316,9 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
});
}
public boolean protectsCityDecoration(net.minecraft.core.BlockPos pos) {
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 == 16 || generation == 17 || generation == 19) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 17 || generation == 19) && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ())
|| generation == 19 && heritagePlan19.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());
@@ -311,20 +340,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 || generation == 17) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
return (generation == 16 || generation == 17 || generation == 19) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
}
public fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan undergroundPlan16(ExpansionIsland island) {
if (generation != 16 && generation != 17) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
if (generation != 16 && generation != 17 && generation != 19) 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 && generation != 17) return List.of();
if (generation != 16 && generation != 17 && generation != 19) return List.of();
lostCityPlan(island);
return citySupports16.getOrDefault(cityIdentity(island), List.of());
}
public LostCityPlanner16.Fallback cityReservation16(ExpansionIsland island) {
if (generation != 16 && generation != 17) return null;
if (generation != 16 && generation != 17 && generation != 19) return null;
lostCityPlan(island);
return cityReservations16.get(cityIdentity(island));
}
@@ -336,8 +365,10 @@ 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))
if ((generation == 17 || generation == 19) && 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);
if (generation == 19 && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.KEY))
return fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.findNearest(level, structures, origin, radius, skipReferenced);
return super.findNearestMapStructure(level, structures, origin, radius, skipReferenced);
}
@Override protected Context context(ExpansionIsland island) {
@@ -365,7 +396,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 || generation == 17) && cityStructuresEnabled) {
if ((generation == 16 || generation == 17 || generation == 19) && 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.rootGeneration() == 17) && generator.cityReservation16(island) != null)
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19) && 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 || generator.rootGeneration() == 17)) for (var support : generator.citySupports16(island)) {
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19)) 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 || generator.rootGeneration() == 17) ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
district.parcels().forEach(parcel -> pieces.addPiece((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19) ? 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 || generator.rootGeneration() == 17)) for (var network : generator.undergroundPlan16(island).networks()) {
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19)) 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,34 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.util.Locale;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component;
import net.minecraft.world.level.Level;
/** Read-only operator catalogue. Inspecting a plan neither loads chunks nor activates an expansion. */
public final class HeritageCommands19 {
private HeritageCommands19() {}
public static void register() {
CommandRegistrationCallback.EVENT.register((dispatcher, registries, environment) ->
dispatcher.register(Commands.literal("sanctuary")
.requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS))
.then(Commands.literal("structures").executes(context -> {
var source = context.getSource();
var level = source.getLevel();
if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 19 || !generator.experimentalStructures()) {
source.sendFailure(Component.translatable("sanctuary.heritage.unavailable"));
return 0;
}
var plan = generator.heritagePlan19();
source.sendSuccess(() -> Component.translatable("sanctuary.heritage.header", plan.sites().size(), plan.links().size()), false);
for (var site : plan.sites()) source.sendSuccess(() -> Component.translatable("sanctuary.heritage.entry",
Component.translatable("sanctuary.heritage.kind." + site.kind().name().toLowerCase(Locale.ROOT)),
site.entry().x(), site.entry().y(), site.entry().z()), false);
return plan.sites().size();
}))));
}
}
@@ -0,0 +1,185 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import java.util.ArrayList;
import java.util.Set;
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;
/** Self-contained schema19 geometry. Reload never needs the planner or a distant route destination. */
public final class HeritagePiece19 extends StructurePiece {
private static final Set<String> SITE_KEYS = Set.of("Schema", "Type", "Id", "Kind", "MinX", "FloorY", "MinZ",
"Width", "Depth", "Height", "Facing", "Seed", "FoundationDepth", "Supports", "Entry");
private static final Set<String> ROUTE_KEYS = Set.of("Schema", "Type", "Id", "FromId", "ToId", "Kind",
"StartChunkX", "StartChunkZ", "Cells", "Seed");
private static final Set<String> SUPPORT_KEYS = Set.of("X", "MinY", "Z", "MaxY");
private static final Set<String> ENTRY_KEYS = Set.of("X", "Y", "Z", "Facing");
private static final Set<String> CELL_KEYS = Set.of("X", "FloorY", "Z", "FoundationDepth", "Rail", "RailPresent");
private final HeritagePlan19.Site site;
private final HeritagePlan19.Segment segment;
public HeritagePiece19(HeritagePlan19.Site site) {
super(HeritageStructures19.PIECE, 0, bounds(site));
this.site = site; this.segment = null; setOrientation(null);
}
public HeritagePiece19(HeritagePlan19.Segment segment) {
super(HeritageStructures19.PIECE, 0, bounds(segment));
this.site = null; this.segment = segment; setOrientation(null);
}
public HeritagePiece19(StructurePieceSerializationContext context, CompoundTag tag) {
super(HeritageStructures19.PIECE, tag);
var payload = tag.getCompound("SanctuaryHeritage").orElseThrow(() -> new IllegalArgumentException("Missing saved heritage geometry"));
String type = string(payload, "Type");
if (type.equals("SITE")) { site = decodeSite(payload); segment = null; }
else if (type.equals("ROUTE")) { site = null; segment = decodeSegment(payload); }
else throw new IllegalArgumentException("Unknown heritage piece type");
if (!getBoundingBox().equals(isSite() ? bounds(site) : bounds(segment)))
throw new IllegalArgumentException("Heritage geometry disagrees with its saved native bounds");
setOrientation(null);
}
public boolean isSite() { return site != null; }
public HeritagePlan19.Site site() { if (site == null) throw new IllegalStateException("This heritage piece is a route"); return site; }
public HeritagePlan19.Segment segment() { if (segment == null) throw new IllegalStateException("This heritage piece is a site"); return segment; }
@Override public BlockPos getLocatorPosition() {
if (isSite()) return locator(site);
var cell = segment.cells().getFirst(); return new BlockPos(cell.x(), cell.floorY() + 1, cell.z());
}
public static BlockPos locator(HeritagePlan19.Site site) { return new BlockPos(site.entry().x(), site.entry().y(), site.entry().z()); }
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
tag.put("SanctuaryHeritage", isSite() ? encodeSite(site) : encodeSegment(segment));
}
public static CompoundTag encodeSite(HeritagePlan19.Site site) {
var tag = header("SITE", site.id(), site.kind().name(), site.seed());
tag.putInt("MinX", site.minX()); tag.putInt("FloorY", site.floorY()); tag.putInt("MinZ", site.minZ());
tag.putInt("Width", site.width()); tag.putInt("Depth", site.depth()); tag.putInt("Height", site.height());
tag.putInt("Facing", site.facing()); tag.putInt("FoundationDepth", site.foundationDepth());
var entry = new CompoundTag(); entry.putInt("X", site.entry().x()); entry.putInt("Y", site.entry().y());
entry.putInt("Z", site.entry().z()); entry.putInt("Facing", site.entry().facing()); tag.put("Entry", entry);
var supports = new ListTag();
for (var support : site.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 CompoundTag encodeSegment(HeritagePlan19.Segment segment) {
var tag = header("ROUTE", segment.id(), segment.kind().name(), segment.seed());
tag.putString("FromId", segment.fromId()); tag.putString("ToId", segment.toId());
tag.putInt("StartChunkX", segment.startChunkX()); tag.putInt("StartChunkZ", segment.startChunkZ());
var cells = new ListTag();
for (var cell : segment.cells()) {
var value = new CompoundTag(); value.putInt("X", cell.x()); value.putInt("FloorY", cell.floorY());
value.putInt("Z", cell.z()); value.putInt("FoundationDepth", cell.foundationDepth());
value.putString("Rail", cell.rail().name()); value.putBoolean("RailPresent", cell.railPresent()); cells.add(value);
}
tag.put("Cells", cells); return tag;
}
public static HeritagePlan19.Site decodeSite(CompoundTag tag) {
schema(tag, "SITE", SITE_KEYS);
var entry = compound(tag, "Entry", ENTRY_KEYS);
var saved = list(tag, "Supports", 9); var supports = new ArrayList<HeritagePlan19.Support>();
for (var value : saved) {
var support = element(value, SUPPORT_KEYS);
supports.add(new HeritagePlan19.Support(coordinate(support, "X"), integer(support, "MinY"),
coordinate(support, "Z"), integer(support, "MaxY")));
}
return new HeritagePlan19.Site(id(tag, "Id"), HeritagePlan19.Kind.valueOf(string(tag, "Kind")),
coordinate(tag, "MinX"), integer(tag, "FloorY"), coordinate(tag, "MinZ"), integer(tag, "Width"),
integer(tag, "Depth"), integer(tag, "Height"), integer(tag, "Facing"), seed(tag), integer(tag, "FoundationDepth"),
supports, new HeritagePlan19.Entry(coordinate(entry, "X"), integer(entry, "Y"), coordinate(entry, "Z"), integer(entry, "Facing")));
}
public static HeritagePlan19.Segment decodeSegment(CompoundTag tag) {
schema(tag, "ROUTE", ROUTE_KEYS);
var saved = list(tag, "Cells", 256); var cells = new ArrayList<HeritagePlan19.Cell>();
for (var value : saved) {
var cell = element(value, CELL_KEYS);
type(cell, "RailPresent", Tag.TAG_BYTE);
byte present = cell.getByte("RailPresent").orElseThrow();
if (present != 0 && present != 1) throw new IllegalArgumentException("Invalid saved rail presence");
cells.add(new HeritagePlan19.Cell(coordinate(cell, "X"), integer(cell, "FloorY"), coordinate(cell, "Z"),
integer(cell, "FoundationDepth"), HeritagePlan19.Rail.valueOf(string(cell, "Rail")), present == 1));
}
String from = id(tag, "FromId"), to = id(tag, "ToId");
var kind = HeritagePlan19.RouteKind.valueOf(string(tag, "Kind"));
if (from.equals(to) && kind != HeritagePlan19.RouteKind.PATH)
throw new IllegalArgumentException("Only a local pedestrian landing may join a destination to itself");
return new HeritagePlan19.Segment(id(tag, "Id"), from, to, kind,
integer(tag, "StartChunkX"), integer(tag, "StartChunkZ"), cells, seed(tag));
}
private static CompoundTag header(String type, String id, String kind, long seed) {
var value = new CompoundTag(); value.putInt("Schema", 19); value.putString("Type", type);
value.putString("Id", id); value.putString("Kind", kind); value.putLong("Seed", seed); return value;
}
private static void schema(CompoundTag tag, String expected, Set<String> keys) {
if (!tag.keySet().equals(keys) || integer(tag, "Schema") != 19 || !string(tag, "Type").equals(expected))
throw new IllegalArgumentException("Unsupported or incomplete saved heritage schema");
}
private static CompoundTag compound(CompoundTag tag, String key, Set<String> keys) {
type(tag, key, Tag.TAG_COMPOUND); return element(tag.get(key), keys);
}
private static CompoundTag element(Tag tag, Set<String> keys) {
if (!(tag instanceof CompoundTag value) || !value.keySet().equals(keys))
throw new IllegalArgumentException("Unsupported or incomplete saved heritage element");
return value;
}
private static ListTag list(CompoundTag tag, String key, int maximum) {
type(tag, key, Tag.TAG_LIST); var value = tag.getList(key).orElseThrow();
if (value.size() > maximum) throw new IllegalArgumentException("Oversized saved heritage list"); return value;
}
private static long seed(CompoundTag tag) { type(tag, "Seed", Tag.TAG_LONG); return tag.getLong("Seed").orElseThrow(); }
private static String id(CompoundTag tag, String key) {
var value = string(tag, key);
if (value.isBlank() || value.length() > 128) throw new IllegalArgumentException("Invalid saved heritage identifier"); return value;
}
private static String string(CompoundTag tag, String key) { type(tag, key, Tag.TAG_STRING); return tag.getString(key).orElseThrow(); }
private static int coordinate(CompoundTag tag, String key) {
int value = integer(tag, key);
if (value < -30_000_000 || value > 30_000_000) throw new IllegalArgumentException("Heritage coordinate outside world bounds"); return value;
}
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 heritage field: " + key);
}
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
final int x0 = isSite() ? site.minX() : segment.minX();
final int y0 = isSite() ? site.floorY() : segment.minY();
final int z0 = isSite() ? site.minZ() : segment.minZ();
var writer = new LostCityRenderer.BlockWriter() {
private BlockPos world(int x, int y, int z) { return new BlockPos(Math.addExact(x0, x), Math.addExact(y0, y), Math.addExact(z0, 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);
}
};
HeritageStructures19.placing(() -> {
if (isSite()) HeritageRenderer19.render(site, writer);
else HeritageRenderer19.renderSegment(segment, writer);
});
}
public static BoundingBox bounds(HeritagePlan19.Site site) {
return new BoundingBox(site.minX(), site.boundsMinY(), site.minZ(), site.maxX(), site.boundsMaxY(), site.maxZ());
}
public static BoundingBox bounds(HeritagePlan19.Segment segment) {
return new BoundingBox(segment.minX(), segment.boundsMinY(), segment.minZ(), segment.maxX(), segment.boundsMaxY(), segment.maxZ());
}
}
@@ -0,0 +1,200 @@
package fr.koka.sanctuary.worldgen.heritage;
import java.util.HashSet;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** Immutable generation19 catalogue and real-destination routes. Pieces save geometry, not terrain queries. */
public final class HeritagePlan19 {
private final List<Site> sites;
private final List<Landmark> landmarks;
private final List<Link> links;
private final List<Segment> segments;
private final int samples, columns;
private final Map<Kind, String> diagnostics;
private final Map<Long, List<Range>> protectedColumns;
private final Map<Long, List<Box>> protectedChunks;
public static final int MAX_COLUMNS = 16_000, MAX_SAMPLES = 800_000;
public static final HeritagePlan19 EMPTY = new HeritagePlan19(List.of(), List.of(), List.of(), List.of(), 0, 0, Map.of());
public enum Kind { FREIGHT_STATION, POST_OFFICE, COMMUNAL_DEPOT, EXPEDITION_OFFICE, OBSERVATORY,
RAIL_WORKSHOP, PUMP_STATION, SUBSTATION, GREENHOUSE, VOID_DOCK, RADIO_RELAY, PHOTO_CLUB,
IMPOSSIBLE_SERVICE, CONSTRUCTION_REFUGE }
public enum RouteKind { FREIGHT, PATH }
public enum LandmarkKind { CITY, MINE, UTILITY }
/** NONE is a side column or path; a missing rail retains its intended shape with railPresent=false. */
public enum Rail { NONE, NORTH_SOUTH, EAST_WEST, ASCENDING_EAST, ASCENDING_WEST, ASCENDING_NORTH,
ASCENDING_SOUTH, SOUTH_EAST, SOUTH_WEST, NORTH_WEST, NORTH_EAST }
/** Coordinates of the player's feet, immediately outside the facade. Facing0=N,1=E,2=S,3=W. */
public record Entry(int x, int y, int z, int facing) {
public Entry { if (!coordinate(x) || !coordinate(z) || y < 2 || y > 380 || facing < 0 || facing > 3) throw new IllegalArgumentException("Invalid heritage entry"); }
}
public record Support(int x, int minY, int z, int maxY) {
public Support { if (!coordinate(x) || !coordinate(z) || minY < 2 || maxY < minY || maxY - minY >= 12 || maxY > 370) throw new IllegalArgumentException("Invalid heritage support"); }
}
public record Site(String id, Kind kind, int minX, int floorY, int minZ, int width, int depth, int height,
int facing, long seed, int foundationDepth, List<Support> supports, Entry entry) {
public Site {
supports = List.copyOf(supports);
if (!identifier(id) || !coordinate(minX) || !coordinate(minZ) || kind == null || width != 17 || depth != 17 || height < 7 || height > 24
|| floorY < 32 || floorY + height > 380 || facing < 0 || facing > 3
|| foundationDepth < 3 || foundationDepth > 8 || supports.size() > 9 || entry == null)
throw new IllegalArgumentException("Invalid heritage site");
int ex = facing == 1 ? minX + width : facing == 3 ? minX - 1 : minX + width / 2;
int ez = facing == 2 ? minZ + depth : facing == 0 ? minZ - 1 : minZ + depth / 2;
if (entry.x() != ex || entry.z() != ez || entry.y() != floorY + 1 || entry.facing() != facing)
throw new IllegalArgumentException("Heritage entry does not meet its rotated facade");
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() != floorY - foundationDepth - 1
|| !positions.add(key(support.x(), support.z()))) throw new IllegalArgumentException("Invalid site footing");
}
public int maxX() { return minX + width - 1; }
public int maxZ() { return minZ + depth - 1; }
public int minY() { return floorY - foundationDepth; }
public int maxY() { return floorY + height; }
public int boundsMinY() { return supports.stream().mapToInt(Support::minY).min().orElse(minY()); }
public int boundsMaxY() { return maxY(); }
public int centerX() { return minX + width / 2; }
public int centerZ() { return minZ + depth / 2; }
public int startChunkX() { return centerX() >> 4; }
public int startChunkZ() { return centerZ() >> 4; }
}
/** A pre-existing, read-only destination. Its coordinate is never granted a mask bypass. */
public record Landmark(String id, LandmarkKind kind, Entry entry) {
public Landmark { if (!identifier(id) || kind == null || entry == null) throw new IllegalArgumentException("Invalid heritage landmark"); }
}
/** Floor/block-support Y, not player feet. Endpoints may be read-only landmark coordinates. */
public record Point(int x, int floorY, int z) {
public Point { if (!coordinate(x) || !coordinate(z) || floorY < 2 || floorY > 370) throw new IllegalArgumentException("Invalid route point"); }
}
public record Link(String id, String fromId, String toId, RouteKind kind, List<Point> path) {
public Link {
path = List.copyOf(path);
if (!identifier(id) || !identifier(fromId) || !identifier(toId) || fromId.equals(toId)
|| kind == null || path.size() < 2 || path.size() > 1024) throw new IllegalArgumentException("Invalid heritage link");
for (int i = 1; i < path.size(); i++) {
var a = path.get(i - 1); var b = path.get(i);
if (Math.abs(a.x() - b.x()) + Math.abs(a.z() - b.z()) != 1 || Math.abs(a.floorY() - b.floorY()) > 1)
throw new IllegalArgumentException("Heritage route has a discontinuous centerline");
}
}
}
/** Each written ground column belongs to exactly one Segment. Clear local y1..3 before rail placement. */
public record Cell(int x, int floorY, int z, int foundationDepth, Rail rail, boolean railPresent) {
public Cell {
if (!coordinate(x) || !coordinate(z) || floorY < 14 || floorY > 350 || foundationDepth < 1 || foundationDepth > 12 || rail == null
|| rail == Rail.NONE && railPresent) throw new IllegalArgumentException("Invalid heritage route cell");
}
public int minY() { return floorY - foundationDepth; }
public int maxY() { return floorY + 3; }
}
/** Chunk-local route piece: stable ownership prevents neighboring links painting a shared column twice. */
public record Segment(String id, String fromId, String toId, RouteKind kind, int startChunkX, int startChunkZ,
List<Cell> cells, long seed) {
public Segment {
cells = List.copyOf(cells);
if (!identifier(id) || !identifier(fromId) || !identifier(toId) || fromId.equals(toId) && kind != RouteKind.PATH || kind == null || cells.isEmpty() || cells.size() > 256)
throw new IllegalArgumentException("Invalid heritage segment");
var positions = new HashSet<Long>();
for (var cell : cells) if ((cell.x() >> 4) != startChunkX || (cell.z() >> 4) != startChunkZ
|| !positions.add(key(cell.x(), cell.z()))) throw new IllegalArgumentException("Route cells escaped their owning chunk");
}
public int minX() { return cells.stream().mapToInt(Cell::x).min().orElseThrow(); }
public int maxX() { return cells.stream().mapToInt(Cell::x).max().orElseThrow(); }
public int minZ() { return cells.stream().mapToInt(Cell::z).min().orElseThrow(); }
public int maxZ() { return cells.stream().mapToInt(Cell::z).max().orElseThrow(); }
public int minY() { return cells.stream().mapToInt(Cell::minY).min().orElseThrow(); }
public int maxY() { return cells.stream().mapToInt(Cell::maxY).max().orElseThrow(); }
public int boundsMinY() { return minY(); }
public int boundsMaxY() { return maxY(); }
}
public HeritagePlan19(List<Site> sites, List<Landmark> landmarks, List<Link> links, List<Segment> segments,
int samples, int columns, Map<Kind, String> diagnostics) {
sites = List.copyOf(sites); landmarks = List.copyOf(landmarks); links = List.copyOf(links); segments = List.copyOf(segments);
diagnostics = Map.copyOf(diagnostics);
if (sites.size() > 14 || landmarks.size() > 16 || links.size() > 24 || segments.size() > 512
|| samples < 0 || samples > MAX_SAMPLES || columns < 0 || columns > MAX_COLUMNS)
throw new IllegalArgumentException("Invalid heritage plan budget");
var destinations = new HashSet<String>();
for (var site : sites) if (!destinations.add(site.id())) throw new IllegalArgumentException("Duplicate heritage destination");
for (var landmark : landmarks) if (!destinations.add(landmark.id())) throw new IllegalArgumentException("Duplicate heritage destination");
for (var link : links) if (!destinations.contains(link.fromId()) || !destinations.contains(link.toId()))
throw new IllegalArgumentException("A heritage route promises a nonexistent destination");
var occupied = new HashSet<Long>();
for (var segment : segments) {
if (!destinations.contains(segment.fromId()) || !destinations.contains(segment.toId()))
throw new IllegalArgumentException("A heritage segment promises a nonexistent destination");
for (var cell : segment.cells()) if (!occupied.add(key(cell.x(), cell.z())))
throw new IllegalArgumentException("Heritage segments write the same column");
}
this.sites = sites; this.landmarks = landmarks; this.links = links; this.segments = segments;
this.samples = samples; this.columns = columns; this.diagnostics = diagnostics;
var columnIndex = new HashMap<Long, List<Range>>(); var chunkIndex = new HashMap<Long, List<Box>>();
for (var site : sites) {
index(columnIndex, chunkIndex, new Box(site.minX(), site.minY() - 2, site.minZ(), site.maxX(), site.maxY(), site.maxZ()));
for (var support : site.supports()) index(columnIndex, chunkIndex,
new Box(support.x(), support.minY() - 2, support.z(), support.x(), support.maxY(), support.z()));
}
for (var segment : segments) for (var cell : segment.cells()) index(columnIndex, chunkIndex,
new Box(cell.x(), cell.minY() - 1, cell.z(), cell.x(), cell.maxY(), cell.z()));
var usedDestinations = new HashSet<String>();
for (var link : links) { usedDestinations.add(link.fromId()); usedDestinations.add(link.toId()); }
for (var landmark : landmarks) if (usedDestinations.contains(landmark.id())) {
Entry e = landmark.entry();
for (int side = -1; side <= 1; side++) {
int x = e.x() + ((e.facing() & 1) == 0 ? side : 0);
int z = e.z() + ((e.facing() & 1) == 1 ? side : 0);
// Read-only dependencies: these cells never enter contains() or any native writer.
index(columnIndex, chunkIndex, new Box(x, e.y() - 2, z, x, e.y() + 2, z));
}
}
columnIndex.replaceAll((key, values) -> List.copyOf(values)); chunkIndex.replaceAll((key, values) -> List.copyOf(values));
protectedColumns = Map.copyOf(columnIndex); protectedChunks = Map.copyOf(chunkIndex);
}
public List<Site> sites() { return sites; }
public List<Landmark> landmarks() { return landmarks; }
public List<Link> links() { return links; }
public List<Segment> segments() { return segments; }
public int samples() { return samples; }
public int columns() { return columns; }
public Map<Kind, String> diagnostics() { return diagnostics; }
public boolean isEmpty() { return sites.isEmpty(); }
public boolean contains(int x, int y, int z) {
for (var site : sites) {
if (x < site.minX() || x > site.maxX() || z < site.minZ() || z > site.maxZ()) continue;
if (y >= site.minY() && y <= site.maxY()) return true;
for (var support : site.supports()) if (x == support.x() && z == support.z() && y >= support.minY() && y <= support.maxY()) return true;
}
for (var segment : segments) if ((x >> 4) == segment.startChunkX() && (z >> 4) == segment.startChunkZ())
for (var cell : segment.cells()) if (cell.x() == x && cell.z() == z && y >= cell.minY() && y <= cell.maxY()) return true;
return false;
}
/** Protect the original rock below slabs as well as every written volume. */
public boolean protects(int x, int y, int z) {
for (var range : protectedColumns.getOrDefault(key(x, z), List.of())) if (y >= range.minY() && y <= range.maxY()) return true;
return false;
}
public boolean intersectsProtection(int x0, int y0, int z0, int x1, int y1, int z1) {
if (x0 > x1 || y0 > y1 || z0 > z1) throw new IllegalArgumentException("Reversed heritage query");
for (int cx = x0 >> 4; cx <= x1 >> 4; cx++) for (int cz = z0 >> 4; cz <= z1 >> 4; cz++)
for (var box : protectedChunks.getOrDefault(key(cx, cz), List.of()))
if (x0 <= box.maxX() && x1 >= box.minX() && z0 <= box.maxZ() && z1 >= box.minZ()
&& y0 <= box.maxY() && y1 >= box.minY()) return true;
return false;
}
private record Range(int minY, int maxY) {}
private record Box(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {}
private static void index(Map<Long, List<Range>> columns, Map<Long, List<Box>> chunks, Box box) {
for (int x = box.minX(); x <= box.maxX(); x++) for (int z = box.minZ(); z <= box.maxZ(); z++)
columns.computeIfAbsent(key(x, z), ignored -> new ArrayList<>()).add(new Range(box.minY(), box.maxY()));
for (int x = box.minX() >> 4; x <= box.maxX() >> 4; x++) for (int z = box.minZ() >> 4; z <= box.maxZ() >> 4; z++)
chunks.computeIfAbsent(key(x, z), ignored -> new ArrayList<>()).add(box);
}
private static boolean coordinate(int value) { return Math.abs((long) value) <= 29_999_000; }
private static boolean identifier(String value) { return value != null && !value.isBlank() && value.length() <= 128; }
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
}
@@ -0,0 +1,545 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.*;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/** Generation19 only. A finite catalogue is admitted before complete, repairable routes are published.
* Everything samples native columns; no chunk, player object or future continent is requested. */
public final class HeritagePlanner19 {
public static final int MAX_COLUMNS = HeritagePlan19.MAX_COLUMNS, MAX_SAMPLES = HeritagePlan19.MAX_SAMPLES;
public static final int MAX_SITE_CUT = 6, MAX_ROUTE_CUT = 3;
private static final int SITE_COLUMNS = 11_000, SITE_SAMPLES = 540_000;
private static final int INVALID = Integer.MIN_VALUE;
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
private HeritagePlanner19() {}
public interface Terrain {
boolean owns(int x, int z);
int surfaceY(int x, int z);
boolean solid(int x, int y, int z);
boolean protectedAt(int x, int y, int z);
default List<Landmark> landmarks() { return List.of(); }
}
public static HeritagePlan19 plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
if (!List.of(512, 724, 1024).contains(diameter)) throw new IllegalArgumentException("Heritage19 requires a starter size");
var probe = new Probe(terrain, cx, cz, diameter);
List<Landmark> landmarks = List.copyOf(terrain.landmarks()).stream().limit(16).toList();
var sites = new ArrayList<Site>(); var cells = new LinkedHashMap<Long, OwnedCell>(); var diagnostics = new EnumMap<Kind, String>(Kind.class);
for (var kind : Kind.values()) {
int remaining = Kind.values().length - kind.ordinal() - 1;
probe.columnLimit = SITE_COLUMNS - remaining * (SITE_COLUMNS / 14);
probe.sampleLimit = SITE_SAMPLES - remaining * (SITE_SAMPLES / 14);
SiteDraft admitted = null; int attempts = 0;
var reasons = new java.util.TreeMap<String, Integer>(); probe.reasons = reasons;
try {
for (var candidate : candidates(seed, kind, cx, cz, diameter, sites, landmarks, probe)) {
attempts++;
admitted = site(seed, kind, candidate, sites, landmarks, cells, probe);
if (admitted != null) break;
}
} catch (BudgetExceeded exhausted) { reasons.merge(exhausted.columns ? "column-budget" : "probe-budget", 1, Integer::sum); }
if (admitted != null) {
sites.add(admitted.site());
Request landing = new Request(admitted.site().id() + "_landing", admitted.site().id(), admitted.site().id(), RouteKind.PATH);
for (var cell : admitted.landings()) cells.put(key(cell.x(), cell.z()), new OwnedCell(landing, cell));
}
diagnostics.put(kind, (admitted == null ? "not admitted" : "admitted") + "; candidates=" + attempts + "; " + reasons);
}
probe.columnLimit = MAX_COLUMNS; probe.sampleLimit = MAX_SAMPLES;
var links = new ArrayList<Link>();
var routeResults = new LinkedHashMap<String, String>();
var routeReasons = new java.util.TreeMap<String, Integer>(); probe.reasons = routeReasons;
Map<String, Endpoint> endpoints = new LinkedHashMap<>();
for (var site : sites) endpoints.put(site.id(), new Endpoint(site.id(), site.entry(), false));
for (var landmark : landmarks) if (!endpoints.containsKey(landmark.id()))
endpoints.put(landmark.id(), new Endpoint(landmark.id(), landmark.entry(), true));
List<Request> requests = requests(sites, landmarks).stream()
.filter(r -> endpoints.containsKey(r.from()) && endpoints.containsKey(r.to())).toList();
requests = new ArrayList<>(requests);
requests.sort(Comparator.comparingInt((Request r) -> r.kind() == RouteKind.FREIGHT ? 0
: span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry()) >= diameter / 4 ? 1 : 2)
.thenComparingInt(r -> span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry())));
int adaptiveLong = 0;
for (int requestIndex = 0; requestIndex < requests.size(); requestIndex++) {
var request = requests.get(requestIndex);
int remaining = requests.size() - requestIndex - 1;
// Failed alignments must not starve every later destination. Unused allowances carry forward.
boolean longAxis = span(endpoints.get(request.from()).entry(), endpoints.get(request.to()).entry()) >= diameter / 4;
int allowance = longAxis && adaptiveLong++ < 2 ? 4_500 : 1_400;
probe.columnLimit = Math.min(MAX_COLUMNS - remaining * 120, probe.columns.size() + allowance);
probe.sampleLimit = Math.min(MAX_SAMPLES - remaining * 4_000, probe.samples + (longAxis ? 150_000 : 75_000));
Endpoint from = endpoints.get(request.from()), to = endpoints.get(request.to());
if (from == null || to == null) continue;
int beforeRouteColumns = probe.columns.size();
try {
RouteDraft route = route(seed, request, from, to, sites, landmarks, cells, probe);
if (route == null) { probe.reject("no-complete-route"); routeResults.put(request.id(), "refused; columns=" + (probe.columns.size() - beforeRouteColumns)); continue; }
links.add(new Link(request.id(), request.from(), request.to(), request.kind(), route.path()));
routeResults.put(request.id(), "admitted; span=" + span(from.entry(), to.entry()) + "; length=" + route.path().size() + "; columns=" + (probe.columns.size() - beforeRouteColumns));
for (var cell : route.cells().values()) {
long key = key(cell.x(), cell.z());
OwnedCell old = cells.get(key);
if (old == null) cells.put(key, new OwnedCell(request, cell));
else if (cell.rail() != Rail.NONE && old.cell().rail() == Rail.NONE)
cells.put(key, new OwnedCell(old.request(), cell));
else if (cell.rail() != Rail.NONE && old.cell().rail() != Rail.NONE && cell.rail() != old.cell().rail()) {
// A missing switch is intentional; never emit a rail with contradictory connections.
var c = old.cell(); cells.put(key, new OwnedCell(old.request(),
new Cell(c.x(), c.floorY(), c.z(), c.foundationDepth(), c.rail(), false)));
}
}
} catch (BudgetExceeded exhausted) { probe.reject(exhausted.columns ? "route-column-budget" : "route-probe-budget"); routeResults.put(request.id(), "budget; columns=" + (probe.columns.size() - beforeRouteColumns)); }
}
var segments = segments(seed, cells);
if (Boolean.getBoolean("sanctuary.test.heritage19") || Boolean.getBoolean("sanctuary.heritage.plannerDiagnostics"))
System.getLogger(HeritagePlanner19.class.getName()).log(System.Logger.Level.INFO,
"Heritage19 seed=" + seed + " diameter=" + diameter + " sites=" + sites.stream().map(Site::kind).toList()
+ " links=" + links.size() + " segments=" + segments.size() + " columns=" + probe.columns.size()
+ " samples=" + probe.samples + " catalogue=" + diagnostics + " routes=" + routeReasons + " routeResults=" + routeResults);
return new HeritagePlan19(sites, landmarks, links, segments, probe.samples, probe.columns.size(), diagnostics);
}
private static List<Candidate> candidates(long seed, Kind kind, int cx, int cz, int diameter,
List<Site> sites, List<Landmark> landmarks, Probe probe) {
double angle = unit(mix(seed ^ 0x19ACEL)) * Math.PI * 2 + kind.ordinal() * 2.399963229728653;
double radius = diameter * (kind == Kind.VOID_DOCK ? .40 : kind == Kind.OBSERVATORY || kind == Kind.RADIO_RELAY ? .31 : .22);
int x = cx + (int) Math.round(Math.cos(angle) * radius), z = cz + (int) Math.round(Math.sin(angle) * radius);
Kind parent = switch (kind) {
case POST_OFFICE, COMMUNAL_DEPOT, RAIL_WORKSHOP -> Kind.FREIGHT_STATION;
case EXPEDITION_OFFICE, PHOTO_CLUB -> Kind.POST_OFFICE;
case SUBSTATION, CONSTRUCTION_REFUGE -> Kind.RAIL_WORKSHOP;
case GREENHOUSE, IMPOSSIBLE_SERVICE -> Kind.PUMP_STATION;
default -> null;
};
Site neighbor = parent == null ? null : find(sites, parent);
if (neighbor != null) {
double local = unit(mix(seed ^ kind.ordinal() * 179L)) * Math.PI * 2;
int distance = kind == Kind.RAIL_WORKSHOP ? 65 : 40;
x = neighbor.centerX() + (int) Math.round(Math.cos(local) * distance);
z = neighbor.centerZ() + (int) Math.round(Math.sin(local) * distance);
}
var candidates = new ArrayList<Candidate>();
for (int i = 0; i < 16; i++) {
double turn = angle + i * Math.PI / 3;
int offset = i == 0 ? 0 : i < 5 ? 24 : i < 8 ? 48 : i < 12 ? 72 : 96;
int px = x + (int) Math.round(Math.cos(turn) * offset), pz = z + (int) Math.round(Math.sin(turn) * offset);
if (Math.hypot((double) px - cx, (double) pz - cz) > diameter / 2.0 - 18) continue;
if (overlapsSites(px - 15, pz - 15, px + 15, pz + 15, sites)) continue;
int low = 384, high = -1; boolean dry = true;
for (int dz : new int[] {-8, 0, 8}) for (int dx : new int[] {-8, 0, 8}) {
int top = probe.surface(px + dx, pz + dz);
if (top < 48 || top > 330 || !probe.solid(px + dx, top, pz + dz)
|| probe.protectedAt(px + dx, top, pz + dz)) { dry = false; break; }
low = Math.min(low, top); high = Math.max(high, top);
}
if (!dry || high - low > 13) { probe.reject("coarse-terrain"); continue; }
int facing = direction(px, pz, neighbor == null ? cx : neighbor.centerX(), neighbor == null ? cz : neighbor.centerZ());
if (kind == Kind.VOID_DOCK) facing = (direction(px, pz, cx, cz) + 2) & 3;
int score = (i >= 8 ? 1_000 : 0) + high - low + (kind == Kind.OBSERVATORY || kind == Kind.RADIO_RELAY ? (300 - high) / 4 : 0);
candidates.add(new Candidate(px - 8, pz - 8, facing, score, mix(seed ^ kind.ordinal() * 463L ^ i)));
}
candidates.sort(Comparator.comparingInt(Candidate::score).thenComparingLong(Candidate::seed));
return candidates;
}
private static SiteDraft site(long seed, Kind kind, Candidate candidate, List<Site> previous,
List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
int x = candidate.x(), z = candidate.z(), low = 384, high = -1;
for (var landmark : landmarks) if (landmark.entry().x() >= x - 2 && landmark.entry().x() <= x + 18
&& landmark.entry().z() >= z - 2 && landmark.entry().z() <= z + 18) { probe.reject("existing-entry"); return null; }
var heights = new ArrayList<Integer>();
for (int dz = 0; dz < 17; dz++) for (int dx = 0; dx < 17; dx++) {
int top = probe.surface(x + dx, z + dz);
if (top < 48 || top > 330 || !probe.solid(x + dx, top, z + dz)) { probe.reject("dry-footprint"); return null; }
heights.add(top); low = Math.min(low, top); high = Math.max(high, top);
}
int minimum = Math.max(48, high - MAX_SITE_CUT), maximum = Math.min(330, low + 7);
if (minimum > maximum) { probe.reject("site-slope"); return null; }
heights.sort(Integer::compareTo);
int floor = Math.clamp(heights.get(heights.size() / 2), minimum, maximum);
int foundation = Math.max(3, floor - low + 1), height = height(kind);
int base = floor - foundation;
for (int dz = 0; dz < 17; dz++) for (int dx = 0; dx < 17; dx++)
for (int y = base; y <= floor + height; y++) if (probe.protectedAt(x + dx, y, z + dz)) {
probe.reject("site-protected-volume"); return null;
}
var supports = new ArrayList<Support>();
for (int dz : new int[] {1, 8, 15}) for (int dx : new int[] {1, 8, 15}) {
int bottom = base - 1, rock = bottom;
while (rock >= Math.max(2, base - 13) && (!probe.solid(x + dx, rock, z + dz)
|| !probe.solid(x + dx, rock - 1, z + dz))) rock--;
if (rock < Math.max(2, base - 13)) { probe.reject("site-no-bearing"); return null; }
for (int y = rock - 1; y <= bottom; y++) if (probe.protectedAt(x + dx, y, z + dz)) { probe.reject("support-protected"); return null; }
if (rock < bottom) supports.add(new Support(x + dx, rock + 1, z + dz, bottom));
}
int ex = candidate.facing() == 1 ? x + 17 : candidate.facing() == 3 ? x - 1 : x + 8;
int ez = candidate.facing() == 2 ? z + 17 : candidate.facing() == 0 ? z - 1 : z + 8;
Entry entry = new Entry(ex, floor + 1, ez, candidate.facing());
Site result = new Site(id(kind), kind, x, floor, z, 17, 17, height, candidate.facing(),
mix(seed ^ candidate.seed()), foundation, supports, entry);
var landings = landing(result.entry(), previous, probe);
if (landings == null) { probe.reject("entry-landing"); return null; }
for (var cell : landings) {
OwnedCell old = existing.get(key(cell.x(), cell.z()));
if (old != null && old.cell().floorY() != cell.floorY()) { probe.reject("landing-collision"); return null; }
}
return new SiteDraft(result, landings);
}
private static int height(Kind kind) {
return switch (kind) {
case FREIGHT_STATION, POST_OFFICE, RAIL_WORKSHOP, SUBSTATION, GREENHOUSE, PHOTO_CLUB -> 10;
case EXPEDITION_OFFICE, IMPOSSIBLE_SERVICE -> 12;
case COMMUNAL_DEPOT, VOID_DOCK, CONSTRUCTION_REFUGE -> 14;
case PUMP_STATION -> 18; case OBSERVATORY -> 20; case RADIO_RELAY -> 22;
};
}
private static List<Request> requests(List<Site> sites, List<Landmark> landmarks) {
var result = new ArrayList<Request>();
request(result, Kind.FREIGHT_STATION, Kind.COMMUNAL_DEPOT, RouteKind.FREIGHT);
request(result, Kind.FREIGHT_STATION, Kind.RAIL_WORKSHOP, RouteKind.FREIGHT);
request(result, Kind.COMMUNAL_DEPOT, Kind.PUMP_STATION, RouteKind.FREIGHT);
Site workshop = find(sites, Kind.RAIL_WORKSHOP);
for (var landmark : landmarks) if (landmark.kind() == LandmarkKind.MINE && workshop != null)
result.add(new Request("mine_freight", landmark.id(), workshop.id(), RouteKind.FREIGHT));
request(result, Kind.FREIGHT_STATION, Kind.POST_OFFICE, RouteKind.PATH);
request(result, Kind.POST_OFFICE, Kind.EXPEDITION_OFFICE, RouteKind.PATH);
request(result, Kind.EXPEDITION_OFFICE, Kind.OBSERVATORY, RouteKind.PATH);
request(result, Kind.COMMUNAL_DEPOT, Kind.VOID_DOCK, RouteKind.PATH);
request(result, Kind.RAIL_WORKSHOP, Kind.SUBSTATION, RouteKind.PATH);
request(result, Kind.SUBSTATION, Kind.PUMP_STATION, RouteKind.PATH);
request(result, Kind.PUMP_STATION, Kind.GREENHOUSE, RouteKind.PATH);
request(result, Kind.POST_OFFICE, Kind.PHOTO_CLUB, RouteKind.PATH);
request(result, Kind.PUMP_STATION, Kind.IMPOSSIBLE_SERVICE, RouteKind.PATH);
request(result, Kind.OBSERVATORY, Kind.RADIO_RELAY, RouteKind.PATH);
request(result, Kind.RAIL_WORKSHOP, Kind.CONSTRUCTION_REFUGE, RouteKind.PATH);
Site post = find(sites, Kind.POST_OFFICE);
for (var landmark : landmarks) if (landmark.kind() == LandmarkKind.CITY && post != null) {
result.add(new Request("city_post_" + result.size(), landmark.id(), post.id(), RouteKind.PATH)); break;
}
return result;
}
private static void request(List<Request> requests, Kind from, Kind to, RouteKind kind) {
requests.add(new Request(id(from) + "_" + id(to), id(from), id(to), kind));
}
/** Always persisted, including for an isolated destination: five steps return to natural terrain. */
private static List<Cell> landing(Entry entry, List<Site> sites, Probe probe) {
int facing = entry.facing(), floor = entry.y() - 1;
int endX = entry.x() + DX[facing] * 4, endZ = entry.z() + DZ[facing] * 4;
int target = probe.surface(endX, endZ);
if (target < 14 || target > 350 || Math.abs(target - floor) > 3) return null;
var result = new ArrayList<Cell>();
for (int step = 0; step < 5; step++) {
// Keep the first two rows flat for the facade and the first rail turn.
int y = floor + (int) Math.round((target - floor) * Math.max(0, step - 1) / 3.0);
for (int cross = -1; cross <= 1; cross++) {
int x = entry.x() + DX[facing] * step + DZ[facing] * cross;
int z = entry.z() + DZ[facing] * step - DX[facing] * cross;
if (overlapsSites(x, z, x, z, sites)) return null;
Cell cell = routeCell(x, y, z, Rail.NONE, false, probe);
if (cell == null) return null;
result.add(cell);
if (step == 4) {
int beyond = probe.surface(x + DX[facing], z + DZ[facing]);
if (Math.abs(beyond - y) > 1 || !probe.solid(x + DX[facing], beyond, z + DZ[facing])) return null;
}
}
}
return List.copyOf(result);
}
private static RouteDraft route(long seed, Request request, Endpoint from, Endpoint to, List<Site> sites,
List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
Entry a = from.entry(), b = to.entry();
XZ start = new XZ(a.x(), a.z()), end = new XZ(b.x(), b.z());
XZ outA = start.move(a.facing(), 6), outB = end.move(b.facing(), 6);
int middleX = (outA.x() + outB.x()) / 2, middleZ = (outA.z() + outB.z()) / 2;
var alternatives = List.of(
List.of(outA, new XZ(outB.x(), outA.z()), outB),
List.of(outA, new XZ(outA.x(), outB.z()), outB),
List.of(outA, new XZ(middleX, outA.z()), new XZ(middleX, outB.z()), outB),
List.of(outA, new XZ(outA.x(), middleZ), new XZ(outB.x(), middleZ), outB),
List.of(outA, new XZ(outA.x(), middleZ - 16), new XZ(outB.x(), middleZ - 16), outB),
List.of(outA, new XZ(middleX + 16, outA.z()), new XZ(middleX + 16, outB.z()), outB));
for (var waypoints : alternatives) {
var points = new ArrayList<XZ>(); points.add(start);
boolean okay = true;
for (var waypoint : waypoints) if (!line(points, waypoint)) { okay = false; break; }
if (!okay || !line(points, end) || points.size() > 1024 || new HashSet<>(points).size() != points.size()) continue;
RouteDraft result = profile(seed, request, from, to, points, sites, landmarks, existing, probe);
if (result != null) return result;
}
return adaptive(seed, request, from, to, start, end, outA, outB, sites, landmarks, existing, probe);
}
/** Adaptive fallback. The coarse graph only proposes an alignment; it grants no writing permission. */
private static RouteDraft adaptive(long seed, Request request, Endpoint from, Endpoint to, XZ start, XZ end,
XZ outA, XZ outB, List<Site> sites, List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
int stepSize = span(from.entry(), to.entry()) >= 160 ? 8 : 4;
int certificateReserve = Math.min(2_200, distance(outA, outB) * 3 + 80);
var excluded = new HashSet<XZ>();
for (int attempt = 0; attempt < 3; attempt++) {
var open = new java.util.PriorityQueue<SearchNode>(Comparator.comparingDouble(SearchNode::priority)
.thenComparingDouble(SearchNode::cost).thenComparingInt(n -> n.position().x()).thenComparingInt(n -> n.position().z()));
var best = new HashMap<XZ, Double>(); var parents = new HashMap<XZ, XZ>(); var closed = new HashSet<XZ>();
var terrainHeights = new HashMap<XZ, Integer>();
int begin = coarseRoute(outA, sites, terrainHeights, probe);
if (begin == INVALID) return null;
open.add(new SearchNode(outA, 0, distance(outA, outB))); best.put(outA, 0.0);
List<XZ> found = null;
int searchColumns = Math.max(probe.columns.size() + 150, probe.columnLimit - certificateReserve);
while (!open.isEmpty() && closed.size() < 500 && probe.columns.size() < searchColumns) {
var current = open.remove(); XZ p = current.position();
if (!closed.add(p) || current.cost() > best.getOrDefault(p, Double.POSITIVE_INFINITY)) continue;
if (distance(p, outB) <= stepSize * 2) {
var reversed = new ArrayList<XZ>(); XZ ancestor = p;
while (ancestor != null) { reversed.add(ancestor); ancestor = parents.get(ancestor); }
java.util.Collections.reverse(reversed);
for (int order = 0; order < 2 && found == null; order++) {
var candidate = new ArrayList<XZ>(); candidate.add(start); boolean valid = true;
for (XZ step : reversed) if (!line(candidate, step)) { valid = false; break; }
XZ turn = order == 0 ? new XZ(outB.x(), p.z()) : new XZ(p.x(), outB.z());
if (valid && line(candidate, turn) && line(candidate, outB) && line(candidate, end)
&& new HashSet<>(candidate).size() == candidate.size()) found = candidate;
}
if (found != null) break;
}
int top = terrainHeights.get(p); XZ parent = parents.get(p);
for (int d = 0; d < 4; d++) {
if (parent == null && d == ((from.entry().facing() + 2) & 3)) continue;
XZ next = p.move(d, stepSize);
if (closed.contains(next) || excluded.contains(next)
|| Math.min(distance(next, outA), distance(next, outB)) > distance(outA, outB) + 64) continue;
int height = coarseRoute(next, sites, terrainHeights, probe);
if (height == INVALID || Math.abs(height - top) > stepSize + 3) continue;
double turnCost = parent == null || parent.x() == next.x() || parent.z() == next.z() ? 0 : 1.5;
double cost = current.cost() + stepSize + Math.abs(height - top) * 1.1 + turnCost;
if (cost >= best.getOrDefault(next, Double.POSITIVE_INFINITY)) continue;
best.put(next, cost); parents.put(next, p);
open.add(new SearchNode(next, cost, cost + distance(next, outB) * 1.6));
}
}
if (found == null) { probe.reject("adaptive-no-alignment"); return null; }
RouteDraft certified = profile(seed, request, from, to, found, sites, landmarks, existing, probe);
if (certified != null) { probe.reject("adaptive-admitted"); return certified; }
if (probe.routeBlock == null) return null;
// A failed exact corridor removes only nearby coarse nodes on the next proposal.
XZ bad = probe.routeBlock;
int gx = outA.x() + (int) Math.round((bad.x() - outA.x()) / (double) stepSize) * stepSize;
int gz = outA.z() + (int) Math.round((bad.z() - outA.z()) / (double) stepSize) * stepSize;
excluded.add(new XZ(gx, gz));
for (int d = 0; d < 4; d++) excluded.add(new XZ(gx, gz).move(d, stepSize));
}
return null;
}
private record SearchNode(XZ position, double cost, double priority) {}
private static int distance(XZ a, XZ b) { return Math.abs(a.x() - b.x()) + Math.abs(a.z() - b.z()); }
private static int coarseRoute(XZ p, List<Site> sites, Map<XZ, Integer> cache, Probe probe) {
Integer old = cache.get(p); if (old != null) return old;
if (overlapsSites(p.x() - 2, p.z() - 2, p.x() + 2, p.z() + 2, sites)) { cache.put(p, INVALID); return INVALID; }
int top = probe.surface(p.x(), p.z());
if (top < 14 || top > 350 || !probe.solid(p.x(), top, p.z()) || probe.protectedAt(p.x(), top, p.z())) {
cache.put(p, INVALID); return INVALID;
}
for (int d = 0; d < 4; d++) {
XZ side = p.move(d, 1); int sideTop = probe.surface(side.x(), side.z());
if (sideTop < 14 || sideTop > 350 || Math.abs(sideTop - top) > 4 || probe.protectedAt(side.x(), sideTop, side.z())) {
cache.put(p, INVALID); return INVALID;
}
}
cache.put(p, top); return top;
}
/** Finite orthogonal alignments, each certified at full block resolution before publication. */
private static boolean line(List<XZ> points, XZ destination) {
XZ p = points.getLast();
if (p.x() != destination.x() && p.z() != destination.z()) return false;
int dx = Integer.signum(destination.x() - p.x()), dz = Integer.signum(destination.z() - p.z());
while (!p.equals(destination)) {
p = new XZ(p.x() + dx, p.z() + dz); points.add(p);
if (points.size() > 1024) return false;
}
return true;
}
private static RouteDraft profile(long seed, Request request, Endpoint from, Endpoint to, List<XZ> points,
List<Site> sites, List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
int size = points.size(); probe.routeBlock = null;
int[] lo = new int[size], hi = new int[size], desired = new int[size], group = new int[size];
boolean[] flat = new boolean[size - 1];
for (int i = 1; i < size - 1; i++) if (turn(points, i)) { flat[i - 1] = true; flat[i] = true; }
for (int i = 0; i < size; i++) {
XZ point = points.get(i); probe.routeBlock = point; lo[i] = 14; hi[i] = 350;
desired[i] = probe.surface(point.x(), point.z());
boolean readOnly = i == 0 && from.readOnly() || i == size - 1 && to.readOnly();
for (XZ column : strip(points, i)) {
if (overlapsSites(column.x(), column.z(), column.x(), column.z(), sites)) { probe.reject("route-building"); return null; }
int top = probe.surface(column.x(), column.z());
if (top < 14 || top > 350 || !probe.solid(column.x(), top, column.z())) { probe.reject("route-dry-ground"); return null; }
lo[i] = Math.max(lo[i], top - MAX_ROUTE_CUT); hi[i] = Math.min(hi[i], top + 4);
OwnedCell old = existing.get(key(column.x(), column.z()));
if (old != null) { lo[i] = Math.max(lo[i], old.cell().floorY()); hi[i] = Math.min(hi[i], old.cell().floorY()); }
if (readOnly && top != (i == 0 ? from.entry().y() : to.entry().y()) - 1) return null;
}
if (i == 0 || i == size - 1) {
int exact = (i == 0 ? from.entry().y() : to.entry().y()) - 1;
lo[i] = Math.max(lo[i], exact); hi[i] = Math.min(hi[i], exact);
}
if (lo[i] > hi[i]) { probe.reject("route-grade-interval"); return null; }
}
// Curves and both adjacent rails share one elevation; a Minecraft curved rail cannot slope.
int groups = 1;
for (int i = 1; i < size; i++) { if (!flat[i - 1]) groups++; group[i] = groups - 1; }
int[] gl = new int[groups], gh = new int[groups], gd = new int[groups], y = new int[groups];
java.util.Arrays.fill(gl, 14); java.util.Arrays.fill(gh, 350);
for (int i = 0; i < size; i++) { int g = group[i]; gl[g] = Math.max(gl[g], lo[i]); gh[g] = Math.min(gh[g], hi[i]); gd[g] = desired[i]; }
for (int g = 1; g < groups; g++) { gl[g] = Math.max(gl[g], gl[g - 1] - 1); gh[g] = Math.min(gh[g], gh[g - 1] + 1); }
for (int g = groups - 2; g >= 0; g--) { gl[g] = Math.max(gl[g], gl[g + 1] - 1); gh[g] = Math.min(gh[g], gh[g + 1] + 1); }
for (int g = 0; g < groups; g++) {
if (gl[g] > gh[g]) { probe.reject("route-grade"); return null; }
int lower = g == 0 ? gl[g] : Math.max(gl[g], y[g - 1] - 1), upper = g == 0 ? gh[g] : Math.min(gh[g], y[g - 1] + 1);
y[g] = Math.clamp(gd[g], lower, upper);
}
var path = new ArrayList<Point>(); var floors = new LinkedHashMap<Long, Integer>();
for (int i = 0; i < size; i++) {
XZ point = points.get(i); int floor = y[group[i]]; path.add(new Point(point.x(), floor, point.z()));
if (i == 0 && from.readOnly() || i == size - 1 && to.readOnly()) continue;
for (XZ column : strip(points, i)) {
long key = key(column.x(), column.z()); Integer old = floors.putIfAbsent(key, floor);
if (old != null && old != floor) { probe.reject("route-self-grade"); return null; }
// Historical endpoints and their lateral landing are read-only, even at another nearby turn.
for (var landmark : landmarks) if (landingColumn(landmark.entry(), column)) {
boolean actualEnd = from.readOnly() && landmark.id().equals(from.id())
|| to.readOnly() && landmark.id().equals(to.id());
if (!actualEnd || floor != landmark.entry().y() - 1) { probe.reject("route-historical-entry"); return null; }
floors.remove(key); break;
}
}
}
var cells = new LinkedHashMap<Long, Cell>();
for (var entry : floors.entrySet()) {
int x = (int) (entry.getKey() >> 32), z = (int) (long) entry.getKey();
probe.routeBlock = new XZ(x, z);
Cell cell = routeCell(x, entry.getValue(), z, Rail.NONE, false, probe);
if (cell == null) { probe.reject("route-column-certificate"); return null; }
cells.put(entry.getKey(), cell);
}
if (request.kind() == RouteKind.FREIGHT) for (int i = 0; i < path.size(); i++) {
Point point = path.get(i); long key = key(point.x(), point.z()); Cell cell = cells.get(key);
if (cell == null) continue;
Rail rail = rail(path, i);
// A one-block valley cannot carry two ascending slopes on the same rail. Keep a repairable rail gap.
boolean intact = rail != Rail.NONE && Math.floorMod(mix(seed ^ key ^ request.id().hashCode()), 19) != 0;
if (rail == Rail.NONE) rail = points.get(Math.max(0, i - 1)).x() == points.get(Math.min(size - 1, i + 1)).x() ? Rail.NORTH_SOUTH : Rail.EAST_WEST;
cells.put(key, new Cell(cell.x(), cell.floorY(), cell.z(), cell.foundationDepth(), rail, intact));
}
return new RouteDraft(List.copyOf(path), cells);
}
private static boolean landingColumn(Entry entry, XZ column) {
int dx = column.x() - entry.x(), dz = column.z() - entry.z();
return DX[entry.facing()] * dx + DZ[entry.facing()] * dz == 0 && Math.abs(dx) + Math.abs(dz) <= 1;
}
private static List<XZ> strip(List<XZ> points, int i) {
XZ p = points.get(i); var result = new ArrayList<XZ>();
if (turn(points, i)) {
for (int dx = -1; dx <= 1; dx++) for (int dz = -1; dz <= 1; dz++) result.add(new XZ(p.x() + dx, p.z() + dz));
} else {
XZ other = points.get(i == points.size() - 1 ? i - 1 : i + 1);
int dx = Integer.signum(other.x() - p.x()), dz = Integer.signum(other.z() - p.z());
for (int cross = -1; cross <= 1; cross++) result.add(new XZ(p.x() + dz * cross, p.z() - dx * cross));
}
return result;
}
private static boolean turn(List<XZ> points, int i) {
if (i == 0 || i == points.size() - 1) return false;
XZ a = points.get(i - 1), b = points.get(i + 1);
return a.x() != b.x() && a.z() != b.z();
}
private static Rail rail(List<Point> path, int i) {
Point p = path.get(i), a = path.get(Math.max(0, i - 1)), b = path.get(Math.min(path.size() - 1, i + 1));
if (a.floorY() > p.floorY() && b.floorY() > p.floorY()) return Rail.NONE;
Point high = a.floorY() > p.floorY() ? a : b.floorY() > p.floorY() ? b : null;
if (high != null) return switch (direction(p.x(), p.z(), high.x(), high.z())) {
case 0 -> Rail.ASCENDING_NORTH; case 1 -> Rail.ASCENDING_EAST; case 2 -> Rail.ASCENDING_SOUTH; default -> Rail.ASCENDING_WEST;
};
if (a.x() == b.x()) return Rail.NORTH_SOUTH;
if (a.z() == b.z()) return Rail.EAST_WEST;
boolean n = a.z() < p.z() || b.z() < p.z(), s = a.z() > p.z() || b.z() > p.z();
boolean e = a.x() > p.x() || b.x() > p.x();
return n ? e ? Rail.NORTH_EAST : Rail.NORTH_WEST : s && e ? Rail.SOUTH_EAST : Rail.SOUTH_WEST;
}
private static Cell routeCell(int x, int floor, int z, Rail rail, boolean present, Probe probe) {
int top = probe.surface(x, z);
if (floor < 14 || floor > 350 || top < floor - 4 || top > floor + MAX_ROUTE_CUT) return null;
int foundation = Math.max(1, floor - top + 1), base = floor - foundation;
if (!probe.solid(x, base - 1, z) || !probe.solid(x, base, z)) return null;
for (int y = base - 1; y <= floor + 3; y++) if (probe.protectedAt(x, y, z)) return null;
return new Cell(x, floor, z, foundation, rail, present);
}
private record XZ(int x, int z) { XZ move(int facing, int distance) { return new XZ(x + DX[facing] * distance, z + DZ[facing] * distance); } }
private record SegmentKey(Request request, int cx, int cz) {}
private static List<Segment> segments(long seed, Map<Long, OwnedCell> cells) {
var groups = new LinkedHashMap<SegmentKey, List<Cell>>();
for (var owned : cells.values()) {
Cell c = owned.cell(); var key = new SegmentKey(owned.request(), c.x() >> 4, c.z() >> 4);
groups.computeIfAbsent(key, ignored -> new ArrayList<>()).add(c);
}
var result = new ArrayList<Segment>();
for (var entry : groups.entrySet()) {
var k = entry.getKey(); var r = k.request();
// IDs are stable, bounded and independent of native generation order.
String id = "piece_" + r.id() + "_" + k.cx() + "_" + k.cz();
result.add(new Segment(id, r.from(), r.to(), r.kind(), k.cx(), k.cz(), entry.getValue(), mix(seed ^ key(k.cx(), k.cz()) ^ r.id().hashCode())));
}
return List.copyOf(result);
}
private static int span(Entry a, Entry b) { return Math.max(Math.abs(a.x() - b.x()), Math.abs(a.z() - b.z())); }
private static Site find(List<Site> sites, Kind kind) { return sites.stream().filter(site -> site.kind() == kind).findFirst().orElse(null); }
private static String id(Kind kind) { return kind.name().toLowerCase(Locale.ROOT); }
private static int direction(int x, int z, int tx, int tz) { return Math.abs(tx - x) >= Math.abs(tz - z) ? tx > x ? 1 : 3 : tz > z ? 2 : 0; }
private static boolean overlapsSites(int x0, int z0, int x1, int z1, List<Site> sites) {
for (var site : sites) if (x0 <= site.maxX() && x1 >= site.minX() && z0 <= site.maxZ() && z1 >= site.minZ()) return true;
return false;
}
private record SiteDraft(Site site, List<Cell> landings) {}
private record Candidate(int x, int z, int facing, int score, long seed) {}
private record Endpoint(String id, Entry entry, boolean readOnly) {}
private record Request(String id, String from, String to, RouteKind kind) {}
private record OwnedCell(Request request, Cell cell) {}
private record RouteDraft(List<Point> path, Map<Long, Cell> cells) {}
private static final class BudgetExceeded extends RuntimeException {
final boolean columns; BudgetExceeded(boolean columns) { this.columns = columns; }
}
private static final class Probe {
final Terrain terrain; final int cx, cz; final double radius;
final Map<Long, Integer> columns = new HashMap<>();
XZ routeBlock; Map<String, Integer> reasons = new HashMap<>(); int samples, columnLimit, sampleLimit;
Probe(Terrain terrain, int cx, int cz, int diameter) { this.terrain = java.util.Objects.requireNonNull(terrain); this.cx = cx; this.cz = cz; radius = diameter / 2.0 - 2; }
int surface(int x, int z) {
if (Math.hypot((double) x - cx, (double) z - cz) > radius || !terrain.owns(x, z)) return INVALID;
long key = key(x, z); var cached = columns.get(key); if (cached != null) return cached;
if (columns.size() >= columnLimit) throw new BudgetExceeded(true);
count(); int top = terrain.surfaceY(x, z); columns.put(key, top); return top;
}
boolean solid(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) == INVALID) return false; count(); return terrain.solid(x, y, z); }
boolean protectedAt(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) == INVALID) return true; count(); return terrain.protectedAt(x, y, z); }
void count() { if (samples >= sampleLimit) throw new BudgetExceeded(false); samples++; }
void reject(String reason) { reasons.merge(reason, 1, Integer::sum); }
}
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
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,361 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.worldgen.city.LostCityPalette;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import net.minecraft.core.Direction;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.block.Block;
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.BedPart;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.DoorHingeSide;
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
import net.minecraft.world.level.block.state.properties.Half;
import net.minecraft.world.level.block.state.properties.RailShape;
/** Fourteen original, physical generation19 places. The saved site controls all geometry and loot.
* Site writes are local to minX/floorY/minZ; route writes are local to the segment's minimum bounds. */
public final class HeritageRenderer19 {
private HeritageRenderer19() {}
public record Target(String name, int x, int y, int z) {}
/** These are already rotated local standing positions, not world coordinates. */
public static List<Target> targets(HeritagePlan19.Site site) {
var b = new Builder(site, null); b.layout(); return List.copyOf(b.targets);
}
public static void render(HeritagePlan19.Site site, LostCityRenderer.BlockWriter writer) {
var b = new Builder(site, writer); b.ground(); b.layout();
// Footings have absolute saved coordinates and must not undergo a second facade rotation.
for (var support : site.supports()) for (int y = support.minY(); y <= support.maxY(); y++)
writer.set(support.x() - site.minX(), y - site.floorY(), support.z() - site.minZ(),
y == support.minY() ? Blocks.SMOOTH_STONE.defaultBlockState() : Blocks.STONE_BRICKS.defaultBlockState());
}
public static void renderSegment(HeritagePlan19.Segment segment, LostCityRenderer.BlockWriter writer) {
int x0 = segment.minX(), y0 = segment.minY(), z0 = segment.minZ();
for (var cell : segment.cells()) {
int x = cell.x() - x0, z = cell.z() - z0, floor = cell.floorY() - y0;
for (int y = cell.minY() - y0; y <= floor; y++) {
Block material = Blocks.STONE;
if (y == floor) material = segment.kind() == HeritagePlan19.RouteKind.FREIGHT
? cell.rail() != HeritagePlan19.Rail.NONE && Math.floorMod(cell.x() + cell.z(), 3) == 0
? Blocks.DARK_OAK_PLANKS : Blocks.GRAVEL
: LostCityPalette.roll(segment.seed(), cell.x(), y, cell.z(), 4) == 0 ? Blocks.ANDESITE : Blocks.STONE;
writer.set(x, y, z, material.defaultBlockState());
}
for (int y = floor + 1; y <= floor + 3; y++) writer.set(x, y, z, Blocks.AIR.defaultBlockState());
if (cell.railPresent()) writer.set(x, floor + 1, z, Blocks.RAIL.defaultBlockState()
.setValue(BlockStateProperties.RAIL_SHAPE, RailShape.valueOf(cell.rail().name())));
}
}
private static final class Builder {
final HeritagePlan19.Site s;
final LostCityRenderer.BlockWriter out;
final List<Target> targets = new ArrayList<>();
final Rotation rotation;
Builder(HeritagePlan19.Site s, LostCityRenderer.BlockWriter out) {
this.s = s; this.out = out;
rotation = switch (s.facing()) { case 1 -> Rotation.CLOCKWISE_90; case 2 -> Rotation.CLOCKWISE_180;
case 3 -> Rotation.COUNTERCLOCKWISE_90; default -> Rotation.NONE; };
}
int rx(int x, int z) { return switch (s.facing()) { case 1 -> 16 - z; case 2 -> 16 - x; case 3 -> z; default -> x; }; }
int rz(int x, int z) { return switch (s.facing()) { case 1 -> x; case 2 -> 16 - z; case 3 -> 16 - x; default -> z; }; }
void set(int x, int y, int z, BlockState state) {
if (x < 0 || x > 16 || z < 0 || z > 16 || y < -s.foundationDepth() || y > s.height())
throw new IllegalArgumentException("Heritage " + s.kind() + " renderer escaped saved site: " + x + "/" + y + "/" + z);
if (out != null) out.set(rx(x, z), y, rz(x, z), state.rotate(rotation));
}
void set(int x, int y, int z, Block b) { set(x, y, z, b.defaultBlockState()); }
void box(int x0, int y0, int z0, int x1, int y1, int z1, Block b) {
for (int y = y0; y <= y1; y++) for (int z = z0; z <= z1; z++) for (int x = x0; x <= x1; x++) set(x, y, z, b);
}
void target(String name, int x, int y, int z) { targets.add(new Target(name, rx(x, z), y, rz(x, z))); }
void ground() {
for (int y = -s.foundationDepth(); y <= s.height(); y++) for (int z = 0; z <= 16; z++) for (int x = 0; x <= 16; x++)
set(x, y, z, y < 0 ? Blocks.STONE.defaultBlockState() : y == 0
? LostCityPalette.pavement(s.seed(), x, z) : Blocks.AIR.defaultBlockState());
}
void layout() {
target("main_entry", 8, 1, 0);
switch (s.kind()) {
case FREIGHT_STATION -> station();
case POST_OFFICE -> post();
case COMMUNAL_DEPOT -> depot();
case EXPEDITION_OFFICE -> expedition();
case OBSERVATORY -> observatory();
case RAIL_WORKSHOP -> railway();
case PUMP_STATION -> pumping();
case SUBSTATION -> substation();
case GREENHOUSE -> greenhouse();
case VOID_DOCK -> dock();
case RADIO_RELAY -> radio();
case PHOTO_CLUB -> photo();
case IMPOSSIBLE_SERVICE -> impossible();
case CONSTRUCTION_REFUGE -> refuge();
}
}
void shell(int x0, int floor, int z0, int x1, int ceiling, int z1) {
for (int y = floor; y <= ceiling; y++) for (int z = z0; z <= z1; z++) for (int x = x0; x <= x1; x++) {
boolean wall = x == x0 || x == x1 || z == z0 || z == z1;
set(x, y, z, y == floor || y == ceiling ? Blocks.SMOOTH_STONE.defaultBlockState()
: wall ? LostCityPalette.masonry(s.seed(), x, y, z) : Blocks.AIR.defaultBlockState());
}
}
void gable(int x0, int z0, int x1, int z1, int y) {
int rise = Math.min(3, (x1 - x0) / 2);
for (int step = 0; step <= rise; step++)
box(x0 + step, y + step, z0, x1 - step, y + step, z1, step == rise ? Blocks.STONE_BRICK_SLAB : Blocks.STONE_BRICKS);
}
void door(int x, int y, int z, Direction facing) { door(x, y, z, facing, false); }
void door(int x, int y, int z, Direction facing, boolean right) {
BlockState state = Blocks.DARK_OAK_DOOR.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing)
.setValue(BlockStateProperties.DOOR_HINGE, right ? DoorHingeSide.RIGHT : DoorHingeSide.LEFT)
.setValue(BlockStateProperties.OPEN, false).setValue(BlockStateProperties.POWERED, false);
set(x, y, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.LOWER));
set(x, y + 1, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.UPPER));
}
void doubleDoor(int x, int y, int z) { door(x, y, z, Direction.SOUTH, true); door(x + 1, y, z, Direction.SOUTH, false); }
void northWindows(int x0, int x1, int z, int y) { box(x0, y, z, x1, y + 1, z, Blocks.GLASS_PANE); }
void hang(int x, int ceiling, int z) { set(x, ceiling - 1, z, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true)); }
void seat(int x, int y, int z, Direction view) {
set(x, y, z, Blocks.DARK_OAK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, view.getOpposite()));
}
void lever(int x, int y, int z, Direction facing) {
set(x, y, z, Blocks.LEVER.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR)
.setValue(BlockStateProperties.HORIZONTAL_FACING, facing));
}
void rail(int x, int z, RailShape shape) { set(x, 1, z, Blocks.RAIL.defaultBlockState().setValue(BlockStateProperties.RAIL_SHAPE, shape)); }
void loot(int x, int y, int z, int approachX, int approachZ) {
target("loot_approach", approachX, y, approachZ);
if (out != null) out.chest(rx(x, z), y, rz(x, z), ResourceKey.create(Registries.LOOT_TABLE,
SanctuaryMod.id("chests/heritage19/" + s.kind().name().toLowerCase(Locale.ROOT))), s.seed() ^ 0x19CA7A10L);
}
void stairs(int x, int z, int base, int rise) {
for (int step = 1; step <= rise; step++) for (int xx = x; xx <= x + 1; xx++) {
box(xx, base + step + 1, z + step - 1, xx, base + step + 3, z + step - 1, Blocks.AIR);
set(xx, base + step - 1, z + step - 1, Blocks.STONE_BRICKS);
set(xx, base + step, z + step - 1, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
}
}
void bed(int x, int y, int z, Direction facing) {
BlockState state = Blocks.BED.white().defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing);
set(x, y, z, state.setValue(BlockStateProperties.BED_PART, BedPart.FOOT));
set(x + facing.getStepX(), y, z + facing.getStepZ(), state.setValue(BlockStateProperties.BED_PART, BedPart.HEAD));
int wx = x + facing.getStepX() * 2, wz = z + facing.getStepZ() * 2;
Direction along = facing.getClockWise();
for (int side = -1; side <= 1; side++) for (int up = 0; up <= 2; up++)
set(wx + along.getStepX() * side, y + up, wz + along.getStepZ() * side, Blocks.SMOOTH_STONE);
}
void station() {
shell(4, 0, 3, 12, 6, 11); gable(4, 3, 12, 11, 6);
door(8, 1, 3, Direction.SOUTH); northWindows(5, 6, 3, 2); northWindows(10, 11, 3, 2);
box(5, 1, 5, 6, 1, 5, Blocks.SMOOTH_STONE); set(5, 1, 8, Blocks.BARREL); seat(10, 1, 5, Direction.WEST);
for (int z = 2; z <= 14; z++) rail(2, z, RailShape.NORTH_SOUTH);
set(2, 1, 15, Blocks.DARK_OAK_FENCE);
rail(8, 0, RailShape.NORTH_SOUTH); rail(8, 1, RailShape.NORTH_WEST);
for (int x = 3; x <= 7; x++) rail(x, 1, RailShape.EAST_WEST);
rail(2, 1, RailShape.SOUTH_EAST);
box(0, 5, 5, 3, 5, 11, Blocks.DARK_OAK_PLANKS);
for (int z : new int[] {5, 11}) box(1, 1, z, 1, 4, z, Blocks.STRIPPED_OAK_LOG);
shell(9, 0, 11, 15, 4, 15); door(10, 1, 11, Direction.SOUTH); door(12, 1, 15, Direction.NORTH);
set(10, 1, 13, Blocks.SMOOTH_STONE); lever(10, 2, 13, Direction.EAST);
northWindows(13, 14, 15, 2); hang(8, 6, 7); hang(12, 4, 12);
target("ticket_counter", 8, 1, 5); target("freight_platform", 3, 1, 9); target("signal_office", 12, 1, 12);
target("secondary_entry", 12, 1, 16); loot(14, 1, 14, 13, 14);
}
void post() {
shell(2, 0, 2, 14, 6, 14); door(8, 1, 2, Direction.SOUTH);
northWindows(3, 6, 2, 2); northWindows(10, 13, 2, 2);
box(3, 1, 7, 13, 5, 7, Blocks.SMOOTH_STONE); door(8, 1, 7, Direction.SOUTH);
box(4, 1, 6, 6, 1, 6, Blocks.DARK_OAK_PLANKS);
for (int z = 9; z <= 11; z++) { set(3, 1, z, Blocks.BARREL); set(3, 2, z, Blocks.BARREL); }
set(6, 1, 10, Blocks.CRAFTING_TABLE); set(8, 1, 12, Blocks.BARREL);
shell(10, 0, 10, 14, 6, 14); door(10, 1, 12, Direction.EAST); door(14, 1, 12, Direction.WEST);
set(11, 1, 11, Blocks.CARTOGRAPHY_TABLE); hang(8, 6, 4); hang(7, 6, 10); hang(12, 6, 12);
target("public_hall", 8, 1, 4); target("sorting_aisle", 7, 1, 10); target("factor_office", 12, 1, 12);
target("secondary_entry", 15, 1, 12); loot(13, 1, 13, 12, 13);
}
void depot() {
shell(1, 0, 2, 15, 9, 14); gable(1, 2, 15, 14, 9);
box(7, 1, 2, 9, 3, 2, Blocks.AIR); door(4, 1, 2, Direction.SOUTH);
for (int z : new int[] {5, 8}) {
box(4, 1, z, 5, 1, z + 1, Blocks.DARK_OAK_PLANKS); set(4, 2, z, Blocks.BARREL);
box(11, 1, z, 13, 1, z + 1, Blocks.DARK_OAK_PLANKS); set(13, 2, z + 1, Blocks.BARREL);
}
box(2, 5, 10, 14, 5, 13, Blocks.SMOOTH_STONE);
for (int x = 5; x <= 14; x++) set(x, 6, 10, Blocks.IRON_BARS);
shell(10, 5, 10, 14, 9, 14); door(10, 6, 12, Direction.EAST);
set(11, 6, 11, Blocks.LECTERN.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
stairs(2, 5, 0, 5); door(15, 1, 12, Direction.WEST); hang(8, 9, 6); hang(12, 9, 12);
for (int z = 0; z <= 8; z++) rail(8, z, RailShape.NORTH_SOUTH);
set(8, 1, 9, Blocks.DARK_OAK_FENCE);
target("unloading_bay", 8, 1, 5); target("inventory_mezzanine", 5, 6, 12);
target("secondary_entry", 16, 1, 12); loot(13, 6, 13, 12, 13);
}
void expedition() {
shell(2, 0, 2, 12, 6, 12); door(8, 1, 2, Direction.SOUTH);
northWindows(3, 5, 2, 2); northWindows(10, 11, 2, 2);
set(4, 1, 4, Blocks.CARTOGRAPHY_TABLE); set(5, 1, 4, Blocks.LECTERN.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH)); set(3, 1, 6, Blocks.BARREL);
shell(2, 0, 8, 6, 6, 12); door(6, 1, 10, Direction.WEST); set(3, 1, 9, Blocks.BOOKSHELF);
box(3, 0, 13, 14, 0, 15, Blocks.DARK_OAK_PLANKS);
box(3, 1, 15, 14, 1, 15, Blocks.DARK_OAK_FENCE); door(10, 1, 12, Direction.NORTH);
seat(6, 1, 13, Direction.SOUTH); hang(8, 6, 5); hang(4, 6, 10);
target("expedition_table", 5, 1, 5); target("balcony", 10, 1, 14);
target("secondary_entry", 10, 1, 13); loot(3, 1, 11, 4, 11);
}
boolean tower(int x, int z) { return x >= 3 && x <= 13 && z >= 3 && z <= 13 && Math.abs(x - 8) + Math.abs(z - 8) <= 8; }
void observatory() {
for (int y = 0; y <= 12; y++) for (int z = 3; z <= 13; z++) for (int x = 3; x <= 13; x++) if (tower(x, z)) {
boolean wall = !tower(x - 1, z) || !tower(x + 1, z) || !tower(x, z - 1) || !tower(x, z + 1);
set(x, y, z, y == 0 || y == 6 || y == 12 ? Blocks.SMOOTH_STONE.defaultBlockState()
: wall ? LostCityPalette.masonry(s.seed(), x, y, z) : Blocks.AIR.defaultBlockState());
if (y == 12 && wall) set(x, 13, z, Blocks.IRON_BARS);
}
door(8, 1, 3, Direction.SOUTH); door(8, 1, 13, Direction.NORTH);
northWindows(7, 9, 3, 8); box(13, 8, 7, 13, 9, 9, Blocks.GLASS_PANE);
shell(9, 0, 9, 12, 4, 12); door(9, 1, 10, Direction.EAST); set(10, 1, 11, Blocks.BARREL);
set(10, 7, 5, Blocks.CARTOGRAPHY_TABLE); set(11, 7, 8, Blocks.SMOOTH_STONE); lever(11, 8, 8, Direction.WEST);
stairs(4, 5, 0, 6); stairs(4, 5, 6, 6);
box(4, 12, 11, 6, 12, 11, Blocks.SMOOTH_STONE); box(4, 13, 11, 6, 15, 11, Blocks.AIR);
box(10, 13, 9, 10, 18, 9, Blocks.DARK_OAK_FENCE); box(9, 18, 9, 11, 18, 9, Blocks.IRON_BARS);
hang(8, 6, 6); hang(10, 4, 11);
target("instruments", 9, 7, 8); target("sky_platform", 8, 13, 8);
target("secondary_entry", 8, 1, 14); loot(11, 1, 11, 11, 10);
}
void railway() {
shell(1, 0, 2, 15, 6, 14); gable(1, 2, 15, 14, 6); doubleDoor(7, 1, 2);
for (int z = 3; z <= 13; z++) for (int x : new int[] {5, 11}) set(x, 1, z, Blocks.RAIL);
box(4, 1, 2, 6, 3, 2, Blocks.AIR); rail(8, 0, RailShape.NORTH_SOUTH); rail(8, 1, RailShape.NORTH_WEST);
rail(7, 1, RailShape.EAST_WEST); rail(6, 1, RailShape.EAST_WEST); rail(5, 1, RailShape.SOUTH_EAST); rail(5, 2, RailShape.NORTH_SOUTH);
box(7, -2, 5, 9, -2, 11, Blocks.SMOOTH_STONE); box(7, -1, 5, 9, 0, 11, Blocks.AIR);
for (int step = 0; step <= 2; step++) set(8, -step, 4 + step, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
for (int z = 6; z <= 11; z++) { set(6, 1, z, Blocks.IRON_BARS); set(10, 1, z, Blocks.IRON_BARS); }
set(2, 1, 4, Blocks.CRAFTING_TABLE); set(3, 1, 4, Blocks.FURNACE.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
set(2, 1, 6, Blocks.GRINDSTONE.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR));
shell(1, 0, 10, 5, 5, 14); door(5, 1, 12, Direction.WEST); door(8, 1, 14, Direction.NORTH);
set(5, 1, 9, Blocks.DARK_OAK_FENCE);
set(2, 1, 11, Blocks.ANVIL); hang(8, 6, 8); hang(3, 5, 12);
target("maintenance_pit", 8, -1, 9); target("workbench", 3, 1, 5);
target("secondary_entry", 8, 1, 15); loot(2, 1, 13, 3, 13);
}
void pumping() {
shell(2, 0, 3, 9, 6, 12); door(8, 1, 3, Direction.SOUTH);
set(3, 1, 5, Blocks.DISPENSER.defaultBlockState().setValue(BlockStateProperties.FACING, Direction.EAST));
set(4, 1, 5, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 3));
set(3, 2, 5, Blocks.POLISHED_ANDESITE); box(3, 3, 5, 3, 5, 8, Blocks.IRON_BARS);
for (int x : new int[] {10, 15}) for (int z : new int[] {5, 11}) box(x, 1, z, x, 7, z, Blocks.POLISHED_ANDESITE);
shell(10, 8, 5, 15, 13, 11); box(11, 9, 6, 14, 11, 10, Blocks.WATER);
box(10, 3, 6, 14, 3, 13, Blocks.SMOOTH_STONE); stairs(10, 3, 0, 3);
shell(2, 0, 10, 6, 5, 15); door(5, 1, 10, Direction.SOUTH); door(5, 1, 15, Direction.NORTH);
set(3, 1, 11, Blocks.BARREL); hang(7, 6, 7); hang(4, 5, 12);
target("pump_room", 6, 1, 6); target("reservoir_walkway", 13, 4, 12);
target("secondary_entry", 5, 1, 16); loot(3, 1, 14, 4, 14);
}
void substation() {
for (int z = 2; z <= 14; z++) { set(1, 1, z, Blocks.IRON_BARS); set(15, 1, z, Blocks.IRON_BARS); }
for (int x = 1; x <= 15; x++) if (x < 7 || x > 9) set(x, 1, 2, Blocks.IRON_BARS);
for (int z : new int[] {5, 8}) {
box(3, 1, z, 4, 2, z + 1, Blocks.POLISHED_ANDESITE); box(3, 3, z, 4, 3, z, Blocks.IRON_BARS);
}
shell(10, 0, 4, 15, 5, 10); door(10, 1, 6, Direction.EAST); northWindows(12, 14, 4, 2);
set(12, 1, 5, Blocks.SMOOTH_STONE); lever(12, 2, 5, Direction.SOUTH); set(14, 1, 5, Blocks.CRAFTING_TABLE);
shell(10, 0, 10, 15, 5, 15); door(12, 1, 10, Direction.SOUTH); door(15, 1, 13, Direction.WEST);
box(6, -2, 10, 7, -2, 13, Blocks.SMOOTH_STONE); box(6, -1, 10, 7, 0, 13, Blocks.AIR);
for (int step = 0; step <= 2; step++) set(6, -step, 9 + step, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
hang(13, 5, 7); hang(12, 5, 13); target("distribution_yard", 8, 1, 6); target("cable_trench", 6, -1, 12);
target("secondary_entry", 16, 1, 13); loot(14, 1, 14, 13, 14);
}
void greenhouse() {
shell(1, 0, 2, 11, 6, 14);
for (int y = 2; y <= 5; y++) for (int z = 3; z <= 13; z++) { set(1, y, z, Blocks.GLASS); set(11, y, z, Blocks.GLASS); }
northWindows(2, 5, 2, 2); northWindows(7, 10, 2, 2); door(6, 1, 2, Direction.SOUTH);
for (int x = 2; x <= 10; x++) for (int z = 3; z <= 13; z++) set(x, 6, z, x == 6 ? Blocks.SMOOTH_STONE : Blocks.GLASS);
for (int x : new int[] {2, 3, 4, 8, 9, 10}) for (int z = 4; z <= 10; z++) {
set(x, 0, z, Blocks.FARMLAND.defaultBlockState().setValue(BlockStateProperties.MOISTURE, 7));
set(x, 1, z, Blocks.WHEAT.defaultBlockState().setValue(BlockStateProperties.AGE_7,
2 + LostCityPalette.roll(s.seed(), x, 1, z, 6)));
}
set(3, 0, 6, Blocks.WATER); set(9, 0, 6, Blocks.WATER);
set(3, 1, 6, Blocks.AIR); set(9, 1, 6, Blocks.AIR);
shell(11, 0, 9, 15, 5, 15); door(11, 1, 12, Direction.EAST); door(13, 1, 15, Direction.NORTH);
set(12, 1, 10, Blocks.CRAFTING_TABLE); set(13, 1, 10, Blocks.FLOWER_POT); set(9, 1, 12, Blocks.COMPOSTER);
hang(6, 6, 5); hang(13, 5, 12); target("glasshouse_aisle", 6, 1, 7); target("potting_shed", 13, 1, 12);
target("secondary_entry", 13, 1, 16); loot(14, 1, 14, 13, 14);
}
void dock() {
box(0, 0, 0, 16, 0, 16, Blocks.DARK_OAK_PLANKS); box(0, 1, 16, 16, 1, 16, Blocks.DARK_OAK_FENCE);
box(0, 1, 1, 0, 1, 15, Blocks.DARK_OAK_FENCE); box(16, 1, 1, 16, 1, 15, Blocks.DARK_OAK_FENCE);
shell(2, 0, 3, 7, 5, 10); door(7, 1, 6, Direction.WEST); seat(3, 1, 5, Direction.EAST);
shell(2, 0, 10, 7, 5, 15); door(5, 1, 10, Direction.SOUTH); door(7, 1, 13, Direction.WEST);
box(12, 1, 5, 12, 11, 5, Blocks.STRIPPED_OAK_LOG); box(8, 11, 5, 14, 11, 5, Blocks.STRIPPED_OAK_LOG);
box(14, 7, 5, 14, 10, 5, Blocks.IRON_CHAIN); hang(10, 11, 5); hang(5, 5, 6); hang(5, 5, 12);
target("departure_deck", 8, 1, 14); target("waiting_room", 5, 1, 6); target("secondary_entry", 8, 1, 13);
loot(3, 1, 14, 4, 14);
}
void radio() {
shell(2, 0, 3, 10, 6, 12); door(8, 1, 3, Direction.SOUTH); northWindows(3, 5, 3, 2);
bed(4, 1, 5, Direction.NORTH); set(8, 1, 5, Blocks.CARTOGRAPHY_TABLE);
shell(2, 0, 8, 6, 6, 12); door(6, 1, 10, Direction.WEST); door(2, 1, 10, Direction.EAST);
box(11, 5, 6, 15, 5, 10, Blocks.SMOOTH_STONE);
for (int x : new int[] {11, 15}) for (int z : new int[] {6, 10}) box(x, 1, z, x, 4, z, Blocks.POLISHED_ANDESITE);
box(13, 1, 8, 13, 18, 8, Blocks.POLISHED_ANDESITE); box(11, 17, 8, 15, 17, 8, Blocks.IRON_BARS);
box(13, 19, 8, 13, 21, 8, Blocks.IRON_BARS); stairs(11, 1, 0, 5);
hang(7, 6, 6); hang(4, 6, 10);
target("caretaker_room", 7, 1, 6); target("antenna_platform", 12, 6, 9); target("secondary_entry", 1, 1, 10);
loot(3, 1, 11, 4, 11);
}
void photo() {
shell(2, 0, 2, 14, 6, 14); door(8, 1, 2, Direction.SOUTH); northWindows(3, 6, 2, 2); northWindows(10, 13, 2, 2);
box(3, 1, 8, 13, 5, 8, Blocks.SMOOTH_STONE); door(4, 1, 8, Direction.SOUTH);
box(8, 1, 9, 8, 5, 13, Blocks.SMOOTH_STONE); door(8, 1, 11, Direction.EAST);
box(10, 1, 5, 12, 1, 5, Blocks.DARK_OAK_PLANKS); set(3, 1, 4, Blocks.BOOKSHELF);
for (int z : new int[] {10, 12}) set(3, 1, z, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 3));
box(11, 4, 10, 11, 5, 12, Blocks.IRON_CHAIN); set(12, 1, 10, Blocks.CRAFTING_TABLE);
door(12, 1, 14, Direction.NORTH); hang(8, 6, 4); hang(5, 6, 11); hang(12, 6, 12);
target("club_room", 8, 1, 4); target("darkroom", 5, 1, 11); target("drying_room", 10, 1, 11);
target("secondary_entry", 12, 1, 15); loot(13, 1, 13, 12, 13);
}
void impossible() {
shell(3, 0, 2, 11, 4, 7); door(8, 1, 2, Direction.SOUTH);
for (int z = 3; z <= 5; z++) { set(4, 1, z, Blocks.BARREL); set(4, 2, z, Blocks.BARREL); }
seat(10, 1, 4, Direction.WEST);
shell(1, 0, 7, 15, 10, 15); door(8, 1, 7, Direction.SOUTH);
for (int x = 2; x <= 14; x++) for (int z = 8; z <= 14; z++) set(x, 0, z,
((x + z) & 1) == 0 ? Blocks.CALCITE : Blocks.POLISHED_ANDESITE);
for (int x : new int[] {3, 6, 10, 13}) box(x, 1, 11, x, 9, 11, Blocks.CALCITE);
shell(1, 0, 12, 5, 5, 15); door(5, 1, 13, Direction.WEST); door(3, 1, 15, Direction.NORTH);
set(2, 2, 12, Blocks.GLASS); set(2, 1, 13, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 2));
hang(8, 4, 4); hang(8, 10, 11); hang(3, 5, 13);
target("lockers", 8, 1, 4); target("oversized_tiled_hall", 8, 1, 11);
target("secondary_entry", 3, 1, 16); loot(2, 1, 14, 3, 14);
}
void refuge() {
box(3, 4, 5, 14, 4, 15, Blocks.DARK_OAK_PLANKS);
for (int x : new int[] {3, 14}) for (int z : new int[] {5, 15}) box(x, 1, z, x, 3, z, Blocks.STONE_BRICKS);
shell(5, 4, 6, 13, 10, 14); gable(5, 6, 13, 14, 10); door(8, 5, 6, Direction.SOUTH);
bed(6, 5, 8, Direction.NORTH); set(12, 5, 8, Blocks.CRAFTING_TABLE);
shell(6, 0, 9, 11, 4, 14); door(6, 1, 11, Direction.EAST);
// Carry the ladder through the floor so opening the hatch leaves a climbable lip.
for (int y = 1; y <= 4; y++) set(10, y, 11, Blocks.LADDER.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST));
set(10, 5, 11, Blocks.DARK_OAK_TRAPDOOR.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST)
.setValue(BlockStateProperties.HALF, Half.BOTTOM));
stairs(7, 1, 0, 4); box(2, 1, 3, 3, 11, 4, Blocks.STONE_BRICKS); box(2, 11, 3, 14, 11, 4, Blocks.STRIPPED_OAK_LOG);
hang(10, 10, 8); hang(8, 4, 12);
target("suspended_hut", 9, 5, 8); target("hidden_floor_hatch", 10, 5, 11); target("secondary_entry", 5, 1, 11);
loot(7, 1, 13, 8, 13);
}
}
}
@@ -0,0 +1,42 @@
package fr.koka.sanctuary.worldgen.heritage;
import com.mojang.serialization.MapCodec;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.util.ArrayList;
import java.util.Optional;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureType;
/** Island-scale routes are split into small independent native starts before rendering. */
public final class HeritageStructure19 extends Structure {
public static final MapCodec<HeritageStructure19> CODEC = simpleCodec(HeritageStructure19::new);
public HeritageStructure19(StructureSettings settings) { super(settings); }
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 19 || !generator.experimentalStructures()) return Optional.empty();
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
if (island == null || !island.origin() || island.generation() != 19) return Optional.empty();
var plan = generator.heritagePlan19();
int x = context.chunkPos().x(), z = context.chunkPos().z();
var pieces = new ArrayList<HeritagePiece19>();
// Local route segments render before buildings. No piece needs a distant link endpoint loaded.
for (var segment : plan.segments())
if (segment.startChunkX() == x && segment.startChunkZ() == z)
pieces.add(new HeritagePiece19(segment));
for (var site : plan.sites()) if (site.startChunkX() == x && site.startChunkZ() == z)
pieces.add(new HeritagePiece19(site));
if (pieces.isEmpty()) return Optional.empty();
for (var piece : pieces) {
var box = piece.getBoundingBox();
if (Math.abs((long) Math.floorDiv(box.minX(), 16) - x) > 7
|| Math.abs((long) Math.floorDiv(box.maxX(), 16) - x) > 7
|| Math.abs((long) Math.floorDiv(box.minZ(), 16) - z) > 7
|| Math.abs((long) Math.floorDiv(box.maxZ(), 16) - z) > 7)
throw new IllegalStateException("Heritage piece exceeds its local native reference contract");
}
var locator = pieces.stream().filter(HeritagePiece19::isSite).findFirst().orElse(pieces.getFirst()).getLocatorPosition();
return Optional.of(new GenerationStub(locator, builder -> pieces.forEach(builder::addPiece)));
}
@Override public StructureType<?> type() { return HeritageStructures19.TYPE; }
}
@@ -0,0 +1,110 @@
package fr.koka.sanctuary.worldgen.heritage;
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 generation19 uses independent native starts, references and saved room pieces. */
public final class HeritageStructures19 {
public static final ResourceKey<Structure> KEY = ResourceKey.create(Registries.STRUCTURE, SanctuaryMod.id("heritage_networks"));
public static final StructureType<HeritageStructure19> TYPE = Registry.register(BuiltInRegistries.STRUCTURE_TYPE,
SanctuaryMod.id("heritage_networks_v19"), () -> HeritageStructure19.CODEC);
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
SanctuaryMod.id("heritage_piece_v19"), HeritagePiece19::new);
private static final ThreadLocal<Integer> PLACEMENT_DEPTH = ThreadLocal.withInitial(() -> 0);
private HeritageStructures19() {}
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() != 19
|| !sanctuary.experimentalStructures()) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
var plan = sanctuary.heritagePlan19();
if (!hasStart(plan, chunk.getPos().x(), 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() != 19 || !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<HeritagePlan19.Site>();
int originChunkX = origin.getX() >> 4, originChunkZ = origin.getZ() >> 4;
for (var network : generator.heritagePlan19().sites()) {
var island = generator.islandForChunk(network.startChunkX(), network.startChunkZ());
if (island == null || !island.origin() || island.generation() != generator.rootGeneration() || !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 HeritagePiece19 site
&& site.isSite()).findFirst().orElseThrow();
return Pair.of(entrance.getLocatorPosition(), holder);
}
return null;
}
static BlockPos locator(HeritagePlan19.Site site) { return HeritagePiece19.locator(site); }
static boolean hasStart(HeritagePlan19 plan, int x, int z) {
return plan.sites().stream().anyMatch(s -> s.startChunkX() == x && s.startChunkZ() == z)
|| plan.segments().stream().anyMatch(s -> s.startChunkX() == x && s.startChunkZ() == z);
}
}
@@ -0,0 +1,110 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import fr.koka.sanctuary.worldgen.city.LostCityPlan;
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
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 pre-decoration terrain. No loaded chunks or player-edited blocks enter a plan. */
public final class HeritageTerrain19 implements HeritagePlanner19.Terrain {
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
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<>();
private List<HeritagePlan19.Landmark> landmarks = List.of();
public HeritageTerrain19(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 previous = columns.get(key);
if (previous != null) return previous;
if (columns.size() >= HeritagePlan19.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;
}
@Override public List<HeritagePlan19.Landmark> landmarks() { return landmarks; }
public int columnsUsed() { return columns.size(); }
/** Select actual dry ground abutting existing walkable platforms. Their protected blocks stay intact. */
public void discoverLandmarks(LostCityPlan city, RuinsPlan17 ruins) {
var result = new ArrayList<HeritagePlan19.Landmark>();
for (var network : ruins.networks()) {
var access = network.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS).findFirst().orElse(null);
if (access == null) continue;
int deck = access.maxY() - 2;
for (int direction = 0; direction < 4; direction++) {
var entry = dryEntry(access.centerX() + DX[direction] * 3, access.centerZ() + DZ[direction] * 3, deck, direction);
if (entry == null) continue;
var kind = network.family() == RuinsPlan17.Family.MINE ? HeritagePlan19.LandmarkKind.MINE : HeritagePlan19.LandmarkKind.UTILITY;
result.add(new HeritagePlan19.Landmark("old_" + network.id(), kind, entry));
break;
}
}
int cityEntries = 0;
outer: for (var district : city.districts()) for (var parcel : district.parcels()) {
if (parcel.kind() != LostCityPlan.Kind.STREET) continue;
boolean eastWest = parcel.width() > parcel.depth();
for (int direction : eastWest ? new int[]{1, 3} : new int[]{0, 2}) {
int x = direction == 1 ? parcel.width() - 1 : direction == 3 ? 0 : parcel.width() / 2;
int z = direction == 2 ? parcel.depth() - 1 : direction == 0 ? 0 : parcel.depth() / 2;
var entry = dryEntry(parcel.minX() + x + DX[direction], parcel.minZ() + z + DZ[direction], parcel.surfaceY(x, z), direction);
if (entry == null) continue;
result.add(new HeritagePlan19.Landmark("old_city_" + cityEntries++, HeritagePlan19.LandmarkKind.CITY, entry));
if (cityEntries == 2) break outer;
}
}
landmarks = List.copyOf(result);
}
private HeritagePlan19.Entry dryEntry(int x, int z, int deck, int direction) {
int y = surfaceY(x, z);
if (y < 32 || y > 348 || Math.abs(y - deck) > 1) return null;
// A three-wide arrival can abut a platform without cutting its wall or footings.
for (int side = -1; side <= 1; side++) {
int px = x + DZ[direction] * side, pz = z + DX[direction] * side;
var column = column(px, pz);
if (column == null || surfaceY(px, pz) != y || !solid(px, y, pz)) return null;
for (int by = y - 1; by <= y + 3; by++) if (protectedAt(px, by, pz)) return null;
for (int by = y + 1; by <= y + 3; by++) if (!column.getBlock(by).isAir()) return null;
}
return new HeritagePlan19.Entry(x, y + 1, z, direction);
}
}
@@ -13,9 +13,9 @@ public final class RuinsStructure17 extends Structure {
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 17 || !generator.experimentalStructures()) return Optional.empty();
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19) || !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();
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) 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();
@@ -53,10 +53,10 @@ public final class RuinsStructures17 {
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
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 17 && generator.rootGeneration() != 19)
|| !sanctuary.experimentalStructures()) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || island.generation() != 17) return;
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
var plan = sanctuary.ruinsPlan17();
if (plan.networks().stream().noneMatch(n -> n.startChunkX() == chunk.getPos().x()
&& n.startChunkZ() == chunk.getPos().z())) return;
@@ -76,7 +76,7 @@ public final class RuinsStructures17 {
BlockPos origin, int radius, boolean skipReferenced) {
if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 17 || !generator.experimentalStructures()
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19) || !generator.experimentalStructures()
|| !level.structureManager().shouldGenerateStructures()) return null;
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
if (!structures.contains(holder)) return null;
@@ -84,7 +84,7 @@ public final class RuinsStructures17 {
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 (island == null || !island.origin() || island.generation() != generator.rootGeneration() || !island.status().equals("ready")) continue;
if (Math.abs((long) network.startChunkX() - originChunkX) <= radius
&& Math.abs((long) network.startChunkZ() - originChunkZ) <= radius) candidates.add(network);
}
@@ -127,5 +127,24 @@
"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"
"structure.sanctuary.forgotten_networks": "Forgotten networks",
"sanctuary.world_options.heritage_hint": "Adds forgotten networks, buildings and abandoned routes to the new island. This choice is saved with the world.",
"structure.sanctuary.heritage_networks": "Abandoned infrastructure",
"sanctuary.heritage.kind.freight_station": "Freight station",
"sanctuary.heritage.kind.post_office": "Post office",
"sanctuary.heritage.kind.communal_depot": "Communal depot",
"sanctuary.heritage.kind.expedition_office": "Expedition office and map archive",
"sanctuary.heritage.kind.observatory": "Observatory and weather station",
"sanctuary.heritage.kind.rail_workshop": "Rail workshop",
"sanctuary.heritage.kind.pump_station": "Pump station",
"sanctuary.heritage.kind.substation": "Utility substation",
"sanctuary.heritage.kind.greenhouse": "Greenhouse and seed store",
"sanctuary.heritage.kind.void_dock": "Expedition dock",
"sanctuary.heritage.kind.radio_relay": "Radio relay",
"sanctuary.heritage.kind.photo_club": "Photography club",
"sanctuary.heritage.kind.impossible_service": "Changing rooms and strange service rooms",
"sanctuary.heritage.kind.construction_refuge": "Construction refuge",
"sanctuary.heritage.unavailable": "This survey requires a Sanctuary alpha.19 world with experimental structures enabled.",
"sanctuary.heritage.header": "Abandoned infrastructure: %s planned sites and %s links. Entrance coordinates:",
"sanctuary.heritage.entry": "%s : %s / %s / %s"
}
@@ -127,5 +127,24 @@
"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"
"structure.sanctuary.forgotten_networks": "Réseaux oubliés",
"sanctuary.world_options.heritage_hint": "Ajoute les anciens réseaux, bâtiments et voies abandonnées sur la nouvelle île. Ce choix est enregistré avec le monde.",
"structure.sanctuary.heritage_networks": "Anciennes infrastructures",
"sanctuary.heritage.kind.freight_station": "Gare de marchandises",
"sanctuary.heritage.kind.post_office": "Centre postal",
"sanctuary.heritage.kind.communal_depot": "Dépôt communal",
"sanctuary.heritage.kind.expedition_office": "Bureau des expéditions et cartothèque",
"sanctuary.heritage.kind.observatory": "Observatoire et station météo",
"sanctuary.heritage.kind.rail_workshop": "Atelier ferroviaire",
"sanctuary.heritage.kind.pump_station": "Station de pompage",
"sanctuary.heritage.kind.substation": "Sous-station",
"sanctuary.heritage.kind.greenhouse": "Serre et graineterie",
"sanctuary.heritage.kind.void_dock": "Quai dexpédition",
"sanctuary.heritage.kind.radio_relay": "Relais radio",
"sanctuary.heritage.kind.photo_club": "Club photo",
"sanctuary.heritage.kind.impossible_service": "Vestiaires et salles étranges",
"sanctuary.heritage.kind.construction_refuge": "Refuge de chantier",
"sanctuary.heritage.unavailable": "Ce relevé nécessite un monde Sanctuary alpha.19 avec les structures expérimentales activées.",
"sanctuary.heritage.header": "Anciennes infrastructures : %s lieux et %s liaisons prévus. Coordonnées des entrées :",
"sanctuary.heritage.entry": "%s : %s / %s / %s"
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:iron_axe"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:torch",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:bread",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:iron_shovel"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:bread",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:torch",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:compass"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:paper",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:map",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:minecart"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:rail",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:coal",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:iron_hoe"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:wheat_seeds",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:pumpkin_seeds",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:clock"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:torch",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:string",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:spyglass"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:paper",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:book",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:book"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:paper",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:glass_pane",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:compass"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:paper",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:string",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:bucket"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:string",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:torch",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:compass"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:redstone",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:paper",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:iron_pickaxe"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:rail",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:coal",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:clock"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:redstone",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:copper_ingot",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:lead"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:torch",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:bread",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -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:heritage_networks_v19",
"biomes": "#sanctuary:has_structure/heritage_networks",
"step": "underground_structures",
"terrain_adaptation": "none",
"spawn_overrides": {}
}
@@ -3,7 +3,7 @@
"minecraft:overworld": {
"type": "sanctuary:sanctuary_384",
"generator": {
"type": "sanctuary:island_v17",
"type": "sanctuary:island_v19",
"biome_source": {
"biomes": [
"minecraft:the_void",
@@ -0,0 +1,81 @@
{
"dimensions": {
"minecraft:overworld": {
"type": "sanctuary:sanctuary_384",
"generator": {
"type": "sanctuary:island_v17",
"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"
}
}
}
}
@@ -158,6 +158,21 @@ public final class ExpansionJournalSmoke {
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");
var heritageRoot = ExpansionIsland.origin(724, SEED, 19);
var heritageDirectory = scratch.resolve("heritage-19");
var heritageJournal = new ExpansionJournal(heritageDirectory, SEED, 724, 19);
var heritageChild = ExpansionPlacement.candidates(SEED, List.of(heritageRoot), "heritage", "sanctuary", "east", "lost_city", 128, "natural", 0).getFirst()
.withReuseToken("f".repeat(64));
heritageJournal.commit(List.of(heritageRoot, heritageChild));
require(new ExpansionJournal(heritageDirectory, SEED, 724, 19).islands().equals(List.of(heritageRoot, heritageChild)),
"Generation19 preserves the new world and its expansions across journal reload");
byte[] heritageBytes = Files.readAllBytes(heritageDirectory.resolve(ExpansionJournal.FILE_NAME));
reject(() -> new ExpansionJournal(heritageDirectory, SEED, 724, 17), "No implicit downgrade from19 to17");
reject(() -> new ExpansionJournal(experimentDirectory, SEED, 724, 19), "No implicit upgrade from17 to19");
reject(() -> heritageJournal.commit(List.of(heritageRoot, experimentChild)), "No generation17 child in19");
require(Arrays.equals(heritageBytes, Files.readAllBytes(heritageDirectory.resolve(ExpansionJournal.FILE_NAME)))
&& Arrays.equals(experimentBytes, Files.readAllBytes(experimentDirectory.resolve(ExpansionJournal.FILE_NAME))),
"Rejected migrations preserve both generation17 and19 journals");
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,85 @@
package fr.koka.sanctuary.worldgen.heritage;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.IntTag;
import net.minecraft.nbt.ListTag;
/** Exact, bounded independent schema19 persistence for catalogue sites and chunk-local abandoned routes. */
public final class HeritagePiece19Smoke {
public static void main(String[] args) {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); run();
System.out.println("HeritagePiece19Smoke: 14 kinds, 4 facades, every rail profile, missing rails, strict NBT and local native bounds passed.");
}
public static void run() {
for (var kind : HeritagePlan19.Kind.values()) for (int face = 0; face < 4; face++) {
int x = -32, z = 40, floor = 160;
var entry = new HeritagePlan19.Entry(face == 1 ? x + 17 : face == 3 ? x - 1 : x + 8,
floor + 1, face == 2 ? z + 17 : face == 0 ? z - 1 : z + 8, face);
var supportList = new ArrayList<HeritagePlan19.Support>();
supportList.add(new HeritagePlan19.Support(x + 1, 150, z + 1, 155));
var site = new HeritagePlan19.Site("site_" + kind.name(), kind, x, floor, z, 17, 17, 20, face,
Long.MIN_VALUE + kind.ordinal(), 4, supportList, entry);
supportList.clear();
var saved = HeritagePiece19.encodeSite(site);
require(HeritagePiece19.decodeSite(saved).equals(site) && HeritagePiece19.encodeSite(HeritagePiece19.decodeSite(saved)).equals(saved), "Site roundtrip changed geometry");
require(site.supports().size() == 1 && HeritagePiece19.bounds(site).minY() == 150, "Support input is mutable or excluded from native bounds");
require(HeritagePiece19.locator(site).getX() == entry.x() && HeritagePiece19.locator(site).getY() == entry.y()
&& HeritagePiece19.locator(site).getZ() == entry.z(), "Locate lost the rotated facade entry");
badSite(saved, t -> t.remove("Facing")); badSite(saved, t -> t.putInt("Schema", 17));
badSite(saved, t -> t.putString("Type", "ROUTE")); badSite(saved, t -> t.putString("Kind", "FUTURE"));
badSite(saved, t -> t.putDouble("FloorY", 160)); badSite(saved, t -> t.putInt("Seed", 1));
badSite(saved, t -> t.putInt("Future", 1)); badSite(saved, t -> t.putInt("MinX", Integer.MAX_VALUE));
badSite(saved, t -> t.putInt("Width", 18)); badSite(saved, t -> t.putInt("Facing", 4));
badSite(saved, t -> t.putInt("FoundationDepth", 0)); badSite(saved, t -> t.putString("Id", "x".repeat(129)));
badSite(saved, t -> t.getCompound("Entry").orElseThrow().putInt("X", entry.x() + 1));
badSite(saved, t -> t.getCompound("Entry").orElseThrow().putLong("Y", entry.y()));
badSite(saved, t -> t.getList("Supports").orElseThrow().getCompound(0).orElseThrow().putInt("MinY", 130));
badSite(saved, t -> t.getList("Supports").orElseThrow().getCompound(0).orElseThrow().putInt("X", site.maxX() + 1));
badSite(saved, t -> {
var list = t.getList("Supports").orElseThrow(); list.add(list.getFirst().copy());
});
}
var cells = new ArrayList<HeritagePlan19.Cell>();
int index = 0;
for (var rail : HeritagePlan19.Rail.values()) {
cells.add(new HeritagePlan19.Cell(-32 + index, 150 + index % 3, 32, 2 + index % 4, rail, rail != HeritagePlan19.Rail.NONE && index % 2 == 0)); index++;
}
var segment = new HeritagePlan19.Segment("freight/part0", "station", "mine", HeritagePlan19.RouteKind.FREIGHT, -2, 2, cells, Long.MAX_VALUE);
cells.clear();
var landing = new HeritagePlan19.Segment("station/landing", "station", "station", HeritagePlan19.RouteKind.PATH,
-2, 2, List.of(new HeritagePlan19.Cell(-32, 150, 32, 2, HeritagePlan19.Rail.NONE, false)), 42);
require(HeritagePiece19.decodeSegment(HeritagePiece19.encodeSegment(landing)).equals(landing), "Standalone entry landing lost its site binding");
var saved = HeritagePiece19.encodeSegment(segment);
require(HeritagePiece19.decodeSegment(saved).equals(segment) && HeritagePiece19.encodeSegment(HeritagePiece19.decodeSegment(saved)).equals(saved), "Route roundtrip changed shapes, supports or deliberate rail gaps");
require(segment.cells().size() == HeritagePlan19.Rail.values().length, "External route list mutation changed its saved cells");
require(HeritagePiece19.bounds(segment).intersectingChunks().count() == 1, "Segment escaped its one-chunk native start");
badRoute(saved, t -> t.putInt("Schema", 20)); badRoute(saved, t -> t.putString("Type", "SITE"));
badRoute(saved, t -> t.remove("FromId")); badRoute(saved, t -> t.putString("ToId", "station"));
badRoute(saved, t -> t.putString("Id", " ")); badRoute(saved, t -> t.putInt("StartChunkX", -1));
badRoute(saved, t -> t.putDouble("Seed", 42)); badRoute(saved, t -> t.put("Cells", new ListTag()));
badRoute(saved, t -> cell(t).putInt("RailPresent", 1)); badRoute(saved, t -> cell(t).putByte("RailPresent", (byte) 2));
badRoute(saved, t -> cell(t).putString("Rail", "FUTURE")); badRoute(saved, t -> cell(t).putBoolean("RailPresent", true));
badRoute(saved, t -> cell(t).putInt("X", -16)); badRoute(saved, t -> cell(t).putInt("FoundationDepth", 13));
badRoute(saved, t -> cell(t).putInt("FloorY", 2)); badRoute(saved, t -> cell(t).putInt("Extra", 0));
badRoute(saved, t -> { var list = t.getList("Cells").orElseThrow(); list.add(list.getFirst().copy()); });
badRoute(saved, t -> { var list = new ListTag(); list.add(IntTag.valueOf(1)); t.put("Cells", list); });
badRoute(saved, t -> { var list = t.getList("Cells").orElseThrow(); while (list.size() <= 256) list.add(list.getFirst().copy()); });
}
private static CompoundTag cell(CompoundTag tag) { return tag.getList("Cells").orElseThrow().getCompound(0).orElseThrow(); }
private static void badSite(CompoundTag original, Consumer<CompoundTag> mutation) {
var value = original.copy(); mutation.accept(value);
try { HeritagePiece19.decodeSite(value); } catch (IllegalArgumentException expected) { return; }
throw new AssertionError("Malformed saved site accepted: " + value);
}
private static void badRoute(CompoundTag original, Consumer<CompoundTag> mutation) {
var value = original.copy(); mutation.accept(value);
try { HeritagePiece19.decodeSegment(value); } catch (IllegalArgumentException expected) { return; }
throw new AssertionError("Malformed saved route accepted: " + value);
}
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
}
@@ -0,0 +1,197 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.*;
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 geography contract: full catalogue, actual destinations, independent entrances and supported axes. */
public final class HeritagePlanner19Smoke {
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
public static void main(String[] args) {
var catalogue = EnumSet.noneOf(Kind.class);
int freight = 0, paths = 0, missingRails = 0, islandAxes = 0;
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 = HeritagePlanner19.plan(seed, 0, 0, diameter, terrain);
verify(plan, terrain);
require(plan.sites().size() >= 8, "Flat starter loses most of its small physical catalogue: " + plan.diagnostics());
require(plan.links().size() >= 2, "Flat starter has no useful inter-destination axes");
plan.sites().forEach(site -> catalogue.add(site.kind()));
freight += (int) plan.links().stream().filter(l -> l.kind() == RouteKind.FREIGHT).count();
islandAxes += (int) plan.links().stream().filter(l -> {
Point first = l.path().getFirst(), last = l.path().getLast();
return Math.max(Math.abs(first.x() - last.x()), Math.abs(first.z() - last.z())) >= diameter / 4;
}).count();
paths += (int) plan.links().stream().filter(l -> l.kind() == RouteKind.PATH).count();
missingRails += (int) plan.segments().stream().flatMap(s -> s.cells().stream())
.filter(c -> c.rail() != Rail.NONE && !c.railPresent()).count();
var repeat = HeritagePlanner19.plan(seed, 0, 0, diameter, new Fixture((x, z) -> 220));
require(plan.sites().equals(repeat.sites()) && plan.links().equals(repeat.links())
&& plan.segments().equals(repeat.segments()) && plan.samples() == repeat.samples()
&& plan.diagnostics().equals(repeat.diagnostics()), "Planning depends on generation order or mutable state");
}
require(catalogue.equals(EnumSet.allOf(Kind.class)), "A requested heritage family never appears: " + catalogue);
require(islandAxes > 0, "All published networks remain local; no axis reaches island scale");
require(freight > 0 && paths > 0 && missingRails > 0, "Both networks and repairable missing rails must be represented");
var flat = HeritagePlanner19.plan(0, 0, 0, 724, new Fixture((x, z) -> 220));
require(!flat.sites().equals(HeritagePlanner19.plan(1, 0, 0, 724, new Fixture((x, z) -> 220)).sites()), "Seed does not affect sites");
Fixture wet = new Fixture((x, z) -> 220); wet.protection = (x, y, z) -> true;
var blocked = HeritagePlanner19.plan(0, 0, 0, 724, wet); verify(blocked, wet);
require(blocked.isEmpty() && blocked.links().isEmpty(), "Protected water or earlier city is overwritten");
Fixture voidTerrain = new Fixture((x, z) -> -1);
var absent = HeritagePlanner19.plan(0, 0, 0, 724, voidTerrain); verify(absent, voidTerrain);
require(absent.isEmpty() && absent.segments().isEmpty(), "Void receives unsupported structures or fictional routes");
Fixture thin = new Fixture((x, z) -> 220); thin.rock = (x, y, z) -> y >= 219 && y <= 220;
var thinPlan = HeritagePlanner19.plan(0, 0, 0, 724, thin); verify(thinPlan, thin);
require(thinPlan.isEmpty(), "Thin shelves receive buildings without anchored footings");
// Every site keeps its local access even when a protected ring rejects every inter-site alignment.
Set<Long> allowed = new HashSet<>();
for (var site : flat.sites()) for (int x = site.minX(); x <= site.maxX(); x++) for (int z = site.minZ(); z <= site.maxZ(); z++) allowed.add(key(x, z));
for (var segment : flat.segments()) if (segment.fromId().equals(segment.toId())) for (var c : segment.cells()) allowed.add(key(c.x(), c.z()));
Fixture isolated = new Fixture((x, z) -> 220); isolated.protection = (x, y, z) -> !allowed.contains(key(x, z));
var islands = HeritagePlanner19.plan(0, 0, 0, 724, isolated); verify(islands, isolated);
require(!islands.sites().isEmpty(), "Independent access fixture has no retained site");
require(islands.sites().stream().anyMatch(site -> islands.links().stream().noneMatch(l -> l.fromId().equals(site.id()) || l.toId().equals(site.id()))),
"The isolated destination witness unexpectedly has every long route");
// Historical landmark feet are read-only and have no mask bypass, even when a route reaches them.
Site station = flat.sites().stream().filter(s -> s.kind() == Kind.FREIGHT_STATION).findFirst().orElseThrow();
Entry historical = new Entry(station.entry().x() + DX[station.facing()] * 30,
221, station.entry().z() + DZ[station.facing()] * 30, (station.facing() + 2) & 3);
Fixture landmark = new Fixture((x, z) -> 220);
landmark.landmarks = List.of(new Landmark("old_mine", LandmarkKind.MINE, historical));
landmark.protection = (x, y, z) -> x == historical.x() && z == historical.z();
var linked = HeritagePlanner19.plan(0, 0, 0, 724, landmark); verify(linked, landmark);
require(!linked.contains(historical.x(), historical.y(), historical.z()), "A historical endpoint receives route painting");
Landmark left = new Landmark("left", LandmarkKind.CITY, new Entry(0, 101, 0, 1));
Landmark right = new Landmark("right", LandmarkKind.MINE, new Entry(1, 101, 0, 3));
Landmark unused = new Landmark("unused", LandmarkKind.UTILITY, new Entry(30, 101, 0, 0));
var dependency = new HeritagePlan19(List.of(), List.of(left, right, unused),
List.of(new Link("existing_access", "left", "right", RouteKind.PATH, List.of(new Point(0, 100, 0), new Point(1, 100, 0)))),
List.of(), 0, 0, Map.of());
for (int z = -1; z <= 1; z++) for (int y = 99; y <= 103; y++) {
require(dependency.protects(0, y, z) && dependency.intersectsProtection(0, y, z, 0, y, z), "Used historical access lost its rock/headroom dependency");
require(!dependency.contains(0, y, z), "Read-only historical access received writing permission");
}
require(!dependency.protects(30, 101, 0), "Unused historical landmark reserves unrelated terrain");
// Smooth slopes exercise certified cut/fill and route grade without allowing an unsupported plateau.
Fixture sloped = new Fixture((x, z) -> 220 + Math.floorDiv(z, 12));
var slopes = HeritagePlanner19.plan(42, 0, 0, 724, sloped); verify(slopes, sloped);
require(slopes.sites().size() >= 5, "A modest slope makes the entire catalogue inadmissible");
Fixture shifted = new Fixture((x, z) -> 220);
var translated = HeritagePlanner19.plan(42, -1024, 512, 724, shifted); verify(translated, shifted);
require(!translated.isEmpty(), "Negative chunk coordinates prevent generation");
require(flat.diagnostics().size() == Kind.values().length && blocked.diagnostics().size() == Kind.values().length,
"Missing kinds have no explicit diagnostic");
expectInvalid(() -> new Link("self", "a", "a", RouteKind.PATH, List.of(new Point(0, 100, 0), new Point(1, 100, 0))));
expectInvalid(() -> new Segment("self", "a", "a", RouteKind.FREIGHT, 0, 0, List.of(new Cell(0, 100, 0, 1, Rail.NONE, false)), 0));
expectInvalid(() -> new Cell(0, 100, 0, 13, Rail.NONE, false));
System.out.println("HeritagePlanner19Smoke passed: fourteen families, deterministic placements, persisted isolated entrances, real route destinations, bounded slopes and supported rails, unique chunk-local cells, indexed protection and budgets");
}
private static void verify(HeritagePlan19 plan, Fixture terrain) {
require(plan.samples() == terrain.samples && plan.samples() <= HeritagePlan19.MAX_SAMPLES, "Probe accounting differs from native calls");
require(plan.columns() == terrain.columns.size() && plan.columns() <= HeritagePlan19.MAX_COLUMNS, "Column budget exceeded");
Map<Long, Cell> cells = new HashMap<>(); Map<String, Entry> destinations = new HashMap<>();
Set<Long> footprints = new HashSet<>();
for (var site : plan.sites()) {
destinations.put(site.id(), site.entry());
require(site.foundationDepth() <= 8 && site.maxY() <= 380, "Architecture exceeds its bounded envelope");
require(Math.abs((site.minX() >> 4) - site.startChunkX()) <= 7 && Math.abs((site.maxX() >> 4) - site.startChunkX()) <= 7
&& Math.abs((site.minZ() >> 4) - site.startChunkZ()) <= 7 && Math.abs((site.maxZ() >> 4) - site.startChunkZ()) <= 7, "Site exceeds native reference radius");
for (int z = site.minZ(); z <= site.maxZ(); z++) for (int x = site.minX(); x <= site.maxX(); x++) {
require(footprints.add(key(x, z)), "Two buildings overlap");
require(terrain.height.applyAsInt(x, z) - site.floorY() <= HeritagePlanner19.MAX_SITE_CUT, "Site cuts an excessive mound");
for (int y = site.minY(); y <= site.maxY(); y++) require(!terrain.protection.test(x, y, z), "Architecture intersects protected water/old structure");
require(plan.contains(x, site.floorY(), z) && plan.protects(x, site.maxY(), z)
&& plan.intersectsProtection(x, site.maxY(), z, x, site.maxY(), z), "Site is absent from indexed protection");
}
for (int dx : new int[] {1, 8, 15}) for (int dz : new int[] {1, 8, 15}) {
int x = site.minX() + dx, z = site.minZ() + dz;
var support = site.supports().stream().filter(s -> s.x() == x && s.z() == z).findFirst();
int rock = support.isPresent() ? support.get().minY() - 1 : site.minY() - 1;
require(terrain.rock.test(x, rock, z) && terrain.rock.test(x, rock - 1, z), "Site has no two-block rock bearing");
}
for (var support : site.supports()) {
require(support.maxY() - support.minY() < 12 && support.maxY() == site.minY() - 1, "Unbounded/disconnected architectural pier");
require(plan.protects(support.x(), support.minY() - 2, support.z()), "Pier bearing exposed to decoration");
for (int y = support.minY(); y <= support.maxY(); y++) require(!terrain.protection.test(support.x(), y, support.z()), "Pier overwrites protected terrain");
}
}
plan.landmarks().forEach(l -> destinations.put(l.id(), l.entry()));
for (var segment : plan.segments()) {
require(destinations.containsKey(segment.fromId()) && destinations.containsKey(segment.toId()), "Segment has a fictional destination");
for (Cell cell : segment.cells()) {
require(cells.put(key(cell.x(), cell.z()), cell) == null, "Two segments write the same column");
require(!footprints.contains(key(cell.x(), cell.z())), "Route crosses a building envelope");
require((cell.x() >> 4) == segment.startChunkX() && (cell.z() >> 4) == segment.startChunkZ(), "Route piece is not chunk-local");
require(terrain.rock.test(cell.x(), cell.minY() - 1, cell.z()), "Route ballast floats without bearing");
require(terrain.height.applyAsInt(cell.x(), cell.z()) - cell.floorY() <= HeritagePlanner19.MAX_ROUTE_CUT, "Route makes excessive terrain cuts");
for (int y = cell.minY(); y <= cell.maxY(); y++) require(!terrain.protection.test(cell.x(), y, cell.z()), "Route overwrites protected water/old structure");
require(plan.contains(cell.x(), cell.floorY(), cell.z()) && plan.protects(cell.x(), cell.minY() - 1, cell.z())
&& plan.intersectsProtection(cell.x(), cell.minY() - 1, cell.z(), cell.x(), cell.minY() - 1, cell.z()), "Route/ballast missing from protection index");
require(!cell.railPresent() || cell.rail() != Rail.NONE, "Present rail has no saved geometry");
}
}
for (var site : plan.sites()) {
Entry e = site.entry();
for (int cross = -1; cross <= 1; cross++) {
Cell cell = cells.get(key(e.x() + DZ[e.facing()] * cross, e.z() - DX[e.facing()] * cross));
require(cell != null && cell.floorY() == e.y() - 1, "Unlinked entrance has no independently painted three-wide landing");
}
// A five-row access profile must meet natural terrain; never stop on an isolated retaining ledge.
var reached = new HashSet<Long>(); var queue = new ArrayDeque<Long>();
queue.add(key(e.x(), e.z())); reached.add(key(e.x(), e.z())); boolean meetsGround = false;
while (!queue.isEmpty()) {
long key = queue.remove(); Cell c = cells.get(key);
if (Math.abs(c.x() - e.x()) + Math.abs(c.z() - e.z()) >= 4
&& Math.abs(c.floorY() - terrain.height.applyAsInt(c.x(), c.z())) <= 1) meetsGround = true;
for (int d = 0; d < 4; d++) {
long next = key(c.x() + DX[d], c.z() + DZ[d]); Cell n = cells.get(next);
if (n != null && Math.abs(n.floorY() - c.floorY()) <= 1 && reached.add(next)) queue.add(next);
}
}
require(meetsGround, "An entrance platform has no traversable return to the sampled terrain");
}
for (var link : plan.links()) {
Entry from = destinations.get(link.fromId()), to = destinations.get(link.toId());
require(link.path().getFirst().equals(new Point(from.x(), from.y() - 1, from.z()))
&& link.path().getLast().equals(new Point(to.x(), to.y() - 1, to.z())), "Route does not actually join its promised entrances");
for (int i = 0; i < link.path().size(); i++) {
Point p = link.path().get(i); Cell c = cells.get(key(p.x(), p.z()));
boolean historical = plan.landmarks().stream().anyMatch(l -> l.entry().x() == p.x() && l.entry().z() == p.z() && l.entry().y() == p.floorY() + 1);
require(c != null && c.floorY() == p.floorY() || historical, "A complete route contains an unpainted gap");
if (i > 0) {
Point a = link.path().get(i - 1);
require(Math.abs(a.x() - p.x()) + Math.abs(a.z() - p.z()) == 1 && Math.abs(a.floorY() - p.floorY()) <= 1, "Route has a jump or excessive grade");
}
}
}
require(!plan.protects(100_000, 200, 100_000) && !plan.intersectsProtection(100_000, 0, 100_000, 100_002, 383, 100_002), "Protection escaped accepted constructions");
}
@FunctionalInterface private interface Volume { boolean test(int x, int y, int z); }
private static final class Fixture implements HeritagePlanner19.Terrain {
final IntBinaryOperator height; final Set<Long> columns = new HashSet<>(); int samples;
Volume protection = (x, y, z) -> false;
Volume rock; List<Landmark> landmarks = List.of();
Fixture(IntBinaryOperator height) { this.height = height; rock = (x, y, z) -> y >= 8 && y <= height.applyAsInt(x, z); }
public boolean owns(int x, int z) { return true; }
public int surfaceY(int x, int z) { samples++; columns.add(key(x, z)); return height.applyAsInt(x, z); }
public boolean solid(int x, int y, int z) { samples++; return rock.test(x, y, z); }
public boolean protectedAt(int x, int y, int z) { samples++; return protection.test(x, y, z); }
public List<Landmark> landmarks() { return landmarks; }
}
private static void expectInvalid(Runnable action) { boolean rejected = false; try { action.run(); } catch (IllegalArgumentException expected) { rejected = true; } require(rejected, "Invalid saved geometry was accepted"); }
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
}
@@ -0,0 +1,328 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import java.util.ArrayDeque;
import java.util.ArrayList;
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.EmptyBlockGetter;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.DoorBlock;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.TrapDoorBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BedPart;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
import net.minecraft.world.level.block.state.properties.Half;
import net.minecraft.world.level.storage.loot.LootTable;
import net.minecraft.world.phys.AABB;
/** Native shapes and final geometry: distinguish real rooms, reachable secrets and supported routes. */
public final class HeritageRenderer19Smoke {
private HeritageRenderer19Smoke() {}
private static final Direction[] DIRECTIONS = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
public static void main(String[] args) {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); run();
System.out.println("HeritageRenderer19Smoke passed: fourteen distinct architectures, four rotations, actual doors/bed/seat/light supports, every target and secret reachable, contained water, immutable loot and shaped abandoned routes");
}
public static void run() {
Set<Set<BlockPos>> silhouettes = new HashSet<>();
for (var kind : HeritagePlan19.Kind.values()) for (long seed : new long[] {0, 43}) {
var canonical = render(site(kind, 0, seed));
if (seed == 0) {
Set<BlockPos> shape = new HashSet<>(); canonical.blocks.forEach((p, b) -> { if (p.getY() > 0 && !b.isAir()) shape.add(p); });
require(silhouettes.add(shape), kind + " only repaints another catalogue building");
}
for (int facing = 0; facing < 4; facing++) {
var c = facing == 0 ? canonical : render(site(kind, facing, seed));
var repeat = render(c.site);
require(c.blocks.equals(repeat.blocks) && c.loot.equals(repeat.loot), label(c) + " is not deterministic");
rotate(canonical, c);
var targets = HeritageRenderer19.targets(c.site);
var reached = walk(c, at(targets.getFirst()));
for (var target : targets) require(reached.contains(at(target)), label(c) + " cannot reach " + target.name() + " at " + at(target));
require(targets.stream().anyMatch(t -> t.name().equals("secondary_entry")), "Missing secondary physical approach");
var secondary = targets.stream().filter(t -> t.name().equals("secondary_entry")).findFirst().orElseThrow();
Set<BlockPos> returnPath = walk(c, at(secondary));
require(returnPath.contains(at(targets.getFirst())), label(c) + " service exit cannot return through the certified courtyard");
require(c.loot.size() == 1, label(c) + " must have one shared, deferred discovery chest");
var chest = c.loot.entrySet().iterator().next();
require(chest.getValue().table().identifier().toString().equals("sanctuary:chests/heritage19/" + kind.name().toLowerCase(java.util.Locale.ROOT)), "Loot table is not generation19-specific");
require(approachable(chest.getKey(), reached), label(c) + " hidden chest cannot be opened from a reachable standing cell");
for (var t : targets) if (t.name().equals("loot_approach")) require(at(t).distManhattan(chest.getKey()) == 1, "Secret witness is not next to the actual chest");
furnishings(c, reached);
refugeShaft(c, reached);
railConnection(c);
fluids(c);
require(c.blocks.values().stream().noneMatch(b -> b.is(Blocks.SPAWNER) || b.is(Blocks.LAVA)), "Heritage added an unrequested spawner or lava");
identity(c);
}
}
require(silhouettes.size() == 14, "Catalogue does not contain fourteen physical architectures");
routes(); chunkPartition();
}
private static HeritagePlan19.Site site(HeritagePlan19.Kind kind, int facing, long seed) {
int height = switch (kind) {
case COMMUNAL_DEPOT, VOID_DOCK, CONSTRUCTION_REFUGE -> 14;
case EXPEDITION_OFFICE, IMPOSSIBLE_SERVICE -> 12;
case OBSERVATORY -> 20; case PUMP_STATION -> 18; case RADIO_RELAY -> 22;
default -> 10;
};
int x = -43, y = 180, z = 59;
var entry = new HeritagePlan19.Entry(facing == 1 ? x + 17 : facing == 3 ? x - 1 : x + 8, y + 1,
facing == 2 ? z + 17 : facing == 0 ? z - 1 : z + 8, facing);
return new HeritagePlan19.Site("test", kind, x, y, z, 17, 17, height, facing, seed, 3,
List.of(new HeritagePlan19.Support(x, y - 7, z, y - 4)), entry);
}
private static String label(Capture c) { return c.site.kind() + " facing=" + c.site.facing() + " seed=" + c.site.seed(); }
private static Capture render(HeritagePlan19.Site site) { var c = new Capture(site); HeritageRenderer19.render(site, c); return c; }
private static BlockPos at(HeritageRenderer19.Target t) { return new BlockPos(t.x(), t.y(), t.z()); }
private static BlockPos rotate(BlockPos p, int facing) {
return switch (facing) { case 1 -> new BlockPos(16 - p.getZ(), p.getY(), p.getX());
case 2 -> new BlockPos(16 - p.getX(), p.getY(), 16 - p.getZ());
case 3 -> new BlockPos(p.getZ(), p.getY(), 16 - p.getX()); default -> p; };
}
private static void rotate(Capture base, Capture other) {
int facing = other.site.facing();
Rotation rotation = switch (facing) { case 1 -> Rotation.CLOCKWISE_90; case 2 -> Rotation.CLOCKWISE_180;
case 3 -> Rotation.COUNTERCLOCKWISE_90; default -> Rotation.NONE; };
for (var e : base.blocks.entrySet()) {
// Saved footings are already in world coordinates; they do not rotate with the facade.
if (e.getKey().getY() < -base.site.foundationDepth() || base.loot.containsKey(e.getKey())) continue;
require(other.block(rotate(e.getKey(), facing)).equals(e.getValue().rotate(rotation)), label(other) + " rotates shape and block state differently at " + e.getKey());
}
for (var e : base.loot.entrySet()) require(e.getValue().equals(other.loot.get(rotate(e.getKey(), facing))), "Rotating a site changes its deferred treasure");
}
private static void furnishings(Capture c, Set<BlockPos> reached) {
for (var e : c.blocks.entrySet()) {
var p = e.getKey(); var b = e.getValue();
if (b.getBlock() instanceof DoorBlock) {
var half = b.getValue(BlockStateProperties.DOUBLE_BLOCK_HALF);
var partner = half == DoubleBlockHalf.LOWER ? p.above() : p.below();
require(c.block(partner).equals(b.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF,
half == DoubleBlockHalf.LOWER ? DoubleBlockHalf.UPPER : DoubleBlockHalf.LOWER)), label(c) + " has a truncated door at " + p);
if (half == DoubleBlockHalf.LOWER) {
require(full(c.block(p.below()), p.below()), "Door has no full threshold");
Direction normal = b.getValue(BlockStateProperties.HORIZONTAL_FACING);
require(reached.contains(p.relative(normal)) && reached.contains(p.relative(normal.getOpposite())),
label(c) + " door does not connect two usable sides at " + p);
}
}
if (b.is(Blocks.LADDER)) require(full(c.block(p.relative(b.getValue(BlockStateProperties.HORIZONTAL_FACING).getOpposite())), p),
label(c) + " floating ladder at " + p);
if (b.is(Blocks.LANTERN)) require(b.getValue(BlockStateProperties.HANGING) && full(c.block(p.above()), p.above()),
label(c) + " lantern does not hang from a complete ceiling at " + p);
if (b.is(Blocks.LEVER) || b.is(Blocks.GRINDSTONE)) require(full(c.block(p.below()), p.below()), "Usable fixture has no full support");
if (b.is(Blocks.DARK_OAK_STAIRS)) {
var front = p.relative(b.getValue(BlockStateProperties.HORIZONTAL_FACING).getOpposite());
require(reached.contains(front), label(c) + " seat faces an obstruction at " + p);
}
if (b.hasProperty(BlockStateProperties.BED_PART)) {
Direction direction = b.getValue(BlockStateProperties.HORIZONTAL_FACING);
boolean head = b.getValue(BlockStateProperties.BED_PART) == BedPart.HEAD;
var partner = p.relative(head ? direction.getOpposite() : direction);
require(c.block(partner).equals(b.setValue(BlockStateProperties.BED_PART, head ? BedPart.FOOT : BedPart.HEAD)), "Bed halves disagree");
if (head) {
for (int y = 0; y < 3; y++) require(full(c.block(p.relative(direction).above(y)), p), "Bed head is not against its real wall");
boolean side = false;
for (Direction d : new Direction[] {direction.getClockWise(), direction.getCounterClockWise()})
side |= reached.contains(p.relative(d)) && reached.contains(partner.relative(d));
require(side, "Bed has no reachable full lateral approach");
}
}
if (b.is(Blocks.CRAFTING_TABLE) || b.is(Blocks.CARTOGRAPHY_TABLE) || b.is(Blocks.FURNACE)
|| b.is(Blocks.GRINDSTONE) || b.is(Blocks.LECTERN))
require(approachable(p, reached), label(c) + " functional furnishing cannot be approached at " + p);
}
}
private static boolean full(BlockState b, BlockPos p) {
return b.getFluidState().isEmpty() && b.getCollisionShape(EmptyBlockGetter.INSTANCE, p).toAabbs().stream()
.anyMatch(a -> a.minX == 0 && a.minY == 0 && a.minZ == 0 && a.maxX == 1 && a.maxY == 1 && a.maxZ == 1);
}
private static boolean approachable(BlockPos p, Set<BlockPos> reached) {
for (var d : DIRECTIONS) if (reached.contains(p.relative(d))) return true;
return false;
}
private static boolean clear(Capture c, BlockPos p) {
var body = new AABB(p.getX() + .2, p.getY(), p.getZ() + .2, p.getX() + .8, p.getY() + 1.8, p.getZ() + .8);
for (var at : new BlockPos[] {p, p.above()}) {
var b = opened(c.block(at)); if (!b.getFluidState().isEmpty()) return false;
for (var shape : b.getCollisionShape(EmptyBlockGetter.INSTANCE, at).toAabbs())
if (shape.move(at.getX(), at.getY(), at.getZ()).intersects(body)) return false;
}
return true;
}
private static boolean stand(Capture c, BlockPos p) {
if (p.getX() < 0 || p.getX() > 16 || p.getZ() < 0 || p.getZ() > 16
|| p.getY() < -c.site.foundationDepth() + 1 || p.getY() >= c.site.height() || !clear(c, p)) return false;
if (climb(c.block(p)) || climb(c.block(p.below()))) return true;
return opened(c.block(p.below())).getCollisionShape(EmptyBlockGetter.INSTANCE, p.below()).toAabbs().stream()
.anyMatch(a -> a.maxY >= .5 && a.minX <= .5 && a.maxX >= .5 && a.minZ <= .5 && a.maxZ >= .5);
}
private static BlockState opened(BlockState state) {
return state.getBlock() instanceof DoorBlock || state.getBlock() instanceof TrapDoorBlock
? state.setValue(BlockStateProperties.OPEN, true) : state;
}
private static void refugeShaft(Capture c, Set<BlockPos> reached) {
if (c.site.kind() != HeritagePlan19.Kind.CONSTRUCTION_REFUGE) return;
var hatch = rotate(new BlockPos(10, 5, 11), c.site.facing());
require(c.block(hatch).is(Blocks.DARK_OAK_TRAPDOOR)
&& c.block(hatch).getValue(BlockStateProperties.HALF) == Half.BOTTOM,
label(c) + " hatch is not at the climbable upper lip");
var direction = c.block(hatch).getValue(BlockStateProperties.HORIZONTAL_FACING);
require(stand(c, hatch) && reached.contains(hatch), label(c) + " open hatch has no reachable climbable support");
for (int y = 1; y <= 4; y++) {
var rung = new BlockPos(hatch.getX(), y, hatch.getZ());
require(c.block(rung).is(Blocks.LADDER)
&& c.block(rung).getValue(BlockStateProperties.HORIZONTAL_FACING) == direction,
label(c) + " hatch descent loses a matching ladder at " + rung);
require(stand(c, rung) && reached.contains(rung) && clear(c, rung.above()),
label(c) + " hatch descent cannot traverse vertically at " + rung);
}
}
private static boolean climb(BlockState b) { return b.is(Blocks.LADDER); }
private static Set<BlockPos> walk(Capture c, BlockPos start) {
require(stand(c, start), label(c) + " starts without a standing surface at " + start);
Set<BlockPos> seen = new HashSet<>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
while (!queue.isEmpty()) {
var p = queue.removeFirst();
for (var d : DIRECTIONS) for (int dy : new int[] {0, 1, -1}) {
var q = p.relative(d).offset(0, dy, 0);
if (dy > 0 && !clear(c, p.above())) continue;
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 void fluids(Capture c) {
Set<BlockPos> water = new HashSet<>(); c.blocks.forEach((p, b) -> { if (!b.getFluidState().isEmpty()) water.add(p); });
int expected = c.site.kind() == HeritagePlan19.Kind.PUMP_STATION ? 60 : c.site.kind() == HeritagePlan19.Kind.GREENHOUSE ? 2 : 0;
require(water.size() == expected, label(c) + " has unexpected water quantity: " + water.size());
for (var p : water) {
require(c.block(p).is(Blocks.WATER), "Only explicitly contained water is allowed");
for (var d : new Direction[] {Direction.DOWN, Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}) {
var q = p.relative(d);
require(water.contains(q) || !c.block(q).isAir(), label(c) + " lets tank/irrigation water escape into air at " + q);
}
}
}
private static void identity(Capture c) {
switch (c.site.kind()) {
case FREIGHT_STATION -> { has(c, Blocks.RAIL); has(c, Blocks.LEVER); }
case POST_OFFICE -> { has(c, Blocks.BARREL); has(c, Blocks.CARTOGRAPHY_TABLE); }
case COMMUNAL_DEPOT -> { has(c, Blocks.BARREL); has(c, Blocks.LECTERN); }
case EXPEDITION_OFFICE -> { has(c, Blocks.CARTOGRAPHY_TABLE); has(c, Blocks.BOOKSHELF); }
case OBSERVATORY -> { has(c, Blocks.CARTOGRAPHY_TABLE); has(c, Blocks.IRON_BARS); }
case RAIL_WORKSHOP -> { has(c, Blocks.GRINDSTONE); has(c, Blocks.RAIL); has(c, Blocks.FURNACE); }
case PUMP_STATION -> { has(c, Blocks.DISPENSER); has(c, Blocks.WATER); }
case SUBSTATION -> { has(c, Blocks.LEVER); has(c, Blocks.IRON_BARS); }
case GREENHOUSE -> { has(c, Blocks.WHEAT); has(c, Blocks.FARMLAND); has(c, Blocks.COMPOSTER); }
case VOID_DOCK -> { has(c, Blocks.IRON_CHAIN); has(c, Blocks.STRIPPED_OAK_LOG); }
case RADIO_RELAY -> { has(c, Blocks.BED.white()); has(c, Blocks.IRON_BARS); }
case PHOTO_CLUB -> { has(c, Blocks.WATER_CAULDRON); has(c, Blocks.IRON_CHAIN); }
case IMPOSSIBLE_SERVICE -> { has(c, Blocks.CALCITE); has(c, Blocks.WATER_CAULDRON); }
case CONSTRUCTION_REFUGE -> { has(c, Blocks.BED.white()); has(c, Blocks.LADDER); has(c, Blocks.DARK_OAK_TRAPDOOR); }
}
}
private static void has(Capture c, Block b) { require(c.blocks.values().stream().anyMatch(v -> v.is(b)), label(c) + " loses its " + b); }
private static void railConnection(Capture c) {
BlockPos destination = switch (c.site.kind()) {
case FREIGHT_STATION -> new BlockPos(2, 1, 8);
case RAIL_WORKSHOP -> new BlockPos(5, 1, 7);
case COMMUNAL_DEPOT -> new BlockPos(8, 1, 7);
default -> null;
};
if (destination == null) return;
var start = rotate(new BlockPos(8, 1, 0), c.site.facing());
destination = rotate(destination, c.site.facing());
Set<BlockPos> seen = new HashSet<>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
while (!queue.isEmpty()) {
var p = queue.removeFirst(); var b = c.block(p);
require(b.is(Blocks.RAIL), label(c) + " freight entry has no supported rail");
for (var d : railEnds(b)) {
var next = p.relative(d); var state = c.block(next);
if (state.is(Blocks.RAIL) && railEnds(state).contains(d.getOpposite()) && seen.add(next)) queue.add(next);
}
}
require(seen.contains(destination), label(c) + " interior rail does not connect to its real external freight entry");
}
private static List<Direction> railEnds(BlockState b) {
return switch (b.getValue(BlockStateProperties.RAIL_SHAPE)) {
case NORTH_SOUTH, ASCENDING_NORTH, ASCENDING_SOUTH -> List.of(Direction.NORTH, Direction.SOUTH);
case EAST_WEST, ASCENDING_EAST, ASCENDING_WEST -> List.of(Direction.EAST, Direction.WEST);
case SOUTH_EAST -> List.of(Direction.SOUTH, Direction.EAST);
case SOUTH_WEST -> List.of(Direction.SOUTH, Direction.WEST);
case NORTH_WEST -> List.of(Direction.NORTH, Direction.WEST);
case NORTH_EAST -> List.of(Direction.NORTH, Direction.EAST);
};
}
private static void routes() {
var cells = new ArrayList<HeritagePlan19.Cell>();
int x = -15;
for (var rail : HeritagePlan19.Rail.values()) {
cells.add(new HeritagePlan19.Cell(x++, 160, 2, 2, rail, rail != HeritagePlan19.Rail.NONE));
}
cells.add(new HeritagePlan19.Cell(-1, 161, 2, 3, HeritagePlan19.Rail.ASCENDING_EAST, false));
var segment = new HeritagePlan19.Segment("route", "a", "b", HeritagePlan19.RouteKind.FREIGHT, -1, 0, cells, 7);
Map<BlockPos, BlockState> result = new HashMap<>();
HeritageRenderer19.renderSegment(segment, new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState b) {
var p = new BlockPos(segment.minX() + x, segment.minY() + y, segment.minZ() + z);
var cell = cells.stream().filter(c -> c.x() == p.getX() && c.z() == p.getZ()).findFirst().orElseThrow();
require(p.getY() >= cell.minY() && p.getY() <= cell.maxY(), "Route escaped its saved column mask"); result.put(p, b);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { throw new AssertionError("A route generated treasure"); }
});
for (var c : cells) {
var p = new BlockPos(c.x(), c.floorY() + 1, c.z());
require(full(result.get(p.below()), p.below()), "Rail or missing-rail witness lost its ballast support");
if (c.railPresent()) require(result.get(p).is(Blocks.RAIL)
&& result.get(p).getValue(BlockStateProperties.RAIL_SHAPE).name().equals(c.rail().name()), "Saved rail shape was replaced by a straight track");
else require(result.get(p).isAir(), "An intentional abandoned-track gap was repaired by rendering");
}
}
private static void chunkPartition() {
var s = site(HeritagePlan19.Kind.CONSTRUCTION_REFUGE, 3, 43); var expected = render(s); var parts = new Capture(s);
for (int cz = s.maxZ() >> 4; cz >= s.minZ() >> 4; cz--) for (int cx = s.maxX() >> 4; cx >= s.minX() >> 4; cx--) {
final int chunkX = cx, chunkZ = cz;
HeritageRenderer19.render(s, new LostCityRenderer.BlockWriter() {
boolean owns(int x, int z) { return ((s.minX() + x) >> 4) == chunkX && ((s.minZ() + z) >> 4) == chunkZ; }
@Override public void set(int x, int y, int z, BlockState state) { if (owns(x, z)) parts.set(x, y, z, state); }
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { if (owns(x, z)) parts.chest(x, y, z, table, seed); }
});
}
require(parts.blocks.equals(expected.blocks) && parts.loot.equals(expected.loot), "Native chunk order changes a site");
}
private record Loot(ResourceKey<LootTable> table, long seed) {}
private static final class Capture implements LostCityRenderer.BlockWriter {
final HeritagePlan19.Site site;
final Map<BlockPos, BlockState> blocks = new HashMap<>();
final Map<BlockPos, Loot> loot = new HashMap<>();
Capture(HeritagePlan19.Site site) { this.site = site; }
@Override public void set(int x, int y, int z, BlockState state) {
int wy = y + site.floorY();
boolean main = x >= 0 && x <= 16 && z >= 0 && z <= 16 && wy >= site.minY() && wy <= site.maxY();
boolean footing = site.supports().stream().anyMatch(s -> s.x() == x + site.minX() && s.z() == z + site.minZ() && wy >= s.minY() && wy <= s.maxY());
require(main || footing, label(this) + " writes outside its exact body and footing mask"); blocks.put(new BlockPos(x, y, z), state);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
require(loot.put(new BlockPos(x, y, z), new Loot(table, seed)) == null, "Duplicate shared treasure"); set(x, y, z, Blocks.CHEST.defaultBlockState());
}
BlockState block(BlockPos p) { return blocks.getOrDefault(p, Blocks.AIR.defaultBlockState()); }
}
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
}
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "0.1.0-alpha.17"
version = "0.1.0-alpha.19"
pack-format = "packwiz:1.1.0"
[index]