worldgen: sober palettes and connected industrial galleries for alpha21
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-10 10:03:30 +02:00
parent ae4c6200db
commit bb63fab388
58 changed files with 8518 additions and 69 deletions
+22 -7
View File
@@ -15,11 +15,26 @@ infrastructures qui permettront progressivement d'ouvrir de nouveaux continents.
Ce dépôt démarre la nouvelle base Fabric. La [vision complète](docs/vision.md) Ce dépôt démarre la nouvelle base Fabric. La [vision complète](docs/vision.md)
conserve les intentions ; le [backlog](docs/backlog.md) suit les conserve les intentions ; le [backlog](docs/backlog.md) suit les
tickets. Les systèmes d’économie, de progression collective et les dimensions tickets. Les systèmes d’économie, de progression collective et les dimensions
décrits dans la vision ne sont pas encore implémentés. **Lalpha.20 est publiée** : elle reprend décrits dans la vision ne sont pas encore implémentés. **Lalpha.21 est construite et
larchitecture des bâtiments, leurs palettes, les blocs sculptés et les raccords vérifiée** : palettes sobres, couvertures portées, petits quartiers reliés et
des vitres. Les villes, infrastructures et réseaux gardent leurs implantations. premières galeries industrielles. Voir [le catalogue alpha.21](docs/architecture-alpha21.md),
Voir le [catalogue illustré](docs/architecture-alpha20.md) et l’état du canal [les essais alpha.21](docs/testing-alpha21.md)
packwiz dans [Distribution](docs/packwiz.md). et l’état du canal packwiz dans [Distribution](docs/packwiz.md).
## Alpha.21 — matériaux, quartiers et traversées
La ville revient à la pierre lisse, pierre, briques de pierre, andésite et chêne.
Le tuf accompagne surtout les chemins et les sous-sols. Les toits et ornements
sont raccordés à leurs appuis ; les pièces gardent leurs aménagements. La ville
recherche des petits quartiers séparés par du terrain naturel, avec des chemins
vers le cœur. Les lieux patrimoniaux recherchent davantage de raccords.
Loption expérimentale ajoute une recherche de galeries industrielles : chaussée
cinq blocs, deux trottoirs, salles et escaliers de surface. Les placements sur
pente sont assouplis et les destinations isolées sont prioritaires pour les chemins.
Créer un **nouveau monde Sanctuary** pour ce générateur21. Les anciens mondes
conservent leur génération. Les vérifications et coordonnées du
témoin sont consignées dans [les essais](docs/testing-alpha21.md).
## Alpha.20 — identité des bâtiments ## Alpha.20 — identité des bâtiments
@@ -443,7 +458,7 @@ sera une mise à jour explicite, avec vérification des API et des sauvegardes.
| Fabric API | 0.160.0+26.3 | | Fabric API | 0.160.0+26.3 |
| Fabric Loom | 1.17.20 | | Fabric Loom | 1.17.20 |
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 | | Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
| Sanctuary / pack | 0.1.0-alpha.19 | | Sanctuary / pack | 0.1.0-alpha.21 |
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise 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 uniquement la bibliothèque standard et repère aussi une installation Python
@@ -472,7 +487,7 @@ décrits dans [Validation](docs/testing.md).
Résultats : Résultats :
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.19.jar` : mod à installer avec - `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.21.jar` : mod à installer avec
Fabric API sur la version Minecraft indiquée. Fabric API sur la version Minecraft indiquée.
- `build/packwiz/` : pack de développement complet, avec le mod construit et - `build/packwiz/` : pack de développement complet, avec le mod construit et
l'index vérifié. Voir [Installation packwiz](packwiz/README.md). l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
+24
View File
@@ -0,0 +1,24 @@
# Alpha.21 — palettes et couvertures
La ville revient à une base commune : pierre lisse, pierre, briques de pierre,
andésite et chêne. Les façades, seuils, corniches et verrières gardent leurs
formes ; les pièces conservent leurs fonctions et leur circulation. La serre
emploie le bouleau comme bois propre. Le tuf accompagne les routes et les
parties souterraines.
![Modèles des bâtiments alpha.21](images/architecture-alpha21-overview.png)
Ces vues sont calculées depuis les blocs produits par les renderers21 et les
modèles/textures de Minecraft 26.3-pre-2. Elles montrent des variantes choisies,
sans terrain, éclairage ni végétation de la partie. Ce ne sont pas des captures
du client et elles ne prouvent pas les raccords avec le relief.
Les couvertures de latelier, du dépôt, de la serre, du relais radio et du refuge
reposent sur des murs ou charpentes. Lancien plafond opaque sous leur nouvelle
couverture est retiré. Les vérifications contrôlent aussi les petits appuis des
excavations, piscines, rampes de garage et ornements.
![Coupe des couvertures](images/architecture-alpha21-roofs.png)
La seconde planche coupe volontairement les murs pour voir les dessous des toits.
Les détails de validation et la graine dessai sont dans [le protocole](testing-alpha21.md).
+32
View File
@@ -779,6 +779,38 @@ en briques de pierre. Ajouter des blocs sculptés et du relief architectural.
**État : livré en alpha.20 ; code, artefacts et même instance Prism vérifiés.** Voir **État : livré en alpha.20 ; code, artefacts et même instance Prism vérifiés.** Voir
[le contrat et les essais alpha.20](testing-alpha20.md). [le contrat et les essais alpha.20](testing-alpha20.md).
### WG-24 — Palettes sobres, toits portés et galeries — alpha.21
**Branche :** `codex/palettes-reseaux-alpha21`.
Reprendre les extérieurs sans changer les plans intérieurs approuvés : smooth
stone, stone brick, stone, andésite et chêne dominants en ville ; un bois cohérent
par bâtiment unique et tuf surtout au sol et sous terre. Corriger les toits
suspendus et les anciens plafonds superposés, avec appuis vérifiables.
Étendre la ville par au plus deux petits quartiers séparés de terrain naturel,
reliés par des chemins praticables ; rapprocher les destinations patrimoniales
par un graphe de chemins. Ajouter des galeries industrielles abandonnées :
chaussée de cinq blocs, deux trottoirs, quelques grandes salles et deux accès
par escaliers depuis la surface, en respectant les eaux et structures existantes.
Ajouter des fronts rocheux et de petits filons intacts dans les galeries et
leurs salles de maintenance : ressources vanilla réellement minables, sans
régénération de butin ni remplacement des blocs déjà extraits. Fournir un MRpack
pour lessai séparé demandé.
Assouplir les refus de placement : admettre les pentes, des volumes partiellement
enfouis et des appuis adaptés ; rechercher des détours et prioriser les lieux
isolés pour les chemins. Garder une ville fragmentée sans socle géant.
Nouvelle génération21 réservée aux nouveaux mondes ; tous les codecs et rendus
20 et antérieurs restent disponibles. Tailles512/724/1024, Moyen par défaut ;
option expérimentale enregistrée. Aucun monde personnel modifié. Vérifier les
pièces natives, accès, connexions, supports, création/réouverture, ancien monde20,
puis livrer le pack stable21 et synchroniser la même instance Prism.
**État : implémenté et vérifié ; MRpack fourni, publication et synchronisation en cours.**
## Réserve de thèmes futurs ## 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. 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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

+138
View File
@@ -0,0 +1,138 @@
# Alpha.21 — contrat et vérification
Ticket WG-24, branche `codex/palettes-reseaux-alpha21`.
Minecraft 26.3-pre-2, Fabric Loader 0.19.5, Fabric API 0.160.0+26.3,
Java 25. Les dépendances restent celles de lalpha.20.
## Résultat recherché
La ville reprend une palette de pierre lisse, pierre, briques de pierre,
andésite et chêne. Les plans intérieurs sont conservés ; les corrections
portent sur les matériaux, les appuis et les couvertures. Chaque infrastructure
emploie un bois cohérent. Le tuf reste surtout au sol et dans les sous-sols.
La ville peut accueillir deux petits quartiers supplémentaires reliés à son
cœur, avec du terrain naturel entre eux. La réserve centrale existante nest
pas agrandie. Les infrastructures reçoivent davantage de chemins entre des
entrées réelles. Les recherches respectent des budgets de colonnes et refusent
un quartier si sa liaison entière ne peut pas être construite.
Les galeries industrielles expérimentales ont cinq blocs de chaussée et un
trottoir de chaque côté, des salles de maintenance et deux accès par escaliers.
Le plan est recherché après les structures précédentes et évite leurs volumes,
lhydrologie. Un tablier continu peut franchir une cavité entre ses appuis ;
les travées sans appuis complets sont limitées à 34 blocs, avec des piliers
locaux dau plus 16 blocs et deux couches de roche intacte sous les appuis. Chaque tronçon a une pièce native
locale ; aucun démarrage de structure ne charge toute l’île.
## Sauvegardes et comparaison
Créer un **nouveau monde Sanctuary**, taille **Moyen**, graine **0**, avec
**Structures expérimentales** activées. Petit 512, Moyen 724 et Grand 1024 restent
les tailles proposées par le bouton natif **Personnaliser**.
Le nouveau codec est `sanctuary:island_v21`. Les codecs et pièces 20 et
antérieurs restent disponibles, sans modifier leurs rendus. Le journal 21
réutilise le schéma de persistance existant avec une génération distincte ;
les ouvertures entre versions incompatibles sont refusées. Aucune conversion
ni rénovation automatique des mondes précédents nest effectuée.
Pour comparer sans les infrastructures, créer un autre monde avec loption
expérimentale désactivée. Une sauvegarde 21 nécessite le binaire 21 ou un futur
binaire compatible ; elle ne se rétrograde pas en remplaçant simplement le JAR.
## Compatibilité vérifiée
373 fichiers de production historiques sont identiques au commit de publication
20 (`0edac29`). Le preset caché `sanctuary_v20` est une copie exacte de son
ancien preset public. Création et réouverture natives de la génération 20 donnent
les mêmes plans, structures, appuis, accès et coffres que les rapports publiés.
La comparaison ignore seulement lidentifiant du processus, le chemin du monde
et deux nouveaux compteurs de détail ajoutés à loracle de connexion des tests.
## Galeries vérifiées dans Minecraft
Moyen, graine 0 : 23 modules dont deux accès et une salle, sur un axe de 340 blocs.
Les entrées sont `-135 253 -178` et `-135 249 162` ; chaussée à Y 216. La portée
sans appuis complets la plus longue du témoin est 17 blocs. Le plan emploie
8 686 colonnes de bruit, avec un budget maximal de 12 000 ; cette recherche prend
environ 11 secondes sur la machine de développement, hors reste du monde.
La création et la réouverture dans deux JVM distinctes passent : 34 chunks
FULL, 57 635 blocs vérifiés, mêmes pièces natives, mêmes coffres différés et
modification témoin conservée. Loracle de collision de Minecraft vérifie un
corps de 0,6×1,8 bloc, 74 cibles et 520 pas entre les deux portiques ; il utilise
les vrais escaliers et les demi-marches des trottoirs.
Une alcôve dexcavation inachevée ouvre le parement de la salle sur une paroi
irrégulière de tuf, pierre et deepslate. Certains tronçons exposent de petits
filons ordinaires. Le témoin contient cinq minerais de charbon, dix-neuf de
fer, cinq de redstone, un de diamant et un d’émeraude ajoutés dans ces fronts.
Cuivre, or et lapis font aussi partie des variantes possibles. Ce sont des
blocs finis, sans mécanisme de réapparition après minage. Ces quantités excluent
les minerais naturels de l’île et ne constituent pas une garantie par graine.
## Autres vérifications
La création et la réouverture natives de la ville passent. Le témoin possède
un cœur et deux quartiers reliés, 65 pièces de rue, trois immeubles de logements,
un hôtel, une bibliothèque, un jardin et une place. La réserve centrale nest
pas étendue. Les deux nouveaux chemins sont longs de 51 et 38 cellules. Une
expansion de test distincte est également vérifiée ; aucune expansion personnelle
nest activée.
La piscine couverte, le garage, la salle dexpansion, leurs accès et les égouts
restent présents. Les contrôles natifs vérifient les bassins et sorties après
exécution observée de la simulation des fluides.
Les quatorze types de lieux patrimoniaux apparaissent sur Moyen, graine 0,
avec douze liaisons et 127 segments. Leur création native passe : 141 pièces,
91 271 blocs, 140 chunks FULL et quatorze coffres différés. Les trois réseaux
oubliés sont également parcourus. Les chemins rejoignent deux entrées de ville.
Lobservatoire et le quai du vide restent isolés sur ce témoin : leurs chemins
nont pas trouvé de tracé admissible dans le budget. La commande `/sanctuary structures` indique leurs entrées ; aucun raccord
construit complet nest revendiqué.
Les smokes des renderers, plans, pièces enregistrées et raccords passent.
La réouverture des lieux patrimoniaux conserve également le plan, les pièces,
les coffres et la modification témoin. Loption expérimentale désactivée passe :
elle ne publie aucun module de galerie. `check build assemblePack` passe, avec les huit GameTests généraux. Le
MRpack, son index et le JAR embarqué sont vérifiés ; la publication et la
synchronisation restent à consigner.
Les chemins de quartier regroupent leurs pentes à lintérieur de rectangles ;
leurs frontières et raccords sont des paliers plats. Les structures restent
conditionnées par le terrain admissible, dans un budget borné. La traversée
industrielle ne relie pas encore tous les réseaux souterrains historiques.
Les vues de modèles éventuelles ne remplacent pas une inspection dans le client.
Aucune validation Windows native ni charge multijoueur nest revendiquée.
## Reproduire les contrôles
Depuis ce dépôt, avec Java 25, les mondes de test restent sous `mods/sanctuary/build/`.
Pour chaque scénario, exécuter la création puis sa réouverture avant tout autre
scénario qui nettoie ce monde de développement :
```sh
./gradlew :sanctuary:runGameTest -PsanctuaryTransit21Tests=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --max-workers=1
./gradlew :sanctuary:runGameTest -PsanctuaryTransit21Tests=true -PsanctuaryExpansionReload=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --max-workers=1
```
Remplacer `sanctuaryTransit21Tests` par `sanctuaryLostCity21Tests` pour la ville
ou `sanctuaryHeritage21Tests` pour les infrastructures. Le cas OFF ajoute
`-PsanctuaryExperimentalStructures=false` à une création du scénario transit.
Les rapports de cette livraison sont conservés localement dans
`build/alpha21-evidence/`. `./gradlew check build assemblePack` constitue la
vérification complète avant export.
## Artefacts vérifiés
MRpack : `Sanctuary-0.1.0-alpha.21.mrpack`. Son JAR est identique au JAR du build.
Les versions et hashes de lindex packwiz sont contrôlés, ainsi que le ZIP
damorçage Prism. SHA-256 du JAR :
```text
e528e42d99f9e1ea221f780ea089b49b42a1a4e9e24e4ddd001d045183202c21
```
+2 -2
View File
@@ -8,6 +8,6 @@ loader_version=0.19.5
loom_version=1.17.20 loom_version=1.17.20
fabric_api_version=0.160.0+26.3 fabric_api_version=0.160.0+26.3
mod_version=0.1.0-alpha.20 mod_version=0.1.0-alpha.21
pack_version=0.1.0-alpha.20 pack_version=0.1.0-alpha.21
maven_group=fr.koka.sanctuary maven_group=fr.koka.sanctuary
+42 -2
View File
@@ -40,7 +40,10 @@ tasks.named('runGameTest') {
systemProperty('sanctuary.test.unified', providers.gradleProperty('sanctuaryUnifiedTests').getOrElse('true')) systemProperty('sanctuary.test.unified', providers.gradleProperty('sanctuaryUnifiedTests').getOrElse('true'))
systemProperty('sanctuary.test.diameter', providers.gradleProperty('sanctuaryTestDiameter').getOrElse('724')) systemProperty('sanctuary.test.diameter', providers.gradleProperty('sanctuaryTestDiameter').getOrElse('724'))
systemProperty('sanctuary.test.generation', providers.gradleProperty('sanctuaryTestGeneration').getOrElse( 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' : providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false) ? '17' : providers.gradleProperty('sanctuaryHeritage19Tests').map { it.toBoolean() }.getOrElse(false) ? '19' : '20')) 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' : providers.gradleProperty('sanctuaryHeritage19Tests').map { it.toBoolean() }.getOrElse(false) ? '19' : (providers.gradleProperty('sanctuaryHeritage20Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryLostCity20Tests').map { it.toBoolean() }.getOrElse(false)) ? '20' : '21'))
systemProperty('sanctuary.test.heritage21', providers.gradleProperty('sanctuaryHeritage21Tests').getOrElse('false'))
systemProperty('sanctuary.test.lostCity21', providers.gradleProperty('sanctuaryLostCity21Tests').getOrElse('false'))
systemProperty('sanctuary.test.transit21', providers.gradleProperty('sanctuaryTransit21Tests').getOrElse('false'))
systemProperty('sanctuary.test.expansion', providers.gradleProperty('sanctuaryExpansionTests').getOrElse('false')) systemProperty('sanctuary.test.expansion', providers.gradleProperty('sanctuaryExpansionTests').getOrElse('false'))
systemProperty('sanctuary.test.expansionReload', providers.gradleProperty('sanctuaryExpansionReload').getOrElse('false')) systemProperty('sanctuary.test.expansionReload', providers.gradleProperty('sanctuaryExpansionReload').getOrElse('false'))
systemProperty('sanctuary.test.lostCity', providers.gradleProperty('sanctuaryLostCityTests').getOrElse('false')) systemProperty('sanctuary.test.lostCity', providers.gradleProperty('sanctuaryLostCityTests').getOrElse('false'))
@@ -76,12 +79,18 @@ tasks.named('processGametestResources') {
inputs.property('lostCity20', lostCity20) inputs.property('lostCity20', lostCity20)
def heritage20 = providers.gradleProperty('sanctuaryHeritage20Tests').map { it.toBoolean() }.getOrElse(false) def heritage20 = providers.gradleProperty('sanctuaryHeritage20Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('heritage20', heritage20) inputs.property('heritage20', heritage20)
def heritage21 = providers.gradleProperty('sanctuaryHeritage21Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('heritage21', heritage21)
def lostCity21 = providers.gradleProperty('sanctuaryLostCity21Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('lostCity21', lostCity21)
def transit21 = providers.gradleProperty('sanctuaryTransit21Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('transit21', transit21)
def heritage19 = providers.gradleProperty('sanctuaryHeritage19Tests').map { it.toBoolean() }.getOrElse(false) def heritage19 = providers.gradleProperty('sanctuaryHeritage19Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('heritage19', heritage19) inputs.property('heritage19', heritage19)
def voidReuse = providers.gradleProperty('sanctuaryVoidReuseTests').map { it.toBoolean() }.getOrElse(false) def voidReuse = providers.gradleProperty('sanctuaryVoidReuseTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('voidReuse', voidReuse) inputs.property('voidReuse', voidReuse)
filesMatching('fabric.mod.json') { filesMatching('fabric.mod.json') {
expand(gametest_entrypoint: lostCity20 ? 'fr.koka.sanctuary.gametest.LostCity20WorldGameTests' : heritage20 ? 'fr.koka.sanctuary.gametest.Heritage20WorldGameTests' : 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 expand(gametest_entrypoint: transit21 ? 'fr.koka.sanctuary.gametest.Transit21WorldGameTests' : lostCity21 ? 'fr.koka.sanctuary.gametest.LostCity21WorldGameTests' : heritage21 ? 'fr.koka.sanctuary.gametest.Heritage21WorldGameTests' : lostCity20 ? 'fr.koka.sanctuary.gametest.LostCity20WorldGameTests' : heritage20 ? 'fr.koka.sanctuary.gametest.Heritage20WorldGameTests' : 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' ? 'fr.koka.sanctuary.gametest.WorldgenPerformanceGameTests'
: unified ? 'fr.koka.sanctuary.gametest.UnifiedWorldGameTests' : unified ? 'fr.koka.sanctuary.gametest.UnifiedWorldGameTests'
: 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests') : 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests')
@@ -468,3 +477,34 @@ tasks.named('lostCitySurface20Smoke') {
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent()) if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
systemProperty('sanctuary.city20.exteriorPreview', rootProject.layout.buildDirectory.file('alpha20-preview/exterior.json').get().asFile.absolutePath) systemProperty('sanctuary.city20.exteriorPreview', rootProject.layout.buildDirectory.file('alpha20-preview/exterior.json').get().asFile.absolutePath)
} }
// Generation21 keeps published20 renderers immutable.
[
heritage21RendererSmoke: 'heritage.HeritageRenderer21Smoke',
heritage21PieceSmoke: 'heritage.HeritagePiece21Smoke',
heritage21PlannerSmoke: 'heritage.HeritagePlanner21Smoke',
lostCity21PlannerSmoke: 'city.LostCityPlanner21Smoke',
lostCitySurface21Smoke: 'city.LostCitySurface21Smoke',
lostCityInteriors21Smoke: 'city.LostCityInteriors21Smoke',
transit21Smoke: 'transit.Transit21Smoke'
].each { name, harness ->
def smoke = tasks.register(name, JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = "fr.koka.sanctuary.worldgen.${harness}"
}
tasks.named('check') { dependsOn(smoke) }
}
tasks.named('heritage21RendererSmoke') {
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
systemProperty('sanctuary.test.heritage21.export', rootProject.layout.buildDirectory.dir('alpha21-preview/heritage').get().asFile.absolutePath)
}
tasks.named('lostCityInteriors21Smoke') {
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
systemProperty('sanctuary.city21.preview', rootProject.layout.buildDirectory.file('alpha21-preview/city.json').get().asFile.absolutePath)
}
tasks.named('lostCitySurface21Smoke') {
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
systemProperty('sanctuary.city21.exteriorPreview', rootProject.layout.buildDirectory.file('alpha21-preview/exterior.json').get().asFile.absolutePath)
}
@@ -0,0 +1,554 @@
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.HeritagePiece21;
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19;
import fr.koka.sanctuary.worldgen.heritage.HeritageRenderer21;
import fr.koka.sanctuary.worldgen.heritage.HeritageStructures19;
import fr.koka.sanctuary.worldgen.ruins.RuinsPiece20;
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17;
import fr.koka.sanctuary.worldgen.ruins.RuinsRenderer20;
import fr.koka.sanctuary.worldgen.ruins.RuinsStructures17;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.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 generation21 starter island. */
public final class Heritage21WorldGameTests {
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() == 21, "Use a fresh generation21 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<>();
final List<StructureStart> ruinsStarts = 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("Heritage21 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("Heritage21 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() == 21
&& copy.experimentalStructures() == generator.experimentalStructures(), "Generation21 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(HeritagePiece21.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 HeritagePiece21, "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(HeritagePiece21.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");
var ruins = generator.ruinsPlan17();
report.put("ruins_plan_sha256", hash(ruins.networks().toString()));
if (reload) helper.assertTrue(previous.get("ruins_plan_sha256").getAsString().equals(hash(ruins.networks().toString())), "Reload changed forgotten room planning");
helper.assertTrue(!ruins.isEmpty(), "Reference20 must demonstrate native experimental room architecture");
var ruinsType = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(RuinsStructures17.KEY);
var ruinsOrigins = new HashSet<ChunkPos>();
for (var network : ruins.networks()) {
var origin = new ChunkPos(network.startChunkX(), network.startChunkZ()); if (!ruinsOrigins.add(origin)) continue;
var nativeStart = level.getChunk(origin.x(), origin.z(), ChunkStatus.STRUCTURE_STARTS, true).getStartForStructure(ruinsType.value());
helper.assertTrue(nativeStart != null && nativeStart.isValid(), "Missing native20 forgotten network: " + network.id());
ruinsStarts.add(nativeStart);
for (var piece : nativeStart.getPieces()) {
helper.assertTrue(piece instanceof RuinsPiece20, "Generation21 loaded a historical forgotten piece");
piece.getBoundingBox().intersectingChunks().forEach(uniqueChunks::add);
}
}
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;
var connections = NativeConnections20.Stats.empty();
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(HeritagePiece21.encodeSite(s)));
plan.segments().stream().filter(s -> s.startChunkX() == origin.x() && s.startChunkZ() == origin.z()).forEach(s -> expectedPayloads.add(HeritagePiece21.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 = (HeritagePiece21) value; pieces++;
var tag = piece.createTag(context); var restored = new HeritagePiece21(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", 22);
boolean rejected = false; try { new HeritagePiece21(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");
}
connections = connections.add(NativeConnections20.verify(helper, expected.blocks(), reload ? Set.of(gold) : Set.of()));
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("connected_blocks", connections);
helper.assertTrue(connections.panes() > 0 && connections.stairs() > 0 && connections.nativeOracles() > 0,
"Reference20 needs real panes/stairs validated by vanilla neighbor rules");
if (level.getSeed() == 0 && generator.initialDiameter() == 724)
helper.assertTrue(connections.crossChunkPaneLinks() > 0, "Reference20 must demonstrate a real pane join across a chunk boundary");
verifyRuins();
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(Heritage21WorldGameTests::routeSpan).max().orElse(0);
report.put("greatest_single_route_span", greatestSpan);
var reachedIds = new HashSet<String>(); plan.links().forEach(l -> { reachedIds.add(l.fromId()); reachedIds.add(l.toId()); });
report.put("connected_destination_ids", reachedIds.stream().sorted().toList());
report.put("isolated_destination_ids", plan.sites().stream().map(HeritagePlan19.Site::id).filter(id -> !reachedIds.contains(id)).toList());
report.put("connected_city_arrivals", plan.landmarks().stream().filter(l -> l.kind() == HeritagePlan19.LandmarkKind.CITY && reachedIds.contains(l.id())).map(HeritagePlan19.Landmark::id).toList());
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 -> HeritagePiece21.locator(s).equals(located.getFirst())), "Locate must lead to a real exterior site entry");
report.put("locate", coords(located.getFirst()));
}
void verifyRuins() {
var context = StructurePieceSerializationContext.fromLevel(level);
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(RuinsStructures17.KEY);
var summaries = new ArrayList<Map<String, Object>>(); int count = 0, blockCount = 0, lootCount = 0;
var connections = NativeConnections20.Stats.empty();
for (var start : ruinsStarts) {
var origin = start.getChunkPos(); var saved = start.createTag(context, origin); var digest = hash(saved.toString());
var loaded = StructureStart.loadStaticStart(context, saved, level.getSeed());
helper.assertTrue(loaded != null && loaded.createTag(context, origin).equals(saved), "Forgotten20 native start NBT changed");
var expectedRooms = generator.ruinsPlan17().networks().stream().filter(n -> n.startChunkX() == origin.x() && n.startChunkZ() == origin.z()).flatMap(n -> n.rooms().stream()).toList();
var actualRooms = start.getPieces().stream().map(RuinsPiece20.class::cast).map(RuinsPiece20::room).toList();
helper.assertTrue(expectedRooms.size() == actualRooms.size() && new HashSet<>(expectedRooms).equals(new HashSet<>(actualRooms)), "Forgotten20 native rooms differ from plan");
if (reload) {
JsonObject prior = null;
for (var value : previous.getAsJsonArray("ruins_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(digest), "Forgotten20 native start changed on a second JVM load");
}
for (var raw : start.getPieces()) {
var piece = (RuinsPiece20) raw; var room = piece.room(); count++;
var tag = piece.createTag(context); var roundtrip = new RuinsPiece20(context, tag);
helper.assertTrue(roundtrip.room().equals(room) && roundtrip.createTag(context).equals(tag), "Forgotten20 piece lost saved parameters");
piece.getBoundingBox().intersectingChunks().forEach(at -> helper.assertTrue(level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true).getReferencesForStructure(registry.value()).contains(origin.pack()), "Forgotten20 chunk lost native references"));
var expected = rendered(room); blockCount += expected.blocks().size();
for (var entry : expected.blocks().entrySet()) {
helper.assertTrue(piece.getBoundingBox().isInside(entry.getKey()), "Forgotten20 renderer escaped saved bounds");
helper.assertTrue(level.getBlockState(entry.getKey()).is(entry.getValue().getBlock()), "Forgotten20 native geometry changed at " + entry.getKey());
}
connections = connections.add(NativeConnections20.verify(helper, expected.blocks(), Set.of()));
for (var support : room.supports()) assertRock(helper, new BlockPos(support.x(), support.minY() - 1, support.z()));
for (var chest : expected.chests()) {
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Forgotten20 missing deferred chest");
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
helper.assertTrue(chest.table().equals(entity.getLootTable()) && chest.seed() == entity.getLootTableSeed(), "Forgotten20 consumed/changed saved loot"); lootCount++;
}
}
summaries.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", digest, "pieces", start.getPieces().size()));
}
report.put("ruins_starts", summaries); report.put("ruins_native_pieces", count); report.put("ruins_native_blocks_checked", blockCount);
report.put("ruins_unopened_deferred_chests", lootCount); report.put("ruins_connections", connections);
report.put("ruins_access", generator.ruinsPlan17().networks().stream().map(n -> verifyAccess(helper, n)).toList());
}
}
private record Chest(BlockPos position, ResourceKey<LootTable> table, long seed) {}
private record Rendered(Map<BlockPos, BlockState> blocks, List<Chest> chests) {}
private static Rendered rendered(HeritagePiece21 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()) HeritageRenderer21.render(piece.site(), writer); else HeritageRenderer21.renderSegment(piece.segment(), writer);
return new Rendered(blocks, chests);
}
private static Rendered rendered(RuinsPlan17.Room room) {
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
RuinsRenderer20.render(room, new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(at(room, x, y, z), state); }
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
var position = at(room, x, y, z); blocks.put(position, Blocks.CHEST.defaultBlockState()); chests.add(new Chest(position, table, RandomSource.create(seed).nextLong()));
}
});
return new Rendered(blocks, chests);
}
private static Map<String, Object> verifyAccess(GameTestHelper helper, RuinsPlan17.Network network) {
var level = helper.getLevel(); var access = network.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS).findFirst().orElseThrow();
var hatches = new HashMap<BlockPos, BlockState>();
rendered(access).blocks().forEach((at, state) -> { if (state.is(Blocks.DARK_OAK_TRAPDOOR)) hatches.put(at, level.getBlockState(at)); });
helper.assertTrue(hatches.size() == 1, "Each network must have one usable wooden surface hatch");
hatches.forEach((at, state) -> {
helper.assertTrue(state.is(Blocks.DARK_OAK_TRAPDOOR) && state.canSurvive(level, at), "Surface hatch is missing its native support");
level.setBlock(at, state.setValue(BlockStateProperties.OPEN, true), 2);
});
try {
Predicate<BlockPos> inside = pos -> network.rooms().stream().anyMatch(r -> r.contains(pos.getX(), pos.getY(), pos.getZ()));
var start = RuinsPiece20.locator(access); var reached = walk(helper, start, inside);
int targets = 0;
for (var room : network.rooms()) for (var target : RuinsRenderer20.targets(room)) {
var point = at(room, target.x(), target.y(), target.z());
helper.assertTrue(reached.contains(point), "Native network " + network.id() + " cannot reach " + room.kind() + "/" + target.name() + " at " + point);
targets++;
}
for (var hatch : hatches.keySet()) helper.assertTrue(reached.contains(hatch), "Surface approach cannot enter its ladder shaft");
return Map.of("id", network.id(), "family", network.family().name(), "rooms", network.rooms().size(),
"entrance", coords(start), "walkable_targets", targets, "standing_cells", reached.size());
} finally { hatches.forEach((at, state) -> level.setBlock(at, state, 2)); }
}
private static BlockPos at(RuinsPlan17.Room room, int x, int y, int z) { return new BlockPos(room.minX() + x, room.minY() + y, room.minZ() + z); }
private static 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 HeritagePiece21(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 = HeritagePiece21.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 : HeritageRenderer21.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("heritage21-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", "heritage21-" + (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); }
}
}
@@ -211,7 +211,7 @@ public final class PopulationDiagnostics {
void prepare() throws IOException { void prepare() throws IOException {
boolean heritage = level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator sanctuary boolean heritage = level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator sanctuary
&& (sanctuary.rootGeneration() == 19 || sanctuary.rootGeneration() == 20) && sanctuary.experimentalStructures(); && (sanctuary.rootGeneration() == 19 || sanctuary.rootGeneration() == 20 || sanctuary.rootGeneration() == 21) && sanctuary.experimentalStructures();
// Generation19 explicitly prospects destinations across the root island before publishing them. // 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. // A consulted hydrology region is a bounded immutable plan, not a loaded/generated chunk.
// Historical generators retain their original near-spawn contract. // Historical generators retain their original near-spawn contract.
@@ -0,0 +1,453 @@
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.transit.TransitPiece21;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21;
import fr.koka.sanctuary.worldgen.transit.TransitRenderer21;
import fr.koka.sanctuary.worldgen.transit.TransitStructures21;
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.HashMap;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderSet;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.StairBlock;
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.LevelResource;
import net.minecraft.world.level.storage.loot.LootTable;
import net.minecraft.world.phys.AABB;
/** Opt-in native proof confined to the new generation21 starter island in a disposable development world. */
public final class Transit21WorldGameTests {
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
@GameTest(maxTicks = 12000)
public void industrialTransitPersistsNatively(GameTestHelper helper) {
helper.assertTrue(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator,
"The industrial-transits proof requires a Sanctuary generator");
var generator = (SanctuaryChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
helper.assertTrue(generator.rootGeneration() == 21, "Use a fresh generation21 test world; do not convert old saves");
new Scenario(helper, generator, Boolean.getBoolean("sanctuary.test.expansionReload")).advance();
}
private static final class Scenario {
final GameTestHelper helper;
final ServerLevel level;
final SanctuaryChunkGenerator generator;
final TransitPlan21 plan;
final boolean reload;
final Map<String, Object> report = new LinkedHashMap<>();
final List<ChunkPos> chunks = new ArrayList<>();
final List<StructureStart> starts = new ArrayList<>();
final Map<BlockPos, BlockState> allGeometry = new HashMap<>();
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.transitPlan21();
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("planning_diagnostic", plan.diagnostic());
report.put("scope", "Native starts, references, geometry, rock bearings, unopened loot and a player-sized stair walk between two surface gates. "
+ "No expansion is created and no personal save is opened. A single outer-wall gold edit in this disposable world "
+ "is saved and checked in a second JVM. Traversal remains inside the modules and certified portico approaches; underground one-block rises need real correctly oriented stairs. Only the certified natural portico approaches permit an ordinary one-block jump. No flight, ladder or external terrain shortcut is allowed. No fluid simulation is claimed.");
}
void advance() {
try {
if (!prepared) {
prepare(); prepared = true;
if (plan.isEmpty()) { finishDisabled(); return; }
}
if (nextChunk < chunks.size()) {
var pos = chunks.get(nextChunk++);
var loaded = level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true);
helper.assertTrue(loaded instanceof LevelChunk, "Every room footprint must reach native FULL: " + pos);
if (nextChunk % 10 == 0) SanctuaryMod.LOGGER.info("Industrial transit21: loaded {}/{} footprint chunks", nextChunk, chunks.size());
helper.runAtTickTime(helper.getTick() + 1, this::advance); return;
}
verifyPlaced();
report.put("passed", true);
if (!reload) {
helper.assertTrue(gold != null && level.getBlockState(gold).isCollisionShapeFullBlock(level, gold),
"Gameplay witness must replace a solid outer wall away from every doorway");
level.setBlock(gold, Blocks.GOLD_BLOCK.defaultBlockState(), 3);
report.put("gold_witness", coords(gold));
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush native structures, deferred loot and the gold edit");
report.put("world_flushed_before_shutdown", true);
} else {
helper.assertTrue(level.getBlockState(gold).is(Blocks.GOLD_BLOCK), "Reload replaced the existing wall edit");
report.put("gold_witness", coords(gold)); report.put("gold_witness_preserved", true);
}
write(level, report, reload); helper.succeed();
} catch (Exception failure) {
report.put("passed", false); report.put("failure", failure.toString());
try { write(level, report, reload); } catch (IOException ignored) { }
throw new IllegalStateException("Industrial transit21 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() == 21
&& copy.experimentalStructures() == generator.experimentalStructures(), "The generation21 codec must preserve its experimental switch");
report.put("generator_codec", encoded); report.put("plan_sha256", planHash(plan)); report.put("planning_samples", plan.samples());
helper.assertTrue(plan.samples() <= TransitPlan21.MAX_SAMPLES, "Transit planning exceeded its budget");
if (reload) {
previous = JsonParser.parseString(Files.readString(proof(level))).getAsJsonObject();
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("world_flushed_before_shutdown").getAsBoolean(),
"Reload requires the successful, flushed creation proof");
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload must run in a separate JVM");
helper.assertTrue(previous.get("seed").getAsLong() == level.getSeed()
&& previous.get("size").getAsInt() == generator.initialDiameter()
&& previous.get("world_directory").getAsString().equals(world(level).toString()), "Reload used another world, seed or size");
helper.assertTrue(previous.get("generator_codec").equals(encoded) && previous.get("plan_sha256").getAsString().equals(planHash(plan)),
"Saved switch or planned generation21 room identities changed on reload");
if (previous.has("gold_witness")) gold = readPos(previous.getAsJsonArray("gold_witness"));
}
boolean enabled = generator.experimentalStructures() && level.structureManager().shouldGenerateStructures();
if (!enabled) {
helper.assertTrue(plan.isEmpty(), "Experimental=false or generateStructures=false must publish no industrial transit");
return;
}
helper.assertTrue(!plan.isEmpty(), "Enabled reference island has no experimental network");
helper.assertTrue(plan.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.ACCESS).count() == 2,
"A through-route needs two independent saved surface stairs");
int spanX = plan.modules().stream().mapToInt(TransitPlan21.Module::maxX).max().orElseThrow()
- plan.modules().stream().mapToInt(TransitPlan21.Module::minX).min().orElseThrow() + 1;
int spanZ = plan.modules().stream().mapToInt(TransitPlan21.Module::maxZ).max().orElseThrow()
- plan.modules().stream().mapToInt(TransitPlan21.Module::minZ).min().orElseThrow() + 1;
report.put("span_x", spanX); report.put("span_z", spanZ);
helper.assertTrue(Math.max(spanX, spanZ) >= generator.initialDiameter() * .4,
"The native transit reference must cross at least40% of the island's nominal diameter");
report.put("modules", plan.modules().size());
report.put("halls", plan.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.HALL).count());
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(TransitStructures21.KEY);
var origins = new HashSet<Long>(); var uniqueChunks = new HashSet<ChunkPos>();
for (var network : plan.modules()) {
long origin = new ChunkPos(network.startChunkX(), network.startChunkZ()).pack();
if (!origins.add(origin)) continue;
var chunk = level.getChunk(network.startChunkX(), network.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
var start = chunk.getStartForStructure(structure.value());
helper.assertTrue(start != null && start.isValid(), "Accepted experimental network has no native start: " + network.kind() + " at " + network.centerX() + "," + network.centerZ());
starts.add(start);
for (var piece : start.getPieces()) {
helper.assertTrue(piece instanceof TransitPiece21, "Industrial transit loaded a foreign or historical piece type");
piece.getBoundingBox().intersectingChunks().forEach(at -> {
helper.assertTrue(Math.abs(at.x() - network.startChunkX()) <= 7 && Math.abs(at.z() - network.startChunkZ()) <= 7,
"Transit native start exceeds its local reference range");
uniqueChunks.add(at);
});
}
}
helper.assertTrue(uniqueChunks.size() <= 256, "The bounded root-only proof unexpectedly covers more than256 chunks");
chunks.addAll(uniqueChunks.stream().sorted(java.util.Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList());
}
void finishDisabled() throws IOException {
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(TransitStructures21.KEY);
var chunk = level.getChunk(0, 0, ChunkStatus.STRUCTURE_STARTS, true);
var start = chunk.getStartForStructure(structure.value());
helper.assertTrue(start == null || !start.isValid(), "Disabled experimental generation emitted a native start");
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
helper.assertTrue(located == null, "Disabled experimental structures must not be located or prepared");
report.put("empty_plan_verified", true); report.put("passed", true);
if (!reload) {
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save the disabled experiment setting");
report.put("world_flushed_before_shutdown", true);
}
write(level, report, reload); helper.succeed();
}
void verifyPlaced() {
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(TransitStructures21.KEY);
var context = StructurePieceSerializationContext.fromLevel(level);
var sites = new ArrayList<Map<String, Object>>();
var modules = new ArrayList<Map<String, Object>>();
int blocks = 0, supports = 0, chests = 0, pieces = 0;
var bearingMasks = new HashMap<TransitPlan21.Module, Integer>();
for (var start : starts) {
var origin = start.getChunkPos(); var saved = start.createTag(context, origin);
var copy = StructureStart.loadStaticStart(context, saved, level.getSeed());
helper.assertTrue(copy != null && copy.isValid() && copy.createTag(context, origin).equals(saved), "The native start changed on an NBT roundtrip");
var expectedRooms = plan.modulesForChunk(origin.x(), origin.z());
var actualRooms = start.getPieces().stream().map(TransitPiece21.class::cast).map(TransitPiece21::module).toList();
helper.assertTrue(actualRooms.size() == expectedRooms.size() && new HashSet<>(actualRooms).equals(new HashSet<>(expectedRooms)),
"Native start did not persist every planned room exactly once");
String nativeHash = hash(saved.toString());
if (reload) {
JsonObject prior = null;
for (var value : previous.getAsJsonArray("starts")) {
var site = value.getAsJsonObject(); var at = site.getAsJsonArray("start_chunk");
if (at.get(0).getAsInt() == origin.x() && at.get(1).getAsInt() == origin.z()) prior = site;
}
helper.assertTrue(prior != null && prior.get("native_start_sha256").getAsString().equals(nativeHash),
"Reload altered native room parameters, supports or reference count");
}
var loot = new ArrayList<Map<String, Object>>();
for (var piece : start.getPieces()) {
var roomPiece = (TransitPiece21) piece; var room = roomPiece.module(); pieces++;
piece.getBoundingBox().intersectingChunks().forEach(at -> {
var full = level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true);
helper.assertTrue(full.getReferencesForStructure(structure.value()).contains(origin.pack()), "A saved FULL chunk lost the native structure reference");
});
var roomTag = roomPiece.createTag(context);
var roundtrip = new TransitPiece21(context, roomTag);
helper.assertTrue(roundtrip.module().equals(room) && roundtrip.createTag(context).equals(roomTag), "The native room failed its exact saved-field roundtrip");
var corrupt = roomTag.copy(); var payload = corrupt.getCompound("SanctuaryModule").orElseThrow(); payload.putInt("Schema", 99);
boolean rejected = false;
try { new TransitPiece21(context, corrupt); } catch (IllegalArgumentException expected) { rejected = true; }
helper.assertTrue(rejected, "An unknown generation21 piece schema must be refused");
var expected = rendered(room); blocks += expected.blocks().size();
expected.blocks().forEach((at, state) -> helper.assertTrue(allGeometry.putIfAbsent(at, state) == null,
"Two saved transit modules paint the same position: " + at));
modules.add(Map.of("kind", room.kind().name(), "min", List.of(room.minX(), room.minY(), room.minZ()),
"max", List.of(room.maxX(), room.maxY(), room.maxZ()), "floor_y", room.floorY(),
"surface_y", room.surfaceY(), "openings", room.openings(), "supports", room.supports().size(),
"start_chunk", List.of(room.startChunkX(), room.startChunkZ())));
for (var entry : expected.blocks().entrySet()) {
var at = entry.getKey(); var wanted = entry.getValue();
helper.assertTrue(TransitPiece21.bounds(room).isInside(at), "Renderer escaped its saved room/support bounds");
helper.assertTrue(plan.protects(at.getX(), at.getY(), at.getZ()) && generator.protectsCityDecoration(at),
"Transit construction/cleared air is missing its published decoration protection at " + at);
if (reload && at.equals(gold)) continue;
helper.assertTrue(level.getBlockState(at).is(wanted.getBlock()),
"Native clipping or later decoration changed " + room.kind() + " at " + at + ": expected " + wanted + " got " + level.getBlockState(at));
helper.assertTrue(!level.getBlockState(at).is(Blocks.SPAWNER), "Experimental architecture must not create a spawner");
}
supports += verifyBearings(helper, generator, room, bearingMasks);
for (var entry : expected.blocks().entrySet()) if (entry.getValue().is(Blocks.LANTERN)) {
var at = entry.getKey(); var state = level.getBlockState(at);
helper.assertTrue(state.canSurvive(level, at) && level.getBlockState(at.above()).isFaceSturdy(level, at.above(), Direction.DOWN),
"Transit lamp lost its complete ceiling bearing at " + at);
}
for (var chest : expected.chests()) {
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Deferred native chest block entity is missing");
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
helper.assertTrue(chest.table().equals(entity.getLootTable()) && entity.getLootTableSeed() == chest.seed(),
"Native chest loot must remain deferred with its exact table and seed");
loot.add(Map.of("position", coords(chest.position()), "table", chest.table().identifier().toString(), "seed", chest.seed())); chests++;
}
if (!reload && gold == null && room.kind() != TransitPlan21.Kind.ACCESS && room.kind() != TransitPlan21.Kind.JUNCTION)
gold = new BlockPos(room.minX(), room.floorY() + 1, room.minZ() + 1);
}
sites.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", nativeHash,
"pieces", start.getPieces().size(), "unopened_chests", loot));
}
report.put("module_coordinates", modules);
report.put("starts", sites);
int weakSpan = 0, longestSpan = 0;
for (var room : plan.modules()) {
weakSpan = bearingMasks.get(room) == 15 ? 0 : weakSpan + 1;
longestSpan = Math.max(longestSpan, weakSpan);
helper.assertTrue(weakSpan <= 2, "Native deck spans more than 34 blocks without distributed rock bearings");
}
report.put("longest_span_without_full_bearings", longestSpan * 17);
var ores = new java.util.TreeMap<String, Integer>();
allGeometry.values().forEach(state -> {
var id = net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString();
if (id.endsWith("_ore")) ores.merge(id, 1, Integer::sum);
});
report.put("finite_exposed_ores", ores);
var traversal = verifyAccess(helper, plan);
report.put("native_connections", NativeConnections20.verify(helper, allGeometry,
reload && gold != null ? Set.of(gold) : Set.of()));
report.put("module_coordinates", modules);
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
helper.assertTrue(located != null && located.getSecond().is(TransitStructures21.KEY), "Native locate cannot find a saved industrial transit");
helper.assertTrue(plan.modules().stream().filter(r -> r.kind() == TransitPlan21.Kind.ACCESS)
.anyMatch(r -> TransitPiece21.locator(r).equals(located.getFirst())), "Locate must lead to a surface entrance rather than an underground floor");
report.put("starts", sites); report.put("native_pieces", pieces); report.put("native_blocks_checked", blocks);
report.put("full_chunks_checked", chunks.size()); report.put("rock_supports_checked", supports);
report.put("unopened_deferred_chests", chests); report.put("native_routes", traversal); report.put("locate", coords(located.getFirst()));
}
}
private record Chest(BlockPos position, ResourceKey<LootTable> table, long seed) {}
private record Rendered(Map<BlockPos, BlockState> blocks, List<Chest> chests) {}
private static Rendered rendered(TransitPlan21.Module room) {
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
TransitRenderer21.render(room, new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(at(room, x, y, z), state); }
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
var position = at(room, x, y, z); blocks.put(position, Blocks.CHEST.defaultBlockState()); chests.add(new Chest(position, table, RandomSource.create(seed).nextLong()));
}
});
return new Rendered(blocks, chests);
}
private static int verifyBearings(GameTestHelper helper, SanctuaryChunkGenerator generator, TransitPlan21.Module room, Map<TransitPlan21.Module, Integer> masks) {
var level = helper.getLevel(); int checked = 0, quarters = 0;
for (int z = room.minZ();; z = Math.min(z + 4, room.maxZ())) {
for (int x = room.minX();; x = Math.min(x + 4, room.maxX())) {
int anchor = room.minY() - 1;
for (var support : room.supports()) if (support.x() == x && support.z() == z) anchor = support.minY() - 1;
var bearing = new BlockPos(x, anchor, z);
boolean supported = level.getBlockState(bearing).isSolid() && level.getFluidState(bearing).isEmpty()
&& level.getBlockState(bearing.below()).isSolid() && level.getFluidState(bearing.below()).isEmpty();
if (supported) {
quarters |= 1 << ((x < room.centerX() ? 0 : 1) + (z < room.centerZ() ? 0 : 2));
for (int dy = 0; dy >= -1; dy--) {
var at = new BlockPos(x, anchor + dy, z);
helper.assertTrue(level.getBlockState(at).isSolid() && level.getFluidState(at).isEmpty(),
"Transit bearing grid lost one of its two untouched rock layers at " + at);
helper.assertTrue(generator.protectsCityDecoration(at), "A certified transit rock dependency is unprotected at " + at);
checked++;
}
}
if (x == room.maxX()) break;
}
if (z == room.maxZ()) break;
}
helper.assertTrue(room.kind() == TransitPlan21.Kind.GALLERY || room.kind() == TransitPlan21.Kind.JUNCTION || quarters == 15, "The continuous deck lost its distributed rock bearings");
masks.put(room, quarters);
return checked;
}
private static Map<String, Object> verifyAccess(GameTestHelper helper, TransitPlan21 plan) {
var level = helper.getLevel();
var accesses = plan.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.ACCESS).toList();
helper.assertTrue(accesses.size() == 2, "Native transit requires exactly two surface gates");
Predicate<BlockPos> inside = pos -> plan.contains(pos.getX(), pos.getY(), pos.getZ())
|| accesses.stream().anyMatch(m -> pos.getX() >= m.centerX() - 2 && pos.getX() <= m.centerX() + 2
&& pos.getZ() >= m.minZ() - 3 && pos.getZ() < m.minZ()
&& pos.getY() >= m.surfaceY() && pos.getY() <= m.surfaceY() + 2);
var start = TransitPiece21.locator(accesses.getFirst());
Predicate<BlockPos> naturalApproach = pos -> accesses.stream().anyMatch(m ->
pos.getX() >= m.centerX() - 2 && pos.getX() <= m.centerX() + 2
&& pos.getZ() >= m.minZ() - 3 && pos.getZ() < m.minZ()
&& pos.getY() >= m.surfaceY() && pos.getY() <= m.surfaceY() + 2);
var reached = walk(helper, start, inside, naturalApproach); int targets = 0;
var arrivals = new ArrayList<Map<String, Object>>();
for (var module : plan.modules()) for (var target : TransitRenderer21.targets(module)) {
var point = at(module, target.x(), target.y(), target.z());
helper.assertTrue(reached.containsKey(Foot.at(point)), "Native transit cannot walk from its first portico to "
+ module.kind() + "/" + target.name() + " at " + point);
targets++;
}
for (var access : accesses) {
var gate = TransitPiece21.locator(access);
helper.assertTrue(reached.containsKey(Foot.at(gate)), "The second independent surface gate is not connected by real stairs");
int approaches = 0;
for (int x = access.centerX() - 2; x <= access.centerX() + 2; x++) for (int z = access.minZ() - 3; z < access.minZ(); z++) {
var high = new BlockPos(x, access.surfaceY() + 1, z);
var low = high.below(); var position = standable(level, high) ? high : low;
helper.assertTrue(standable(level, position) && reached.containsKey(Foot.at(position)),
"The certified natural approach no longer joins its portico at " + position);
helper.assertTrue(level.getBlockState(position.below()).isSolid()
&& level.getFluidState(position.below()).isEmpty(), "A surface arrival is suspended above air or water");
approaches++;
}
arrivals.add(Map.of("gate", coords(gate), "roadway", coords(at(access, 8, 2, 8)),
"rise", access.surfaceY() - access.floorY(), "steps_from_first_gate", reached.get(Foot.at(gate)),
"natural_approach_cells", approaches));
}
return Map.of("surface_accesses", arrivals, "walkable_targets", targets, "standing_cells", reached.size(),
"surface_to_surface_steps", reached.get(Foot.at(TransitPiece21.locator(accesses.getLast()))),
"traversal_policy", "0.6 by1.8 native collision volume, half-block sidewalk steps; underground rises require a real forward-facing stair; natural portico approaches alone permit a normal one-block jump; no vertical moves");
}
/** Half-block standing elevations retain real sidewalk slabs without pretending an integer cell can hover over them. */
private record Foot(int x, int halfY, int z) {
static Foot at(BlockPos pos) { return new Foot(pos.getX(), pos.getY() * 2, pos.getZ()); }
double y() { return halfY / 2.0; }
BlockPos block() { return new BlockPos(x, Math.floorDiv(halfY, 2), z); }
BlockPos bearing() { return new BlockPos(x, Math.floorDiv(halfY - 1, 2), z); }
Foot rise(int halves) { return new Foot(x, halfY + halves, z); }
Foot move(Direction direction, int halves) { return new Foot(x + direction.getStepX(), halfY + halves, z + direction.getStepZ()); }
}
private static Map<Foot, Integer> walk(GameTestHelper helper, BlockPos start, Predicate<BlockPos> inside, Predicate<BlockPos> naturalApproach) {
var level = helper.getLevel(); var first = Foot.at(start);
helper.assertTrue(inside.test(start) && standable(level, first), "Blocked native surface portico: " + start);
var seen = new HashMap<Foot, Integer>(); var queue = new ArrayDeque<Foot>(); seen.put(first, 0); queue.add(first);
while (!queue.isEmpty()) {
var pos = queue.removeFirst();
for (var direction : HORIZONTAL) for (int dy = -2; dy <= 2; dy++) {
var next = pos.move(direction, dy);
if (!inside.test(next.block()) || seen.containsKey(next)) continue;
boolean ordinaryThreshold = naturalApproach.test(pos.block()) || naturalApproach.test(next.block());
// Native step height is enough for a half slab; a full block needs the two treads of a stair.
if (dy > 0 && (!clearBody(level, pos.rise(dy))
|| dy == 2 && !ordinaryThreshold && !ascendingStair(level, next.bearing(), direction))) continue;
if (dy < 0 && (!clearBody(level, next.rise(-dy))
|| dy == -2 && !ordinaryThreshold && !ascendingStair(level, pos.bearing(), direction.getOpposite()))) continue;
if (standable(level, next)) { seen.put(next, seen.get(pos) + 1); queue.add(next); }
}
helper.assertTrue(seen.size() <= 120_000, "Native transit traversal exceeded its bounded room volumes");
}
return seen;
}
private static boolean ascendingStair(ServerLevel level, BlockPos pos, Direction direction) {
var state = level.getBlockState(pos);
return state.getBlock() instanceof StairBlock
&& state.getValue(BlockStateProperties.HORIZONTAL_FACING) == direction
&& state.getValue(BlockStateProperties.HALF) == net.minecraft.world.level.block.state.properties.Half.BOTTOM;
}
private static boolean standable(ServerLevel level, BlockPos pos) { return standable(level, Foot.at(pos)); }
private static boolean standable(ServerLevel level, Foot pos) {
if (!clearBody(level, pos)) return false;
var below = pos.bearing(); double top = pos.y() - below.getY();
return level.getBlockState(below).getCollisionShape(level, below).toAabbs().stream()
.anyMatch(b -> Math.abs(b.maxY - top) < .0001 && b.minX <= .5 && b.maxX >= .5 && b.minZ <= .5 && b.maxZ >= .5);
}
private static boolean clearBody(ServerLevel level, Foot pos) {
var body = new AABB(pos.x() + .2, pos.y(), pos.z() + .2, pos.x() + .8, pos.y() + 1.8, pos.z() + .8);
for (int y = (int) Math.floor(pos.y()); y <= (int) Math.floor(pos.y() + 1.8 - .0001); y++) {
var at = new BlockPos(pos.x(), y, pos.z());
if (!level.getFluidState(at).isEmpty()) return false;
for (var shape : level.getBlockState(at).getCollisionShape(level, at).toAabbs())
if (shape.move(at.getX(), at.getY(), at.getZ()).intersects(body)) return false;
}
return true;
}
private static BlockPos at(TransitPlan21.Module room, int x, int y, int z) { return new BlockPos(room.minX() + x, room.minY() + y, room.minZ() + z); }
private static List<Integer> coords(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
private static BlockPos readPos(com.google.gson.JsonArray value) { return new BlockPos(value.get(0).getAsInt(), value.get(1).getAsInt(), value.get(2).getAsInt()); }
private static Path world(ServerLevel level) { return level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize(); }
private static Path proof(ServerLevel level) { return world(level).resolve("transit21-create-proof.json"); }
private static String planHash(TransitPlan21 plan) { return hash(plan.modules().toString()); }
private static void write(ServerLevel level, Map<String, Object> report, boolean reload) throws IOException {
var path = Path.of("diagnostics", "transit21-" + (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); }
}
}
@@ -52,7 +52,10 @@ final class UnifiedWorldSettingsDiagnostics {
helper.assertTrue(historical19.overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 19 helper.assertTrue(historical19.overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 19
&& old.source().generation() == 19 && old.experimentalStructures(), "Historical19 keeps its original renderer and world settings"); && old.source().generation() == 19 && old.experimentalStructures(), "Historical19 keeps its original renderer and world settings");
var generator = (SanctuaryChunkGenerator) standard.overworld(); var generator = (SanctuaryChunkGenerator) standard.overworld();
helper.assertTrue(generator.rootGeneration() == 20 && generator.source().generation() == 20 && generator.experimentalStructures(), "The public preset uses generation20 with its new-world option"); var historical20 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v20")));
helper.assertTrue(historical20.value().createWorldDimensions().overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 20
&& old.source().generation() == 20 && old.experimentalStructures(), "Historical20 keeps its original architecture");
helper.assertTrue(generator.rootGeneration() == 21 && generator.source().generation() == 21 && generator.experimentalStructures(), "The public preset uses generation21 with its new-world option");
helper.assertTrue(generator.initialDiameter() == 724, "The public preset defaults to Medium / 724 blocks"); helper.assertTrue(generator.initialDiameter() == 724, "The public preset defaults to Medium / 724 blocks");
for (var tag : List.of(WorldPresetTags.NORMAL, WorldPresetTags.EXTENDED)) { for (var tag : List.of(WorldPresetTags.NORMAL, WorldPresetTags.EXTENDED)) {
var visible = presets.getOrThrow(tag).stream().map(holder -> holder.getRegisteredName()) var visible = presets.getOrThrow(tag).stream().map(holder -> holder.getRegisteredName())
@@ -62,11 +65,11 @@ final class UnifiedWorldSettingsDiagnostics {
} }
var ops = RegistryOps.create(JsonOps.INSTANCE, access); var ops = RegistryOps.create(JsonOps.INSTANCE, access);
var activeCodec = generator.rootGeneration() == 20 ? SanctuaryChunkGenerator.ARCHITECTURE20_CODEC : 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 activeCodec = generator.rootGeneration() == 21 ? SanctuaryChunkGenerator.NETWORK21_CODEC : generator.rootGeneration() == 20 ? SanctuaryChunkGenerator.ARCHITECTURE20_CODEC : 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(); var serialized = activeCodec.codec().encodeStart(ops, generator).getOrThrow().getAsJsonObject();
serialized.remove("experimental_structures"); serialized.remove("experimental_structures");
helper.assertTrue(activeCodec.codec().parse(ops, serialized).getOrThrow().experimentalStructures(), helper.assertTrue(activeCodec.codec().parse(ops, serialized).getOrThrow().experimentalStructures(),
"Generation20 omitted experimental option defaults to enabled"); "Generation21 omitted experimental option defaults to enabled");
helper.assertTrue(!((SanctuaryChunkGenerator) historical16.overworld()).withExperimentalStructures(true).experimentalStructures(), helper.assertTrue(!((SanctuaryChunkGenerator) historical16.overworld()).withExperimentalStructures(true).experimentalStructures(),
"The new-world switch cannot activate experimental generation in a historical16 generator"); "The new-world switch cannot activate experimental generation in a historical16 generator");
serialized.remove("initial_diameter"); 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. // generation test's deadline before its IO/workers can complete. This is test-only pacing.
@Inject(method = "waitUntilNextTick", at = @At("RETURN")) @Inject(method = "waitUntilNextTick", at = @At("RETURN"))
private void sanctuary$paceExpansionWorkers(CallbackInfo ci) { private void sanctuary$paceExpansionWorkers(CallbackInfo ci) {
if (Boolean.getBoolean("sanctuary.test.heritage20") || Boolean.getBoolean("sanctuary.test.lostCity20") if ((Boolean.getBoolean("sanctuary.test.heritage20") || Boolean.getBoolean("sanctuary.test.heritage21") || Boolean.getBoolean("sanctuary.test.lostCity21") || Boolean.getBoolean("sanctuary.test.transit21")) || Boolean.getBoolean("sanctuary.test.lostCity20")
|| Boolean.getBoolean("sanctuary.test.heritage19") || Boolean.getBoolean("sanctuary.test.ruins17") || Boolean.getBoolean("sanctuary.test.expansion") || Boolean.getBoolean("sanctuary.test.lostCity") || 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")) { || Boolean.getBoolean("sanctuary.test.lostCity16") || Boolean.getBoolean("sanctuary.test.lostCity15") || Boolean.getBoolean("sanctuary.test.voidReuse")) {
long until = System.nanoTime() + 20_000_000L; long until = System.nanoTime() + 20_000_000L;
@@ -47,9 +47,9 @@ public abstract class SanctuaryGameTestServerMixin {
"Lnet/minecraft/world/level/levelgen/presets/WorldPresets;FLAT_ALL_DIMENSIONS:Lnet/minecraft/resources/ResourceKey;")) "Lnet/minecraft/world/level/levelgen/presets/WorldPresets;FLAT_ALL_DIMENSIONS:Lnet/minecraft/resources/ResourceKey;"))
private static ResourceKey<WorldPreset> sanctuary$selectActualPreset() { private static ResourceKey<WorldPreset> sanctuary$selectActualPreset() {
if (Boolean.getBoolean("sanctuary.test.unified")) { if (Boolean.getBoolean("sanctuary.test.unified")) {
int generation = Integer.getInteger("sanctuary.test.generation", 20); int generation = Integer.getInteger("sanctuary.test.generation", 21);
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19 && generation != 20) throw new IllegalArgumentException("Unsupported test generation"); if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21) throw new IllegalArgumentException("Unsupported test generation");
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 20 ? "sanctuary" : "sanctuary_v" + generation)); return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 21 ? "sanctuary" : "sanctuary_v" + generation));
} }
if (Boolean.getBoolean("sanctuary.test.expansion")) return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("expansion_lab")); if (Boolean.getBoolean("sanctuary.test.expansion")) return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("expansion_lab"));
int players = Integer.getInteger("sanctuary.test.players", 10); int players = Integer.getInteger("sanctuary.test.players", 10);
@@ -65,6 +65,9 @@ public final class SanctuaryMod implements ModInitializer {
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v19"), SanctuaryBiomeSource.HERITAGE19_CODEC); Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v19"), SanctuaryBiomeSource.HERITAGE19_CODEC);
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v20"), SanctuaryChunkGenerator.ARCHITECTURE20_CODEC); Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v20"), SanctuaryChunkGenerator.ARCHITECTURE20_CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v20"), SanctuaryBiomeSource.ARCHITECTURE20_CODEC); Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v20"), SanctuaryBiomeSource.ARCHITECTURE20_CODEC);
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v21"), SanctuaryChunkGenerator.NETWORK21_CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v21"), SanctuaryBiomeSource.NETWORK21_CODEC);
fr.koka.sanctuary.worldgen.transit.TransitStructures21.register();
fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.register(); fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.register();
fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.register(); fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.register();
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("population_island"), PopulationIslandDensity.CODEC); Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("population_island"), PopulationIslandDensity.CODEC);
@@ -51,11 +51,11 @@ public final class SanctuaryWorldOptionsScreen extends Screen {
contents.addChild(sizeButton); contents.addChild(sizeButton);
diameterHint = contents.addChild(new MultiLineTextWidget(diameterHint(), font) diameterHint = contents.addChild(new MultiLineTextWidget(diameterHint(), font)
.setMaxWidth(310).setCentered(true)); .setMaxWidth(310).setCentered(true));
if (generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20) { if (generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21) {
contents.addChild(CycleButton.builder((Boolean enabled) -> enabled ? CommonComponents.OPTION_ON : CommonComponents.OPTION_OFF, experimental) 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"), .withValues(true, false).create(0, 0, 280, 20, Component.translatable("sanctuary.world_options.experimental"),
(button, value) -> experimental = value)); (button, value) -> experimental = value));
contents.addChild(new MultiLineTextWidget(Component.translatable((generator.rootGeneration() == 19 || generator.rootGeneration() == 20) ? "sanctuary.world_options.heritage_hint" : "sanctuary.world_options.experimental_hint"), font) contents.addChild(new MultiLineTextWidget(Component.translatable((generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21) ? "sanctuary.world_options.heritage_hint" : "sanctuary.world_options.experimental_hint"), font)
.setMaxWidth(310).setCentered(true)); .setMaxWidth(310).setCentered(true));
} }
layout.addToContents(contents); layout.addToContents(contents);
@@ -123,6 +123,7 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
fr.koka.sanctuary.worldgen.city.LostCityStructures.generate(this, registries, state, structures, chunk, templates, 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.ruins.RuinsStructures17.generate(this, registries, state, structures, chunk, templates, dimension);
fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.generate(this, registries, state, structures, chunk, templates, dimension); fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.generate(this, registries, state, structures, chunk, templates, dimension);
fr.koka.sanctuary.worldgen.transit.TransitStructures21.generate(this, registries, state, structures, chunk, templates, dimension);
ExpansionStructures.generate(this, registries, state, structures, chunk, templates, dimension); ExpansionStructures.generate(this, registries, state, structures, chunk, templates, dimension);
} }
@Override public void spawnOriginalMobs(WorldGenRegion region) { @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 (!((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 (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 (!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, 17, 19 or 20 expansion"); if (climate.equals("lost_city") && (!spatialGeneration(generation) || id.equals("sanctuary"))) throw new IllegalArgumentException("Lost City requires a generation 14, 15, 16, 17, 19, 20 or 21 expansion");
if (reuseToken == null || !reuseToken.isEmpty() && (!spatialGeneration(generation) || id.equals("sanctuary") || !reuseToken.matches("[0-9a-f]{64}"))) if (reuseToken == null || !reuseToken.isEmpty() && (!spatialGeneration(generation) || id.equals("sanctuary") || !reuseToken.matches("[0-9a-f]{64}")))
throw new IllegalArgumentException("Invalid void reuse certificate token"); 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); 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 || generation == 19 || generation == 20; } public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21; }
public boolean origin() { return id.equals("sanctuary"); } public boolean origin() { return id.equals("sanctuary"); }
public double referenceDiameter() { return origin() && diameter == 724 ? Math.sqrt(2) * 512 : 512; } public double referenceDiameter() { return origin() && diameter == 724 ? Math.sqrt(2) * 512 : 512; }
public double horizontalScale() { return origin() ? 1.0 : diameter / referenceDiameter(); } public double horizontalScale() { return origin() ? 1.0 : diameter / referenceDiameter(); }
@@ -26,7 +26,8 @@ public abstract class UnifiedDecorationProtectionMixin {
if (owner == null || destination == null || !owner.id().equals(destination.id()) if (owner == null || destination == null || !owner.id().equals(destination.id())
|| !fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing() || !fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
&& !fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.isPlacing() && !fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.isPlacing()
&& !fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.isPlacing() && generator.protectsCityDecoration(pos)) && !fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.isPlacing()
&& !fr.koka.sanctuary.worldgen.transit.TransitStructures21.isPlacing() && generator.protectsCityDecoration(pos))
callback.setReturnValue(false); callback.setReturnValue(false);
} }
@@ -41,7 +42,7 @@ public abstract class UnifiedDecorationProtectionMixin {
if (state.is(Blocks.SUGAR_CANE)) return; if (state.is(Blocks.SUGAR_CANE)) return;
// Generation16 reserves its architectural volume before decoration. Only its own // Generation16 reserves its architectural volume before decoration. Only its own
// native pieces may replace water there; published generations retain the old rule. // native pieces may replace water there; published generations retain the old rule.
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20) if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21)
&& fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing() && fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
&& generator.protectsCityDecoration(pos)) return; && generator.protectsCityDecoration(pos)) return;
if (generator.protectsDecoration(pos) && !region.getBlockState(pos).equals(state)) { if (generator.protectsDecoration(pos) && !region.getBlockState(pos).equals(state)) {
@@ -50,6 +50,10 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes) Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 20))); ).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 20)));
public static final MapCodec<SanctuaryBiomeSource> NETWORK21_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 21)));
private final List<Holder<Biome>> allBiomes; private final List<Holder<Biome>> allBiomes;
private final PopulationIslandBiomeSource population; private final PopulationIslandBiomeSource population;
private final boolean cities; private final boolean cities;
@@ -83,7 +87,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
cityPlans = java.util.Map.copyOf(plans); cityPlans = java.util.Map.copyOf(plans);
} }
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; } @Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : 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) { @Override public BiomeResolver createResolver(Climate.Sampler sampler) {
BiomeResolver woodlands = population.createResolver(sampler); BiomeResolver woodlands = population.createResolver(sampler);
@@ -57,11 +57,25 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures) Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 20, experimental))); ).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 20, experimental)));
public static final MapCodec<SanctuaryChunkGenerator> NETWORK21_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
SanctuaryBiomeSource.NETWORK21_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, 21, experimental)));
private final boolean experimentalStructures; private final boolean experimentalStructures;
private volatile fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17 = fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.EMPTY; private volatile fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17 = fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.EMPTY;
private boolean ruinsPrepared; private boolean ruinsPrepared;
private volatile fr.koka.sanctuary.worldgen.heritage.HeritagePlan19 heritagePlan19 = fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.EMPTY; private volatile fr.koka.sanctuary.worldgen.heritage.HeritagePlan19 heritagePlan19 = fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.EMPTY;
private boolean heritagePrepared; private boolean heritagePrepared;
private final java.util.Map<ExpansionIsland, fr.koka.sanctuary.worldgen.city.LostCityPlanner21.Result> cityPlacements21 = new java.util.concurrent.ConcurrentHashMap<>();
private volatile fr.koka.sanctuary.worldgen.transit.TransitPlan21 transitPlan21 = fr.koka.sanctuary.worldgen.transit.TransitPlan21.EMPTY;
private boolean transitPrepared;
public fr.koka.sanctuary.worldgen.transit.TransitPlan21 transitPlan21() { return transitPlan21; }
public fr.koka.sanctuary.worldgen.city.LostCityPlanner21.Result cityPlacement21(ExpansionIsland island) {
lostCityPlan(island);
return cityPlacements21.get(cityIdentity(island));
}
private volatile Context originContext; private volatile Context originContext;
private final int generation; private final int generation;
private net.minecraft.world.level.LevelHeightAccessor heightAccessor; private net.minecraft.world.level.LevelHeightAccessor heightAccessor;
@@ -97,12 +111,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
} }
private SanctuaryChunkGenerator(SanctuaryBiomeSource source, int initialDiameter, int generation, boolean experimental) { private SanctuaryChunkGenerator(SanctuaryBiomeSource source, int initialDiameter, int generation, boolean experimental) {
super(source, initialDiameter, true); super(source, initialDiameter, true);
this.experimentalStructures = (generation == 17 || generation == 19 || generation == 20) && experimental; this.experimentalStructures = (generation == 17 || generation == 19 || generation == 20 || generation == 21) && experimental;
this.generation = generation; this.generation = generation;
if (generation != source.generation()) throw new IllegalArgumentException("Sanctuary generator and biome source versions must agree"); if (generation != source.generation()) throw new IllegalArgumentException("Sanctuary generator and biome source versions must agree");
} }
@Override public SanctuaryBiomeSource source() { return (SanctuaryBiomeSource) super.source(); } @Override public SanctuaryBiomeSource source() { return (SanctuaryBiomeSource) super.source(); }
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; } @Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : 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) { public SanctuaryChunkGenerator withInitialDiameter(int diameter) {
return new SanctuaryChunkGenerator(source().copy(), diameter, generation, experimentalStructures); return new SanctuaryChunkGenerator(source().copy(), diameter, generation, experimentalStructures);
} }
@@ -113,8 +127,9 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
public fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17() { return ruinsPlan17; } public fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17() { return ruinsPlan17; }
public fr.koka.sanctuary.worldgen.heritage.HeritagePlan19 heritagePlan19() { return heritagePlan19; } 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) { public boolean intersectsExperimentalStructures(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
return (generation == 17 || generation == 19 || generation == 20) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ) return (generation == 17 || generation == 19 || generation == 20 || generation == 21) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 19 || generation == 20) && heritagePlan19.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ); || (generation == 19 || generation == 20 || generation == 21) && heritagePlan19.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| generation == 21 && transitPlan21.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ);
} }
public IslandCapacity capacity() { public IslandCapacity capacity() {
return switch (initialDiameter()) { return switch (initialDiameter()) {
@@ -151,12 +166,14 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
prepareCities(islands); prepareCities(islands);
prepareRuins(islands); prepareRuins(islands);
prepareHeritage(islands); prepareHeritage(islands);
prepareTransit(islands);
} }
@Override public void setIslands(List<ExpansionIsland> islands) { @Override public void setIslands(List<ExpansionIsland> islands) {
// Complete and publish city geometry before workers can observe the new owner list. // Complete and publish city geometry before workers can observe the new owner list.
prepareCities(islands); prepareCities(islands);
prepareRuins(islands); prepareRuins(islands);
prepareHeritage(islands); prepareHeritage(islands);
prepareTransit(islands);
super.setIslands(islands); super.setIslands(islands);
} }
private void prepareCities(List<ExpansionIsland> islands) { private void prepareCities(List<ExpansionIsland> islands) {
@@ -167,7 +184,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
source().setCityPlans(java.util.Map.of()); source().setCityPlans(java.util.Map.of());
return; return;
} }
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20) && !cityStructuresEnabled) { if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21) && !cityStructuresEnabled) {
cityProtection16 = fr.koka.sanctuary.worldgen.city.LostCityProtection16.EMPTY; cityProtection16 = fr.koka.sanctuary.worldgen.city.LostCityProtection16.EMPTY;
activeCityPlans = java.util.Map.of(); activeCityPlans = java.util.Map.of();
source().setCityPlans(java.util.Map.of()); source().setCityPlans(java.util.Map.of());
@@ -183,7 +200,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
} }
cityProtection15 = protection.build(); cityProtection15 = protection.build();
} }
if (generation == 16 || generation == 17 || generation == 19 || generation == 20) { if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21) {
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder(); var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
for (var island : islands) { for (var island : islands) {
protection.add(published.get(island.id()), undergroundPlan16(island)); protection.add(published.get(island.id()), undergroundPlan16(island));
@@ -198,7 +215,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
source().setCityPlans(published); source().setCityPlans(published);
} }
private void prepareRuins(List<ExpansionIsland> islands) { private void prepareRuins(List<ExpansionIsland> islands) {
if ((generation != 17 && generation != 19 && generation != 20) || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return; if ((generation != 17 && generation != 19 && generation != 20 && generation != 21) || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow(); var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var terrain = context(root); var terrain = context(root);
var sampled = new fr.koka.sanctuary.worldgen.ruins.RuinsTerrain17(root, terrain.generator(), terrain.random(), var sampled = new fr.koka.sanctuary.worldgen.ruins.RuinsTerrain17(root, terrain.generator(), terrain.random(),
@@ -211,9 +228,23 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
root.seed(), plan.networks().size(), plan.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000); root.seed(), plan.networks().size(), plan.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000);
} }
private void prepareHeritage(List<ExpansionIsland> islands) { private void prepareHeritage(List<ExpansionIsland> islands) {
if ((generation != 19 && generation != 20) || !experimentalStructures || !cityStructuresEnabled || heritagePrepared) return; if ((generation != 19 && generation != 20 && generation != 21) || !experimentalStructures || !cityStructuresEnabled || heritagePrepared) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow(); var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var terrain = context(root); var terrain = context(root);
if (generation == 21) {
var sampled = new fr.koka.sanctuary.worldgen.heritage.HeritageTerrain21(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.HeritagePlanner21.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
heritagePlan19 = plan;
heritagePrepared = true;
SanctuaryMod.LOGGER.info("Heritage21 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());
return;
}
var sampled = new fr.koka.sanctuary.worldgen.heritage.HeritageTerrain19(root, terrain.generator(), terrain.random(), 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()) heightAccessor, pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ())); || ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()));
@@ -226,6 +257,19 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
root.seed(), plan.sites().size(), plan.links().size(), plan.segments().size(), plan.samples(), sampled.columnsUsed(), root.seed(), plan.sites().size(), plan.links().size(), plan.segments().size(), plan.samples(), sampled.columnsUsed(),
(System.nanoTime() - start) / 1_000_000, plan.diagnostics()); (System.nanoTime() - start) / 1_000_000, plan.diagnostics());
} }
private void prepareTransit(List<ExpansionIsland> islands) {
if (generation != 21 || !experimentalStructures || !cityStructuresEnabled || transitPrepared) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var terrain = context(root);
var sampled = new fr.koka.sanctuary.worldgen.transit.TransitTerrain21(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()) || heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ()));
long start = System.nanoTime();
transitPlan21 = fr.koka.sanctuary.worldgen.transit.TransitPlanner21.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
transitPrepared = true;
SanctuaryMod.LOGGER.info("Transit21 seed {}: {} modules, {} probes, {} columns in {} ms; {}", root.seed(),
transitPlan21.modules().size(), transitPlan21.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000, transitPlan21.diagnostic());
}
private static ExpansionIsland cityIdentity(ExpansionIsland island) { private static ExpansionIsland cityIdentity(ExpansionIsland island) {
// State and receipt do not change the terrain geometry used by a cached plan. // State and receipt do not change the terrain geometry used by a cached plan.
var ready = island.status().equals("ready") ? island : island.ready(); var ready = island.status().equals("ready") ? island : island.ready();
@@ -236,6 +280,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
var key = cityIdentity(island); var key = cityIdentity(island);
if (source().islands().stream().noneMatch(saved -> cityIdentity(saved).equals(key))) { if (source().islands().stream().noneMatch(saved -> cityIdentity(saved).equals(key))) {
cityPlans.remove(key); cityPlans.remove(key);
cityPlacements21.remove(key);
undergroundPlans.remove(key); undergroundPlans.remove(key);
undergroundPlans16.remove(key); undergroundPlans16.remove(key);
citySupports.remove(key); citySupports.remove(key);
@@ -251,6 +296,39 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
return cityPlans.computeIfAbsent(cityIdentity(island), ignored -> { return cityPlans.computeIfAbsent(cityIdentity(island), ignored -> {
long start = System.nanoTime(); long start = System.nanoTime();
var terrain = context(island); var terrain = context(island);
if (generation == 21) {
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain21(island, terrain.generator(), terrain.random(),
heightAccessor, pos -> island.origin() && protectsDecoration(pos));
var placement = fr.koka.sanctuary.worldgen.city.LostCityPlanner21.plan(island.seed(), island.centerX(), island.centerZ(),
island.diameter(), island.origin(), sampled);
var surface = placement.surface();
cityPlacements21.put(cityIdentity(island), placement);
citySupports16.put(cityIdentity(island), placement.supports());
if (placement.reservation() != null) cityReservations16.put(cityIdentity(island), placement.reservation());
var supports = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
for (var support : placement.supports()) supports.addViaductSupport(support);
if (placement.reservation() != null) supports.addReservation(placement.reservation());
var extraDistricts = surface.districts().stream()
.filter(d -> !placement.core().surface().districts().contains(d)).toList();
supports.add(new fr.koka.sanctuary.worldgen.city.LostCityPlan(extraDistricts),
new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0));
var supportMask = supports.build();
var underground = fr.koka.sanctuary.worldgen.city.LostCityUnderground16.plan(island.seed(), placement.core().surface(),
new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Terrain() {
@Override public boolean solid(int x, int y, int z) { return sampled.solid(x, y, z); }
@Override public boolean owns(int x, int z) { return sampled.owns(x, z); }
@Override public boolean exteriorAir(int x, int y, int z, int dx, int dz) { return sampled.exteriorAir(x, y, z, dx, dz); }
@Override public boolean protectedAt(int x, int y, int z) {
return supportMask.contains(x, y, z) || sampled.protectedAt(x, y, z);
}
});
undergroundPlans16.put(cityIdentity(island), underground);
SanctuaryMod.LOGGER.info("Lost City21 {} seed {}: {} city districts, {} underground networks, {} noise columns in {} ms",
island.id(), island.seed(), surface.districts().size(), underground.networks().size(), sampled.columnsUsed(),
(System.nanoTime() - start) / 1_000_000);
SanctuaryMod.LOGGER.info("City21 extensions: {}", placement.diagnostics());
return surface;
}
if (generation == 16 || generation == 17 || generation == 19 || generation == 20) { if (generation == 16 || generation == 17 || generation == 19 || generation == 20) {
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain16(island, terrain.generator(), terrain.random(), var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain16(island, terrain.generator(), terrain.random(),
heightAccessor, pos -> island.origin() && protectsDecoration(pos)); heightAccessor, pos -> island.origin() && protectsDecoration(pos));
@@ -322,9 +400,10 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
}); });
} }
public boolean protectsCityDecoration(net.minecraft.core.BlockPos pos) { public boolean protectsCityDecoration(net.minecraft.core.BlockPos pos) {
if (generation == 16 || generation == 17 || generation == 19 || generation == 20) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()) if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 17 || generation == 19 || generation == 20) && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()) || (generation == 17 || generation == 19 || generation == 20 || generation == 21) && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 19 || generation == 20) && heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ()); || (generation == 19 || generation == 20 || generation == 21) && heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ())
|| generation == 21 && transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ());
if (generation == 15) return cityProtection15.contains(pos.getX(), pos.getY(), pos.getZ()); if (generation == 15) return cityProtection15.contains(pos.getX(), pos.getY(), pos.getZ());
if (generation != 14) return false; if (generation != 14) return false;
var island = islandAt(pos.getX(), pos.getZ()); var island = islandAt(pos.getX(), pos.getZ());
@@ -346,20 +425,20 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
return citySupports.getOrDefault(cityIdentity(island), List.of()); return citySupports.getOrDefault(cityIdentity(island), List.of());
} }
public boolean intersectsCityConstruction16(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) { public boolean intersectsCityConstruction16(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
return (generation == 16 || generation == 17 || generation == 19 || generation == 20) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ); return (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
} }
public fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan undergroundPlan16(ExpansionIsland island) { public fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan undergroundPlan16(ExpansionIsland island) {
if (generation != 16 && generation != 17 && generation != 19 && generation != 20) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0); if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
lostCityPlan(island); lostCityPlan(island);
return undergroundPlans16.getOrDefault(cityIdentity(island), new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0)); return undergroundPlans16.getOrDefault(cityIdentity(island), new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0));
} }
public List<LostCityPlanner16.Support> citySupports16(ExpansionIsland island) { public List<LostCityPlanner16.Support> citySupports16(ExpansionIsland island) {
if (generation != 16 && generation != 17 && generation != 19 && generation != 20) return List.of(); if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21) return List.of();
lostCityPlan(island); lostCityPlan(island);
return citySupports16.getOrDefault(cityIdentity(island), List.of()); return citySupports16.getOrDefault(cityIdentity(island), List.of());
} }
public LostCityPlanner16.Fallback cityReservation16(ExpansionIsland island) { public LostCityPlanner16.Fallback cityReservation16(ExpansionIsland island) {
if (generation != 16 && generation != 17 && generation != 19 && generation != 20) return null; if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21) return null;
lostCityPlan(island); lostCityPlan(island);
return cityReservations16.get(cityIdentity(island)); return cityReservations16.get(cityIdentity(island));
} }
@@ -371,10 +450,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
net.minecraft.core.BlockPos origin, int radius, boolean skipReferenced) { net.minecraft.core.BlockPos origin, int radius, boolean skipReferenced) {
if (structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.city.LostCityStructures.KEY)) 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); return fr.koka.sanctuary.worldgen.city.LostCityStructures.findNearest(level, structures, origin, radius, skipReferenced);
if ((generation == 17 || generation == 19 || generation == 20) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.KEY)) if ((generation == 17 || generation == 19 || generation == 20 || generation == 21) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.KEY))
return fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.findNearest(level, structures, origin, radius, skipReferenced); return fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.findNearest(level, structures, origin, radius, skipReferenced);
if ((generation == 19 || generation == 20) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.KEY)) if ((generation == 19 || generation == 20 || generation == 21) && 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 fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.findNearest(level, structures, origin, radius, skipReferenced);
if (generation == 21 && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.transit.TransitStructures21.KEY))
return fr.koka.sanctuary.worldgen.transit.TransitStructures21.findNearest(level, structures, origin, radius, skipReferenced);
return super.findNearestMapStructure(level, structures, origin, radius, skipReferenced); return super.findNearestMapStructure(level, structures, origin, radius, skipReferenced);
} }
@Override protected Context context(ExpansionIsland island) { @Override protected Context context(ExpansionIsland island) {
@@ -402,7 +483,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
@Override protected void afterTerrain(ExpansionIsland island, ChunkAccess chunk) { @Override protected void afterTerrain(ExpansionIsland island, ChunkAccess chunk) {
// Root water/lava are already applied by the Population hook on the named delegate. // Root water/lava are already applied by the Population hook on the named delegate.
if (!island.origin()) super.afterTerrain(island, chunk); if (!island.origin()) super.afterTerrain(island, chunk);
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20) && cityStructuresEnabled) { if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21) && cityStructuresEnabled) {
var reserved = cityReservations16.get(cityIdentity(island)); var reserved = cityReservations16.get(cityIdentity(island));
if (reserved != null) { if (reserved != null) {
var mutable = new net.minecraft.core.BlockPos.MutableBlockPos(); var mutable = new net.minecraft.core.BlockPos.MutableBlockPos();
@@ -0,0 +1,767 @@
package fr.koka.sanctuary.worldgen.city;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Kind;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Parcel;
import java.util.ArrayList;
import fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
import java.util.List;
import net.minecraft.core.Direction;
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.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.SlabType;
import net.minecraft.world.level.storage.loot.BuiltInLootTables;
import net.minecraft.world.level.storage.loot.LootTable;
/** Complete generation-21 buildings. Published renderers deliberately never call this renderer.
* A north-facing plan is rotated once; all positions remain relative to the saved parcel. */
public final class LostCityInteriors21 {
private static final BlockState AIR = Blocks.AIR.defaultBlockState();
private static final BlockState STONE = Blocks.STONE.defaultBlockState();
private static final BlockState SMOOTH = Blocks.SMOOTH_STONE.defaultBlockState();
private static final BlockState WOOD = Blocks.OAK_PLANKS.defaultBlockState();
private static final BlockState TILE = Blocks.POLISHED_ANDESITE.defaultBlockState();
private static final int STOREY = 5;
private LostCityInteriors21() {}
/** Reachable standing positions, in the same rotated local coordinates as BlockWriter. */
public record Room(String name, int x, int y, int z) {}
/** Local, already rotated enclosure including the floor, full ceiling, four walls and door. */
public record Bathroom(String name, int minX, int minY, int minZ, int maxX, int maxY, int maxZ,
int doorX, int doorY, int doorZ) {}
private record Area(int left, int front, int right, int back) {}
/** Adjacent storeys always change plan; the saved parcel seed chooses which plan comes first. */
public static int layout(Parcel parcel, int floor) {
validate(parcel);
if (floor < 0 || floor >= parcel.floors()) throw new IllegalArgumentException("Invalid interior floor");
return (int) ((parcel.seed() ^ (parcel.seed() >>> 32)) & 1L) ^ (floor & 1);
}
private static Area rear(Parcel p, int floor, boolean east) {
boolean wide = layout(p, floor) == 1;
return east ? new Area(wide ? 10 : 11, wide ? 10 : 11, 14, 14)
: new Area(2, wide ? 10 : 11, wide ? 6 : 5, 14);
}
private static int frontWall(Parcel p, int floor) { return layout(p, floor) == 0 ? 7 : 8; }
private static String frontUse(Parcel p, int floor) {
int variant = layout(p, floor);
return switch (p.kind()) {
case APARTMENT -> variant == 0 ? "kitchen_dining" : "studio_bedroom";
case HOTEL -> floor == 0 ? "reception" : "guest_suite";
case LIBRARY -> variant == 0 ? "book_stacks" : "periodicals_reading";
case RESTAURANT -> variant == 0 ? "dining_room" : "kitchen";
case BARRACKS -> variant == 0 ? "dormitory" : "duty_office";
case HALL -> floor == 0 ? "ticket_office" : "administration";
default -> throw new IllegalArgumentException("Not an interior kind");
};
}
private static String rearUse(Parcel p, int floor, boolean east) {
int variant = layout(p, floor);
return switch (p.kind()) {
case APARTMENT -> east ? "bathroom" : variant == 0 ? "bedroom" : "kitchen_dining";
case HOTEL -> floor == 0 ? east ? "laundry" : "lounge"
: variant == 0 ? east ? "guest_room" : "bedroom" : east ? "bathroom" : "guest_lounge";
case LIBRARY -> variant == 0 ? east ? "archive" : "reading_room" : east ? "bathroom" : "catalogue_office";
case RESTAURANT -> variant == 0 ? east ? "kitchen" : "washrooms" : east ? "pantry" : "dining_room";
case BARRACKS -> east ? "equipment_store" : variant == 0 ? "washrooms" : "officer_bedroom";
case HALL -> variant == 0 ? east ? "bathroom" : "meeting_room" : east ? "meeting_room" : "archive";
default -> throw new IllegalArgumentException("Not an interior kind");
};
}
public static void render(Parcel parcel, LostCityRenderer.BlockWriter output) {
validate(parcel);
var connected = ConnectedBlocks20.buffer(output);
new Building(parcel, connected).render();
connected.flush();
}
/** These describe actual rooms and services, not decorative labels placed in the world. */
public static List<Room> rooms(Parcel parcel) {
validate(parcel);
List<Room> result = new ArrayList<>();
add(result, parcel, "street_entrance", 8, 1, 0);
add(result, parcel, "vestibule", 8, 1, 3);
for (int floor = 0; floor < parcel.floors(); floor++) {
int y = floor * STOREY + 1;
String prefix = "floor_" + floor + "/";
add(result, parcel, prefix + "corridor", 8, y, 8);
add(result, parcel, prefix + "stair_landing", 4, y, 8);
add(result, parcel, prefix + frontUse(parcel, floor), 11, y, 6);
if (parcel.kind() == Kind.HALL && floor < 2) {
if (floor == 0) {
add(result, parcel, prefix + "auditorium", 8, y, 11);
add(result, parcel, prefix + "stage", 9, y + 1, 13);
} else add(result, parcel, prefix + "gallery", 8, y, 10);
} else {
Area west = rear(parcel, floor, false), east = rear(parcel, floor, true);
add(result, parcel, prefix + rearUse(parcel, floor, false), west.right(), y, 11);
add(result, parcel, prefix + rearUse(parcel, floor, true), east.left(), y, 11);
}
}
for (var bath : bathroomPlans(parcel)) {
int targetZ = bath.southDoor() ? bath.z1() - 1 : bath.z0() + 1;
add(result, parcel, bath.name(), bath.doorX(), bath.floor() * STOREY + 1, targetZ);
}
add(result, parcel, "roof", 8, parcel.floors() * STOREY + 1, 8);
switch (parcel.kind()) {
case HOTEL -> add(result, parcel, "roof_laundry_pavilion", 10, parcel.floors() * STOREY + 1, 5);
case APARTMENT -> add(result, parcel, "roof_residents_terrace", 8, parcel.floors() * STOREY + 1, 10);
case LIBRARY -> add(result, parcel, "roof_reading_terrace", 11, parcel.floors() * STOREY + 1, 10);
case RESTAURANT -> add(result, parcel, "roof_dining_terrace", 11, parcel.floors() * STOREY + 1, 8);
case BARRACKS -> add(result, parcel, "roof_service_stair", 4, parcel.floors() * STOREY + 1, 8);
default -> { }
}
return List.copyOf(result);
}
private record BathPlan(String name, int floor, int x0, int z0, int x1, int z1, int doorX, boolean southDoor) {}
private static List<BathPlan> bathroomPlans(Parcel p) {
List<BathPlan> result = new ArrayList<>();
for (int floor = 0; floor < p.floors(); floor++) {
if (p.kind() == Kind.HOTEL && floor > 0)
result.add(new BathPlan("floor_" + floor + "/suite_bathroom", floor, 10, 1, 15, 4, 12, true));
if (p.kind() == Kind.HALL && floor < 2) continue;
for (boolean east : new boolean[]{false, true}) {
String use = rearUse(p, floor, east);
if (!use.equals("bathroom") && !use.equals("washrooms")) continue;
Area room = rear(p, floor, east);
result.add(new BathPlan("floor_" + floor + "/" + (east ? "east" : "west") + "_bathroom_interior",
floor, room.left() - 1, 12, room.right() + 1, 15, east ? 12 : 4, false));
}
}
return List.copyOf(result);
}
public static List<Bathroom> bathrooms(Parcel p) {
validate(p);
List<Bathroom> result = new ArrayList<>();
for (var b : bathroomPlans(p)) {
int x0 = rx(b.x0(), b.z0(), p.facing()), z0 = rz(b.x0(), b.z0(), p.facing());
int x1 = rx(b.x1(), b.z1(), p.facing()), z1 = rz(b.x1(), b.z1(), p.facing());
int doorZ = b.southDoor() ? b.z1() : b.z0();
result.add(new Bathroom(b.name(), Math.min(x0, x1), b.floor() * STOREY, Math.min(z0, z1),
Math.max(x0, x1), b.floor() * STOREY + 4, Math.max(z0, z1), rx(b.doorX(), doorZ, p.facing()),
b.floor() * STOREY + 1, rz(b.doorX(), doorZ, p.facing())));
}
return List.copyOf(result);
}
private static void validate(Parcel p) {
if (!p.kind().building() || p.width() != 17 || p.depth() != 17 || p.floors() < 2)
throw new IllegalArgumentException("Generation-21 interiors require a 17x17 building with 2..5 floors");
}
private static void add(List<Room> result, Parcel p, String name, int x, int y, int z) {
result.add(new Room(name, rx(x, z, p.facing()), y, rz(x, z, p.facing())));
}
private static int rx(int x, int z, int facing) {
return switch (facing) { case 0 -> x; case 1 -> 16 - z; case 2 -> 16 - x; default -> z; };
}
private static int rz(int x, int z, int facing) {
return switch (facing) { case 0 -> z; case 1 -> x; case 2 -> 16 - z; default -> 16 - x; };
}
private static final class Building {
final Parcel p;
final LostCityRenderer.BlockWriter output;
final Rotation rotation;
Building(Parcel p, LostCityRenderer.BlockWriter output) {
this.p = p;
this.output = output;
rotation = switch (p.facing()) {
case 0 -> Rotation.NONE; case 1 -> Rotation.CLOCKWISE_90;
case 2 -> Rotation.CLOCKWISE_180; default -> Rotation.COUNTERCLOCKWISE_90;
};
}
void set(int x, int y, int z, BlockState state) {
output.set(rx(x, z, p.facing()), y, rz(x, z, p.facing()), state.rotate(rotation));
}
void set(int x, int y, int z, Block block) { set(x, y, z, block.defaultBlockState()); }
void box(int x0, int y0, int z0, int x1, int y1, int z1, BlockState state) {
for (int x = x0; x <= x1; x++) for (int z = z0; z <= z1; z++)
for (int y = y0; y <= y1; y++) set(x, y, z, state);
}
BlockState facing(Block block, Direction direction) {
return block.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, direction);
}
BlockState brick(int x, int y, int z) { return LostCityPalette.masonry(p.seed(), x, y, z); }
BlockState weatheredBrick(int x, int y, int z) {
// Weathering follows a few masonry joints, never private room partitions.
int weather = roll(x, y, z, 12);
return (weather == 0 ? Blocks.MOSSY_STONE_BRICKS : weather < 3
? Blocks.CRACKED_STONE_BRICKS : Blocks.STONE_BRICKS).defaultBlockState();
}
int roll(int x, int y, int z, int bound) { return LostCityPalette.roll(p.seed(), x, y, z, bound); }
void render() {
int roof = p.floors() * STOREY;
box(0, -p.foundationDepth(), 0, 16, -1, 16, Blocks.STONE_BRICKS.defaultBlockState());
box(0, 0, 0, 16, 0, 16, SMOOTH);
box(0, 1, 0, 16, roof + 4, 16, AIR);
shell(roof);
architecturalEnvelope(roof);
for (int floor = 0; floor < p.floors(); floor++) {
partitions(floor);
furnishings(floor);
}
stairs();
entrance();
roof(roof);
// Only one deferred loot table per building; empty furniture is not a resource vault.
int lootX = 6;
int lootZ = 6;
box(lootX, 2, lootZ, lootX, 3, lootZ, AIR);
ResourceKey<LootTable> table = switch (p.kind()) {
case LIBRARY, HALL -> BuiltInLootTables.VILLAGE_CARTOGRAPHER;
case RESTAURANT -> BuiltInLootTables.VILLAGE_BUTCHER;
case BARRACKS -> BuiltInLootTables.VILLAGE_FLETCHER;
default -> BuiltInLootTables.VILLAGE_PLAINS_HOUSE;
};
output.chest(rx(lootX, lootZ, p.facing()), 1, rz(lootX, lootZ, p.facing()), table,
p.seed() ^ 0x21c17a7eL ^ p.kind().ordinal());
}
void shell(int roof) {
for (int floor = 0; floor <= p.floors(); floor++) {
int base = floor * STOREY;
for (int x = 1; x <= 15; x++) for (int z = 1; z <= 15; z++)
set(x, base, z, floor == p.floors() ? SMOOTH : floorMaterial(floor, x, z));
if (floor == p.floors()) continue;
// A pale ceiling and contrasting structural floor are separate layers.
box(2, base + 4, 2, 14, base + 4, 14,
Blocks.SMOOTH_STONE_SLAB.defaultBlockState().setValue(BlockStateProperties.SLAB_TYPE, SlabType.TOP));
for (int y = 1; y <= 4; y++) for (int horizontal = 1; horizontal <= 15; horizontal++) {
facade(horizontal, base + y, 1, horizontal, y, true);
facade(horizontal, base + y, 15, horizontal, y, true);
facade(1, base + y, horizontal, horizontal, y, false);
facade(15, base + y, horizontal, horizontal, y, false);
}
// Floor bands remain intact even where windows have fallen out.
for (int n = 1; n <= 15; n++) {
set(n, base, 1, SMOOTH); set(n, base, 15, SMOOTH);
set(1, base, n, SMOOTH); set(15, base, n, SMOOTH);
}
if (p.kind() == Kind.HOTEL || p.kind() == Kind.LIBRARY) {
for (int x : new int[]{3, 13}) for (int y = 0; y <= 4; y++) set(x, base + y, 0, SMOOTH);
}
if (p.kind() == Kind.APARTMENT) for (int x = 11; x <= 14; x++)
set(x, base + 1, 0, Blocks.SMOOTH_STONE_SLAB);
if (p.kind() == Kind.RESTAURANT) box(10, base + 4, 0, 14, base + 4, 0,
Blocks.OAK_SLAB.defaultBlockState());
}
if (p.kind() == Kind.HALL) {
// The auditorium spans two storeys; its upper level is a guarded gallery.
box(3, 4, 11, 13, 5, 14, AIR);
for (int x = 3; x <= 13; x++) set(x, 6, 11, rail(true));
for (int z = 12; z <= 14; z++) { set(3, 6, z, rail(false)); set(13, 6, z, rail(false)); }
}
}
BlockState floorMaterial(int floor, int x, int z) {
if (x == 1 || z == 1 || x == 15 || z == 15) return SMOOTH;
return switch (p.kind()) {
case LIBRARY -> (x <= 5 || z >= 10 ? Blocks.OAK_PLANKS : Blocks.SMOOTH_STONE).defaultBlockState();
case APARTMENT, HOTEL -> WOOD;
case RESTAURANT -> ((x / 3 + z / 3) & 1) == 0 ? SMOOTH : TILE;
case BARRACKS -> x <= 5 ? TILE : WOOD;
default -> x == 7 || x == 9 ? TILE : SMOOTH;
};
}
BlockState pier() {
return switch (p.kind()) {
case APARTMENT, LIBRARY -> Blocks.STRIPPED_OAK_LOG.defaultBlockState();
case HOTEL, RESTAURANT -> SMOOTH;
case BARRACKS -> Blocks.STONE_BRICKS.defaultBlockState();
default -> Blocks.CHISELED_STONE_BRICKS.defaultBlockState();
};
}
void facade(int x, int y, int z, int horizontal, int withinFloor, boolean eastWest) {
boolean column = horizontal == 1 || horizontal == 6 || horizontal == 10 || horizontal == 15;
boolean window = !column && (withinFloor == 2 || withinFloor == 3
|| withinFloor == 1 && (p.kind() == Kind.LIBRARY || p.kind() == Kind.RESTAURANT || p.kind() == Kind.HALL));
if (column) set(x, y, z, pier());
else if (window) {
if (roll(x, y / 5, z, 17) != 0) set(x, y, z,
p.kind() == Kind.BARRACKS ? rail(eastWest) : pane(eastWest));
} else set(x, y, z, withinFloor == 4 && horizontal % 5 == 0
? weatheredBrick(x, y, z) : SMOOTH);
}
/** Volume belongs to its use: projected bays, clipped corners and distinct roof profiles. */
void architecturalEnvelope(int roof) {
for (int floor = 0; floor < p.floors(); floor++) {
int y = floor * STOREY;
// Project the floor plates as a continuous, one-block cornice inside the parcel.
for (int n = 1; n <= 15; n++) {
set(n, y, 0, stairTrim(Direction.NORTH)); set(n, y, 16, stairTrim(Direction.SOUTH));
set(0, y, n, stairTrim(Direction.WEST)); set(16, y, n, stairTrim(Direction.EAST));
}
switch (p.kind()) {
case APARTMENT -> {
// Bay windows increase the living room rather than drawing flat stripes on it.
for (int z : new int[]{4, 5, 6, 11, 12, 13}) {
set(16, y, z, Blocks.OAK_PLANKS);
set(16, y + 1, z, Blocks.OAK_PLANKS);
set(16, y + 2, z, Blocks.GLASS_PANE);
set(16, y + 3, z, Blocks.GLASS_PANE);
set(16, y + 4, z, Blocks.OAK_SLAB.defaultBlockState().setValue(BlockStateProperties.SLAB_TYPE, SlabType.TOP));
}
for (int z : new int[]{3, 7, 10, 14}) box(16, y + 1, z, 16, y + 4, z, pier());
// Original wall remains the sill, with an open bay above it; privacy walls restore themselves later.
for (int z : new int[]{4, 5, 6, 11, 12, 13}) box(15, y + 2, z, 15, y + 3, z, AIR);
bevelFront(y);
}
case HOTEL -> {
for (int x : new int[]{2, 5, 11, 14}) box(x, y + 1, 0, x, y + 4, 0, pier());
for (int x : new int[]{3, 4, 12, 13}) set(x, y + 1, 0, Blocks.OAK_FENCE);
box(2, y + 4, 0, 5, y + 4, 0, Blocks.OAK_PLANKS.defaultBlockState());
box(11, y + 4, 0, 14, y + 4, 0, Blocks.OAK_PLANKS.defaultBlockState());
}
case LIBRARY -> {
// Tall fins frame broad glazing and project beyond the reading rooms.
for (int x : new int[]{2, 5, 11, 14}) box(x, y + 1, 0, x, y + 4, 0, Blocks.STRIPPED_OAK_LOG.defaultBlockState());
for (int x : new int[]{3, 4, 12, 13}) {
set(x, y + 4, 0, Blocks.OAK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
}
bevelFront(y);
}
case RESTAURANT -> {
// Separate fascia and timber valance shelter the shop windows.
for (int x = 10; x <= 14; x++) {
set(x, y + 3, 0, Blocks.OAK_SLAB);
set(x, y + 4, 0, Blocks.OAK_PLANKS);
}
for (int z : new int[]{3, 7, 11, 14}) set(16, y + 4, z, Blocks.ANDESITE);
}
case BARRACKS -> {
for (int z : new int[]{2, 7, 12, 15}) box(0, y + 1, z, 0, y + 4, z, pier());
for (int x : new int[]{2, 5, 11, 14}) box(x, y + 1, 0, x, y + 4, 0, Blocks.STONE_BRICKS.defaultBlockState());
for (int x : new int[]{3, 4, 12, 13}) set(x, y + 1, 0, Blocks.CHISELED_STONE_BRICKS);
}
case HALL -> {
for (int x : new int[]{2, 5, 11, 14}) box(x, y + 1, 0, x, y + 4, 0, pier());
for (int x = 2; x <= 14; x++) set(x, y + 4, 0,
Blocks.ANDESITE_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
}
default -> throw new IllegalStateException();
}
}
}
BlockState stairTrim(Direction outward) {
Block block = Blocks.STONE_BRICK_STAIRS;
return facing(block, outward.getOpposite()).setValue(BlockStateProperties.HALF,
net.minecraft.world.level.block.state.properties.Half.TOP);
}
void bevelFront(int y) {
// Remove two rectangular corners and close them with actual stepped returns.
for (int x : new int[]{1, 15}) {
box(x, y, 1, x, y + 4, 1, AIR);
int adjacent = x == 1 ? 2 : 14;
box(adjacent, y + 1, 1, adjacent, y + 4, 1, pier());
box(x, y + 1, 2, x, y + 4, 2, pier());
}
}
void partitions(int floor) {
int y = floor * STOREY;
int front = frontWall(p, floor);
// The west stairwell, central corridor and vestibule are independent of furniture.
box(5, y + 1, 2, 5, y + 4, 9, SMOOTH);
door(5, y + 1, 8, Direction.WEST, false);
box(10, y + 1, 2, 10, y + 4, front, SMOOTH);
box(10, y + 1, front, 15, y + 4, front, SMOOTH);
door(10, y + 1, 6, Direction.EAST, false);
if (floor == 0) {
box(6, 1, 4, 9, 4, 4, SMOOTH);
door(8, 1, 4, Direction.SOUTH, false);
}
if (p.kind() == Kind.HALL && floor < 2) {
if (floor == 0) {
box(2, 1, 10, 14, 4, 10, SMOOTH);
door(7, 1, 10, Direction.SOUTH, true);
door(8, 1, 10, Direction.SOUTH, false);
}
} else {
Area west = rear(p, floor, false), east = rear(p, floor, true);
box(1, y + 1, west.front() - 1, west.right() + 1, y + 4, west.front() - 1, SMOOTH);
box(west.right() + 1, y + 1, west.front() - 1, west.right() + 1, y + 4, 15, SMOOTH);
box(east.left() - 1, y + 1, east.front() - 1, 15, y + 4, east.front() - 1, SMOOTH);
box(east.left() - 1, y + 1, east.front() - 1, east.left() - 1, y + 4, 15, SMOOTH);
door(west.right() + 1, y + 1, 11, Direction.WEST, false);
door(east.left() - 1, y + 1, 11, Direction.EAST, false);
}
// A hanging lantern touches a full ceiling block, never the recessed underside of a top slab.
set(8, y + 4, 8, SMOOTH);
set(8, y + 3, 8, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
// Weathering stays on facades and roofs: it must not replace a privacy partition.
}
void furnishings(int floor) {
int y = floor * STOREY;
int variant = layout(p, floor);
switch (p.kind()) {
case APARTMENT -> {
if (variant == 0) { kitchenFront(y); bedroom(floor, false); }
else { frontBedroom(floor); kitchenRear(floor, false); }
}
case HOTEL -> {
if (floor == 0) { reception(y); lounge(floor, false); laundry(floor); }
else {
anchoredBed(14, y + 1, frontWall(p, floor) - 2, Direction.SOUTH);
set(11, y + 1, 5, Blocks.BARREL); set(11, y + 2, 5, Blocks.FLOWER_POT);
if (variant == 0) { bedroom(floor, false); bedroom(floor, true); }
else lounge(floor, false);
}
}
case LIBRARY -> {
if (variant == 0) {
box(14, y + 1, 2, 14, y + 3, 6, Blocks.BOOKSHELF.defaultBlockState());
box(12, y + 1, 2, 12, y + 2, 3, Blocks.BOOKSHELF.defaultBlockState());
desk(12, y, 5, Direction.SOUTH);
readingRoom(floor, false); archive(floor, true);
} else {
readingFront(y); readingRoom(floor, false);
set(2, y + 1, 14, Blocks.CARTOGRAPHY_TABLE);
}
}
case RESTAURANT -> {
if (variant == 0) {
table(13, y, 3); table(13, y, 5);
set(11, y + 1, 2, Blocks.POTTED_DEAD_BUSH);
kitchenRear(floor, true);
} else {
kitchenFront(y); diningRear(floor, false); pantry(floor, true);
}
}
case BARRACKS -> {
if (variant == 0) {
anchoredBed(12, y + 1, 3, Direction.NORTH); anchoredBed(14, y + 1, 3, Direction.NORTH);
set(11, y + 1, 2, Blocks.BARREL); set(11, y + 2, 2, Blocks.BARREL);
seat(13, y + 1, 6, Direction.NORTH);
} else { reception(y); bedroom(floor, false); }
box(12, y + 1, 14, 14, y + 2, 14, Blocks.BARREL.defaultBlockState());
set(14, y + 1, 11, Blocks.FLETCHING_TABLE);
set(12, y + 1, 11, Blocks.GRINDSTONE.defaultBlockState()
.setValue(BlockStateProperties.ATTACH_FACE,
net.minecraft.world.level.block.state.properties.AttachFace.FLOOR));
}
case HALL -> {
if (floor == 0 || floor > 1 && variant == 0) reception(y); else readingFront(y);
if (floor == 0) auditorium();
else if (floor >= 2) {
if (variant == 0) readingRoom(floor, false);
else { archive(floor, false); readingRoom(floor, true); }
}
}
default -> throw new IllegalArgumentException("Not an interior kind");
}
for (var bath : bathroomPlans(p)) if (bath.floor() == floor) bathroom(bath);
}
void kitchenFront(int y) {
kitchenLine(y, 11, 2, Direction.SOUTH);
table(13, y, 5);
}
void kitchenRear(int floor, boolean east) {
int y = floor * STOREY;
Area room = rear(p, floor, east);
kitchenLine(y, room.left(), 14, Direction.NORTH);
if (east) {
set(14, y + 1, 11, SMOOTH); set(14, y + 2, 11, Blocks.FLOWER_POT);
set(12, y + 1, 11, Blocks.CRAFTING_TABLE);
} else table(room.left() + 2, y, 11);
}
void kitchenLine(int y, int left, int z, Direction face) {
set(left, y + 1, z, facing(Blocks.FURNACE, face));
set(left + 1, y + 1, z, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 2));
set(left + 2, y + 1, z, facing(Blocks.SMOKER, face));
set(left + 3, y + 1, z, Blocks.BARREL);
box(left, y + 3, z, left + 3, y + 3, z, SMOOTH);
set(left + 2, y + 2, z, Blocks.IRON_TRAPDOOR);
for (int x = left; x <= left + 3; x++) set(x, y + 3, z, Blocks.SMOOTH_STONE);
for (int x = left; x <= left + 3; x++) set(x, y, z, (x & 1) == 0 ? SMOOTH : TILE);
}
void bedroom(int floor, boolean east) {
int y = floor * STOREY;
Area room = rear(p, floor, east);
anchoredBed(east ? room.right() - 1 : room.left() + 1, y + 1, 13, east ? Direction.EAST : Direction.WEST);
set(east ? room.right() : room.left(), y + 1, 11, Blocks.BARREL);
desk(east ? room.left() + 1 : room.right() - 1, y, 11, Direction.SOUTH);
set(east ? room.left() : room.right(), y + 1, 14, Blocks.BOOKSHELF);
}
void frontBedroom(int floor) {
int y = floor * STOREY;
anchoredBed(14, y + 1, 3, Direction.NORTH);
set(11, y + 1, 2, Blocks.BARREL); set(11, y + 2, 2, Blocks.FLOWER_POT);
desk(12, y, 5, Direction.SOUTH);
}
void bathroom(BathPlan bath) {
int y = bath.floor() * STOREY;
for (int x = bath.x0(); x <= bath.x1(); x++) for (int z = bath.z0(); z <= bath.z1(); z++) {
set(x, y, z, ((x + z) & 1) == 0 ? SMOOTH : TILE);
set(x, y + 4, z, SMOOTH);
boolean wall = x == bath.x0() || x == bath.x1() || z == bath.z0() || z == bath.z1();
for (int up = 1; up <= 3; up++) set(x, y + up, z, wall ? SMOOTH : AIR);
}
int doorZ = bath.southDoor() ? bath.z1() : bath.z0();
door(bath.doorX(), y + 1, doorZ, bath.southDoor() ? Direction.NORTH : Direction.SOUTH, false);
int fixtureZ = bath.southDoor() ? bath.z0() + 1 : bath.z1() - 1;
int toiletX = bath.southDoor() ? bath.x1() - 2 : bath.x0() + 1;
Direction view = bath.southDoor() ? Direction.SOUTH : Direction.EAST;
set(toiletX, y + 1, fixtureZ, seating(Blocks.POLISHED_ANDESITE_STAIRS, view));
set(toiletX, y + 2, fixtureZ, Blocks.IRON_TRAPDOOR);
set(bath.x1() - 1, y + 1, fixtureZ, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 2));
}
void reception(int y) {
box(12, y + 1, 5, 14, y + 1, 5, Blocks.OAK_PLANKS.defaultBlockState());
set(13, y + 2, 5, Blocks.FLOWER_POT);
set(14, y + 1, 2, Blocks.BOOKSHELF); set(14, y + 2, 2, Blocks.BARREL);
desk(12, y, 3, Direction.SOUTH);
set(11, y + 1, 2, Blocks.POTTED_DEAD_BUSH);
}
void laundry(int floor) {
int y = floor * STOREY;
for (int z = 11; z <= 12; z++) set(14, y + 1, z, Blocks.WATER_CAULDRON.defaultBlockState()
.setValue(BlockStateProperties.LEVEL_CAULDRON, z == 11 ? 1 : 2));
box(12, y + 1, 14, 14, y + 1, 14, Blocks.BARREL.defaultBlockState());
// The rack stands on the storage counter; recessed rear glazing cannot carry it.
for (int x : new int[]{12, 14}) set(x, y + 2, 14, Blocks.OAK_PLANKS);
box(12, y + 3, 14, 14, y + 3, 14, Blocks.OAK_SLAB.defaultBlockState());
set(12, y + 1, 11, Blocks.LOOM);
}
void lounge(int floor, boolean east) {
int y = floor * STOREY;
Area room = rear(p, floor, east);
box(room.left(), y + 1, 14, room.left() + 2, y + 1, 14, seating(Blocks.OAK_STAIRS, Direction.NORTH));
desk(room.left() + 1, y, 11, Direction.SOUTH);
set(room.left(), y + 1, 12, Blocks.BOOKSHELF);
}
void readingRoom(int floor, boolean east) {
int y = floor * STOREY;
Area room = rear(p, floor, east);
desk(east ? room.left() + 1 : room.right() - 1, y, 11, Direction.SOUTH);
box(room.left(), y + 1, 14, room.right(), y + 2, 14, Blocks.BOOKSHELF.defaultBlockState());
set(east ? room.right() : room.left(), y + 1, 11, facing(Blocks.LECTERN, Direction.NORTH));
}
void readingFront(int y) {
desk(12, y, 3, Direction.SOUTH);
box(14, y + 1, 2, 14, y + 2, 5, Blocks.BOOKSHELF.defaultBlockState());
set(11, y + 1, 2, facing(Blocks.LECTERN, Direction.SOUTH));
}
void archive(int floor, boolean east) {
int y = floor * STOREY;
Area room = rear(p, floor, east);
box(room.left(), y + 1, 14, room.right(), y + 3, 14, Blocks.BOOKSHELF.defaultBlockState());
box(east ? room.right() : room.left(), y + 1, 11, east ? room.right() : room.left(), y + 2, 12, Blocks.BARREL.defaultBlockState());
set(room.left() + 1, y + 1, 12, facing(Blocks.LECTERN, Direction.NORTH));
}
void pantry(int floor, boolean east) {
int y = floor * STOREY;
Area room = rear(p, floor, east);
box(room.left(), y + 1, 14, room.right(), y + 2, 14, Blocks.BARREL.defaultBlockState());
set(room.right(), y + 1, 11, Blocks.CRAFTING_TABLE);
set(room.right(), y + 2, 11, Blocks.FLOWER_POT);
}
void diningRear(int floor, boolean east) {
Area room = rear(p, floor, east);
int y = floor * STOREY;
table(room.left() + 2, y, 12);
box(room.left(), y + 1, 14, room.right(), y + 1, 14, seating(Blocks.OAK_STAIRS, Direction.NORTH));
}
void auditorium() {
for (int x : new int[]{3, 4, 5, 11, 12, 13}) seat(x, 1, 11, Direction.SOUTH);
box(3, 1, 13, 13, 1, 14, WOOD);
set(8, 1, 12, facing(Blocks.OAK_STAIRS, Direction.SOUTH));
set(8, 2, 14, facing(Blocks.LECTERN, Direction.NORTH));
for (int x : new int[]{3, 13}) {
box(x, 2, 14, x, 3, 14, WOOD);
set(x, 4, 14, Blocks.STONE_BRICK_SLAB);
}
}
void stairs() {
for (int floor = 0; floor < p.floors(); floor++) {
int base = floor * STOREY;
for (int step = 1; step <= STOREY; step++) {
int z = step + 2;
box(2, base + step, z, 3, base + step + 3, z, AIR);
for (int x = 2; x <= 3; x++) {
// A one-block inclined spine joins successive treads physically, without
// filling the headroom above the preceding storey's matching flight.
set(x, base + step - 1, z, Blocks.STONE_BRICKS);
set(x, base + step, z, facing(Blocks.STONE_BRICK_STAIRS, Direction.SOUTH));
}
}
box(2, base + STOREY, 8, 4, base + STOREY, 9, TILE);
box(2, base + STOREY + 1, 8, 4, base + STOREY + 3, 9, AIR);
}
// This full-height side aisle joins every lower stair approach to its landing door.
for (int floor = 0; floor <= p.floors(); floor++) {
int y = floor * STOREY;
box(4, y + 1, 2, 4, y + 3, 9, AIR);
}
}
void entrance() {
for (int x : new int[]{6, 9}) box(x, 1, 0, x, 3, 0, SMOOTH);
box(6, 4, 0, 9, 4, 1, Blocks.STONE_BRICK_SLAB.defaultBlockState());
box(7, 0, 0, 8, 0, 3, TILE);
// Native SOUTH/RIGHT opens against the west jamb; SOUTH/LEFT against the east jamb.
door(7, 1, 1, Direction.SOUTH, true);
door(8, 1, 1, Direction.SOUTH, false);
set(4, 1, 0, Blocks.POTTED_DEAD_BUSH);
set(12, 1, 16, Blocks.COMPOSTER);
}
void roof(int y) {
// Clipped parapets and a heavy coping keep each accessible roof legible from below.
for (int n = 2; n <= 14; n++) {
set(n, y + 1, 1, Blocks.STONE_BRICKS); set(n, y + 1, 15, Blocks.STONE_BRICKS);
set(1, y + 1, n, Blocks.STONE_BRICKS); set(15, y + 1, n, Blocks.STONE_BRICKS);
set(n, y + 2, 1, Blocks.STONE_BRICK_SLAB); set(n, y + 2, 15, Blocks.STONE_BRICK_SLAB);
set(1, y + 2, n, Blocks.STONE_BRICK_SLAB); set(15, y + 2, n, Blocks.STONE_BRICK_SLAB);
}
switch (p.kind()) {
case APARTMENT -> {
// Two small roof gardens and a timber portico leave the stair landing and central crossing free.
roofPlanter(10, y, 4); roofPlanter(10, y, 12);
for (int x : new int[]{6, 13}) box(x, y + 1, 14, x, y + 3, 14, Blocks.STRIPPED_OAK_LOG.defaultBlockState());
box(6, y + 4, 14, 13, y + 4, 14, Blocks.OAK_SLAB.defaultBlockState());
desk(8, y, 12, Direction.NORTH);
}
case HOTEL -> {
roofRoom(y, 9, 3, 14, 7, Blocks.SMOOTH_STONE);
door(9, y + 1, 5, Direction.EAST, false);
set(12, y + 1, 4, Blocks.BARREL); set(13, y + 1, 4, Blocks.LOOM);
box(11, y + 1, 12, 13, y + 1, 12, seating(Blocks.OAK_STAIRS, Direction.NORTH));
}
case LIBRARY -> {
// One transparent cap above a hollow timber curb, open all the way to the stacks.
// No opaque former roof plate or second glass floor remains under this canopy.
for (int x = 10; x <= 13; x++) for (int z = 3; z <= 7; z++) {
boolean edge = x == 10 || x == 13 || z == 3 || z == 7;
set(x, y + 1, z, edge ? Blocks.STRIPPED_OAK_LOG.defaultBlockState() : AIR);
set(x, y + 2, z, edge ? Blocks.OAK_SLAB : Blocks.GLASS);
}
box(11, y - 1, 4, 12, y + 1, 6, AIR);
desk(11, y, 12, Direction.NORTH); set(14, y + 1, 12, Blocks.LECTERN);
}
case RESTAURANT -> {
// A real dining terrace and rear kitchen extraction with full supporting columns.
table(11, y, 6); table(11, y, 11);
for (int z : new int[]{3, 14}) box(14, y + 1, z, 14, y + 3, z, Blocks.STRIPPED_OAK_LOG.defaultBlockState());
box(14, y + 4, 3, 14, y + 4, 14, Blocks.OAK_PLANKS.defaultBlockState());
box(6, y + 1, 14, 7, y + 3, 14, Blocks.STONE_BRICKS.defaultBlockState());
set(6, y + 4, 14, Blocks.IRON_BARS); set(7, y + 4, 14, Blocks.IRON_BARS);
}
case BARRACKS -> {
// Enclosed stair head gives the barracks a service tower, without hiding its usable exit.
// The whole last flight sits inside the stair head: a front wall at z=6
// would cross the headroom over its penultimate step.
roofRoom(y, 1, 2, 5, 10, Blocks.SMOOTH_STONE);
door(5, y + 1, 8, Direction.EAST, false);
box(10, y + 1, 12, 13, y + 1, 14, Blocks.POLISHED_ANDESITE.defaultBlockState());
set(11, y + 2, 13, Blocks.IRON_TRAPDOOR);
}
case HALL -> {
for (int step = 0; step < 3; step++) {
int edge = 2 + step * 2;
box(edge, y + 1 + step, 1, 16 - edge, y + 1 + step, 2, Blocks.STONE_BRICKS.defaultBlockState());
}
for (int x : new int[]{6, 10}) set(x, y + 4, 2, Blocks.CHISELED_STONE_BRICKS);
desk(11, y, 12, Direction.NORTH);
}
default -> throw new IllegalStateException();
}
}
void roofRoom(int y, int x0, int z0, int x1, int z1, Block wall) {
for (int x = x0; x <= x1; x++) for (int z = z0; z <= z1; z++) {
boolean edge = x == x0 || x == x1 || z == z0 || z == z1;
for (int up = 1; up <= 3; up++) set(x, y + up, z, edge ? wall.defaultBlockState() : AIR);
// A bottom slab touches the full-height walls immediately below it.
// A top slab here would float half a block above the pavilion.
set(x, y + 4, z, Blocks.STONE_BRICK_SLAB.defaultBlockState());
}
}
void roofPlanter(int x, int y, int z) {
for (int dx = 0; dx <= 3; dx++) set(x + dx, y + 1, z, Blocks.STONE_BRICKS);
set(x + 1, y + 1, z, Blocks.DIRT); set(x + 2, y + 1, z, Blocks.DIRT);
set(x + 1, y + 2, z, Blocks.OXEYE_DAISY); set(x + 2, y + 2, z, Blocks.CORNFLOWER);
}
void door(int x, int y, int z, Direction facing, boolean rightHinge) {
BlockState state = facing(Blocks.OAK_DOOR, facing)
.setValue(BlockStateProperties.DOOR_HINGE, rightHinge ? 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 anchoredBed(int x, int y, int z, Direction facing) {
BlockState state = facing(Blocks.BED.white(), 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 wallX = x + facing.getStepX() * 2, wallZ = z + facing.getStepZ() * 2;
Direction alongWall = facing.getClockWise();
for (int width = -1; width <= 1; width++) for (int up = 0; up <= 3; up++)
set(wallX + alongWall.getStepX() * width, y + up, wallZ + alongWall.getStepZ() * width, SMOOTH);
}
void table(int x, int y, int z) {
set(x, y + 1, z, Blocks.OAK_FENCE); set(x, y + 2, z, Blocks.OAK_PRESSURE_PLATE);
seat(x - 1, y + 1, z, Direction.EAST);
seat(x + 1, y + 1, z, Direction.WEST);
}
void desk(int x, int y, int z, Direction approachSide) {
set(x, y + 1, z, Blocks.OAK_FENCE); set(x, y + 2, z, Blocks.OAK_PRESSURE_PLATE);
seat(x + approachSide.getStepX(), y + 1, z + approachSide.getStepZ(), approachSide.getOpposite());
}
void seat(int x, int y, int z, Direction lookingToward) {
set(x, y, z, seating(Blocks.OAK_STAIRS, lookingToward));
}
BlockState seating(Block stair, Direction lookingToward) {
// Stair FACING points uphill, into its full-height back, opposite a seated player's view.
return facing(stair, lookingToward.getOpposite());
}
BlockState rail(boolean eastWest) { return connected(Blocks.IRON_BARS, eastWest); }
BlockState pane(boolean eastWest) { return connected(Blocks.GLASS_PANE, eastWest); }
BlockState connected(Block block, boolean eastWest) {
return block.defaultBlockState().setValue(BlockStateProperties.EAST, eastWest)
.setValue(BlockStateProperties.WEST, eastWest).setValue(BlockStateProperties.NORTH, !eastWest)
.setValue(BlockStateProperties.SOUTH, !eastWest);
}
}
}
@@ -0,0 +1,92 @@
package fr.koka.sanctuary.worldgen.city;
import java.util.Arrays;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
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;
/** Generation21 surface parcel. Its distinct native type preserves all published city renderers. */
public final class LostCityPiece21 extends StructurePiece {
private static final int SCHEMA = 1;
private final LostCityPlan.Parcel parcel;
public LostCityPiece21(LostCityPlan.Parcel parcel) {
super(LostCityStructures.PIECE21, 0, bounds(parcel));
this.parcel = parcel;
setOrientation(null);
}
public LostCityPiece21(StructurePieceSerializationContext context, CompoundTag tag) {
super(LostCityStructures.PIECE21, tag);
if (tag.getInt("SanctuarySchema").orElseThrow() != SCHEMA)
throw new IllegalArgumentException("Unsupported Lost City piece schema");
parcel = new LostCityPlan.Parcel(LostCityPlan.Kind.valueOf(tag.getString("Kind").orElseThrow()),
tag.getInt("MinX").orElseThrow(), tag.getInt("MinZ").orElseThrow(),
tag.getInt("Width").orElseThrow(), tag.getInt("Depth").orElseThrow(),
tag.getInt("FloorY").orElseThrow(), tag.getInt("Floors").orElseThrow(),
tag.getInt("Facing").orElseThrow(), tag.getLong("ParcelSeed").orElseThrow(),
tag.getInt("FoundationDepth").orElseThrow(), tag.getIntArray("SurfaceHeights").orElseThrow());
if (!getBoundingBox().equals(bounds(parcel)))
throw new IllegalArgumentException("Lost City parcel disagrees with its saved native bounding box");
setOrientation(null);
}
public LostCityPlan.Parcel parcel() { return parcel; }
@Override public BlockPos getLocatorPosition() {
int x = parcel.width() / 2, z = parcel.depth() / 2;
return new BlockPos(parcel.minX() + x, parcel.surfaceY(x, z) + 1, parcel.minZ() + z);
}
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
tag.putInt("SanctuarySchema", SCHEMA);
tag.putString("Kind", parcel.kind().name());
tag.putInt("MinX", parcel.minX()); tag.putInt("MinZ", parcel.minZ());
tag.putInt("Width", parcel.width()); tag.putInt("Depth", parcel.depth());
tag.putInt("FloorY", parcel.floorY()); tag.putInt("Floors", parcel.floors());
tag.putInt("Facing", parcel.facing()); tag.putLong("ParcelSeed", parcel.seed());
tag.putInt("FoundationDepth", parcel.foundationDepth());
tag.putIntArray("SurfaceHeights", parcel.surfaceHeights());
}
@Override public void postProcess(WorldGenLevel level, StructureManager structures, ChunkGenerator generator,
RandomSource random, BoundingBox chunkClip, ChunkPos chunkPos, BlockPos pivot) {
LostCityStructures.placing(() -> render(parcel, new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState block) {
BlockPos pos = world(x, y, z);
if (getBoundingBox().isInside(pos) && chunkClip.isInside(pos))
placeBlock(level, block, pos.getX(), pos.getY(), pos.getZ(), chunkClip);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
BlockPos pos = world(x, y, z);
if (getBoundingBox().isInside(pos) && chunkClip.isInside(pos))
createChest(level, chunkClip, RandomSource.create(seed), pos, loot, null);
}
}));
}
private static void render(LostCityPlan.Parcel parcel, LostCityRenderer.BlockWriter writer) {
LostCitySurface21.render(parcel, writer);
}
private BlockPos world(int x, int y, int z) {
return new BlockPos(parcel.minX() + x, parcel.floorY() + y, parcel.minZ() + z);
}
private static BoundingBox bounds(LostCityPlan.Parcel parcel) {
int low = Math.min(parcel.floorY(), Arrays.stream(parcel.surfaceHeights()).min().orElse(parcel.floorY()))
- parcel.foundationDepth();
int high = Math.max(parcel.maxY(), Arrays.stream(parcel.surfaceHeights()).max().orElse(parcel.floorY()) + 5);
return new BoundingBox(parcel.minX(), low, parcel.minZ(), parcel.maxX(), high, parcel.maxZ());
}
}
@@ -0,0 +1,248 @@
package fr.koka.sanctuary.worldgen.city;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.*;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/** Preserve the certified compact16 core. At most two modest natural outlying blocks are admitted
* together with complete three-wide approaches; a failed route publishes neither its road nor its block. */
public final class LostCityPlanner21 {
public static final int MAX_EXTRA_COLUMNS = 4_000, MAX_EXTRA_CHECKS = 220_000;
public static final int MAX_COLUMNS = LostCityPlanner16.MAX_COLUMNS + MAX_EXTRA_COLUMNS;
public static final int MAX_VOLUME_CHECKS = LostCityPlanner16.MAX_VOLUME_CHECKS + MAX_EXTRA_CHECKS;
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
private static final int INVALID = Integer.MIN_VALUE;
private LostCityPlanner21() {}
public record Point(int x, int floorY, int z) {}
public record Connection(int districtIndex, List<Point> path) { public Connection { path = List.copyOf(path); } }
public record Result(LostCityPlanner16.Result core, LostCityPlan surface,
List<LostCityPlanner16.Support> supports, int columns, int volumeChecks,
List<Connection> connections, Map<String, Integer> diagnostics) {
public Result {
supports = List.copyOf(supports); connections = List.copyOf(connections); diagnostics = Map.copyOf(diagnostics);
if (columns > MAX_COLUMNS || volumeChecks > MAX_VOLUME_CHECKS || connections.size() > 2)
throw new IllegalArgumentException("Generation21 city exceeded its bounded extension");
}
public LostCityPlanner16.Fallback reservation() { return core.reservation(); }
}
public static Result plan(long seed, int cx, int cz, int diameter, boolean origin, LostCityPlanner16.Terrain terrain) {
var core = LostCityPlanner16.plan(seed, cx, cz, diameter, origin, terrain);
var districts = new ArrayList<>(core.surface().districts()); var supports = new ArrayList<>(core.supports());
var links = new ArrayList<Connection>(); var probe = new Probe(terrain);
if (origin && !districts.isEmpty()) {
var center = districts.getFirst(); int mx = (center.minX() + center.maxX()) / 2, mz = (center.minZ() + center.maxZ()) / 2;
int first = (int) Long.remainderUnsigned(mix(seed ^ 0x21c175L), 4);
try {
for (int attempt = 0; attempt < 8 && links.size() < 2; attempt++) {
int d = (first + attempt) & 3;
int radius = Math.max(center.maxX() - center.minX(), center.maxZ() - center.minZ()) / 2 + (attempt < 4 ? 42 : 58);
int x = mx + DX[d] * radius, z = mz + DZ[d] * radius;
probe.reject("cluster-attempt");
// The old one-lot natural solver already certifies foundations, ramps and short supported bridges.
var mini = LostCityPlanner16.plan(mix(seed ^ attempt * 3191L ^ 0x2100L), x, z, 96, false, probe);
if (mini.surface().isEmpty()) { probe.reject("no-natural-block"); continue; }
var district = mini.surface().districts().getFirst();
if (overlap(district, districts, 14)) { probe.reject("insufficient-natural-gap"); continue; }
var all = new ArrayList<>(districts); all.add(district);
var path = connect(seed ^ attempt, center, district, all, probe);
if (path == null) { probe.reject("no-complete-approach"); continue; }
var parcels = new ArrayList<>(district.parcels()); parcels.addAll(path.roads());
District joined;
try { joined = new District(district.startChunkX(), district.startChunkZ(), district.seed(), parcels); }
catch (IllegalArgumentException tooWide) { probe.reject("native-reference-radius"); continue; }
links.add(new Connection(districts.size(), path.path())); districts.add(joined); supports.addAll(mini.supports());
probe.reject("connected-cluster");
}
} catch (Exhausted exhausted) { probe.reject("extension-budget"); }
}
return new Result(core, new LostCityPlan(districts), supports, core.columns() + probe.columns.size(),
core.volumeChecks() + probe.checks, links, probe.reasons);
}
private record Entry(int x, int y, int z, int facing) {}
private record XZ(int x, int z) { XZ move(int d, int n) { return new XZ(x + DX[d] * n, z + DZ[d] * n); } }
private record Pair(Entry a, Entry b) {}
private record Road(int x, int z, int floor, int foundation) {}
private record Footprint(int x, int z, int width, int depth) {}
private record Route(List<Point> path, List<Parcel> roads) {}
private static Route connect(long seed, District core, District extra, List<District> all, Probe probe) {
var pairs = new ArrayList<Pair>();
for (var a : entries(core, all)) for (var b : entries(extra, all)) pairs.add(new Pair(a, b));
pairs.sort(Comparator.comparingInt(p -> Math.abs(p.a.x - p.b.x) + Math.abs(p.a.z - p.b.z)));
for (var pair : pairs.stream().limit(6).toList()) {
var a = pair.a; var b = pair.b; var outA = new XZ(a.x, a.z).move(a.facing, 3); var outB = new XZ(b.x, b.z).move(b.facing, 3);
for (int variant = 0; variant < 4; variant++) {
var points = new ArrayList<XZ>(); points.add(new XZ(a.x, a.z)); line(points, outA);
if (variant == 0) line(points, new XZ(outB.x, outA.z));
else if (variant == 1) line(points, new XZ(outA.x, outB.z));
else if (variant == 2) { int mid = (outA.x + outB.x) / 2; line(points, new XZ(mid, outA.z)); line(points, new XZ(mid, outB.z)); }
else { int mid = (outA.z + outB.z) / 2; line(points, new XZ(outA.x, mid)); line(points, new XZ(outB.x, mid)); }
line(points, outB); line(points, new XZ(b.x, b.z));
if (points.size() > 192 || new java.util.HashSet<>(points).size() != points.size()) continue;
var route = profile(seed, points, a, b, all, probe); if (route != null) return route;
}
}
return null;
}
private static List<Entry> entries(District district, List<District> all) {
var result = new ArrayList<Entry>();
for (var p : district.parcels()) if (p.kind() == Kind.STREET && p.width() >= 3 && p.depth() >= 3) {
for (int d = 0; d < 4; d++) {
int lx = d == 1 ? p.width() - 1 : d == 3 ? 0 : p.width() / 2;
int lz = d == 2 ? p.depth() - 1 : d == 0 ? 0 : p.depth() / 2;
int x = p.minX() + lx, z = p.minZ() + lz, floor = p.surfaceY(lx, lz); boolean valid = true;
for (int side = -1; side <= 1; side++) {
int px = x + DZ[d] * side, pz = z + DX[d] * side;
if (existingFloor(px, pz, all) != floor || existingFloor(px + DX[d], pz + DZ[d], all) != INVALID) { valid = false; break; }
}
if (valid) result.add(new Entry(x, floor, z, d));
}
}
return result;
}
private static Route profile(long seed, List<XZ> path, Entry a, Entry b, List<District> all, Probe probe) {
int n = path.size(); int[] low = new int[n], high = new int[n], floor = new int[n];
// Partition the complete footprint before choosing elevations. Every partition boundary
// will be a level landing, so a renderer never needs a neighboring parcel to form a stair.
var footprintCells = new LinkedHashMap<Long, XZ>();
for (int i = 0; i < n; i++) for (var cell : strip(path, i))
if (existingFloor(cell.x, cell.z, all) == INVALID) footprintCells.putIfAbsent(key(cell.x, cell.z), cell);
var footprints = footprints(footprintCells);
var owner = new HashMap<Long, Integer>();
for (int i = 0; i < footprints.size(); i++) {
var f = footprints.get(i);
for (int z = f.z; z < f.z + f.depth; z++) for (int x = f.x; x < f.x + f.width; x++) owner.put(key(x, z), i);
}
int[] grade = new int[n];
for (int i = 1; i < n; i++) {
grade[i] = 1;
for (var aCell : strip(path, i - 1)) for (var bCell : strip(path, i)) {
int distance = Math.abs(aCell.x - bCell.x) + Math.abs(aCell.z - bCell.z);
if (distance == 0 || distance == 1 && !java.util.Objects.equals(
owner.get(key(aCell.x, aCell.z)), owner.get(key(bCell.x, bCell.z)))) grade[i] = 0;
}
}
for (int i = 0; i < n; i++) {
low[i] = 176; high[i] = 350;
for (var p : strip(path, i)) {
int old = existingFloor(p.x, p.z, all);
if (old == -1) return null;
if (old != INVALID) { low[i] = Math.max(low[i], old); high[i] = Math.min(high[i], old); }
else {
var g = probe.ground(p.x, p.z);
if (!g.dry() || g.protectedTerrain() || g.solidDepth() < 3) return null;
low[i] = Math.max(low[i], g.top() - 2); high[i] = Math.min(high[i], g.top() + 3);
}
}
}
low[0] = high[0] = a.y; low[n - 1] = high[n - 1] = b.y;
for (int i = 1; i < n; i++) { low[i] = Math.max(low[i], low[i - 1] - grade[i]); high[i] = Math.min(high[i], high[i - 1] + grade[i]); }
for (int i = n - 2; i >= 0; i--) { low[i] = Math.max(low[i], low[i + 1] - grade[i + 1]); high[i] = Math.min(high[i], high[i + 1] + grade[i + 1]); }
var cells = new LinkedHashMap<Long, Road>(); var result = new ArrayList<Point>();
for (int i = 0; i < n; i++) {
if (low[i] > high[i]) return null;
int min = i == 0 ? low[i] : Math.max(low[i], floor[i - 1] - grade[i]), max = i == 0 ? high[i] : Math.min(high[i], floor[i - 1] + grade[i]);
if (min > max) return null;
var p = path.get(i); floor[i] = Math.clamp(probe.ground(p.x, p.z).top(), min, max); result.add(new Point(p.x, floor[i], p.z));
for (var c : strip(path, i)) {
int old = existingFloor(c.x, c.z, all); if (old != INVALID) { if (old != floor[i]) return null; continue; }
var g = probe.ground(c.x, c.z); int foundation = Math.max(1, floor[i] - g.top() + 1), bottom = floor[i] - foundation;
if (!probe.solid(c.x, bottom, c.z) || !probe.solid(c.x, bottom - 1, c.z)) return null;
for (int y = bottom - 1; y <= floor[i] + 8; y++) if (probe.protectedAt(c.x, y, c.z)) return null;
var previous = cells.putIfAbsent(key(c.x, c.z), new Road(c.x, c.z, floor[i], foundation));
if (previous != null && previous.floor != floor[i]) return null;
}
}
// Bends may put nonconsecutive centerline strips next to each other. Their shared
// partition edge obeys the same level-landing contract, including existing town roads.
for (var cell : cells.values()) for (int d = 0; d < 4; d++) {
int nx = cell.x + DX[d], nz = cell.z + DZ[d];
var neighbor = cells.get(key(nx, nz));
if (neighbor != null && !owner.get(key(cell.x, cell.z)).equals(owner.get(key(nx, nz)))
&& neighbor.floor != cell.floor) return null;
int old = existingFloor(nx, nz, all);
if (old >= 0 && old != cell.floor) return null;
}
var parcels = new ArrayList<Parcel>();
for (var f : footprints) {
int foundation = 1;
for (int z = f.z; z < f.z + f.depth; z++) for (int x = f.x; x < f.x + f.width; x++)
foundation = Math.max(foundation, cells.get(key(x, z)).foundation);
int[] heights = new int[f.width * f.depth];
for (int z = f.z; z < f.z + f.depth; z++) for (int x = f.x; x < f.x + f.width; x++) {
var cell = cells.get(key(x, z)); int bottom = cell.floor - foundation;
// A rectangular parcel uses one foundation depth. Certify its deeper columns
// explicitly rather than extending another column's rock certificate sideways.
if (!probe.solid(x, bottom, z) || !probe.solid(x, bottom - 1, z)) return null;
for (int y = bottom - 1; y < cell.floor - cell.foundation - 1; y++)
if (probe.protectedAt(x, y, z)) return null;
heights[(z - f.z) * f.width + x - f.x] = cell.floor;
}
parcels.add(new Parcel(Kind.STREET, f.x, f.z, f.width, f.depth, heights[0], 0, 0,
mix(seed ^ key(f.x, f.z)), foundation, heights));
}
return new Route(List.copyOf(result), List.copyOf(parcels));
}
/** Disjoint, completely occupied rectangles, each contained in a single native chunk. */
private static List<Footprint> footprints(Map<Long, XZ> cells) {
var remaining = new java.util.HashSet<>(cells.keySet()); var result = new ArrayList<Footprint>();
for (var at : cells.values().stream().sorted(Comparator.comparingInt(XZ::z).thenComparingInt(XZ::x)).toList()) {
if (!remaining.contains(key(at.x, at.z))) continue;
int width = 1, depth = 1;
while ((at.x + width >> 4) == (at.x >> 4) && remaining.contains(key(at.x + width, at.z))) width++;
while ((at.z + depth >> 4) == (at.z >> 4)) {
boolean complete = true;
for (int x = at.x; x < at.x + width; x++) complete &= remaining.contains(key(x, at.z + depth));
if (!complete) break;
depth++;
}
for (int z = at.z; z < at.z + depth; z++) for (int x = at.x; x < at.x + width; x++) remaining.remove(key(x, z));
result.add(new Footprint(at.x, at.z, width, depth));
}
return List.copyOf(result);
}
private static List<XZ> strip(List<XZ> points, int i) {
var p = points.get(i); var result = new java.util.LinkedHashSet<XZ>(); result.add(p);
for (int next : new int[]{i - 1, i + 1}) if (next >= 0 && next < points.size()) {
var q = points.get(next); int dx = Integer.signum(q.x - p.x), dz = Integer.signum(q.z - p.z);
result.add(new XZ(p.x + dz, p.z + dx)); result.add(new XZ(p.x - dz, p.z - dx));
}
return List.copyOf(result);
}
private static void line(List<XZ> points, XZ goal) {
var p = points.getLast(); while (p.x != goal.x) { p = new XZ(p.x + Integer.signum(goal.x - p.x), p.z); points.add(p); }
while (p.z != goal.z) { p = new XZ(p.x, p.z + Integer.signum(goal.z - p.z)); points.add(p); }
}
private static int existingFloor(int x, int z, List<District> all) {
for (var d : all) for (var p : d.parcels()) if (x >= p.minX() && x <= p.maxX() && z >= p.minZ() && z <= p.maxZ())
return p.kind() == Kind.STREET ? p.surfaceY(x - p.minX(), z - p.minZ()) : -1;
return INVALID;
}
private static boolean overlap(District candidate, List<District> all, int gap) {
for (var d : all) if (candidate.minX() <= d.maxX() + gap && candidate.maxX() >= d.minX() - gap
&& candidate.minZ() <= d.maxZ() + gap && candidate.maxZ() >= d.minZ() - gap) return true;
return false;
}
private static final class Exhausted extends RuntimeException {}
private static final class Probe implements LostCityPlanner16.Terrain {
final LostCityPlanner16.Terrain source; final Map<Long, LostCityPlanner.Ground> columns = new HashMap<>();
final Map<String, Integer> reasons = new LinkedHashMap<>(); int checks;
Probe(LostCityPlanner16.Terrain source) { this.source = source; }
public boolean owns(int x, int z) { return source.owns(x, z); }
public LostCityPlanner.Ground ground(int x, int z) {
if (!owns(x, z)) return new LostCityPlanner.Ground(0, 0, false, true);
long key = key(x, z); var old = columns.get(key); if (old != null) return old;
if (columns.size() >= MAX_EXTRA_COLUMNS) throw new Exhausted();
var value = source.ground(x, z); columns.put(key, value); return value;
}
public boolean solid(int x, int y, int z) { count(); ground(x, z); return owns(x, z) && source.solid(x, y, z); }
public boolean protectedAt(int x, int y, int z) { count(); ground(x, z); return !owns(x, z) || source.protectedAt(x, y, z); }
void count() { if (checks >= MAX_EXTRA_CHECKS) throw new Exhausted(); checks++; }
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 long mix(long v) { v = (v ^ v >>> 30) * 0xbf58476d1ce4e5b9L; v = (v ^ v >>> 27) * 0x94d049bb133111ebL; return v ^ v >>> 31; }
}
@@ -29,17 +29,18 @@ public final class LostCityStructure extends Structure {
throw new IllegalStateException("Lost City district exceeds native structure reference range"); throw new IllegalStateException("Lost City district exceeds native structure reference range");
} }
return Optional.of(new GenerationStub(LostCityStructures.locator(district), pieces -> { return Optional.of(new GenerationStub(LostCityStructures.locator(district), pieces -> {
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20) && generator.cityReservation16(island) != null) if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21) && generator.cityReservation16(island) != null
&& (generator.rootGeneration() != 21 || generator.cityPlacement21(island).core().surface().districts().contains(district)))
pieces.addPiece(new LostCityFoundationPiece16(generator.cityReservation16(island), district.seed())); pieces.addPiece(new LostCityFoundationPiece16(generator.cityReservation16(island), district.seed()));
if (generator.rootGeneration() == 15) for (var support : generator.citySupports(island)) { if (generator.rootGeneration() == 15) for (var support : generator.citySupports(island)) {
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ()) if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
pieces.addPiece(new LostCitySupportPiece15(support)); pieces.addPiece(new LostCitySupportPiece15(support));
} }
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20)) for (var support : generator.citySupports16(island)) { if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21)) for (var support : generator.citySupports16(island)) {
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ()) if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
pieces.addPiece(new LostCitySupportPiece16(support)); pieces.addPiece(new LostCitySupportPiece16(support));
} }
district.parcels().forEach(parcel -> pieces.addPiece(generator.rootGeneration() == 20 ? new LostCityPiece20(parcel) : (generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20) ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15 district.parcels().forEach(parcel -> pieces.addPiece(generator.rootGeneration() == 21 ? new LostCityPiece21(parcel) : generator.rootGeneration() == 20 ? new LostCityPiece20(parcel) : (generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21) ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
? new LostCityPiece15(parcel) : new LostCityPiece(parcel))); ? new LostCityPiece15(parcel) : new LostCityPiece(parcel)));
if (generator.rootGeneration() == 15) for (var network : generator.undergroundPlan(island).networks()) { if (generator.rootGeneration() == 15) for (var network : generator.undergroundPlan(island).networks()) {
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue; if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
@@ -49,13 +50,13 @@ public final class LostCityStructure extends Structure {
network.modules().stream().filter(m -> m.kind() == LostCityUnderground15.Kind.ACCESS) network.modules().stream().filter(m -> m.kind() == LostCityUnderground15.Kind.ACCESS)
.forEach(m -> pieces.addPiece(new LostCityUndergroundPiece15(m))); .forEach(m -> pieces.addPiece(new LostCityUndergroundPiece15(m)));
} }
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20)) for (var network : generator.undergroundPlan16(island).networks()) { if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21)) for (var network : generator.undergroundPlan16(island).networks()) {
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue; if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
// Surface comes first, rooms/corridors next, shafts last so every entrance remains open. // Surface comes first, rooms/corridors next, shafts last so every entrance remains open.
network.modules().stream().filter(m -> m.kind() != LostCityUnderground16.Kind.ACCESS) network.modules().stream().filter(m -> m.kind() != LostCityUnderground16.Kind.ACCESS)
.forEach(m -> pieces.addPiece(generator.rootGeneration() == 20 ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m))); .forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
network.modules().stream().filter(m -> m.kind() == LostCityUnderground16.Kind.ACCESS) network.modules().stream().filter(m -> m.kind() == LostCityUnderground16.Kind.ACCESS)
.forEach(m -> pieces.addPiece(generator.rootGeneration() == 20 ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m))); .forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
} }
})); }));
} }
@@ -40,6 +40,8 @@ public final class LostCityStructures {
BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_underground_v15"), LostCityUndergroundPiece15::new); BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_underground_v15"), LostCityUndergroundPiece15::new);
public static final StructurePieceType SUPPORT15 = Registry.register( public static final StructurePieceType SUPPORT15 = Registry.register(
BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_support_v15"), LostCitySupportPiece15::new); BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_support_v15"), LostCitySupportPiece15::new);
public static final StructurePieceType PIECE21 = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
SanctuaryMod.id("lost_city_parcel_v21"), LostCityPiece21::new);
public static final StructurePieceType PIECE20 = Registry.register( public static final StructurePieceType PIECE20 = Registry.register(
BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_parcel_v20"), LostCityPiece20::new); BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_parcel_v20"), LostCityPiece20::new);
public static final StructurePieceType PIECE16 = Registry.register( public static final StructurePieceType PIECE16 = Registry.register(
@@ -0,0 +1,302 @@
package fr.koka.sanctuary.worldgen.city;
import fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
import net.minecraft.core.Direction;
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.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.Half;
import net.minecraft.world.level.block.state.properties.SlabType;
import net.minecraft.world.level.storage.loot.LootTable;
/** New surface architecture on the saved generation16 footprints. Published renderers remain immutable. */
public final class LostCitySurface21 {
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
private LostCitySurface21() {}
public static void render(LostCityPlan.Parcel parcel, LostCityRenderer.BlockWriter output) {
if (parcel.kind().building()) { LostCityInteriors21.render(parcel, output); return; }
var connected = ConnectedBlocks20.buffer(output);
if (parcel.kind() == LostCityPlan.Kind.STREET || parcel.kind() == LostCityPlan.Kind.BRIDGE) {
road(parcel, connected);
} else {
new Exterior(parcel, connected).render();
}
connected.flush();
}
private static void road(LostCityPlan.Parcel p, LostCityRenderer.BlockWriter output) {
boolean alongX = p.width() > p.depth(), bridge = p.kind() == LostCityPlan.Kind.BRIDGE;
for (int z = 0; z < p.depth(); z++) for (int x = 0; x < p.width(); x++) {
int y = p.surfaceY(x, z) - p.floorY();
int across = alongX ? z : x, along = alongX ? x : z, breadth = alongX ? p.depth() : p.width();
boolean edge = across == 0 || across == breadth - 1;
// A legible curb and patterned shoulders, not per-block random pavement.
Block paving = edge ? Blocks.POLISHED_TUFF : across == 1 || across == breadth - 2
? along % 4 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS
: Blocks.POLISHED_ANDESITE;
if (!bridge) for (int down = 1; down <= p.foundationDepth(); down++)
output.set(x, y - down, z, Blocks.STONE_BRICKS.defaultBlockState());
output.set(x, y, z, paving.defaultBlockState());
for (int up = 1; up <= 8; up++) output.set(x, y + up, z, Blocks.AIR.defaultBlockState());
if (bridge && edge) {
// Posts grow from the existing deck only; the certified natural bearings are untouched.
output.set(x, y + 1, z, (along % 5 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.IRON_BARS).defaultBlockState());
if (along % 5 == 0) output.set(x, y + 2, z, Blocks.TUFF_BRICK_SLAB.defaultBlockState());
continue;
}
for (var direction : HORIZONTAL) {
int nx = x + direction.getStepX(), nz = z + direction.getStepZ();
if (nx >= 0 && nx < p.width() && nz >= 0 && nz < p.depth()
&& p.surfaceY(nx, nz) == p.surfaceY(x, z) + 1) {
output.set(x, y + 1, z, Blocks.TUFF_BRICK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, direction));
break;
}
}
}
}
private static final class Exterior implements LostCityRenderer.BlockWriter {
final LostCityPlan.Parcel p;
final LostCityRenderer.BlockWriter output;
final Rotation turn;
Exterior(LostCityPlan.Parcel p, LostCityRenderer.BlockWriter output) {
this.p = p; this.output = output;
turn = switch (p.facing()) { case 1 -> Rotation.CLOCKWISE_90; case 2 -> Rotation.CLOCKWISE_180;
case 3 -> Rotation.COUNTERCLOCKWISE_90; default -> Rotation.NONE; };
}
int x(int x, int z) { return switch (p.facing()) { case 1 -> p.width() - 1 - z; case 2 -> p.width() - 1 - x; case 3 -> z; default -> x; }; }
int z(int x, int z) { return switch (p.facing()) { case 1 -> x; case 2 -> p.depth() - 1 - z; case 3 -> p.depth() - 1 - x; default -> z; }; }
@Override public void set(int x, int y, int z, BlockState state) {
output.set(x(x, z), y, z(x, z), palette(y, state).rotate(turn));
}
BlockState palette(int y, BlockState state) {
Block block = null;
if (state.is(Blocks.STONE) && y < 0) block = Blocks.STONE_BRICKS;
else if (state.is(Blocks.DARK_OAK_PLANKS)) block = Blocks.OAK_PLANKS;
else if (state.is(Blocks.DARK_OAK_FENCE)) block = Blocks.OAK_FENCE;
else if (state.is(Blocks.DARK_OAK_STAIRS)) block = Blocks.OAK_STAIRS;
else if (state.is(Blocks.DARK_OAK_SLAB)) block = Blocks.OAK_SLAB;
else if (state.is(Blocks.CALCITE)) block = Blocks.SMOOTH_STONE;
else if (y > 0) {
if (state.is(Blocks.TUFF_BRICKS)) block = Blocks.STONE_BRICKS;
else if (state.is(Blocks.CHISELED_TUFF_BRICKS) || state.is(Blocks.CHISELED_TUFF)) block = Blocks.CHISELED_STONE_BRICKS;
else if (state.is(Blocks.TUFF_BRICK_SLAB)) block = Blocks.STONE_BRICK_SLAB;
else if (state.is(Blocks.POLISHED_TUFF_SLAB)) block = Blocks.SMOOTH_STONE_SLAB;
else if (state.is(Blocks.TUFF_BRICK_STAIRS)) block = Blocks.STONE_BRICK_STAIRS;
else if (state.is(Blocks.POLISHED_TUFF)) block = Blocks.POLISHED_ANDESITE;
}
if (block == null) return state;
BlockState result = block.defaultBlockState();
for (var property : state.getProperties()) if (result.hasProperty(property)) result = copy(result, state, property);
return result;
}
<T extends Comparable<T>> BlockState copy(BlockState into, BlockState from,
net.minecraft.world.level.block.state.properties.Property<T> property) {
return into.setValue(property, from.getValue(property));
}
void set(int x, int y, int z, Block block) { set(x, y, z, block.defaultBlockState()); }
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
output.chest(x(x, z), y, z(x, z), table, seed ^ 0x21c17a7eL);
}
void box(int x0, int y0, int z0, int x1, int y1, int z1, Block block) {
for (int x = x0; x <= x1; x++) for (int z = z0; z <= z1; z++) for (int y = y0; y <= y1; y++) set(x, y, z, block);
}
void render() {
if (p.width() != 17 || p.depth() != 17) throw new IllegalArgumentException("Generation21 amenities require the certified 17x17 footprint");
// Keep the proven pool containment, excavation steps and garage ramps. All new ornament is
// composed before connection resolution, so clipping cannot split a rail or window design.
var north = new LostCityPlan.Parcel(p.kind(), p.minX(), p.minZ(), 17, 17, p.floorY(), p.floors(), 0,
p.seed(), p.foundationDepth(), p.surfaceHeights());
LostCitySurface16.render(north, new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) {
Exterior.this.set(x, y, z, state);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { Exterior.this.chest(x, y, z, table, seed); }
});
// The north threshold is a level promenade, shared by every admitted parcel.
for (int x = 0; x <= 16; x++) set(x, 0, 0, x % 4 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS);
switch (p.kind()) {
case PARK -> park();
case PARK_GARDEN -> garden();
case PARK_PLAYGROUND -> playground();
case PARK_MEMORIAL -> memorial();
case PLAZA -> plaza();
case MARKET -> market();
case PARKING -> garage();
case POOL -> { if (p.foundationDepth() >= 8) pool(); else park(); }
case EXCAVATION -> { if (p.foundationDepth() >= 8) excavation(); else park(); }
default -> throw new IllegalArgumentException("Not an exterior amenity");
}
}
void paths() {
for (int n = 1; n <= 16; n++) for (int across = 7; across <= 9; across++) {
set(across, 0, n, across == 8 ? Blocks.POLISHED_TUFF : Blocks.TUFF_BRICKS);
set(n, 0, across, across == 8 ? Blocks.POLISHED_TUFF : Blocks.TUFF_BRICKS);
}
}
void bench(int x, int y, int z, int length, Direction view) {
for (int dx = 0; dx < length; dx++) set(x + dx, y, z, Blocks.OAK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, view.getOpposite()));
set(x - 1, y, z, Blocks.TUFF_BRICK_SLAB); set(x + length, y, z, Blocks.TUFF_BRICK_SLAB);
}
void lamp(int x, int z) {
set(x, 1, z, Blocks.CHISELED_TUFF_BRICKS); box(x, 2, z, x, 3, z, Blocks.OAK_FENCE);
set(x, 4, z, Blocks.OAK_PLANKS); set(x, 5, z, Blocks.LANTERN);
}
void park() {
paths(); bench(10, 1, 3, 3, Direction.SOUTH); bench(10, 1, 11, 3, Direction.NORTH);
lamp(5, 11); lamp(11, 5);
// A small oak screen joins the existing trees without introducing another wood palette.
box(3, 1, 12, 3, 4, 12, Blocks.OAK_LOG);
for (int x = 2; x <= 4; x++) for (int z = 11; z <= 13; z++)
set(x, 5, z, Blocks.OAK_LEAVES.defaultBlockState().setValue(BlockStateProperties.PERSISTENT, true));
for (int z = 2; z <= 5; z++) set(6, 0, z, Blocks.POLISHED_ANDESITE);
}
void garden() {
paths();
// Four parterres have low carved kerbs, soil beds and a shared central promenade.
for (int x0 : new int[]{2, 10}) for (int z0 : new int[]{2, 10}) {
for (int n = 0; n < 5; n++) {
set(x0 + n, 0, z0, Blocks.SMOOTH_STONE); set(x0 + n, 0, z0 + 4, Blocks.SMOOTH_STONE);
set(x0, 0, z0 + n, Blocks.SMOOTH_STONE); set(x0 + 4, 0, z0 + n, Blocks.SMOOTH_STONE);
}
set(x0, 1, z0, Blocks.CHISELED_TUFF); set(x0 + 4, 1, z0 + 4, Blocks.CHISELED_TUFF);
}
bench(7, 1, 15, 3, Direction.NORTH);
// Pergola along the west planting bed, not across a circulation route.
for (int z : new int[]{2, 6}) box(1, 1, z, 1, 3, z, Blocks.STRIPPED_OAK_LOG);
box(1, 4, 2, 1, 4, 6, Blocks.OAK_SLAB); lamp(15, 8);
}
void playground() {
paths();
for (int x = 1; x <= 7; x++) { set(x, 0, 1, Blocks.OAK_PLANKS); set(x, 0, 7, Blocks.OAK_PLANKS); }
for (int z = 1; z <= 7; z++) { set(1, 0, z, Blocks.OAK_PLANKS); set(7, 0, z, Blocks.OAK_PLANKS); }
// The hanging seat's upper half meets the chain above; a bottom slab would leave a gap.
set(4, 2, 4, Blocks.OAK_SLAB.defaultBlockState().setValue(BlockStateProperties.SLAB_TYPE, SlabType.TOP));
// The last ascending step used to touch its neighbors only diagonally, over empty air.
set(12, 1, 10, Blocks.OAK_PLANKS);
// Braced climbing platform: low guards leave the three front steps open.
for (int x : new int[]{11, 13}) set(x, 4, 13, Blocks.OAK_FENCE);
set(12, 4, 13, Blocks.OAK_FENCE);
set(11, 4, 11, Blocks.OAK_FENCE); set(13, 4, 11, Blocks.OAK_FENCE);
bench(3, 1, 13, 3, Direction.NORTH); lamp(15, 3);
}
void plaza() {
for (int x = 1; x <= 15; x++) for (int z = 1; z <= 15; z++) {
int d = Math.max(Math.abs(x - 8), Math.abs(z - 8));
set(x, 0, z, d == 3 || d == 6 ? Blocks.POLISHED_ANDESITE : d == 4 || d == 7 ? Blocks.TUFF_BRICKS : Blocks.SMOOTH_STONE);
}
// Dry civic fountain: bevelled bowl, carved pedestal, projecting cap.
for (int x = 6; x <= 10; x++) for (int z = 6; z <= 10; z++) {
if (x != 6 && x != 10 && z != 6 && z != 10) continue;
boolean corner = (x == 6 || x == 10) && (z == 6 || z == 10);
set(x, 1, z, corner ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICK_SLAB);
}
set(8, 1, 8, Blocks.CHISELED_TUFF); set(8, 2, 8, Blocks.CHISELED_TUFF_BRICKS);
set(8, 3, 8, Blocks.POLISHED_TUFF_SLAB); lamp(2, 12); lamp(14, 4);
}
void memorial() {
// The memorial keeps its open central approach and gains stepped stone shoulders.
for (int x = 1; x <= 15; x++) for (int z = 1; z <= 15; z++)
set(x, 0, z, x == 5 || x == 11 || z == 5 || z == 11 ? Blocks.TUFF_BRICKS : Blocks.POLISHED_ANDESITE);
for (int x = 7; x <= 9; x++) {
set(x, 1, 7, Blocks.TUFF_BRICK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
set(x, 1, 9, Blocks.TUFF_BRICK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
}
set(8, 2, 8, Blocks.CHISELED_TUFF); set(8, 3, 8, Blocks.CHISELED_TUFF_BRICKS);
set(8, 4, 8, Blocks.CHISELED_STONE_BRICKS); set(8, 5, 8, Blocks.SMOOTH_STONE_SLAB);
lamp(3, 8); lamp(13, 8);
}
void market() {
for (int x = 1; x <= 15; x++) for (int z = 1; z <= 15; z++)
set(x, 0, z, x >= 7 && x <= 9 || z >= 7 && z <= 9 ? Blocks.TUFF_BRICKS : Blocks.POLISHED_ANDESITE);
for (int x0 : new int[]{2, 11}) for (int z0 : new int[]{2, 11}) {
for (int x = x0; x <= x0 + 3; x++) for (int z = z0; z <= z0 + 2; z++)
set(x, 4, z, Blocks.OAK_SLAB);
for (int x : new int[]{x0, x0 + 3}) {
set(x, 1, z0, Blocks.CHISELED_TUFF_BRICKS);
box(x, 2, z0, x, 3, z0, Blocks.STRIPPED_OAK_LOG);
}
// Only one roof layer: the former raised bottom slab was separated by half a block.
// Rear posts and a full-width beam make the canopy's load path visible from the stall.
for (int x : new int[]{x0, x0 + 3}) box(x, 2, z0 + 2, x, 3, z0 + 2, Blocks.STRIPPED_OAK_LOG);
box(x0, 3, z0, x0 + 3, 3, z0, Blocks.OAK_PLANKS);
box(x0, 3, z0 + 2, x0 + 3, 3, z0 + 2, Blocks.OAK_PLANKS);
}
// The shared chest replaces this stall's counter: a pot cannot sit on its recessed lid.
set(13, 2, 13, Blocks.AIR); set(12, 2, 13, Blocks.FLOWER_POT);
lamp(1, 8); lamp(15, 8);
}
void pool() {
// Each submerged tread bears on a masonry riser down to the basin floor;
// water between diagonal stairs is not a structural connection.
for (int step = 1; step <= 3; step++) for (int x = 7; x <= 9; x++)
for (int y = -3; y < -step; y++) set(x, y, step + 4, Blocks.SMOOTH_STONE);
for (int x = 3; x <= 13; x++) for (int z = 4; z <= 13; z++) {
if (x == 3 || x == 13 || z == 4 || z == 13) set(x, 0, z, Blocks.SMOOTH_STONE);
}
// Changing/showers canopy stands beside the deck; it never enters the sealed basin.
for (int x : new int[]{10, 15}) box(x, 1, 1, x, 3, 1, Blocks.STRIPPED_OAK_LOG);
for (int x : new int[]{10, 15}) box(x, 1, 3, x, 3, 3, Blocks.STRIPPED_OAK_LOG);
box(10, 3, 1, 15, 3, 1, Blocks.OAK_PLANKS);
box(10, 3, 3, 15, 3, 3, Blocks.OAK_PLANKS);
box(10, 4, 1, 15, 4, 3, Blocks.OAK_SLAB);
for (int x = 11; x <= 14; x++) set(x, 1, 1, Blocks.WATER_CAULDRON.defaultBlockState()
.setValue(BlockStateProperties.LEVEL_CAULDRON, 2));
set(15, 4, 2, Blocks.OAK_PLANKS); set(15, 5, 2, Blocks.LANTERN);
for (int z : new int[]{5, 9}) set(15, 1, z, Blocks.OAK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST));
box(2, 0, 14, 14, 0, 15, Blocks.TUFF_BRICKS);
}
void excavation() {
// Each descending stone step rests on a small riser down to the certified pit floor.
for (int step = 1; step <= 5; step++) for (int x = 7; x <= 9; x++)
for (int y = -4; y < -step; y++) set(x, y, step + 2, Blocks.STONE_BRICKS);
// Reinforced temporary footings and timber trestles stay out of the central descent.
for (int x : new int[]{4, 12}) {
box(x, -4, 11, x, -1, 11, Blocks.CHISELED_STONE_BRICKS);
set(x, 0, 11, Blocks.OAK_PLANKS);
}
box(3, 3, 13, 13, 3, 13, Blocks.STRIPPED_OAK_LOG);
for (int x : new int[]{3, 13}) box(x, -4, 13, x, 2, 13, Blocks.STONE_BRICKS);
// Replace the single beam with an open construction shelter, one canopy on grounded posts.
// All four trestles start on the excavated floor, not on the missing former ground plane.
for (int x : new int[]{3, 13}) box(x, -4, 10, x, 2, 10, Blocks.STONE_BRICKS);
box(3, 3, 10, 13, 3, 10, Blocks.STRIPPED_OAK_LOG);
for (int x : new int[]{3, 8, 13}) box(x, 3, 10, x, 3, 13, Blocks.OAK_PLANKS);
box(3, 4, 10, 13, 4, 13, Blocks.OAK_SLAB);
set(3, 4, 13, Blocks.OAK_PLANKS);
set(3, 5, 13, Blocks.LANTERN);
set(1, 1, 11, Blocks.GRINDSTONE); set(1, 1, 10, Blocks.CRAFTING_TABLE);
}
void garage() {
int base = p.foundationDepth() >= 8 ? -5 : 0;
// The ramp's five-wide masonry wedge joins every tread to the garage floor.
if (base < 0) for (int z = 0; z <= 4; z++) for (int x = 6; x <= 10; x++)
for (int y = base + 1; y < -z; y++) set(x, y, z, Blocks.STONE_BRICKS);
for (int x : new int[]{3, 13}) for (int z : new int[]{3, 13}) {
box(x, base + 1, z, x, base + 4, z, Blocks.POLISHED_ANDESITE);
set(x, base + 1, z, Blocks.CHISELED_TUFF_BRICKS);
}
for (int z = 6; z <= 15; z++) for (int x = 1; x <= 15; x++)
if (x <= 5 || x >= 11) set(x, base, z, z % 4 == 0 ? Blocks.SMOOTH_STONE : Blocks.POLISHED_ANDESITE);
// A ribbed structural fascia and ventilation blades make this an open modern garage.
for (int x = 0; x <= 16; x++) {
// The underground ramp cuts through this former ceiling strip: never paint it back.
if (base == 0 || x < 6 || x > 10)
set(x, base + 5, 0, x % 4 == 0 ? Blocks.CHISELED_STONE_BRICKS : Blocks.SMOOTH_STONE);
if (x < 6 || x > 10) set(x, base + 3, 0, Blocks.IRON_TRAPDOOR.defaultBlockState()
.setValue(BlockStateProperties.OPEN, true).setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
}
for (int x : new int[]{5, 11}) {
set(x, base + 4, 9, Blocks.SMOOTH_STONE);
set(x, base + 3, 9, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
}
}
}
}
@@ -0,0 +1,85 @@
package fr.koka.sanctuary.worldgen.city;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import fr.koka.sanctuary.expansion.ExpansionTerrain;
import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Predicate;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.RandomState;
/** Generation21 bounded noise-column cache shared by surface and underground planning, never loaded chunks. */
public final class LostCityTerrain21 implements LostCityUnderground16.Terrain, LostCityPlanner16.Terrain {
public static final int MAX_COLUMNS = 24_096;
private final ExpansionIsland island;
private final NoiseBasedChunkGenerator generator;
private final RandomState random;
private final LevelHeightAccessor height;
private final Predicate<BlockPos> protectedVolume;
private final Map<Long, NoiseColumn> columns = new HashMap<>();
private final BlockPos fluidCandidate;
public LostCityTerrain21(ExpansionIsland island, NoiseBasedChunkGenerator generator, RandomState random,
LevelHeightAccessor height, Predicate<BlockPos> protectedVolume) {
this.island = island; this.generator = generator; this.random = random;
this.height = height; this.protectedVolume = protectedVolume;
fluidCandidate = ExpansionTerrain.pocketCandidate(island);
}
private NoiseColumn column(int x, int z) {
if (!owns(x, z)) return null;
long key = ((long) x << 32) ^ (z & 0xffffffffL);
var cached = columns.get(key);
if (cached != null) return cached;
if (columns.size() >= MAX_COLUMNS) return null;
var result = generator.getBaseColumn(x, z, height, random);
columns.put(key, result);
return result;
}
public LostCityPlanner.Ground ground(int x, int z) {
var column = column(x, z);
if (column == null) return new LostCityPlanner.Ground(0, 0, false, true);
int top = 383;
while (top > 0 && column.getBlock(top).isAir()) top--;
int bottom = top;
while (bottom > 0 && top - bottom < 24 && column.getBlock(bottom).isSolid()
&& column.getBlock(bottom).getFluidState().isEmpty()) bottom--;
boolean dry = column.getBlock(top).isSolid() && column.getBlock(top).getFluidState().isEmpty();
boolean water = island.origin() && top >= 168 && dry && top - bottom >= 6
&& PopulationHydrologyRuntime.protects(generator, random, new BlockPos(x, top, z), 1, 30, 12);
return new LostCityPlanner.Ground(top, top - bottom, dry, water);
}
@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);
if (column == null || !column.getBlock(y).getFluidState().isEmpty()) return true;
if (!island.origin() && island.relief().equals("volcano")
&& Math.abs(x - fluidCandidate.getX()) <= 2 && Math.abs(z - fluidCandidate.getZ()) <= 2) return true;
return protectedVolume.test(new BlockPos(x, y, z));
}
@Override public boolean exteriorAir(int x, int y, int z, int dx, int dz) {
if (Math.abs(dx) + Math.abs(dz) != 1 || y < 1 || y >= 380 || !owns(x, z)) return false;
for (int distance = 0; distance <= 32; distance++) {
int px = x + dx * distance, pz = z + dz * distance;
if (!owns(px, pz)) return false; // Never infer exterior air in another present or future owner.
var sample = column(px, pz);
if (sample == null) return false;
for (int dy = -3; dy < 3; dy++) if (!sample.getBlock(y + dy).isAir()
|| protectedVolume.test(new BlockPos(px, y + dy, pz))) return false;
}
return true;
}
@Override public boolean owns(int x, int z) { return island.contains(x, z); }
public int columnsUsed() { return columns.size(); }
}
@@ -19,7 +19,7 @@ public final class HeritageCommands19 {
var level = source.getLevel(); var level = source.getLevel();
if (!Level.OVERWORLD.equals(level.dimension()) if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator) || !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20) || !generator.experimentalStructures()) { || (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21) || !generator.experimentalStructures()) {
source.sendFailure(Component.translatable("sanctuary.heritage.unavailable")); source.sendFailure(Component.translatable("sanctuary.heritage.unavailable"));
return 0; return 0;
} }
@@ -28,6 +28,11 @@ public final class HeritageCommands19 {
for (var site : plan.sites()) source.sendSuccess(() -> Component.translatable("sanctuary.heritage.entry", for (var site : plan.sites()) source.sendSuccess(() -> Component.translatable("sanctuary.heritage.entry",
Component.translatable("sanctuary.heritage.kind." + site.kind().name().toLowerCase(Locale.ROOT)), Component.translatable("sanctuary.heritage.kind." + site.kind().name().toLowerCase(Locale.ROOT)),
site.entry().x(), site.entry().y(), site.entry().z()), false); site.entry().x(), site.entry().y(), site.entry().z()), false);
if (generator.rootGeneration() == 21) for (var module : generator.transitPlan21().modules()) {
if (module.kind() != fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind.ACCESS) continue;
var at = fr.koka.sanctuary.worldgen.transit.TransitPiece21.locator(module);
source.sendSuccess(() -> Component.translatable("sanctuary.transit.entry", at.getX(), at.getY(), at.getZ()), false);
}
return plan.sites().size(); 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 schema21 geometry. Reload never needs the planner or a distant route destination. */
public final class HeritagePiece21 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 HeritagePiece21(HeritagePlan19.Site site) {
super(HeritageStructures19.PIECE21, 0, bounds(site));
this.site = site; this.segment = null; setOrientation(null);
}
public HeritagePiece21(HeritagePlan19.Segment segment) {
super(HeritageStructures19.PIECE21, 0, bounds(segment));
this.site = null; this.segment = segment; setOrientation(null);
}
public HeritagePiece21(StructurePieceSerializationContext context, CompoundTag tag) {
super(HeritageStructures19.PIECE21, 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", 21); 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") != 21 || !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()) HeritageRenderer21.render(site, writer);
else HeritageRenderer21.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,616 @@
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;
/** Generation21 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 HeritagePlanner21 {
public static final int MAX_COLUMNS = HeritagePlan19.MAX_COLUMNS, MAX_SAMPLES = HeritagePlan19.MAX_SAMPLES;
// Local17x17 sites may cut8 blocks, with the same at-most8-block slab and certified short piers.
// Three-wide paths may cut4/fill5; grade stays one block, and water/old construction is never waived.
public static final int MAX_SITE_CUT = 8, MAX_ROUTE_CUT = 4, MAX_ROUTE_FILL = 5;
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 HeritagePlanner21() {}
public interface Terrain extends HeritagePlanner19.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("Heritage21 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));
var requests = new ArrayList<>(requests(sites, landmarks).stream()
.filter(r -> endpoints.containsKey(r.from()) && endpoints.containsKey(r.to())).toList());
var components = new Components(endpoints.keySet());
// Multiple exterior entries into the certified connected city represent one already walkable destination.
String firstCity = null;
for (var landmark : landmarks) if (landmark.kind() == LandmarkKind.CITY) {
if (firstCity == null) firstCity = landmark.id(); else components.join(firstCity, landmark.id());
}
Request longFreight = requests.stream().filter(r -> r.kind() == RouteKind.FREIGHT
&& span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry()) >= diameter / 4)
.max(Comparator.comparingInt(r -> span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry()))).orElse(null);
var siteIds = new HashSet<String>(); sites.forEach(site -> siteIds.add(site.id()));
var addressed = new HashSet<String>();
var destinationAttempts = new HashMap<String, Integer>();
int attempted = 0;
while (!requests.isEmpty() && attempted < 36 && links.size() < 24) {
// Give each isolated site a first useful route before spending its columns on a cycle.
// Repeated city gates already belong to one walkable component; no redundant freight bypass is needed.
requests.removeIf(r -> components.same(r.from(), r.to()));
if (requests.isEmpty()) break;
requests.sort(Comparator.comparingInt((Request r) -> isolatedSites(r, siteIds, addressed) > 0
&& isolatedAttempts(r, siteIds, addressed, destinationAttempts) == 0 ? 0 : 1)
.thenComparingInt(r -> span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry()))
.thenComparingInt(r -> r.kind() == RouteKind.FREIGHT ? 0 : 1).thenComparing(Request::id));
var request = attempted == 0 && longFreight != null ? longFreight : requests.getFirst(); requests.remove(request);
attempted++;
int distance = Math.abs(endpoints.get(request.from()).entry().x() - endpoints.get(request.to()).entry().x())
+ Math.abs(endpoints.get(request.from()).entry().z() - endpoints.get(request.to()).entry().z());
boolean longAxis = span(endpoints.get(request.from()).entry(), endpoints.get(request.to()).entry()) >= diameter / 4;
int isolated = isolatedSites(request, siteIds, addressed);
var waiting = new HashSet<String>();
for (var next : requests) for (var id : List.of(next.from(), next.to()))
if (siteIds.contains(id) && !addressed.contains(id) && !id.equals(request.from()) && !id.equals(request.to())) waiting.add(id);
int reserve = Math.min(Math.min(waiting.size(), 8), 36 - attempted) * 100;
int allowance = longAxis && attempted == 1 ? 3_800 : isolated > 0 ? Math.clamp(distance * 5 + 350, 900, 2_400) : 900;
probe.columnLimit = Math.min(MAX_COLUMNS - reserve, probe.columns.size() + allowance);
probe.sampleLimit = Math.min(MAX_SAMPLES - reserve * 20, probe.samples + (longAxis ? 150_000 : 80_000));
for (var id : List.of(request.from(), request.to())) if (siteIds.contains(id) && !addressed.contains(id))
destinationAttempts.merge(id, 1, Integer::sum);
Endpoint from = endpoints.get(request.from()), to = endpoints.get(request.to());
int beforeRouteColumns = probe.columns.size();
var beforeReasons = new HashMap<>(routeReasons);
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) + rejectionDelta(beforeReasons, routeReasons, probe.routeBlock)); continue; }
var groups = new HashSet<SegmentKey>();
cells.values().forEach(c -> groups.add(new SegmentKey(c.request(), c.cell().x() >> 4, c.cell().z() >> 4)));
route.cells().values().stream().filter(c -> !cells.containsKey(key(c.x(), c.z())))
.forEach(c -> groups.add(new SegmentKey(request, c.x() >> 4, c.z() >> 4)));
if (groups.size() > 512) { routeResults.put(request.id(), "refused; native-piece-budget"); continue; }
links.add(new Link(request.id(), request.from(), request.to(), request.kind(), route.path()));
components.join(request.from(), request.to());
addressed.add(request.from()); addressed.add(request.to());
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) + rejectionDelta(beforeReasons, routeReasons, probe.routeBlock)); }
}
for (var site : sites) {
var incident = links.stream().filter(l -> l.fromId().equals(site.id()) || l.toId().equals(site.id())).map(Link::id).toList();
var attempts = new LinkedHashMap<String, String>();
routeResults.forEach((id, value) -> { if (id.contains(site.id())) attempts.put(id, value); });
diagnostics.put(site.kind(), diagnostics.get(site.kind()) + "; connected=" + !incident.isEmpty()
+ "; accepted-links=" + incident + "; route-attempts=" + attempts);
}
var segments = segments(seed, cells);
if (Boolean.getBoolean("sanctuary.test.heritage21") || Boolean.getBoolean("sanctuary.heritage.plannerDiagnostics"))
System.getLogger(HeritagePlanner21.class.getName()).log(System.Logger.Level.INFO,
"Heritage21 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 > MAX_SITE_CUT + 7) { 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));
}
// The square envelope and bearing grid have already been certified. A difficult front slope
// should try another real facade before rejecting the entire building; no terrain is weakened.
for (int turn : new int[]{0, 1, 3, 2}) {
int facing = (candidate.facing() + turn) & 3;
int ex = facing == 1 ? x + 17 : facing == 3 ? x - 1 : x + 8;
int ez = facing == 2 ? z + 17 : facing == 0 ? z - 1 : z + 8;
Entry entry = new Entry(ex, floor + 1, ez, facing);
Site result = new Site(id(kind), kind, x, floor, z, 17, 17, height, facing,
mix(seed ^ candidate.seed()), foundation, supports, entry);
var landings = landing(result.entry(), previous, probe);
if (landings == null) { probe.reject("entry-landing"); continue; }
boolean collision = false;
for (var cell : landings) {
OwnedCell old = existing.get(key(cell.x(), cell.z()));
if (old != null && old.cell().floorY() != cell.floorY()) { collision = true; break; }
}
if (collision) { probe.reject("landing-collision"); continue; }
if (turn != 0) probe.reject("admitted-other-facade");
return new SiteDraft(result, landings);
}
return null;
}
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 LinkedHashMap<String, Request>();
var endpoints = new ArrayList<Endpoint>();
sites.forEach(s -> endpoints.add(new Endpoint(s.id(), s.entry(), false)));
landmarks.forEach(l -> endpoints.add(new Endpoint(l.id(), l.entry(), true)));
var freight = new HashSet<String>();
for (var kind : new Kind[] {Kind.FREIGHT_STATION, Kind.COMMUNAL_DEPOT, Kind.RAIL_WORKSHOP, Kind.PUMP_STATION}) {
var site = find(sites, kind); if (site != null) freight.add(site.id());
}
landmarks.stream().filter(l -> l.kind() == LandmarkKind.MINE).forEach(l -> freight.add(l.id()));
for (var a : endpoints) {
// Four alternatives per entrance tolerate one protected cut without a quadratic terrain search.
var nearest = endpoints.stream().filter(b -> !a.id().equals(b.id()))
.sorted(Comparator.comparingInt((Endpoint b) -> span(a.entry(), b.entry())).thenComparing(Endpoint::id)).limit(4).toList();
for (var b : nearest) addRequest(result, a, b, freight.contains(a.id()) && freight.contains(b.id()) ? RouteKind.FREIGHT : RouteKind.PATH);
}
for (var pair : List.of(new Kind[]{Kind.FREIGHT_STATION, Kind.COMMUNAL_DEPOT},
new Kind[]{Kind.FREIGHT_STATION, Kind.RAIL_WORKSHOP}, new Kind[]{Kind.COMMUNAL_DEPOT, Kind.PUMP_STATION})) {
var a = find(sites, pair[0]); var b = find(sites, pair[1]);
if (a != null && b != null) addRequest(result, new Endpoint(a.id(), a.entry(), false), new Endpoint(b.id(), b.entry(), false), RouteKind.FREIGHT);
}
return List.copyOf(result.values());
}
private static void addRequest(Map<String, Request> result, Endpoint a, Endpoint b, RouteKind kind) {
if (a.id().compareTo(b.id()) > 0) { var swap = a; a = b; b = swap; }
String key = a.id() + "__" + b.id(); var old = result.get(key);
if (old == null || kind == RouteKind.FREIGHT) result.put(key, new Request("graph_" + key, a.id(), b.id(), kind));
}
private static String rejectionDelta(Map<String, Integer> before, Map<String, Integer> after, XZ obstacle) {
var changes = new java.util.TreeMap<String, Integer>();
after.forEach((key, count) -> { int difference = count - before.getOrDefault(key, 0); if (difference > 0) changes.put(key, difference); });
return "; reasons=" + changes + (obstacle == null ? "" : "; last-column=" + obstacle.x() + "," + obstacle.z());
}
private static int isolatedSites(Request request, Set<String> siteIds, Set<String> addressed) {
return (siteIds.contains(request.from()) && !addressed.contains(request.from()) ? 1 : 0)
+ (siteIds.contains(request.to()) && !addressed.contains(request.to()) ? 1 : 0);
}
private static int isolatedAttempts(Request request, Set<String> siteIds, Set<String> addressed, Map<String, Integer> attempts) {
int result = Integer.MAX_VALUE;
for (var id : List.of(request.from(), request.to())) if (siteIds.contains(id) && !addressed.contains(id))
result = Math.min(result, attempts.getOrDefault(id, 0));
return result == Integer.MAX_VALUE ? 0 : result;
}
private static final class Components {
final Map<String, String> parents = new HashMap<>();
Components(Set<String> ids) { for (var id : ids) parents.put(id, id); }
String root(String id) { String parent = parents.get(id); while (!parent.equals(id)) { id = parent; parent = parents.get(id); } return parent; }
boolean same(String a, String b) { return root(a).equals(root(b)); }
void join(String a, String b) { parents.put(root(b), root(a)); }
}
/** 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) > MAX_ROUTE_CUT + 1 || 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 + MAX_ROUTE_FILL);
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 - MAX_ROUTE_FILL || 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,770 @@
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 fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
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;
/** Generation21 restrained architectural rendition of the immutable generation19 site geometry. 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 HeritageRenderer21 {
private HeritageRenderer21() {}
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 buffer = ConnectedBlocks20.buffer(writer);
var b = new Builder(site, buffer); 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++)
buffer.set(support.x() - site.minX(), y - site.floorY(), support.z() - site.minZ(),
y == support.minY() ? Blocks.CHISELED_TUFF_BRICKS.defaultBlockState() : Blocks.STONE_BRICKS.defaultBlockState());
buffer.flush();
}
public static void renderSegment(HeritagePlan19.Segment segment, LostCityRenderer.BlockWriter writer) {
var buffer = ConnectedBlocks20.buffer(writer);
var occupied = new java.util.HashSet<Long>();
for (var cell : segment.cells()) occupied.add(columnKey(cell.x(), cell.z()));
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_BRICKS;
if (y == floor) {
boolean edge = false;
for (Direction d : new Direction[] {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}) {
int nx = cell.x() + d.getStepX(), nz = cell.z() + d.getStepZ();
// Unknown neighboring chunks are continuations, not artificial crosswise curbs.
if ((nx >> 4) == segment.startChunkX() && (nz >> 4) == segment.startChunkZ()
&& !occupied.contains(columnKey(nx, nz))) edge = true;
}
if (cell.rail() != HeritagePlan19.Rail.NONE) material = Math.floorMod(cell.x() + cell.z(), 3) == 0 ? Blocks.OAK_PLANKS : Blocks.GRAVEL;
else if (edge) material = Math.floorMod(cell.x() + cell.z(), 8) == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS;
else material = LostCityPalette.roll(segment.seed(), cell.x(), 0, cell.z(), 5) == 0 ? Blocks.TUFF : Blocks.POLISHED_TUFF;
}
buffer.set(x, y, z, material.defaultBlockState());
}
for (int y = floor + 1; y <= floor + 3; y++) buffer.set(x, y, z, Blocks.AIR.defaultBlockState());
if (cell.railPresent()) buffer.set(x, floor + 1, z, Blocks.RAIL.defaultBlockState()
.setValue(BlockStateProperties.RAIL_SHAPE, RailShape.valueOf(cell.rail().name())));
}
buffer.flush();
}
private static long columnKey(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
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), palette(state, x, y, z).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_BRICKS.defaultBlockState() : y == 0
? (x == 0 || z == 0 || x == 16 || z == 16 ? Blocks.TUFF_BRICKS
: x >= 7 && x <= 9 ? Blocks.POLISHED_TUFF
: LostCityPalette.roll(s.seed(), x, 0, z, 7) == 0 ? Blocks.TUFF : Blocks.ANDESITE).defaultBlockState()
: 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();
}
architecture();
roofs21();
}
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 ? floorMaterial().defaultBlockState() : y == ceiling ? Blocks.SMOOTH_STONE.defaultBlockState()
: wall ? wallMaterial(y - floor, ceiling - floor).defaultBlockState() : Blocks.AIR.defaultBlockState());
}
}
Block floorMaterial() {
return switch (s.kind()) {
case POST_OFFICE, EXPEDITION_OFFICE, RADIO_RELAY, CONSTRUCTION_REFUGE -> Blocks.OAK_PLANKS;
case PHOTO_CLUB, GREENHOUSE -> Blocks.BIRCH_PLANKS;
case IMPOSSIBLE_SERVICE -> Blocks.POLISHED_DIORITE;
default -> Blocks.POLISHED_ANDESITE;
};
}
Block wallMaterial(int y, int height) {
if (y == 1) return Blocks.STONE_BRICKS;
if (y == height - 1) return Blocks.ANDESITE;
return s.kind() == HeritagePlan19.Kind.GREENHOUSE ? Blocks.BIRCH_PLANKS : Blocks.SMOOTH_STONE;
}
BlockState palette(BlockState state, int x, int y, int z) {
Block original = state.getBlock(), replacement = original;
boolean birch = s.kind() == HeritagePlan19.Kind.GREENHOUSE;
if (original == Blocks.DARK_OAK_PLANKS || original == Blocks.OAK_PLANKS || original == Blocks.BIRCH_PLANKS)
replacement = birch ? Blocks.BIRCH_PLANKS : Blocks.OAK_PLANKS;
else if (original == Blocks.OAK_WOOD || original == Blocks.BIRCH_WOOD || original == Blocks.STRIPPED_OAK_WOOD)
replacement = birch ? Blocks.BIRCH_WOOD : Blocks.OAK_WOOD;
else if (original == Blocks.OAK_LOG || original == Blocks.BIRCH_LOG || original == Blocks.STRIPPED_OAK_LOG)
replacement = birch ? Blocks.STRIPPED_BIRCH_LOG : Blocks.STRIPPED_OAK_LOG;
else if (original == Blocks.DARK_OAK_SLAB || original == Blocks.OAK_SLAB || original == Blocks.BIRCH_SLAB)
replacement = birch ? Blocks.BIRCH_SLAB : Blocks.OAK_SLAB;
else if (original == Blocks.DARK_OAK_STAIRS || original == Blocks.OAK_STAIRS || original == Blocks.BIRCH_STAIRS)
replacement = birch ? Blocks.BIRCH_STAIRS : Blocks.OAK_STAIRS;
else if (original == Blocks.DARK_OAK_FENCE || original == Blocks.OAK_FENCE || original == Blocks.BIRCH_FENCE)
replacement = birch ? Blocks.BIRCH_FENCE : Blocks.OAK_FENCE;
else if (original == Blocks.DARK_OAK_DOOR || original == Blocks.OAK_DOOR || original == Blocks.BIRCH_DOOR)
replacement = birch ? Blocks.BIRCH_DOOR : Blocks.OAK_DOOR;
else if (original == Blocks.DARK_OAK_TRAPDOOR || original == Blocks.OAK_TRAPDOOR || original == Blocks.BIRCH_TRAPDOOR)
replacement = birch ? Blocks.BIRCH_TRAPDOOR : Blocks.OAK_TRAPDOOR;
else if (original == Blocks.GRANITE || original == Blocks.POLISHED_GRANITE || original == Blocks.DIORITE
|| original == Blocks.POLISHED_DIORITE || original == Blocks.POLISHED_ANDESITE)
replacement = original == Blocks.POLISHED_ANDESITE ? Blocks.ANDESITE : Blocks.SMOOTH_STONE;
else if (original == Blocks.POLISHED_GRANITE_SLAB || original == Blocks.POLISHED_DIORITE_SLAB
|| original == Blocks.POLISHED_ANDESITE_SLAB) replacement = Blocks.SMOOTH_STONE_SLAB;
if (y > 0) {
if (original == Blocks.TUFF || original == Blocks.POLISHED_TUFF) replacement = Blocks.SMOOTH_STONE;
else if (original == Blocks.TUFF_BRICKS) replacement = Blocks.STONE_BRICKS;
else if (original == Blocks.CHISELED_TUFF || original == Blocks.CHISELED_TUFF_BRICKS) replacement = Blocks.CHISELED_STONE_BRICKS;
else if (original == Blocks.TUFF_BRICK_SLAB) replacement = Blocks.SMOOTH_STONE_SLAB;
else if (original == Blocks.TUFF_BRICK_STAIRS) replacement = Blocks.STONE_BRICK_STAIRS;
else if (original == Blocks.TUFF_BRICK_WALL) replacement = Blocks.STONE_BRICK_WALL;
}
if ((original == Blocks.STONE_BRICKS || original == Blocks.CRACKED_STONE_BRICKS
|| original == Blocks.MOSSY_STONE_BRICKS) && y > 1 && y % 5 != 0)
replacement = Blocks.SMOOTH_STONE;
if (replacement == Blocks.ANDESITE && y > 0 && Math.floorMod(x + z, 4) == 0)
replacement = Blocks.STONE;
// Quiet, reproducible weathering follows construction joints, never random holes in access floors.
if (replacement == Blocks.STONE_BRICKS && y > 0) {
int weather = LostCityPalette.roll(s.seed(), x, y, z, 23);
if (weather == 0) replacement = Blocks.CRACKED_STONE_BRICKS;
else if (weather == 1 && y <= 3) replacement = Blocks.MOSSY_STONE_BRICKS;
}
if (replacement == original) return state;
var mapped = replacement.defaultBlockState();
for (var property : state.getProperties()) mapped = copyProperty(mapped, state, property);
return mapped;
}
private static <T extends Comparable<T>> BlockState copyProperty(BlockState destination, BlockState source,
net.minecraft.world.level.block.state.properties.Property<T> property) {
return destination.hasProperty(property) ? destination.setValue(property, source.getValue(property)) : destination;
}
// These additions compose each use separately; all thresholds and maintenance passages stay in the saved footprint.
void architecture() {
switch (s.kind()) {
case FREIGHT_STATION -> stationArchitecture();
case POST_OFFICE -> postArchitecture();
case COMMUNAL_DEPOT -> depotArchitecture();
case EXPEDITION_OFFICE -> expeditionArchitecture();
case OBSERVATORY -> observatoryArchitecture();
case RAIL_WORKSHOP -> railwayArchitecture();
case PUMP_STATION -> pumpingArchitecture();
case SUBSTATION -> substationArchitecture();
case GREENHOUSE -> greenhouseArchitecture();
case VOID_DOCK -> dockArchitecture();
case RADIO_RELAY -> radioArchitecture();
case PHOTO_CLUB -> photoArchitecture();
case IMPOSSIBLE_SERVICE -> impossibleArchitecture();
case CONSTRUCTION_REFUGE -> refugeArchitecture();
}
}
void slab(int x, int y, int z, Block block) { set(x, y, z, block.defaultBlockState().setValue(BlockStateProperties.SLAB_TYPE, net.minecraft.world.level.block.state.properties.SlabType.BOTTOM)); }
void stair(int x, int y, int z, Block block, Direction direction, Half half) {
set(x, y, z, block.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, direction).setValue(BlockStateProperties.HALF, half));
}
/** Cut only the four exterior corner cells; the two adjacent wall faces keep the interior sealed. */
void bevel(int x0, int z0, int x1, int z1, int low, int high) {
for (int x : new int[] {x0, x1}) for (int z : new int[] {z0, z1}) {
box(x, low, z, x, high, z, Blocks.AIR);
set(x, low - 1, z, Blocks.CHISELED_STONE_BRICKS);
}
}
void rim(int x0, int z0, int x1, int z1, int y, Block material) {
for (int x = x0; x <= x1; x++) { slab(x, y, z0, material); slab(x, y, z1, material); }
for (int z = z0 + 1; z < z1; z++) { slab(x0, y, z, material); slab(x1, y, z, material); }
}
void screen(int x0, int z, int x1, int y) {
for (int x = x0; x <= x1; x++) set(x, y, z, Blocks.BIRCH_TRAPDOOR.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH).setValue(BlockStateProperties.OPEN, true));
}
void stationArchitecture() {
bevel(4, 3, 12, 11, 1, 6);
box(4, 7, 3, 12, 10, 11, Blocks.AIR);
rim(3, 2, 13, 12, 6, Blocks.TUFF_BRICK_SLAB);
for (int z = 5; z <= 9; z++) for (int x = 5; x <= 11; x++) {
if (x == 5 || x == 11 || z == 5 || z == 9) box(x, 7, z, x, 8, z, Blocks.GLASS_PANE);
slab(x, 9, z, Blocks.POLISHED_DIORITE_SLAB);
}
// Open the former ceiling beneath the sealed roof glazing. The middle beam
// still supports the existing hanging lantern and joins the two hall walls.
for (int z = 6; z <= 8; z++) for (int x = 6; x <= 10; x++) {
if (z != 7) set(x, 6, z, Blocks.AIR);
set(x, 9, z, Blocks.GLASS);
}
// The long cantilever reads as a train platform, distinct from the glazed passenger hall.
box(0, 5, 4, 3, 5, 12, Blocks.OAK_PLANKS);
for (int z : new int[] {4, 8, 12}) stair(0, 4, z, Blocks.TUFF_BRICK_STAIRS, Direction.EAST, Half.TOP);
for (int z : new int[] {5, 11}) box(1, 1, z, 1, 4, z, Blocks.OAK_WOOD);
for (int x : new int[] {4, 12}) box(x, 2, 4, x, 4, 4, Blocks.CHISELED_TUFF_BRICKS);
screen(5, 4, 6, 3); box(5, 0, 4, 11, 0, 10, Blocks.POLISHED_DIORITE);
box(7, 0, 3, 9, 0, 10, Blocks.BIRCH_PLANKS);
rim(9, 11, 15, 15, 4, Blocks.TUFF_BRICK_SLAB);
}
void postArchitecture() {
bevel(2, 2, 14, 14, 1, 6); rim(1, 1, 15, 15, 6, Blocks.POLISHED_GRANITE_SLAB);
box(4, 7, 4, 12, 7, 12, Blocks.TUFF_BRICKS);
rim(4, 4, 12, 12, 8, Blocks.TUFF_BRICK_SLAB);
for (int x : new int[] {2, 7, 9, 14}) {
box(x, 3, 1, x, 4, 1, Blocks.POLISHED_DIORITE);
stair(x, 5, 1, Blocks.TUFF_BRICK_STAIRS, Direction.SOUTH, Half.TOP);
}
box(5, 4, 1, 11, 4, 1, Blocks.CHISELED_TUFF_BRICKS);
box(4, 1, 6, 6, 1, 6, Blocks.BIRCH_PLANKS); screen(4, 6, 6, 3);
for (int z = 9; z <= 12; z++) { set(3, 3, z, Blocks.OAK_TRAPDOOR.defaultBlockState().setValue(BlockStateProperties.HALF, Half.TOP)); }
for (int x = 4; x <= 12; x++) set(x, 0, 5, x % 2 == 0 ? Blocks.POLISHED_GRANITE : Blocks.OAK_PLANKS);
box(10, 0, 8, 13, 0, 9, Blocks.DIORITE);
}
void depotArchitecture() {
bevel(1, 2, 15, 14, 1, 9); box(1, 10, 2, 15, 14, 14, Blocks.AIR);
// Three industrial north-light roof bays, not another pitched house roof.
for (int z : new int[] {3, 7, 11}) {
box(2, 10, z, 14, 11, z, Blocks.GLASS_PANE);
for (int x = 2; x <= 14; x++) {
slab(x, 12, z, Blocks.TUFF_BRICK_SLAB);
stair(x, 11, z + 1, Blocks.TUFF_BRICK_STAIRS, Direction.NORTH, Half.BOTTOM);
stair(x, 10, z + 2, Blocks.TUFF_BRICK_STAIRS, Direction.NORTH, Half.BOTTOM);
if (x > 2 && x < 14) {
// Both transparent faces close the north-light well, while the
// retained roof outside these bands keeps the warehouse sealed.
set(x, 9, z, Blocks.GLASS);
set(x, 12, z, Blocks.GLASS);
}
}
}
for (int z : new int[] {3, 7, 11}) for (int x : new int[] {1, 15}) box(x, 1, z, x, 9, z, Blocks.POLISHED_ANDESITE);
for (int x : new int[] {6, 10}) { box(x, 1, 1, x, 3, 1, Blocks.CHISELED_TUFF); stair(x, 4, 1, Blocks.TUFF_BRICK_STAIRS, Direction.SOUTH, Half.TOP); }
box(6, 4, 1, 10, 4, 1, Blocks.TUFF_BRICKS);
for (int z : new int[] {5, 8}) {
// Thin wooden covers sit directly on the barrel stack and the lower packing bench.
set(4, 3, z, Blocks.OAK_TRAPDOOR.defaultBlockState().setValue(BlockStateProperties.HALF, Half.BOTTOM));
set(12, 2, z, Blocks.OAK_TRAPDOOR.defaultBlockState().setValue(BlockStateProperties.HALF, Half.BOTTOM));
}
for (int z = 3; z <= 9; z++) { set(6, 0, z, Blocks.TUFF_BRICKS); set(10, 0, z, Blocks.TUFF_BRICKS); }
box(2, 5, 11, 9, 5, 13, Blocks.BIRCH_PLANKS);
}
void expeditionArchitecture() {
bevel(2, 2, 12, 12, 1, 6); rim(1, 1, 13, 13, 6, Blocks.TUFF_BRICK_SLAB);
box(3, 7, 3, 6, 9, 7, Blocks.BIRCH_WOOD); rim(3, 3, 6, 7, 10, Blocks.TUFF_BRICK_SLAB);
box(4, 8, 3, 5, 8, 3, Blocks.GLASS_PANE);
// Sheltered map table, archive wing and broad shaded rear balcony.
box(7, 4, 1, 9, 4, 1, Blocks.CHISELED_TUFF_BRICKS);
for (int x : new int[] {2, 12}) box(x, 2, 3, x, 5, 3, Blocks.OAK_WOOD);
screen(3, 4, 5, 3); box(3, 3, 9, 5, 3, 9, Blocks.OAK_PLANKS);
for (int x = 3; x <= 14; x++) for (int z = 13; z <= 15; z++) set(x, 0, z, x % 3 == 0 ? Blocks.OAK_WOOD : Blocks.OAK_PLANKS);
for (int x = 3; x <= 14; x++) slab(x, 5, 14, Blocks.BIRCH_SLAB);
box(3, 1, 14, 3, 4, 14, Blocks.BIRCH_FENCE); box(14, 1, 14, 14, 4, 14, Blocks.BIRCH_FENCE);
}
void observatoryArchitecture() {
// Eight faceted buttresses below a broken observatory dome with an open central viewing slot.
for (int[] p : new int[][] {{3, 8}, {5, 4}, {11, 4}, {13, 8}, {11, 12}, {5, 12}}) {
box(p[0], 1, p[1], p[0], 11, p[1], Blocks.DIORITE);
set(p[0], 5, p[1], Blocks.CHISELED_TUFF_BRICKS);
stair(p[0], 12, p[1], Blocks.TUFF_BRICK_STAIRS, p[0] < 8 ? Direction.EAST : Direction.WEST, Half.BOTTOM);
}
for (int z = 4; z <= 12; z++) for (int x = 4; x <= 12; x++) {
int distance = Math.max(Math.abs(x - 8), Math.abs(z - 8));
if (distance == 4) set(x, 15, z, Blocks.GLASS_PANE);
if (distance == 3 && x != 8) slab(x, 17, z, Blocks.TUFF_BRICK_SLAB);
}
for (int x : new int[] {4, 12}) for (int z : new int[] {5, 11}) box(x, 13, z, x, 14, z, Blocks.CHISELED_TUFF_BRICKS);
// Four short ribs visibly carry the inset crown; the central viewing slot stays open.
for (int x : new int[] {4, 12}) for (int z : new int[] {5, 11}) {
box(x, 15, z, x, 16, z, Blocks.CHISELED_TUFF_BRICKS);
stair(x < 8 ? x + 1 : x - 1, 16, z, Blocks.TUFF_BRICK_STAIRS,
x < 8 ? Direction.EAST : Direction.WEST, Half.BOTTOM);
}
box(7, 0, 5, 9, 0, 11, Blocks.BIRCH_PLANKS);
box(7, 6, 5, 9, 6, 10, Blocks.POLISHED_DIORITE);
}
void railwayArchitecture() {
bevel(1, 2, 15, 14, 1, 6); box(1, 7, 2, 15, 10, 14, Blocks.AIR);
// Broad barrel-vault ribs sit above the two inspection tracks.
for (int z = 2; z <= 14; z++) for (int x = 1; x <= 15; x++) {
int rise = Math.min(3, Math.min(x - 1, 15 - x));
if (z % 4 == 2) set(x, 6 + rise, z, Blocks.TUFF_BRICKS);
else if (x >= 6 && x <= 10) set(x, 6 + rise, z, Blocks.GLASS);
else slab(x, 6 + rise, z, Blocks.TUFF_BRICK_SLAB);
}
for (int z : new int[] {3, 7, 11}) for (int x : new int[] {1, 15}) box(x, 2, z, x, 5, z, Blocks.GRANITE);
box(7, -2, 5, 9, -2, 11, Blocks.POLISHED_ANDESITE);
for (int z = 6; z <= 11; z++) { set(7, -1, z, Blocks.CHISELED_TUFF_BRICKS); set(9, -1, z, Blocks.CHISELED_TUFF_BRICKS); }
// Side ledges leave the one-block maintenance aisle and its stairs clear.
screen(2, 3, 3, 3); box(2, 3, 11, 4, 3, 11, Blocks.OAK_PLANKS);
}
void pumpingArchitecture() {
bevel(2, 3, 9, 12, 1, 6); rim(1, 2, 9, 13, 6, Blocks.TUFF_BRICK_SLAB);
for (int x : new int[] {10, 15}) for (int z : new int[] {5, 11}) {
box(x, 1, z, x, 7, z, Blocks.CHISELED_TUFF_BRICKS);
set(x, 8, z, Blocks.POLISHED_ANDESITE);
}
rim(9, 4, 16, 12, 13, Blocks.TUFF_BRICK_SLAB);
for (int x = 10; x <= 15; x++) for (int z = 5; z <= 11; z++) slab(x, 14, z, Blocks.POLISHED_ANDESITE_SLAB);
box(3, 7, 5, 3, 12, 5, Blocks.TUFF_BRICK_WALL);
box(3, 3, 6, 3, 5, 8, Blocks.IRON_BARS);
box(3, 0, 4, 8, 0, 9, Blocks.ANDESITE); box(5, 0, 4, 7, 0, 10, Blocks.POLISHED_DIORITE);
screen(3, 11, 4, 3);
}
void substationArchitecture() {
bevel(10, 4, 15, 10, 1, 5); rim(9, 3, 16, 15, 5, Blocks.TUFF_BRICK_SLAB);
for (int z : new int[] {2, 6, 10, 14}) for (int x : new int[] {1, 15}) {
set(x, 0, z, Blocks.STONE_BRICKS); set(x, 1, z, Blocks.CHISELED_TUFF_BRICKS);
}
box(2, 4, 5, 5, 4, 5, Blocks.IRON_BARS); box(2, 4, 8, 5, 4, 8, Blocks.IRON_BARS);
for (int x : new int[] {2, 5}) for (int z : new int[] {5, 8}) box(x, 1, z, x, 3, z, Blocks.TUFF_BRICK_WALL);
box(11, 6, 5, 14, 6, 9, Blocks.POLISHED_TUFF);
for (int z = 5; z <= 9; z += 2) screen(11, z, 14, 7);
for (int z = 3; z <= 8; z++) set(7, 0, z, Blocks.GRANITE);
box(6, -2, 10, 7, -2, 13, Blocks.CHISELED_TUFF_BRICKS);
}
void greenhouseArchitecture() {
// Tall birch ribs and a steep glass ridge give the glasshouse an agricultural silhouette.
for (int z = 2; z <= 14; z++) for (int x = 1; x <= 11; x++) {
int rise = Math.min(3, Math.min(x - 1, 11 - x));
if (z == 2 || z == 8 || z == 14 || x == 6) set(x, 6 + rise, z, Blocks.BIRCH_WOOD);
else set(x, 6 + rise, z, Blocks.GLASS);
}
for (int z : new int[] {3, 7, 11}) { box(1, 1, z, 1, 5, z, Blocks.BIRCH_WOOD); box(11, 1, z, 11, 5, z, Blocks.BIRCH_WOOD); }
rim(11, 9, 15, 15, 5, Blocks.TUFF_BRICK_SLAB);
box(12, 3, 10, 14, 3, 10, Blocks.OAK_PLANKS); screen(12, 10, 14, 4);
for (int z = 3; z <= 13; z++) set(6, 0, z, z % 3 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.POLISHED_TUFF);
}
void dockArchitecture() {
for (int x = 1; x <= 15; x++) for (int z = 0; z <= 15; z++) set(x, 0, z, x % 4 == 0 ? Blocks.OAK_WOOD : Blocks.OAK_PLANKS);
bevel(2, 3, 7, 10, 1, 5); rim(1, 2, 8, 15, 5, Blocks.BIRCH_SLAB);
for (int z : new int[] {2, 8, 14}) {
box(0, 1, z, 0, 2, z, Blocks.CHISELED_TUFF_BRICKS);
box(16, 1, z, 16, 2, z, Blocks.CHISELED_TUFF_BRICKS);
slab(0, 3, z, Blocks.TUFF_BRICK_SLAB); slab(16, 3, z, Blocks.TUFF_BRICK_SLAB);
}
// Brackets and a separated customs roof leave the departure platform open to the void.
for (int x = 2; x <= 7; x++) for (int z = 3; z <= 15; z++) if (z % 4 == 0) slab(x, 6, z, Blocks.BIRCH_SLAB);
stair(11, 9, 5, Blocks.OAK_STAIRS, Direction.EAST, Half.TOP);
stair(13, 9, 5, Blocks.OAK_STAIRS, Direction.WEST, Half.TOP);
screen(3, 4, 6, 3);
}
void radioArchitecture() {
bevel(2, 3, 10, 12, 1, 6); rim(1, 2, 10, 13, 6, Blocks.TUFF_BRICK_SLAB);
for (int x = 2; x <= 10; x++) for (int z = 3; z <= 12; z++) {
int roofY = 7 + Math.min(2, Math.min(x - 2, 10 - x));
slab(x, roofY, z, x <= 5 ? Blocks.BIRCH_SLAB : Blocks.TUFF_BRICK_SLAB);
}
box(9, 7, 4, 9, 11, 4, Blocks.CHISELED_TUFF_BRICKS);
for (int y : new int[] {10, 14, 18}) { box(12, y, 8, 14, y, 8, Blocks.IRON_BARS); box(13, y, 7, 13, y, 9, Blocks.IRON_BARS); }
box(3, 0, 8, 5, 0, 11, Blocks.BIRCH_PLANKS); screen(7, 4, 9, 3);
}
void photoArchitecture() {
bevel(2, 2, 14, 14, 1, 6); rim(1, 1, 15, 15, 6, Blocks.POLISHED_DIORITE_SLAB);
box(3, 7, 9, 13, 7, 13, Blocks.TUFF_BRICKS); rim(3, 9, 13, 13, 8, Blocks.TUFF_BRICK_SLAB);
for (int x : new int[] {3, 6, 10, 13}) stair(x, 4, 1, Blocks.TUFF_BRICK_STAIRS, Direction.SOUTH, Half.TOP);
box(3, 5, 1, 13, 5, 1, Blocks.CHISELED_TUFF_BRICKS);
for (int x = 4; x <= 12; x++) set(x, 0, 4, x % 2 == 0 ? Blocks.POLISHED_GRANITE : Blocks.BIRCH_PLANKS);
box(3, 0, 9, 7, 0, 13, Blocks.POLISHED_ANDESITE);
screen(10, 5, 12, 2); box(3, 3, 10, 3, 3, 12, Blocks.OAK_PLANKS);
}
void impossibleArchitecture() {
bevel(1, 7, 15, 15, 1, 10); rim(0, 6, 16, 16, 10, Blocks.TUFF_BRICK_SLAB);
box(4, 11, 9, 12, 11, 13, Blocks.TUFF_BRICKS);
for (int x : new int[] {4, 12}) for (int z : new int[] {9, 13}) set(x, 12, z, Blocks.CHISELED_TUFF);
// A cramped civil facade gives way to an over-scaled, deliberately ceremonial service hall.
for (int x : new int[] {3, 11}) box(x, 1, 1, x, 4, 1, Blocks.CHISELED_TUFF_BRICKS);
box(3, 5, 1, 11, 5, 1, Blocks.TUFF_BRICKS);
for (int x : new int[] {3, 6, 10, 13}) { set(x, 1, 11, Blocks.CHISELED_TUFF_BRICKS); set(x, 8, 11, Blocks.CHISELED_TUFF_BRICKS); }
screen(4, 3, 4, 3); box(12, 3, 14, 14, 3, 14, Blocks.BIRCH_PLANKS);
}
void refugeArchitecture() {
bevel(5, 6, 13, 14, 5, 10);
box(5, 11, 6, 13, 14, 14, Blocks.AIR);
for (int z = 6; z <= 14; z++) for (int x = 5; x <= 13; x++) {
int rise = Math.min(3, (x - 5) / 2);
slab(x, 11 + rise, z, (z & 1) == 0 ? Blocks.BIRCH_SLAB : Blocks.TUFF_BRICK_SLAB);
}
for (int x : new int[] {3, 14}) for (int z : new int[] {5, 15}) { box(x, 1, z, x, 3, z, Blocks.CHISELED_TUFF_BRICKS); set(x, 4, z, Blocks.OAK_WOOD); }
for (int z = 6; z <= 14; z += 4) box(4, 1, z, 4, 7, z, Blocks.IRON_BARS);
box(2, 1, 3, 3, 10, 4, Blocks.TUFF_BRICKS); box(2, 11, 3, 14, 11, 4, Blocks.OAK_WOOD);
// Upper lockers and a birch sleeping corner avoid the saved hatch and its complete ladder shaft.
screen(11, 7, 12, 7); box(7, 4, 7, 11, 4, 9, Blocks.BIRCH_PLANKS);
}
/** Replacement roofs: one weather skin, connected rafters and retained lamp-bearing beams.
* Clearing starts at the old roof, never at a floor, furnishing or standing threshold. */
void roofs21() {
switch (s.kind()) {
case RAIL_WORKSHOP -> {
vaultRoof(1, 2, 15, 14, 6, false, true);
box(1, 6, 8, 15, 6, 8, Blocks.OAK_WOOD); // existing workshop lamp
// Two missing panes expose an intact neighbouring rafter, not a floating roof fragment.
box(7, 9, 4, 8, 9, 4, Blocks.AIR);
}
case GREENHOUSE -> {
vaultRoof(1, 2, 11, 14, 6, true, true);
box(1, 6, 5, 11, 6, 5, Blocks.BIRCH_WOOD); // existing aisle lamp
}
case COMMUNAL_DEPOT -> sawtoothRoof();
case RADIO_RELAY -> {
vaultRoof(2, 3, 10, 12, 6, true, false);
box(2, 6, 6, 10, 6, 6, Blocks.OAK_WOOD);
box(2, 6, 10, 10, 6, 10, Blocks.OAK_WOOD);
box(9, 7, 4, 9, 11, 4, Blocks.STONE_BRICKS);
}
case CONSTRUCTION_REFUGE -> {
shedRoof(5, 6, 13, 14, 10);
box(5, 10, 8, 13, 10, 8, Blocks.OAK_WOOD); // lamp above the shared sleeping room
}
case POST_OFFICE -> box(5, 7, 5, 11, 7, 11, Blocks.AIR); // a parapet, not a second slab ceiling
case PHOTO_CLUB -> box(4, 7, 10, 12, 7, 12, Blocks.AIR);
default -> { } // Other roofs already have a single supported slab or a sealed working tank.
}
}
void vaultRoof(int x0, int z0, int x1, int z1, int eave, boolean timber, boolean glazed) {
box(x0, eave, z0, x1, s.height(), z1, Blocks.AIR);
int mid = (z0 + z1) / 2;
for (int z = z0; z <= z1; z++) for (int x = x0; x <= x1; x++) {
int top = vaultY(x, x0, x1, eave);
int adjoining = Math.min(vaultY(Math.max(x0, x - 1), x0, x1, eave),
vaultY(Math.min(x1, x + 1), x0, x1, eave));
boolean rib = z == z0 || z == mid || z == z1;
Block skin = rib ? Blocks.OAK_WOOD : glazed && x > x0 + 2 && x < x1 - 2
? Blocks.GLASS : timber ? Blocks.OAK_PLANKS : Blocks.SMOOTH_STONE;
// This short riser joins neighbouring height steps by a face, not just a diagonal corner.
box(x, Math.min(top, adjoining), z, x, top, z, skin);
if (z == z0 || z == z1) box(x, eave, z, x, top, z,
glazed && x > x0 + 1 && x < x1 - 1 ? Blocks.GLASS : Blocks.OAK_WOOD);
}
// Truss feet sit on the existing two long walls; no columns are introduced in an aisle.
box(x0, eave, mid, x1, eave, mid, Blocks.OAK_WOOD);
for (int x : new int[] {x0, x1}) box(x, eave, z0, x, eave, z1, Blocks.OAK_WOOD);
}
int vaultY(int x, int x0, int x1, int eave) {
return eave + Math.min(3, Math.min(x - x0, x1 - x));
}
void shedRoof(int x0, int z0, int x1, int z1, int eave) {
box(x0, eave, z0, x1, s.height(), z1, Blocks.AIR);
for (int z = z0; z <= z1; z++) for (int x = x0; x <= x1; x++) {
int top = eave + (x - x0) / 3;
int previous = eave + Math.max(0, x - 1 - x0) / 3;
box(x, previous, z, x, top, z,
z == z0 || z == z1 || z == (z0 + z1) / 2 ? Blocks.OAK_WOOD : Blocks.OAK_PLANKS);
if (z == z0 || z == z1 || x == x1) box(x, eave, z, x, top, z, Blocks.OAK_WOOD);
}
}
void sawtoothRoof() {
box(1, 9, 2, 15, s.height(), 14, Blocks.AIR);
for (int z = 2; z <= 14; z++) for (int x = 1; x <= 15; x++) {
int phase = Math.floorMod(z - 2, 4), top = z == 14 ? 9 : 12 - phase;
boolean frame = x == 1 || x == 15;
if (phase == 0 && z != 14) box(x, 9, z, x, top, z, frame ? Blocks.OAK_WOOD : Blocks.GLASS);
else box(x, Math.max(9, top - 1), z, x, top, z,
frame ? Blocks.OAK_WOOD : phase == 1 ? Blocks.GLASS : Blocks.SMOOTH_STONE);
if (frame) box(x, 9, z, x, top, z, Blocks.OAK_WOOD);
}
box(1, 9, 6, 15, 9, 6, Blocks.OAK_WOOD); // carries the original hall lamp
// Local, readable storm damage. The perimeter frame and remaining panes still share their bearings.
box(12, 10, 11, 13, 11, 11, Blocks.AIR);
}
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);
}
}
}
@@ -14,7 +14,7 @@ public final class HeritageStructure19 extends Structure {
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) { @Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator) if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20) || !generator.experimentalStructures()) return Optional.empty(); || (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21) || !generator.experimentalStructures()) return Optional.empty();
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z()); var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
if (island == null || !island.origin() || (island.generation() != generator.rootGeneration())) return Optional.empty(); if (island == null || !island.origin() || (island.generation() != generator.rootGeneration())) return Optional.empty();
var plan = generator.heritagePlan19(); var plan = generator.heritagePlan19();
@@ -23,9 +23,9 @@ public final class HeritageStructure19 extends Structure {
// Local route segments render before buildings. No piece needs a distant link endpoint loaded. // Local route segments render before buildings. No piece needs a distant link endpoint loaded.
for (var segment : plan.segments()) for (var segment : plan.segments())
if (segment.startChunkX() == x && segment.startChunkZ() == z) if (segment.startChunkX() == x && segment.startChunkZ() == z)
pieces.add(generator.rootGeneration() == 20 ? new HeritagePiece20(segment) : new HeritagePiece19(segment)); pieces.add(generator.rootGeneration() == 21 ? new HeritagePiece21(segment) : generator.rootGeneration() == 20 ? new HeritagePiece20(segment) : new HeritagePiece19(segment));
for (var site : plan.sites()) if (site.startChunkX() == x && site.startChunkZ() == z) for (var site : plan.sites()) if (site.startChunkX() == x && site.startChunkZ() == z)
pieces.add(generator.rootGeneration() == 20 ? new HeritagePiece20(site) : new HeritagePiece19(site)); pieces.add(generator.rootGeneration() == 21 ? new HeritagePiece21(site) : generator.rootGeneration() == 20 ? new HeritagePiece20(site) : new HeritagePiece19(site));
if (pieces.isEmpty()) return Optional.empty(); if (pieces.isEmpty()) return Optional.empty();
for (var piece : pieces) { for (var piece : pieces) {
var box = piece.getBoundingBox(); var box = piece.getBoundingBox();
@@ -35,7 +35,7 @@ public final class HeritageStructure19 extends Structure {
|| Math.abs((long) Math.floorDiv(box.maxZ(), 16) - z) > 7) || Math.abs((long) Math.floorDiv(box.maxZ(), 16) - z) > 7)
throw new IllegalStateException("Heritage piece exceeds its local native reference contract"); throw new IllegalStateException("Heritage piece exceeds its local native reference contract");
} }
var locator = pieces.stream().filter(piece -> piece instanceof HeritagePiece19 old && old.isSite() || piece instanceof HeritagePiece20 detail && detail.isSite()).findFirst().orElse(pieces.getFirst()).getLocatorPosition(); var locator = pieces.stream().filter(piece -> piece instanceof HeritagePiece19 old && old.isSite() || piece instanceof HeritagePiece20 detail && detail.isSite() || piece instanceof HeritagePiece21 current && current.isSite()).findFirst().orElse(pieces.getFirst()).getLocatorPosition();
return Optional.of(new GenerationStub(locator, builder -> pieces.forEach(builder::addPiece))); return Optional.of(new GenerationStub(locator, builder -> pieces.forEach(builder::addPiece)));
} }
@Override public StructureType<?> type() { return HeritageStructures19.TYPE; } @Override public StructureType<?> type() { return HeritageStructures19.TYPE; }
@@ -35,6 +35,8 @@ public final class HeritageStructures19 {
SanctuaryMod.id("heritage_networks_v19"), () -> HeritageStructure19.CODEC); SanctuaryMod.id("heritage_networks_v19"), () -> HeritageStructure19.CODEC);
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE, public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
SanctuaryMod.id("heritage_piece_v19"), HeritagePiece19::new); SanctuaryMod.id("heritage_piece_v19"), HeritagePiece19::new);
public static final StructurePieceType PIECE21 = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
SanctuaryMod.id("heritage_piece_v21"), HeritagePiece21::new);
public static final StructurePieceType PIECE20 = Registry.register(BuiltInRegistries.STRUCTURE_PIECE, public static final StructurePieceType PIECE20 = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
SanctuaryMod.id("heritage_piece_v20"), HeritagePiece20::new); SanctuaryMod.id("heritage_piece_v20"), HeritagePiece20::new);
private static final ThreadLocal<Integer> PLACEMENT_DEPTH = ThreadLocal.withInitial(() -> 0); private static final ThreadLocal<Integer> PLACEMENT_DEPTH = ThreadLocal.withInitial(() -> 0);
@@ -55,7 +57,7 @@ public final class HeritageStructures19 {
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk, ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> level) { StructureTemplateManager templates, ResourceKey<Level> level) {
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures() if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 19 && generator.rootGeneration() != 20) || !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21)
|| !sanctuary.experimentalStructures()) return; || !sanctuary.experimentalStructures()) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z()); var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return; if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
@@ -77,7 +79,7 @@ public final class HeritageStructures19 {
BlockPos origin, int radius, boolean skipReferenced) { BlockPos origin, int radius, boolean skipReferenced) {
if (!Level.OVERWORLD.equals(level.dimension()) if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator) || !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20) || !generator.experimentalStructures() || (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21) || !generator.experimentalStructures()
|| !level.structureManager().shouldGenerateStructures()) return null; || !level.structureManager().shouldGenerateStructures()) return null;
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY); Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
if (!structures.contains(holder)) return null; if (!structures.contains(holder)) return null;
@@ -99,7 +101,7 @@ public final class HeritageStructures19 {
if (start == null || !start.isValid() || skipReferenced && !start.canBeReferenced()) continue; if (start == null || !start.isValid() || skipReferenced && !start.canBeReferenced()) continue;
if (skipReferenced) level.structureManager().addReference(start); if (skipReferenced) level.structureManager().addReference(start);
var entrance = start.getPieces().stream().filter(p -> (p instanceof HeritagePiece19 site && site.isSite()) var entrance = start.getPieces().stream().filter(p -> (p instanceof HeritagePiece19 site && site.isSite())
|| (p instanceof HeritagePiece20 detail && detail.isSite())).findFirst().orElseThrow(); || (p instanceof HeritagePiece20 detail && detail.isSite()) || (p instanceof HeritagePiece21 current && current.isSite())).findFirst().orElseThrow();
return Pair.of(entrance.getLocatorPosition(), holder); return Pair.of(entrance.getLocatorPosition(), holder);
} }
return null; return null;
@@ -0,0 +1,113 @@
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 HeritageTerrain21 implements HeritagePlanner21.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 HeritageTerrain21(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;
for (var district : city.districts()) {
int districtEntries = 0;
districtLoop: for (var parcel : district.parcels()) {
if (parcel.kind() != LostCityPlan.Kind.STREET || parcel.width() < 3 || parcel.depth() < 3) continue;
for (int direction = 0; direction < 4; direction++) {
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 (++districtEntries == 2 || result.size() >= 16) break districtLoop;
}
}
if (result.size() >= 16) break;
}
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,7 +13,7 @@ public final class RuinsStructure17 extends Structure {
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) { @Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator) if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20) || !generator.experimentalStructures()) return Optional.empty(); || (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21) || !generator.experimentalStructures()) return Optional.empty();
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z()); var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return Optional.empty(); if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return Optional.empty();
var networks = generator.ruinsPlan17().networks().stream() var networks = generator.ruinsPlan17().networks().stream()
@@ -30,9 +30,9 @@ public final class RuinsStructure17 extends Structure {
return Optional.of(new GenerationStub(RuinsStructures17.locator(networks.getFirst()), pieces -> { return Optional.of(new GenerationStub(RuinsStructures17.locator(networks.getFirst()), pieces -> {
// Shafts come last so their surface hatches cannot be filled by a connected room. // Shafts come last so their surface hatches cannot be filled by a connected room.
networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() != RuinsPlan17.Kind.ACCESS) networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() != RuinsPlan17.Kind.ACCESS)
.forEach(r -> pieces.addPiece(generator.rootGeneration() == 20 ? new RuinsPiece20(r) : new RuinsPiece17(r)))); .forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS) networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS)
.forEach(r -> pieces.addPiece(generator.rootGeneration() == 20 ? new RuinsPiece20(r) : new RuinsPiece17(r)))); .forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
})); }));
} }
@Override public StructureType<?> type() { return RuinsStructures17.TYPE; } @Override public StructureType<?> type() { return RuinsStructures17.TYPE; }
@@ -55,7 +55,7 @@ public final class RuinsStructures17 {
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk, ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> level) { StructureTemplateManager templates, ResourceKey<Level> level) {
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures() if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20) || !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21)
|| !sanctuary.experimentalStructures()) return; || !sanctuary.experimentalStructures()) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z()); var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return; if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
@@ -78,7 +78,7 @@ public final class RuinsStructures17 {
BlockPos origin, int radius, boolean skipReferenced) { BlockPos origin, int radius, boolean skipReferenced) {
if (!Level.OVERWORLD.equals(level.dimension()) if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator) || !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20) || !generator.experimentalStructures() || (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21) || !generator.experimentalStructures()
|| !level.structureManager().shouldGenerateStructures()) return null; || !level.structureManager().shouldGenerateStructures()) return null;
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY); Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
if (!structures.contains(holder)) return null; if (!structures.contains(holder)) return null;
@@ -0,0 +1,114 @@
package fr.koka.sanctuary.worldgen.transit;
import java.util.ArrayList;
import java.util.Set;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.ResourceKey;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.loot.LootTable;
/** Saved generation21 room geometry is rendered without replanning or reading neighboring chunks. */
public final class TransitPiece21 extends StructurePiece {
private static final Set<String> MODULE_KEYS = Set.of("Schema", "Kind", "MinX", "MinY", "MinZ", "Width",
"Height", "Depth", "Facing", "Openings", "ModuleSeed", "Supports");
private static final Set<String> SUPPORT_KEYS = Set.of("X", "MinY", "Z", "MaxY");
private final TransitPlan21.Module room;
public TransitPiece21(TransitPlan21.Module room) {
super(TransitStructures21.PIECE, 0, bounds(room));
this.room = room;
setOrientation(null);
}
public TransitPiece21(StructurePieceSerializationContext context, CompoundTag tag) {
super(TransitStructures21.PIECE, tag);
room = decodeModule(tag.getCompound("SanctuaryModule")
.orElseThrow(() -> new IllegalArgumentException("Missing saved transit room")));
if (!getBoundingBox().equals(bounds(room)))
throw new IllegalArgumentException("Transit room disagrees with its saved native bounding box");
setOrientation(null);
}
public TransitPlan21.Module module() { return room; }
@Override public BlockPos getLocatorPosition() { return locator(room); }
public static BlockPos locator(TransitPlan21.Module room) {
return new BlockPos(room.minX() + room.width() / 2,
room.kind() == TransitPlan21.Kind.ACCESS ? room.surfaceY() + 1 : room.floorY() + 1,
room.kind() == TransitPlan21.Kind.ACCESS ? room.minZ() : room.minZ() + room.depth() / 2);
}
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
tag.put("SanctuaryModule", encodeModule(room));
}
public static CompoundTag encodeModule(TransitPlan21.Module room) {
var tag = new CompoundTag(); tag.putInt("Schema", 1); tag.putString("Kind", room.kind().name());
tag.putInt("MinX", room.minX()); tag.putInt("MinY", room.minY()); tag.putInt("MinZ", room.minZ());
tag.putInt("Width", room.width()); tag.putInt("Height", room.height()); tag.putInt("Depth", room.depth());
tag.putInt("Facing", room.facing()); tag.putInt("Openings", room.openings()); tag.putLong("ModuleSeed", room.seed());
var supports = new ListTag();
for (var support : room.supports()) {
var value = new CompoundTag(); value.putInt("X", support.x()); value.putInt("MinY", support.minY());
value.putInt("Z", support.z()); value.putInt("MaxY", support.maxY()); supports.add(value);
}
tag.put("Supports", supports);
return tag;
}
public static TransitPlan21.Module decodeModule(CompoundTag tag) {
if (!tag.keySet().equals(MODULE_KEYS)) throw new IllegalArgumentException("Incomplete or unknown transit room fields");
if (integer(tag, "Schema") != 1) throw new IllegalArgumentException("Unsupported transit room schema");
var kind = TransitPlan21.Kind.valueOf(tag.getString("Kind").orElseThrow(() -> new IllegalArgumentException("Missing room kind")));
type(tag, "ModuleSeed", Tag.TAG_LONG); type(tag, "Supports", Tag.TAG_LIST);
var saved = tag.getList("Supports").orElseThrow();
if (saved.size() > 64) throw new IllegalArgumentException("Too many saved transit room supports");
var supports = new ArrayList<TransitPlan21.Support>();
for (var entry : saved) {
if (!(entry instanceof CompoundTag value) || !value.keySet().equals(SUPPORT_KEYS))
throw new IllegalArgumentException("Invalid transit room support fields");
supports.add(new TransitPlan21.Support(integer(value, "X"), integer(value, "MinY"), integer(value, "Z"), integer(value, "MaxY")));
}
return new TransitPlan21.Module(kind, integer(tag, "MinX"), integer(tag, "MinY"), integer(tag, "MinZ"),
integer(tag, "Width"), integer(tag, "Height"), integer(tag, "Depth"), integer(tag, "Facing"), integer(tag, "Openings"),
tag.getLong("ModuleSeed").orElseThrow(), supports);
}
private static int integer(CompoundTag tag, String key) {
type(tag, key, Tag.TAG_INT);
return tag.getInt(key).orElseThrow();
}
private static void type(CompoundTag tag, String key, byte id) {
if (tag.get(key) == null || tag.get(key).getId() != id)
throw new IllegalArgumentException("Invalid saved transit room field: " + key);
}
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
TransitStructures21.placing(() -> TransitRenderer21.render(room, new LostCityRenderer.BlockWriter() {
private BlockPos world(int x, int y, int z) { return new BlockPos(room.minX() + x, room.minY() + y, room.minZ() + z); }
@Override public void set(int x, int y, int z, BlockState block) {
var position = world(x, y, z);
if (getBoundingBox().isInside(position) && clip.isInside(position))
placeBlock(level, block, position.getX(), position.getY(), position.getZ(), clip);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
var position = world(x, y, z);
if (getBoundingBox().isInside(position) && clip.isInside(position))
createChest(level, clip, RandomSource.create(seed), position, loot, null);
}
}));
}
public static BoundingBox bounds(TransitPlan21.Module room) {
int minY = room.minY(), maxY = Math.addExact(room.minY(), room.height() - 1);
for (var support : room.supports()) {
minY = Math.min(minY, support.minY()); maxY = Math.max(maxY, support.maxY());
}
return new BoundingBox(room.minX(), minY, room.minZ(), Math.addExact(room.minX(), room.width() - 1),
maxY, Math.addExact(room.minZ(), room.depth() - 1));
}
}
@@ -0,0 +1,86 @@
package fr.koka.sanctuary.worldgen.transit;
import java.util.HashSet;
import java.util.List;
/** Immutable final geometry. Each module owns a small native start, never an island-wide start. */
public record TransitPlan21(List<Module> modules, int samples, String diagnostic) {
public static final int MAX_SAMPLES = 3_000_000;
public static final TransitPlan21 EMPTY = new TransitPlan21(List.of(), 0, "disabled");
public enum Kind { GALLERY, JUNCTION, HALL, ACCESS }
public record Support(int x, int minY, int z, int maxY) {
public Support {
if (minY < 8 || maxY > 375 || minY > maxY || maxY - minY >= 16)
throw new IllegalArgumentException("Invalid transit footing");
}
}
/** World-aligned bounds. Openings N1/E2/S4/W8 are seven wide, local y2..8.
* Access top deck is maxY-4. Its north gate, at centerX/minZ, has a certified natural approach. */
public record Module(Kind kind, int minX, int minY, int minZ, int width, int height, int depth,
int facing, int openings, long seed, List<Support> supports) {
public Module {
supports = List.copyOf(supports);
if (kind == null || width < 9 || width > 25 || depth < 9 || depth > 25
|| (width & 1) == 0 || (depth & 1) == 0 || height < 10 || height > 160
|| minY < 16 || minY + height > 383 || facing < 0 || facing > 3
|| openings < 1 || openings > 15 || supports.size() > 64)
throw new IllegalArgumentException("Invalid transit module");
if (kind == Kind.ACCESS && (width != 17 || depth != 17 || height < 20))
throw new IllegalArgumentException("Transit stair access needs its complete 17x17 shaft");
if (kind != Kind.ACCESS && height != 10) throw new IllegalArgumentException("Unexpected gallery height");
var seen = new HashSet<Long>();
for (var s : supports) if (s.x() < minX || s.x() >= minX + width || s.z() < minZ
|| s.z() >= minZ + depth || s.maxY() != minY - 1 || !seen.add(key(s.x(), s.z())))
throw new IllegalArgumentException("Transit footing does not belong to its floor");
}
public int maxX() { return minX + width - 1; }
public int maxY() { return minY + height - 1; }
public int maxZ() { return minZ + depth - 1; }
public int centerX() { return minX + width / 2; }
public int centerZ() { return minZ + depth / 2; }
public int floorY() { return minY + 1; }
public int surfaceY() { return maxY() - 4; }
public int startChunkX() { return centerX() >> 4; }
public int startChunkZ() { return centerZ() >> 4; }
public int boundsMinY() { return supports.stream().mapToInt(Support::minY).min().orElse(minY); }
public boolean contains(int x, int y, int z) {
if (x < minX || x > maxX() || z < minZ || z > maxZ()) return false;
if (y >= minY && y <= maxY()) return true;
return supports.stream().anyMatch(s -> x == s.x() && z == s.z() && y >= s.minY() && y <= s.maxY());
}
public boolean protects(int x, int y, int z) {
if (contains(x, y, z)) return true;
if (kind == Kind.ACCESS && x >= centerX() - 2 && x <= centerX() + 2
&& z >= minZ - 3 && z < minZ && y >= surfaceY() - 2 && y <= surfaceY() + 3) return true;
// Two untouched layers carry the slab and the certified short piers.
if (x >= minX && x <= maxX() && z >= minZ && z <= maxZ() && y >= minY - 2 && y < minY) return true;
return supports.stream().anyMatch(s -> x == s.x() && z == s.z() && y >= s.minY() - 2 && y < s.minY());
}
}
public TransitPlan21 {
modules = List.copyOf(modules);
if (modules.size() > 64 || samples < 0 || samples > MAX_SAMPLES || diagnostic == null || diagnostic.length() > 1024)
throw new IllegalArgumentException("Invalid transit planning result");
if (!modules.isEmpty() && modules.stream().filter(m -> m.kind() == Kind.ACCESS).count() != 2)
throw new IllegalArgumentException("A through-gallery needs two surface accesses");
for (int i = 0; i < modules.size(); i++) for (int j = i + 1; j < modules.size(); j++) {
var a = modules.get(i); var b = modules.get(j);
if (a.minX() <= b.maxX() && a.maxX() >= b.minX() && a.minZ() <= b.maxZ() && a.maxZ() >= b.minZ())
throw new IllegalArgumentException("Transit modules overlap");
}
}
public boolean isEmpty() { return modules.isEmpty(); }
public List<Module> modulesForChunk(int x, int z) {
return modules.stream().filter(m -> m.startChunkX() == x && m.startChunkZ() == z).toList();
}
public boolean contains(int x, int y, int z) { return modules.stream().anyMatch(m -> m.contains(x, y, z)); }
public boolean protects(int x, int y, int z) { return modules.stream().anyMatch(m -> m.protects(x, y, z)); }
public boolean intersectsProtection(int x0, int y0, int z0, int x1, int y1, int z1) {
for (var m : modules) if (m.kind() == Kind.ACCESS && x0 <= m.centerX() + 2 && x1 >= m.centerX() - 2
&& z0 < m.minZ() && z1 >= m.minZ() - 3 && y0 <= m.surfaceY() + 3 && y1 >= m.surfaceY() - 2) return true;
for (var m : modules) if (x0 <= m.maxX() && x1 >= m.minX() && z0 <= m.maxZ() && z1 >= m.minZ()
&& y0 <= m.maxY() && y1 >= m.boundsMinY() - 2) return true;
return false;
}
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
}
@@ -0,0 +1,220 @@
package fr.koka.sanctuary.worldgen.transit;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Module;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Support;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/** A bounded read-only through-route. Failed candidates never publish partial corridors or shafts. */
public final class TransitPlanner21 {
public static final int MAX_COLUMNS = 12_000;
private TransitPlanner21() {}
public interface Terrain {
boolean owns(int x, int z);
boolean solid(int x, int y, int z);
boolean protectedAt(int x, int y, int z);
int surfaceY(int x, int z);
default String obstruction(int x, int y, int z) { return "protected terrain"; }
}
public static TransitPlan21 plan(long seed, int centerX, int centerZ, int diameter, Terrain terrain) {
if (diameter < 512 || diameter > 1024) throw new IllegalArgumentException("Unsupported transit island diameter");
var probe = new Probe(terrain);
int count = (int) Math.ceil(diameter * .42 / 17);
if ((count & 1) == 0) count++;
String last = "no dry supported through-route";
var choices = new ArrayList<Choice>();
// Reserve most of the native-column budget for the exact scan of selected routes.
probe.sampleLimit = 1_800_000; probe.columnLimit = 7_500;
try {
search: for (int advance : new int[]{0, -34, 34}) for (boolean alongX : new boolean[]{true, false}) for (int shift : new int[]{-diameter / 8, diameter / 8, -diameter / 16, diameter / 16, 0, -3 * diameter / 16, 3 * diameter / 16}) {
int cx = centerX + (alongX ? advance : shift), cz = centerZ + (alongX ? shift : advance);
for (int floor : new int[]{216, 200, 224, 208, 184, 232, 160}) {
var route = candidate(seed, cx, cz, count, floor, alongX, probe, false);
String label = "axis=" + (alongX ? "east-west" : "north-south") + " offset=" + shift + " advance=" + advance + " floor=" + floor;
if (route == null) {
last = probe.reason;
diagnostic("preselection reject " + label + " " + last + " columns=" + probe.columns.size());
continue;
}
int cost = route.stream().mapToInt(m -> m.supports().stream().mapToInt(pier -> pier.maxY() - pier.minY() + 1).sum()
+ (m.kind() == Kind.ACCESS ? m.surfaceY() - m.floorY() : 0)).sum();
// Try the intermediate levels first; within each level prefer the better supported axis.
int heightOrder = switch (floor) { case 216 -> 0; case 200 -> 1; case 224 -> 2; case 208 -> 3; case 184 -> 4; case 232 -> 5; default -> 6; };
choices.add(new Choice(route, floor, alongX, heightOrder * 100_000 + cost, label));
diagnostic("preselection admitted " + label + " bearing_cost=" + cost);
if (choices.size() >= 8) break search;
}
}
} catch (BudgetExceeded exhausted) { diagnostic("prescreen budget reached; retain " + choices.size() + " candidates"); }
probe.sampleLimit = TransitPlan21.MAX_SAMPLES; probe.columnLimit = MAX_COLUMNS;
choices.sort(java.util.Comparator.comparingInt(Choice::cost));
try {
for (var choice : choices) {
var finalRoute = new ArrayList<Module>();
for (var rough : choice.route()) {
var complete = certify(rough, probe, true);
if (complete == null) { last = probe.reason; break; }
finalRoute.add(complete);
}
if (finalRoute.size() != choice.route().size()) {
diagnostic("exact reject " + choice.label() + " " + last); continue;
}
addBranch(finalRoute, choice.floor(), choice.alongX(), seed, probe);
return new TransitPlan21(finalRoute, probe.samples, "accepted " + choice.label()
+ " span=" + ((count + 1) * 17) + " columns=" + probe.columns.size()
+ " candidates=" + choices.size() + " hall=" + finalRoute.stream().anyMatch(m -> m.kind() == Kind.HALL));
}
} catch (BudgetExceeded exhausted) { last = "bounded exact search exhausted: " + probe.reason; }
return new TransitPlan21(List.of(), probe.samples, last + "; candidates=" + choices.size() + "; columns=" + probe.columns.size());
}
private record Choice(List<Module> route, int floor, boolean alongX, int cost, String label) {}
private static List<Module> candidate(long seed, int cx, int cz, int count, int floor,
boolean alongX, Probe probe, boolean full) {
int first = -(count + 1) * 17 / 2;
int ax = cx + (alongX ? first : 0), az = cz + (alongX ? 0 : first);
int bx = cx - (alongX ? first : 0), bz = cz - (alongX ? 0 : first);
// Cheap route witnesses reject deep obstacles before two tall shafts are scanned in full.
for (int d = first + 17; d <= -first - 17; d += 8) for (int side : new int[]{-4, 0, 4})
for (int y : new int[]{floor - 2, floor, floor + 4, floor + 8}) {
int x = cx + (alongX ? d : side), z = cz + (alongX ? side : d);
if (probe.protectedAt(x, y, z)) {
probe.reason = "coarse route [" + x + "," + y + "," + z + "] " + probe.terrain.obstruction(x, y, z); return null;
}
}
// Certify the two indispensable exits before spending the route's column budget.
Module left = access(ax, az, floor, alongX ? 2 : 4, seed ^ 0x2101L, probe, full);
if (left == null) return null;
Module right = access(bx, bz, floor, alongX ? 8 : 1, seed ^ 0x2102L, probe, full);
if (right == null) return null;
var result = new ArrayList<Module>(); result.add(left);
int unanchoredRun = 0;
for (int n = 0; n < count; n++) {
int d = first + 17 * (n + 1);
int x = cx + (alongX ? d - 8 : -4), z = cz + (alongX ? -4 : d - 8);
var module = new Module(Kind.GALLERY, x, floor - 1, z, alongX ? 17 : 9, 10, alongX ? 9 : 17,
alongX ? 1 : 0, alongX ? 10 : 5, seed ^ (n * 0x9E3779B97F4A7C15L), List.of());
module = certify(module, probe, full);
if (module == null) return null;
unanchoredRun = probe.bearings.get(module) == 15 ? 0 : unanchoredRun + 1;
if (unanchoredRun > 2) { probe.reason = "unsupported span exceeds 34 blocks"; return null; }
result.add(module);
}
result.add(right);
return result;
}
private static Module access(int cx, int cz, int floor, int opening, long seed, Probe probe, boolean full) {
int top = -1, low = 384;
for (int x = cx - 2; x <= cx + 2; x++) for (int z = cz - 11; z <= cz - 9; z++) {
int surface = probe.surface(x, z); top = Math.max(top, surface); low = Math.min(low, surface);
}
if (low < 0 || top - low > 1) { probe.reason = "access approach relief [" + cx + "," + cz + "] " + low + ".." + top; return null; }
if (top < floor + 18 || top > floor + 145) { probe.reason = "surface access height"; return null; }
// Its visible north portico meets real ground; its three-wide approach is never an unsupported threshold.
for (int x = cx - 2; x <= cx + 2; x++) for (int z = cz - 11; z <= cz - 9; z++) {
int y = probe.surface(x, z);
if (y < top - 1 || y > top || probe.protectedAt(x, y, z)
|| probe.protectedAt(x, top + 1, z) || probe.protectedAt(x, top + 2, z)) {
probe.reason = "access approach [" + x + "," + top + "," + z + "] terrain=" + y; return null;
}
}
for (int x : new int[]{cx - 8, cx, cx + 8}) for (int z : new int[]{cz - 8, cz, cz + 8}) {
int y = probe.surface(x, z);
// The portico alone needs level terrain. A buried shaft can follow a hillside;
// its local walls and checked footings carry the deck without flattening the hill.
if (y < Math.max(floor + 8, top - 24) || y > top + 16) { probe.reason = "access hillside [" + x + "," + top + "," + z + "] terrain=" + y; return null; }
}
return certify(new Module(Kind.ACCESS, cx - 8, floor - 1, cz - 8, 17,
top - (floor - 1) + 5, 17, 0, opening, seed, List.of()), probe, full);
}
private static void addBranch(List<Module> route, int floor, boolean alongX, long seed, Probe probe) {
int index = 1 + (route.size() - 2) / 2;
var junction = route.get(index);
if (probe.bearings.getOrDefault(junction, 0) != 15) return;
for (int side : new int[]{-1, 1}) {
try {
int x = alongX ? junction.centerX() - 4 : side < 0 ? junction.minX() - 17 : junction.maxX() + 1;
int z = alongX ? side < 0 ? junction.minZ() - 17 : junction.maxZ() + 1 : junction.centerZ() - 4;
int width = alongX ? 9 : 17, depth = alongX ? 17 : 9;
var passage = certify(new Module(Kind.GALLERY, x, floor - 1, z, width, 10, depth,
alongX ? 0 : 1, alongX ? 5 : 10, seed ^ 0x21B1L ^ side, List.of()), probe, true);
if (passage == null) continue;
int hx = alongX ? junction.centerX() - 8 : side < 0 ? passage.minX() - 21 : passage.maxX() + 1;
int hz = alongX ? side < 0 ? passage.minZ() - 21 : passage.maxZ() + 1 : junction.centerZ() - 8;
int opening = alongX ? side < 0 ? 4 : 1 : side < 0 ? 2 : 8;
var hall = certify(new Module(Kind.HALL, hx, floor - 1, hz, alongX ? 17 : 21, 10,
alongX ? 21 : 17, alongX ? 0 : 1, opening, seed ^ 0x21A1L ^ side, List.of()), probe, true);
if (hall == null) continue;
int branch = alongX ? side < 0 ? 1 : 4 : side < 0 ? 8 : 2;
route.set(index, new Module(Kind.JUNCTION, junction.minX(), junction.minY(), junction.minZ(),
junction.width(), 10, junction.depth(), junction.facing(), junction.openings() | branch,
junction.seed(), junction.supports()));
route.add(passage); route.add(hall); return;
} catch (BudgetExceeded exhausted) { return; } // The already certified traversal remains complete.
}
}
private static Module certify(Module module, Probe probe, boolean full) {
var supports = new ArrayList<Support>();
int bearingQuadrants = 0;
for (int z = module.minZ();; z = Math.min(z + 4, module.maxZ())) {
for (int x = module.minX();; x = Math.min(x + 4, module.maxX())) {
if (!probe.owns(x, z)) { probe.reason = "outside island"; return null; }
if (!full) for (int y = module.minY(); y <= module.maxY(); y += 4)
if (probe.protectedAt(x, y, z)) {
probe.reason = "coarse " + module.kind() + " [" + x + "," + y + "," + z + "] " + probe.terrain.obstruction(x, y, z); return null;
}
int anchor = module.minY() - 1;
while (anchor >= module.minY() - 17 && !(probe.solid(x, anchor, z) && probe.solid(x, anchor - 1, z))) anchor--;
if (anchor >= module.minY() - 17) {
for (int y = anchor - 1; y < module.minY(); y++) if (probe.protectedAt(x, y, z)) {
probe.reason = "protected footing"; return null;
}
if (anchor < module.minY() - 1) supports.add(new Support(x, anchor + 1, z, module.minY() - 1));
bearingQuadrants |= 1 << ((x < module.centerX() ? 0 : 1) + (z < module.centerZ() ? 0 : 2));
}
if (x == module.maxX()) break;
}
if (z == module.maxZ()) break;
}
// The continuous two-block deck can span gaps between its bearings. Require
// untouched rock in each quarter, not a pier at every point above a cavity.
if (bearingQuadrants != 15 && module.kind() != Kind.GALLERY) { probe.reason = "unsupported quarters " + module.kind()
+ " [" + module.minX() + "," + module.minY() + "," + module.minZ() + "] mask=" + bearingQuadrants; return null; }
if (full) {
// Hydrology and all previous structures are checked at every edited voxel after the cheap footing test.
for (int z = module.minZ(); z <= module.maxZ(); z++) for (int x = module.minX(); x <= module.maxX(); x++) {
if (!probe.owns(x, z)) { probe.reason = "outside island"; return null; }
for (int y = module.minY() - 2; y <= module.maxY(); y++) if (probe.protectedAt(x, y, z)) {
probe.reason = "volume " + module.kind() + " [" + x + "," + y + "," + z + "] " + probe.terrain.obstruction(x, y, z); return null;
}
}
}
var certified = new Module(module.kind(), module.minX(), module.minY(), module.minZ(), module.width(), module.height(),
module.depth(), module.facing(), module.openings(), module.seed(), supports);
probe.bearings.put(certified, bearingQuadrants);
return certified;
}
private static void diagnostic(String message) {
if (Boolean.getBoolean("sanctuary.test.transit21") || Boolean.getBoolean("sanctuary.transit.plannerDiagnostics"))
System.getLogger(TransitPlanner21.class.getName()).log(System.Logger.Level.INFO, "Transit21 " + message);
}
private static final class Probe {
final Terrain terrain; final Set<Long> columns = new HashSet<>(); int samples; int sampleLimit = TransitPlan21.MAX_SAMPLES, columnLimit = MAX_COLUMNS; String reason = "budget";
final java.util.Map<Module, Integer> bearings = new java.util.HashMap<>();
Probe(Terrain terrain) { this.terrain = terrain; }
void visit(int x, int z) {
if (samples >= sampleLimit) throw new BudgetExceeded();
samples++;
long key = (long) x << 32 ^ z & 0xffffffffL;
if (!columns.contains(key) && columns.size() >= columnLimit) throw new BudgetExceeded();
columns.add(key);
}
boolean owns(int x, int z) { visit(x, z); return terrain.owns(x, z); }
boolean solid(int x, int y, int z) { visit(x, z); return terrain.solid(x, y, z); }
boolean protectedAt(int x, int y, int z) { visit(x, z); return terrain.protectedAt(x, y, z); }
int surface(int x, int z) { visit(x, z); return terrain.surfaceY(x, z); }
}
private static final class BudgetExceeded extends RuntimeException { private static final long serialVersionUID = 1L; }
}
@@ -0,0 +1,208 @@
package fr.koka.sanctuary.worldgen.transit;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Module;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.core.Direction;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.storage.loot.BuiltInLootTables;
/** A seven-wide abandoned industrial route: five roadway cells and one pavement on each side. */
public final class TransitRenderer21 {
private TransitRenderer21() {}
public record Target(String name, int x, int y, int z) {}
public static List<Target> targets(Module m) {
var result = new ArrayList<Target>();
result.add(new Target("roadway", m.width() / 2, 2, m.depth() / 2));
if ((m.openings() & 1) != 0) result.add(new Target("north_connection", m.width() / 2, 2, 0));
if ((m.openings() & 2) != 0) result.add(new Target("east_connection", m.width() - 1, 2, m.depth() / 2));
if ((m.openings() & 4) != 0) result.add(new Target("south_connection", m.width() / 2, 2, m.depth() - 1));
if ((m.openings() & 8) != 0) result.add(new Target("west_connection", 0, 2, m.depth() / 2));
if (m.kind() == Kind.ACCESS) {
result.add(new Target("surface_portico", 8, m.surfaceY() - m.minY() + 1, 0));
result.add(new Target("surface_landing", 8, m.surfaceY() - m.minY() + 1, 12));
}
if (m.kind() == Kind.HALL) {
result.add(new Target("maintenance_bench", 3, 2, 4));
result.add(new Target("shared_tool_chest", m.width() - 4, 2, m.depth() - 4));
result.add(new Target("unfinished_rock_face", m.facing() == 1 ? m.width() / 2 : 3,
2, m.facing() == 1 ? 3 : m.depth() / 2));
}
return List.copyOf(result);
}
public static void render(Module module, LostCityRenderer.BlockWriter output) {
var connected = ConnectedBlocks20.buffer(output);
new Painter(module, connected).render();
connected.flush();
}
private static final class Painter {
final Module m; final LostCityRenderer.BlockWriter out;
Painter(Module module, LostCityRenderer.BlockWriter out) { m = module; this.out = out; }
void set(int x, int y, int z, Block block) { set(x, y, z, block.defaultBlockState()); }
void set(int x, int y, int z, BlockState state) { out.set(x, y, z, state); }
void box(int x0, int y0, int z0, int x1, int y1, int z1, Block block) {
for (int x = x0; x <= x1; x++) for (int z = z0; z <= z1; z++) for (int y = y0; y <= y1; y++) set(x, y, z, block);
}
BlockState stair(Direction direction) {
return Blocks.STONE_BRICK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, direction);
}
void render() {
int w = m.width(), d = m.depth();
for (var support : m.supports()) box(support.x() - m.minX(), support.minY() - m.minY(), support.z() - m.minZ(),
support.x() - m.minX(), -1, support.z() - m.minZ(), Blocks.STONE_BRICKS);
box(0, 0, 0, w - 1, 0, d - 1, Blocks.STONE_BRICKS);
box(0, 1, 0, w - 1, 1, d - 1, Blocks.SMOOTH_STONE);
box(0, 2, 0, w - 1, m.height() - 1, d - 1, Blocks.AIR);
if (m.kind() == Kind.ACCESS) { access(); return; }
for (int x = 0; x < w; x++) for (int z = 0; z < d; z++) {
boolean edge = x == 0 || x == w - 1 || z == 0 || z == d - 1;
if (edge) for (int y = 2; y <= 8; y++) set(x, y, z,
y == 2 || y == 7 ? Blocks.TUFF_BRICKS : ((x + z) % 5 == 0 ? Blocks.STONE_BRICKS : Blocks.SMOOTH_STONE));
set(x, 9, z, Blocks.STONE_BRICKS);
boolean alongX = m.facing() == 1;
int across = alongX ? z - d / 2 : x - w / 2;
if (Math.abs(across) <= 2) set(x, 1, z, Blocks.POLISHED_ANDESITE);
else if (Math.abs(across) == 3) set(x, 2, z, Blocks.SMOOTH_STONE_SLAB);
}
openings();
if (m.kind() == Kind.JUNCTION) {
// The side branch's mouth and central crossing retain all five roadway cells.
for (int x = w / 2 - 2; x <= w / 2 + 2; x++) for (int z = 0; z < d; z++) set(x, 2, z, Blocks.AIR);
for (int z = d / 2 - 2; z <= d / 2 + 2; z++) for (int x = 0; x < w; x++) set(x, 2, z, Blocks.AIR);
}
// Ceiling ribs and luminaires are physically joined to the continuous roof slab.
for (int n = 3; n < (m.facing() == 1 ? w : d) - 2; n += 6) {
int x = m.facing() == 1 ? n : w / 2, z = m.facing() == 1 ? d / 2 : n;
set(x, 8, z, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
if (m.facing() == 1) box(x, 8, 1, x, 8, 2, Blocks.POLISHED_TUFF);
else box(1, 8, z, 2, 8, z, Blocks.POLISHED_TUFF);
}
if (m.kind() == Kind.HALL) hall();
exposedRock();
}
void openings() {
for (int y = 2; y <= 8; y++) {
if ((m.openings() & 1) != 0) box(m.width() / 2 - 3, y, 0, m.width() / 2 + 3, y, 0, Blocks.AIR);
if ((m.openings() & 4) != 0) box(m.width() / 2 - 3, y, m.depth() - 1, m.width() / 2 + 3, y, m.depth() - 1, Blocks.AIR);
if ((m.openings() & 2) != 0) box(m.width() - 1, y, m.depth() / 2 - 3, m.width() - 1, y, m.depth() / 2 + 3, Blocks.AIR);
if ((m.openings() & 8) != 0) box(0, y, m.depth() / 2 - 3, 0, y, m.depth() / 2 + 3, Blocks.AIR);
}
}
void hall() {
// Flush crossings join the work bays to the road through the raised pavements.
// They also provide an accessible route without requiring a jump over the curb.
if (m.facing() == 0) {
for (int x : new int[]{m.width() / 2 - 3, m.width() / 2 + 3})
for (int z0 : new int[]{4, m.depth() - 4}) box(x, 2, z0 - 1, x, 2, z0 + 1, Blocks.AIR);
} else {
for (int z : new int[]{m.depth() / 2 - 3, m.depth() / 2 + 3})
for (int x0 : new int[]{3, m.width() - 4}) box(x0 - 1, 2, z, x0 + 1, 2, z, Blocks.AIR);
}
// Maintenance workshop occupies side bays; the centered arrival and return remain unobstructed.
for (int z : new int[]{3, m.depth() - 4}) {
box(1, 2, z, 1, 7, z, Blocks.CHISELED_TUFF_BRICKS);
box(m.width() - 2, 2, z, m.width() - 2, 7, z, Blocks.CHISELED_TUFF_BRICKS);
box(1, 8, z, m.width() - 2, 8, z, Blocks.TUFF_BRICKS);
}
set(2, 2, 3, Blocks.CRAFTING_TABLE); set(2, 2, 4, Blocks.GRINDSTONE);
set(2, 2, 5, Blocks.FURNACE.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST));
for (int z = m.depth() - 6; z <= m.depth() - 3; z++) set(2, 2, z, Blocks.BARREL);
set(m.width() - 3, 2, 3, Blocks.CAULDRON);
out.chest(m.width() - 3, 2, m.depth() - 4, BuiltInLootTables.VILLAGE_TOOLSMITH, m.seed() ^ 0x21A1100L);
}
void exposedRock() {
// Abandoned cuts expose finite, ordinary mineable ore in the saved wall geometry.
// No ticking reward or regeneration: once mined, these blocks remain gone.
if (m.kind() != Kind.HALL && (m.kind() != Kind.GALLERY || Math.floorMod(m.seed(), 4) != 0)) return;
boolean alongX = m.facing() == 1;
int middle = alongX ? m.width() / 2 : m.depth() / 2;
Block[] ore = {Blocks.COAL_ORE, Blocks.COPPER_ORE, Blocks.IRON_ORE,
Blocks.GOLD_ORE, Blocks.REDSTONE_ORE, Blocks.LAPIS_ORE};
Block deposit = ore[Math.floorMod(m.seed() ^ 0x21CAFE, ore.length)];
for (int across = -2; across <= 2; across++) for (int y = 2; y <= 6; y++) {
int x = alongX ? middle + across : 0, z = alongX ? 0 : middle + across;
Block rock = y == 2 ? Blocks.TUFF : y >= 5 ? Blocks.STONE : Blocks.DEEPSLATE;
if (Math.abs(across) + Math.abs(y - 4) <= 1) rock = deposit;
if (m.kind() == Kind.HALL && y == 3 && across == -1) rock = Blocks.DEEPSLATE_DIAMOND_ORE;
if (m.kind() == Kind.HALL && y == 4 && across == 1) rock = Blocks.EMERALD_ORE;
set(x, y, z, rock);
}
if (m.kind() == Kind.HALL) {
// An unfinished excavation bay interrupts the masonry: irregular rock
// shoulders and a low stone arch leave its mineral face accessible.
for (int across = -3; across <= 3; across++) for (int reach = 0; reach <= 2; reach++) {
int x = alongX ? middle + across : reach, z = alongX ? reach : middle + across;
set(x, 1, z, Math.abs(across) == 3 ? Blocks.MOSSY_COBBLESTONE : Blocks.TUFF);
int ceiling = 7 + ((across + reach) & 1);
box(x, ceiling, z, x, 8, z, reach == 0 ? Blocks.DEEPSLATE : Blocks.STONE);
if (Math.abs(across) == 3) box(x, 2, z, x, 6 - reach, z,
reach == 2 ? Blocks.COBBLESTONE : Blocks.TUFF);
}
}
}
void access() {
int top = m.surfaceY() - m.minY();
// Enclosed local shaft, not a filled foundation under the whole route.
for (int n = 0; n <= 16; n++) {
box(n, 2, 0, n, top, 0, Blocks.SMOOTH_STONE);
box(n, 2, 16, n, top, 16, Blocks.SMOOTH_STONE);
box(0, 2, n, 0, top, n, Blocks.STONE_BRICKS);
box(16, 2, n, 16, top, n, Blocks.STONE_BRICKS);
}
box(1, top, 1, 15, top, 15, Blocks.SMOOTH_STONE);
// Narrow bearing walls sit beside the flights, with the bottom route crossing at z8..9.
box(2, 2, 3, 2, top - 1, 7, Blocks.STONE_BRICKS);
box(14, 2, 3, 14, top - 1, 7, Blocks.STONE_BRICKS);
openings();
int rise = top - 1;
for (int step = 1; step <= rise; step++) {
int flight = (step - 1) / 5, within = (step - 1) % 5 + 1;
boolean south = (flight & 1) == 0;
int z = south ? 2 + within : 8 - within, x0 = south ? 3 : 11, y = 1 + step;
box(x0, y, z, x0 + 2, Math.min(top + 3, y + 3), z, Blocks.AIR);
for (int x = x0; x <= x0 + 2; x++) set(x, y, z, stair(south ? Direction.SOUTH : Direction.NORTH));
if (within == 5) {
int z0 = south ? 8 : 1;
box(3, y, z0, 13, y, z0 + 1, Blocks.SMOOTH_STONE);
box(3, y + 1, z0, 13, Math.min(top + 3, y + 3), z0 + 1, Blocks.AIR);
if (y + 3 <= top) {
set(6, y + 3, z0, Blocks.SMOOTH_STONE);
set(6, y + 2, z0, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
// The bracket reaches an actual shaft wall; the last open-air landing needs no hanging lamp.
box(6, y + 3, south ? z0 : 0, 6, y + 3, south ? 16 : z0, Blocks.STONE_BRICKS);
}
}
}
// A north landing just below the surface needs a descending threshold instead
// of a slab sealing its headroom. The last flight still reaches the real top deck.
int belowNorthLanding = rise % 10;
if (belowNorthLanding >= 1 && belowNorthLanding <= 3) {
for (int z = 0; z <= belowNorthLanding; z++) for (int x = 7; x <= 9; x++) {
box(x, top - z, z, x, top + 3, z, Blocks.AIR);
set(x, top - z - 1, z, Blocks.STONE_BRICKS);
set(x, top - z, z, stair(Direction.NORTH));
}
if (belowNorthLanding == 3) {
for (int x : new int[]{6, 10}) {
set(x, top - 3, 3, Blocks.SMOOTH_STONE);
box(x, top - 2, 3, x, top - 1, 3, Blocks.AIR);
}
}
}
// A short, grounded portico identifies each naturally approachable surface gate.
for (int n = 0; n <= 16; n++) {
if (n < 6 || n > 10) set(n, top + 1, 0, Blocks.IRON_BARS);
set(n, top + 1, 16, Blocks.IRON_BARS);
set(0, top + 1, n, Blocks.IRON_BARS); set(16, top + 1, n, Blocks.IRON_BARS);
}
for (int x : new int[]{6, 10}) box(x, top + 1, 0, x, top + 3, 0, Blocks.STONE_BRICKS);
box(6, top + 4, 0, 10, top + 4, 1, Blocks.STONE_BRICK_SLAB);
}
}
}
@@ -0,0 +1,31 @@
package fr.koka.sanctuary.worldgen.transit;
import com.mojang.serialization.MapCodec;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.util.Optional;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureType;
/** A local group of neighboring modules; long routes deliberately have multiple native starts. */
public final class TransitStructure21 extends Structure {
public static final MapCodec<TransitStructure21> CODEC = simpleCodec(TransitStructure21::new);
public TransitStructure21(StructureSettings settings) { super(settings); }
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 21 || !generator.experimentalStructures()) return Optional.empty();
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
if (island == null || !island.origin() || island.generation() != 21) return Optional.empty();
var modules = generator.transitPlan21().modulesForChunk(context.chunkPos().x(), context.chunkPos().z());
if (modules.isEmpty()) return Optional.empty();
for (var module : modules) {
if (Math.abs((module.minX() >> 4) - module.startChunkX()) > 7
|| Math.abs((module.maxX() >> 4) - module.startChunkX()) > 7
|| Math.abs((module.minZ() >> 4) - module.startChunkZ()) > 7
|| Math.abs((module.maxZ() >> 4) - module.startChunkZ()) > 7)
throw new IllegalStateException("Transit module exceeds local native reference range");
}
return Optional.of(new GenerationStub(TransitPiece21.locator(modules.getFirst()), pieces ->
modules.forEach(module -> pieces.addPiece(new TransitPiece21(module)))));
}
@Override public StructureType<?> type() { return TransitStructures21.TYPE; }
}
@@ -0,0 +1,85 @@
package fr.koka.sanctuary.worldgen.transit;
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.Comparator;
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;
public final class TransitStructures21 {
public static final ResourceKey<Structure> KEY = ResourceKey.create(Registries.STRUCTURE, SanctuaryMod.id("industrial_transit"));
public static final StructureType<TransitStructure21> TYPE = Registry.register(BuiltInRegistries.STRUCTURE_TYPE,
SanctuaryMod.id("industrial_transit_v21"), () -> TransitStructure21.CODEC);
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
SanctuaryMod.id("industrial_transit_module_v21"), TransitPiece21::new);
private static final ThreadLocal<Integer> PLACEMENT_DEPTH = ThreadLocal.withInitial(() -> 0);
private TransitStructures21() {}
public static void register() {}
public static boolean isPlacing() { return PLACEMENT_DEPTH.get() > 0; }
static void placing(Runnable action) {
int previous = PLACEMENT_DEPTH.get(); PLACEMENT_DEPTH.set(previous + 1);
try { action.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() != 21
|| !sanctuary.experimentalStructures()) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || island.generation() != 21) return;
if (sanctuary.transitPlan21().modulesForChunk(chunk.getPos().x(), chunk.getPos().z()).isEmpty()) return;
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
StructureStart previous = manager.getStartForStructure(holder.value(), chunk);
if (previous != null && previous.isValid()) return;
var random = generator.randomState(island);
var sampler = random.createClimateSampler(SamplerContext.builder().enableCaches().build());
var start = holder.value().generate(holder, level, registries, generator, generator.getBiomeSource(), sampler,
random, templates, island.seed(), chunk.getPos(), previous == null ? 0 : previous.getReferences(),
chunk, holder.value().biomes()::contains);
if (start.isValid()) manager.setStartForStructure(holder.value(), start, chunk);
}
/** Locate selects a saved surface portico rather than teleporting the player into a deep road. */
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() != 21 || !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 = generator.transitPlan21().modules().stream()
.filter(m -> m.kind() == TransitPlan21.Kind.ACCESS)
.filter(m -> Math.abs((long) m.startChunkX() - (origin.getX() >> 4)) <= radius
&& Math.abs((long) m.startChunkZ() - (origin.getZ() >> 4)) <= radius)
.sorted(Comparator.comparingDouble(m -> origin.distSqr(TransitPiece21.locator(m)))).toList();
for (var module : candidates) {
var chunk = level.getChunk(module.startChunkX(), module.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
var start = level.structureManager().getStartForStructure(holder.value(), chunk);
if (start == null || !start.isValid() || skipReferenced && !start.canBeReferenced()) continue;
if (skipReferenced) level.structureManager().addReference(start);
return Pair.of(TransitPiece21.locator(module), holder);
}
return null;
}
}
@@ -0,0 +1,67 @@
package fr.koka.sanctuary.worldgen.transit;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Predicate;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.RandomState;
/** Read-only, bounded native-noise cache. Unknown columns always reject construction. */
public final class TransitTerrain21 implements TransitPlanner21.Terrain {
public static final int MAX_COLUMNS = 12_000;
private final ExpansionIsland island;
private final NoiseBasedChunkGenerator generator;
private final RandomState random;
private final LevelHeightAccessor height;
private final Predicate<BlockPos> protection;
private final Map<Long, NoiseColumn> columns = new HashMap<>();
public TransitTerrain21(ExpansionIsland island, NoiseBasedChunkGenerator generator, RandomState random,
LevelHeightAccessor height, Predicate<BlockPos> protection) {
this.island = island; this.generator = generator; this.random = random;
this.height = height; this.protection = protection;
}
private NoiseColumn column(int x, int z) {
if (!owns(x, z)) return null;
long key = ((long) x << 32) ^ (z & 0xffffffffL);
var found = columns.get(key);
if (found != null) return found;
if (columns.size() >= MAX_COLUMNS) return null;
var result = generator.getBaseColumn(x, z, height, random);
columns.put(key, result);
return result;
}
@Override public boolean owns(int x, int z) { return island.contains(x, z); }
@Override public boolean solid(int x, int y, int z) {
if (y < 0 || y >= 384) return false;
var column = column(x, z);
return column != null && column.getBlock(y).isSolid() && column.getBlock(y).getFluidState().isEmpty();
}
@Override public boolean protectedAt(int x, int y, int z) {
if (y < 0 || y >= 384) return true;
var column = column(x, z);
return column == null || !column.getBlock(y).getFluidState().isEmpty()
|| protection.test(new BlockPos(x, y, z));
}
@Override public int surfaceY(int x, int z) {
var column = column(x, z);
if (column == null) return -1;
for (int y = 380; y >= 1; y--) {
var state = column.getBlock(y);
if (!state.isAir()) return state.isSolid() && state.getFluidState().isEmpty() ? y : -1;
}
return -1;
}
@Override public String obstruction(int x, int y, int z) {
var sample = column(x, z);
if (sample == null) return "unknown/unowned column";
var state = sample.getBlock(y);
if (!state.getFluidState().isEmpty()) return "native fluid " + state;
return protection.test(new BlockPos(x, y, z)) ? "earlier structure/hydrology mask" : "none";
}
public int columnsUsed() { return columns.size(); }
}
@@ -144,7 +144,8 @@
"sanctuary.heritage.kind.photo_club": "Photography club", "sanctuary.heritage.kind.photo_club": "Photography club",
"sanctuary.heritage.kind.impossible_service": "Changing rooms and strange service rooms", "sanctuary.heritage.kind.impossible_service": "Changing rooms and strange service rooms",
"sanctuary.heritage.kind.construction_refuge": "Construction refuge", "sanctuary.heritage.kind.construction_refuge": "Construction refuge",
"sanctuary.heritage.unavailable": "This survey requires a Sanctuary alpha.19 or alpha.20 world with experimental structures enabled.", "sanctuary.heritage.unavailable": "This survey requires a recent Sanctuary world with experimental structures enabled.",
"sanctuary.heritage.header": "Abandoned infrastructure: %s planned sites and %s links. Entrance coordinates:", "sanctuary.heritage.header": "Abandoned infrastructure: %s planned sites and %s links. Entrance coordinates:",
"sanctuary.heritage.entry": "%s : %s / %s / %s" "sanctuary.heritage.entry": "%s : %s / %s / %s",
"sanctuary.transit.entry": "Industrial galleries — entrance: %s %s %s"
} }
@@ -144,7 +144,8 @@
"sanctuary.heritage.kind.photo_club": "Club photo", "sanctuary.heritage.kind.photo_club": "Club photo",
"sanctuary.heritage.kind.impossible_service": "Vestiaires et salles étranges", "sanctuary.heritage.kind.impossible_service": "Vestiaires et salles étranges",
"sanctuary.heritage.kind.construction_refuge": "Refuge de chantier", "sanctuary.heritage.kind.construction_refuge": "Refuge de chantier",
"sanctuary.heritage.unavailable": "Ce relevé nécessite un monde Sanctuary alpha.19 ou alpha.20 avec les structures expérimentales activées.", "sanctuary.heritage.unavailable": "Ce relevé nécessite un monde Sanctuary récent 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.header": "Anciennes infrastructures : %s lieux et %s liaisons prévus. Coordonnées des entrées :",
"sanctuary.heritage.entry": "%s : %s / %s / %s" "sanctuary.heritage.entry": "%s : %s / %s / %s",
"sanctuary.transit.entry": "Galeries industrielles — entrée : %s %s %s"
} }
@@ -0,0 +1,7 @@
{
"type": "sanctuary:industrial_transit_v21",
"biomes": "#sanctuary:has_structure/forgotten_networks",
"step": "underground_structures",
"terrain_adaptation": "none",
"spawn_overrides": {}
}
@@ -3,7 +3,7 @@
"minecraft:overworld": { "minecraft:overworld": {
"type": "sanctuary:sanctuary_384", "type": "sanctuary:sanctuary_384",
"generator": { "generator": {
"type": "sanctuary:island_v20", "type": "sanctuary:island_v21",
"biome_source": { "biome_source": {
"biomes": [ "biomes": [
"minecraft:the_void", "minecraft:the_void",
@@ -0,0 +1,81 @@
{
"dimensions": {
"minecraft:overworld": {
"type": "sanctuary:sanctuary_384",
"generator": {
"type": "sanctuary:island_v20",
"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"
}
}
}
}
@@ -72,6 +72,18 @@ public final class ExpansionJournalSmoke {
require(Arrays.equals(unifiedMediumBytes, Files.readAllBytes(unifiedMediumDirectory.resolve(ExpansionJournal.FILE_NAME))), require(Arrays.equals(unifiedMediumBytes, Files.readAllBytes(unifiedMediumDirectory.resolve(ExpansionJournal.FILE_NAME))),
"Rejected cross-generation read changed its journal"); "Rejected cross-generation read changed its journal");
for (int generation : List.of(20, 21)) {
var saved = scratch.resolve("architecture-cross-version-" + generation);
var root21 = ExpansionIsland.origin(724, SEED, generation);
var journal21 = new ExpansionJournal(saved, SEED, 724, generation);
var child = ExpansionPlacement.candidates(SEED, List.of(root21), "city21", "sanctuary", "east", "lost_city", 256, "natural", 0).getFirst();
journal21.commit(List.of(root21, child));
byte[] bytes = Files.readAllBytes(saved.resolve(ExpansionJournal.FILE_NAME));
require(new ExpansionJournal(saved, SEED, 724, generation).islands().equals(List.of(root21, child)), "Architecture journal round trip");
reject(() -> new ExpansionJournal(saved, SEED, 724, generation == 20 ? 21 : 20), "Cross-generation architecture journal");
require(Arrays.equals(bytes, Files.readAllBytes(saved.resolve(ExpansionJournal.FILE_NAME))), "Rejected upgrade altered saved journal");
}
Path cityDirectory = scratch.resolve("city-generation"); Path cityDirectory = scratch.resolve("city-generation");
var cityRoot = ExpansionIsland.origin(724, SEED, 14); var cityRoot = ExpansionIsland.origin(724, SEED, 14);
var city = new ExpansionJournal(cityDirectory, SEED, 724, 14, var city = new ExpansionJournal(cityDirectory, SEED, 724, 14,
@@ -0,0 +1,555 @@
package fr.koka.sanctuary.worldgen.city;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Kind;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Parcel;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.HashSet;
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.Blocks;
import net.minecraft.world.level.block.DoorBlock;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.StairBlock;
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.storage.loot.LootTable;
import net.minecraft.world.phys.AABB;
/** Exercises final block geometry rather than reconstructing the painter's room allocation. */
public final class LostCityInteriors21Smoke {
private static final Kind[] KINDS = {Kind.APARTMENT, Kind.HOTEL, Kind.LIBRARY,
Kind.RESTAURANT, Kind.BARRACKS, Kind.HALL};
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
public static void main(String[] args) {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
run();
System.out.println("LostCityInteriors21Smoke: six building uses, alternating physical floor plans, four rotations, connected rooms, fully enclosed bathrooms, wall-backed beds with lateral access, native door/seat geometry, supported lanterns, touching roof supports, oak/smooth-stone palette and deferred loot passed.");
}
/** Also usable after the engine has already bootstrapped its registries. */
public static void run() {
var failures = new java.util.ArrayList<String>();
Set<Map<BlockPos, BlockState>> distinct = new HashSet<>();
Set<Set<BlockPos>> distinctVolumes = new HashSet<>();
var previews = new java.util.ArrayList<Object>();
for (Kind kind : KINDS) for (int floors : new int[]{2, 3, 5}) for (long seed : new long[]{0, 43}) {
try {
Capture canonical = render(parcel(kind, floors, seed, 0));
if (floors == 2 && seed == 0) {
distinct.add(canonical.blocks);
Set<BlockPos> volume = new HashSet<>();
canonical.blocks.forEach((at, state) -> { if (!state.isAir() && at.getY() > 0
&& (at.getY() > 10 || at.getX() == 0 || at.getX() == 16 || at.getZ() == 0 || at.getZ() == 16)) volume.add(at); });
require(distinctVolumes.add(volume), kind + " only recolors an existing facade/roof volume");
}
if (floors == 3 && seed == 43 && System.getProperty("sanctuary.city21.preview") != null) {
var blocks = new java.util.ArrayList<Object>();
canonical.blocks.forEach((at, state) -> { if (!state.isAir()) blocks.add(java.util.List.of(at.getX(), at.getY(), at.getZ(), state.toString())); });
previews.add(Map.of("kind", kind.name(), "seed", seed, "floors", floors, "facing", 0, "blocks", blocks));
}
for (int facing = 0; facing < 4; facing++) {
Parcel parcel = parcel(kind, floors, seed, facing);
Capture result = facing == 0 ? canonical : render(parcel);
Capture repeated = render(parcel);
require(result.blocks.equals(repeated.blocks) && result.loot.equals(repeated.loot), "Saved parcel is not deterministic");
rotationMatches(canonical, result, facing);
require(result.blocks.values().stream().anyMatch(state -> state.is(Blocks.STONE_BRICKS)), "Missing stone-brick foundations");
require(result.blocks.values().stream().anyMatch(state -> state.is(Blocks.SMOOTH_STONE)), "Missing smooth stone");
require(result.blocks.values().stream().noneMatch(state -> state.is(Blocks.SPAWNER)
|| !state.getFluidState().isEmpty()), "Building introduced a spawner or free flowing fluid");
require(result.loot.size() == 1, "Building must keep one deferred loot chest");
var entrance = LostCityInteriors21.rooms(parcel).getFirst();
Set<BlockPos> reached = walk(result, position(entrance));
for (var room : LostCityInteriors21.rooms(parcel)) require(reached.contains(position(room)),
label(parcel) + " cannot walk from its entrance to " + room.name() + " at " + position(room));
verifyDoors(result, reached);
verifyBeds(result, reached);
verifyBathrooms(result, reached);
verifyFurnitureDirections(result);
verifyLanternSupports(result);
for (BlockPos chest : result.loot.keySet()) require(adjacentReached(chest, reached),
label(parcel) + " loot cannot be approached");
semantics(result);
if (facing == 0) verifyStoreyVariation(result);
verifyArchitecture(result);
}
} catch (AssertionError | RuntimeException failure) {
failures.add(kind + " floors=" + floors + " seed=" + seed + ": " + failure.getMessage());
}
}
require(failures.isEmpty(), "City21 interior fixture failures (" + failures.size() + "):\n" + String.join("\n", failures));
require(distinct.size() == KINDS.length, "Building uses only labels to vary an identical interior");
boolean rejected = false;
try { LostCityInteriors21.rooms(new Parcel(Kind.PARK, 0, 0, 17, 17, 160, 2, 0, 0, 4, new int[0])); }
catch (IllegalArgumentException expected) { rejected = true; }
require(rejected, "Interior dispatch must reject non-building parcels");
String preview = System.getProperty("sanctuary.city21.preview");
if (preview != null) try {
var file = java.nio.file.Path.of(preview); if (file.getParent() != null) java.nio.file.Files.createDirectories(file.getParent());
java.nio.file.Files.writeString(file, new com.google.gson.Gson().toJson(previews));
} catch (java.io.IOException e) { throw new AssertionError("Cannot write requested architecture preview", e); }
}
private static Parcel parcel(Kind kind, int floors, long seed, int facing) {
return new Parcel(kind, -81, 119, 17, 17, 160, floors, facing, seed, 4, new int[0]);
}
private static Capture render(Parcel parcel) {
var result = new Capture(parcel);
LostCityInteriors21.render(parcel, result);
return result;
}
private static String label(Parcel p) { return p.kind() + " floors=" + p.floors() + " facing=" + p.facing() + " seed=" + p.seed(); }
private static BlockPos position(LostCityInteriors21.Room room) { return new BlockPos(room.x(), room.y(), room.z()); }
private static void rotationMatches(Capture canonical, Capture rotated, int facing) {
require(canonical.blocks.size() == rotated.blocks.size(), "Rotation changed the number of emitted positions");
Rotation rotation = switch (facing) {
case 0 -> Rotation.NONE; case 1 -> Rotation.CLOCKWISE_90;
case 2 -> Rotation.CLOCKWISE_180; default -> Rotation.COUNTERCLOCKWISE_90;
};
for (var entry : canonical.blocks.entrySet()) {
BlockPos target = rotate(entry.getKey(), facing);
// Native chest placement owns its final facing, so its loot position is checked separately.
if (canonical.loot.containsKey(entry.getKey())) continue;
require(rotated.block(target).equals(entry.getValue().rotate(rotation)), "Geometry or block-state rotation drifted at " + target);
}
for (var entry : canonical.loot.entrySet()) require(entry.getValue().equals(rotated.loot.get(rotate(entry.getKey(), facing))),
"Rotation changed deferred loot identity or position");
}
private static void verifyDoors(Capture result, Set<BlockPos> reached) {
long lowerDoors = 0, pairedLeaves = 0;
for (var entry : result.blocks.entrySet()) {
BlockState state = entry.getValue();
if (!(state.getBlock() instanceof DoorBlock)) continue;
require(state.is(Blocks.OAK_DOOR) && !state.getValue(BlockStateProperties.POWERED), "A room door cannot be opened by an ordinary player");
boolean lower = state.getValue(BlockStateProperties.DOUBLE_BLOCK_HALF) == DoubleBlockHalf.LOWER;
BlockPos partner = lower ? entry.getKey().above() : entry.getKey().below();
BlockState expected = state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF,
lower ? DoubleBlockHalf.UPPER : DoubleBlockHalf.LOWER);
require(result.block(partner).equals(expected), label(result.parcel) + " has an incomplete/mismatched door at " + entry.getKey());
if (!lower) continue;
lowerDoors++;
require(result.block(entry.getKey().below()).isSolid(), "Door has no solid threshold");
Direction normal = state.getValue(BlockStateProperties.HORIZONTAL_FACING);
require(reached.contains(entry.getKey().relative(normal)) && reached.contains(entry.getKey().relative(normal.getOpposite())),
label(result.parcel) + " door does not connect two walkable sides at " + entry.getKey());
for (Direction side : new Direction[]{normal.getClockWise(), normal.getCounterClockWise()}) {
BlockState neighbor = result.block(entry.getKey().relative(side));
if (!neighbor.is(Blocks.OAK_DOOR)
|| neighbor.getValue(BlockStateProperties.DOUBLE_BLOCK_HALF) != DoubleBlockHalf.LOWER
|| neighbor.getValue(BlockStateProperties.HORIZONTAL_FACING) != normal) continue;
AABB opened = state.setValue(BlockStateProperties.OPEN, true)
.getCollisionShape(EmptyBlockGetter.INSTANCE, entry.getKey()).bounds();
require(againstOppositeEdge(opened, side), label(result.parcel) + " double door opens into its partner at " + entry.getKey());
pairedLeaves++;
}
}
require(lowerDoors >= result.parcel.floors() * 3L + 2, "Missing actual doors between distinct rooms");
require(pairedLeaves >= 2, "Entrance must retain a double door with its hinges against the outside jambs");
for (var room : LostCityInteriors21.rooms(result.parcel)) if (room.name().endsWith("/stair_landing")) {
BlockPos landing = position(room);
boolean opensIntoStairwell = false;
for (Direction side : HORIZONTAL) {
BlockPos at = landing.relative(side);
BlockState state = result.block(at);
if (state.is(Blocks.OAK_DOOR)) opensIntoStairwell |=
at.relative(state.getValue(BlockStateProperties.HORIZONTAL_FACING)).equals(landing);
}
require(opensIntoStairwell, "Stairwell door must open into its landing, away from the central corridor");
}
}
private static boolean againstOppositeEdge(AABB box, Direction towardPartner) {
return switch (towardPartner) {
case EAST -> box.maxX <= .25; case WEST -> box.minX >= .75;
case SOUTH -> box.maxZ <= .25; case NORTH -> box.minZ >= .75;
default -> throw new AssertionError("Door partner must be horizontal");
};
}
private static void verifyFurnitureDirections(Capture result) {
int tableSeats = 0;
for (var entry : result.blocks.entrySet()) {
if (!entry.getValue().is(Blocks.OAK_FENCE)
|| !result.block(entry.getKey().above()).is(Blocks.OAK_PRESSURE_PLATE)) continue;
int aroundThisTable = 0;
for (Direction side : HORIZONTAL) {
BlockPos chair = entry.getKey().relative(side);
if (!result.block(chair).is(Blocks.OAK_STAIRS)) continue;
seatLooksToward(result, chair, side.getOpposite());
aroundThisTable++; tableSeats++;
}
require(aroundThisTable > 0, "A desk/table lost every chair facing its surface");
}
if (result.parcel.kind() != Kind.BARRACKS)
require(tableSeats >= result.parcel.floors(), "Every furnished storey needs a usable table or desk seat");
if (result.parcel.kind() == Kind.HALL) for (int x : new int[]{3, 4, 5, 11, 12, 13})
seatLooksToward(result, rotate(new BlockPos(x, 1, 11), result.parcel.facing()), rotated(Direction.SOUTH, result.parcel.facing()));
if (result.parcel.kind() == Kind.HOTEL) for (int x = 2; x <= 4; x++)
seatLooksToward(result, rotate(new BlockPos(x, 1, 14), result.parcel.facing()), rotated(Direction.NORTH, result.parcel.facing()));
if (result.parcel.kind() == Kind.BARRACKS) for (int floor = 0; floor < result.parcel.floors(); floor++)
if (LostCityInteriors21.layout(result.parcel, floor) == 0)
seatLooksToward(result, rotate(new BlockPos(13, floor * 5 + 1, 6), result.parcel.facing()), rotated(Direction.NORTH, result.parcel.facing()));
for (var entry : result.blocks.entrySet()) if (entry.getValue().is(Blocks.POLISHED_ANDESITE_STAIRS)) {
Direction look = entry.getValue().getValue(BlockStateProperties.HORIZONTAL_FACING).getOpposite();
require(clear(result.block(entry.getKey().relative(look))), "Sanitary seat faces a wall instead of its cubicle");
}
}
private static void seatLooksToward(Capture result, BlockPos chair, Direction towardUse) {
BlockState state = result.block(chair);
require(state.is(Blocks.OAK_STAIRS), "Missing chair at " + chair);
var boxes = state.getCollisionShape(EmptyBlockGetter.INSTANCE, chair).toAabbs();
double frontX = .5 + towardUse.getStepX() * .25, frontZ = .5 + towardUse.getStepZ() * .25;
double backX = .5 - towardUse.getStepX() * .25, backZ = .5 - towardUse.getStepZ() * .25;
require(boxes.stream().noneMatch(box -> box.contains(frontX, .75, frontZ))
&& boxes.stream().anyMatch(box -> box.contains(backX, .75, backZ)),
label(result.parcel) + " chair's native back faces its table/stage at " + chair);
require(result.block(chair.below()).isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, chair.below()),
"Chair does not rest on a complete floor");
}
private static Direction rotated(Direction direction, int facing) {
for (int turn = 0; turn < facing; turn++) direction = direction.getClockWise();
return direction;
}
private static void verifyLanternSupports(Capture result) {
int lanterns = 0;
for (var entry : result.blocks.entrySet()) if (entry.getValue().is(Blocks.LANTERN)) {
BlockPos support = entry.getValue().getValue(BlockStateProperties.HANGING) ? entry.getKey().above() : entry.getKey().below();
require(result.block(support).isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, support),
label(result.parcel) + " lantern floats against a slab or lacks a full support block at " + entry.getKey());
lanterns++;
}
require(lanterns == result.parcel.floors(), "A junction lost its supported lamp");
}
private static void verifyBeds(Capture result, Set<BlockPos> reached) {
for (var entry : result.blocks.entrySet()) {
BlockState state = entry.getValue();
if (!state.is(Blocks.BED.white())) continue;
Direction direction = state.getValue(BlockStateProperties.HORIZONTAL_FACING);
BedPart part = state.getValue(BlockStateProperties.BED_PART);
BlockPos partner = entry.getKey().relative(part == BedPart.FOOT ? direction : direction.getOpposite());
require(result.block(partner).equals(state.setValue(BlockStateProperties.BED_PART,
part == BedPart.FOOT ? BedPart.HEAD : BedPart.FOOT)), label(result.parcel) + " has a severed bed at " + entry.getKey());
require(result.block(entry.getKey().below()).isSolid(), "Bed does not sit on a floor");
require(adjacentReached(entry.getKey(), reached) || adjacentReached(partner, reached),
label(result.parcel) + " bed cannot be approached from its room");
if (part == BedPart.HEAD) {
var backing = entry.getKey().relative(direction);
for (int up = 0; up < 3; up++) require(result.block(backing.above(up))
.isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, backing.above(up)),
label(result.parcel) + " bed head is not against a complete wall at " + entry.getKey());
} else {
boolean lateral = false;
for (Direction side : new Direction[]{direction.getClockWise(), direction.getCounterClockWise()})
lateral |= reached.contains(entry.getKey().relative(side)) && reached.contains(partner.relative(side));
require(lateral, label(result.parcel) + " bed lacks a reachable side along its full length at " + entry.getKey());
}
}
}
private static void verifyBathrooms(Capture result, Set<BlockPos> reached) {
for (var bath : LostCityInteriors21.bathrooms(result.parcel)) {
BlockPos door = new BlockPos(bath.doorX(), bath.doorY(), bath.doorZ());
require(result.block(door).is(Blocks.OAK_DOOR), "Enclosed bathroom lost its actual door: " + bath.name());
Direction intoRoom = result.block(door).getValue(BlockStateProperties.HORIZONTAL_FACING);
require(reached.contains(door.relative(intoRoom)), "Bathroom cannot be entered through its door: " + bath.name());
require(bath.minX() == 1 || bath.minZ() == 1 || bath.maxX() == 15 || bath.maxZ() == 15,
"Bathroom enclosure is detached from the exterior room wall");
for (int x = bath.minX(); x <= bath.maxX(); x++) for (int z = bath.minZ(); z <= bath.maxZ(); z++)
for (int y = bath.minY(); y <= bath.maxY(); y++) {
boolean shell = x == bath.minX() || x == bath.maxX() || z == bath.minZ() || z == bath.maxZ()
|| y == bath.minY() || y == bath.maxY();
if (!shell) continue;
BlockPos at = new BlockPos(x, y, z);
if (at.equals(door) || at.equals(door.above())) continue;
require(result.block(at).isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, at),
label(result.parcel) + " bathroom has an incomplete floor, ceiling or connected wall at " + at + " in " + bath.name());
}
}
}
private static void verifyStoreyVariation(Capture result) {
for (int floor = 1; floor < result.parcel.floors(); floor++) {
require(LostCityInteriors21.layout(result.parcel, floor - 1) != LostCityInteriors21.layout(result.parcel, floor),
"Adjacent storeys selected the same plan");
Set<BlockPos> before = rearPartitions(result, floor - 1), after = rearPartitions(result, floor);
require(!before.equals(after), label(result.parcel) + " only changed furniture or labels between storeys " + (floor - 1) + " and " + floor);
Map<BlockPos, BlockState> beforeFixtures = furnishings(result, floor - 1), afterFixtures = furnishings(result, floor);
require(!beforeFixtures.equals(afterFixtures), label(result.parcel)
+ " copied the same furniture between storeys " + (floor - 1) + " and " + floor);
}
}
private static Set<BlockPos> rearPartitions(Capture result, int floor) {
Set<BlockPos> walls = new HashSet<>();
for (int x = 2; x <= 14; x++) for (int z = 9; z <= 14; z++) {
boolean continuous = true;
for (int y = 1; y <= 3; y++) continuous &= result.block(new BlockPos(x, floor * 5 + y, z)).is(Blocks.SMOOTH_STONE);
if (continuous) walls.add(new BlockPos(x, 0, z));
}
return walls;
}
private static Map<BlockPos, BlockState> furnishings(Capture result, int floor) {
Map<BlockPos, BlockState> objects = new HashMap<>();
for (int x = 2; x <= 14; x++) for (int z = 2; z <= 14; z++) for (int up = 1; up <= 3; up++) {
BlockState state = result.block(new BlockPos(x, floor * 5 + up, z));
if (state.is(Blocks.BED.white()) || state.is(Blocks.BARREL) || state.is(Blocks.FURNACE)
|| state.is(Blocks.BOOKSHELF) || state.is(Blocks.WATER_CAULDRON) || state.is(Blocks.OAK_FENCE)
|| state.is(Blocks.LECTERN) || state.is(Blocks.CRAFTING_TABLE) || state.is(Blocks.OAK_STAIRS)
|| state.is(Blocks.OAK_PLANKS) || state.is(Blocks.OAK_PLANKS) || state.is(Blocks.OAK_PLANKS)) objects.put(new BlockPos(x, up, z), state);
}
return objects;
}
private static void semantics(Capture result) {
Map<BlockState, Long> counts = new HashMap<>();
result.blocks.values().forEach(state -> counts.merge(state, 1L, Long::sum));
java.util.function.Predicate<net.minecraft.world.level.block.Block> has = block ->
counts.keySet().stream().anyMatch(state -> state.is(block));
switch (result.parcel.kind()) {
case APARTMENT -> require(has.test(Blocks.FURNACE) && has.test(Blocks.BED.white())
&& has.test(Blocks.WATER_CAULDRON), "Apartment lacks cooking, sleeping or washing fixtures");
case HOTEL -> require(has.test(Blocks.LOOM) && has.test(Blocks.BED.white())
&& has.test(Blocks.WATER_CAULDRON), "Hotel lacks guest rooms or laundry fixtures");
case LIBRARY -> require(result.blocks.values().stream().filter(state -> state.is(Blocks.BOOKSHELF)).count() >= result.parcel.floors() * 12L
&& has.test(Blocks.LECTERN), "Library is missing usable stacks and reading fixtures");
case RESTAURANT -> require(has.test(Blocks.SMOKER) && has.test(Blocks.FURNACE)
&& has.test(Blocks.CRAFTING_TABLE) && has.test(Blocks.WATER_CAULDRON), "Restaurant lacks kitchen and washroom services");
case BARRACKS -> require(has.test(Blocks.BED.white()) && has.test(Blocks.FLETCHING_TABLE)
&& has.test(Blocks.GRINDSTONE), "Barracks lacks dormitory and equipment fixtures");
case HALL -> {
require(has.test(Blocks.LECTERN) && has.test(Blocks.IRON_BARS), "Hall lacks stage or gallery guards");
require(result.block(rotate(new BlockPos(8, 5, 12), result.parcel.facing())).isAir(), "Auditorium lost its double-height volume");
}
default -> throw new AssertionError("Unexpected building");
}
}
/** Player-sized path: two clear cells above support, one-block steps and manually openable doors. */
private static Set<BlockPos> walk(Capture result, BlockPos start) {
require(standable(result, start), label(result.parcel) + " has a blocked entrance");
Set<BlockPos> seen = new HashSet<>();
ArrayDeque<BlockPos> queue = new ArrayDeque<>();
seen.add(start); queue.add(start);
while (!queue.isEmpty()) {
BlockPos current = queue.removeFirst();
for (Direction direction : HORIZONTAL) for (int dy = -1; dy <= 1; dy++) {
BlockPos next = current.relative(direction).offset(0, dy, 0);
if (dy > 0 && !clearBody(result, current.above())) continue;
if (standable(result, next) && seen.add(next)) queue.addLast(next);
}
}
return seen;
}
private static boolean standable(Capture result, BlockPos pos) {
if (pos.getX() < 0 || pos.getX() > 16 || pos.getZ() < 0 || pos.getZ() > 16
|| pos.getY() < 1 || pos.getY() > result.parcel.floors() * 5 + 4) return false;
if (!clearBody(result, pos)) return false;
var below = pos.below();
return opened(result.block(below)).getCollisionShape(EmptyBlockGetter.INSTANCE, below).toAabbs().stream()
.anyMatch(b -> b.maxY >= .5 && b.minX <= .5 && b.maxX >= .5 && b.minZ <= .5 && b.maxZ >= .5);
}
private static BlockState opened(BlockState state) {
return state.getBlock() instanceof net.minecraft.world.level.block.DoorBlock
|| state.getBlock() instanceof net.minecraft.world.level.block.TrapDoorBlock
? state.setValue(BlockStateProperties.OPEN, true) : state;
}
private static boolean clearBody(Capture result, 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()}) {
var state = opened(result.block(at));
if (!state.getFluidState().isEmpty()) return false;
for (var shape : state.getCollisionShape(EmptyBlockGetter.INSTANCE, at).toAabbs())
if (shape.move(at.getX(), at.getY(), at.getZ()).intersects(body)) return false;
}
return true;
}
private static boolean clear(BlockState state) { return state.isAir() || state.is(Blocks.OAK_DOOR); }
private static void verifyArchitecture(Capture c) {
for (int x = 0; x <= 16; x++) for (int z = 0; z <= 16; z++)
require(c.block(new BlockPos(x, -1, z)).is(Blocks.STONE_BRICKS), "Foundation is not dressed stone brick");
long outsideVolume = c.blocks.entrySet().stream().filter(e -> e.getKey().getY() > 0
&& (e.getKey().getX() == 0 || e.getKey().getX() == 16 || e.getKey().getZ() == 0 || e.getKey().getZ() == 16)
&& !e.getValue().isAir()).count();
require(outsideVolume >= 40, label(c.parcel) + " has no cornice, bay, fins or entrance projection");
verifyPalette(c.blocks, true, label(c.parcel));
verifyGroundedGeometry(c.blocks, 0, label(c.parcel));
if (c.parcel.kind() == Kind.BARRACKS) for (int floor = 0; floor < c.parcel.floors(); floor++) {
var grinder = rotate(new BlockPos(12, floor * 5 + 1, 11), c.parcel.facing());
require(c.block(grinder).is(Blocks.GRINDSTONE)
&& c.block(grinder).getValue(BlockStateProperties.ATTACH_FACE)
== net.minecraft.world.level.block.state.properties.AttachFace.FLOOR,
label(c.parcel) + " workshop grinder lacks its floor-mounted frame");
}
if (c.parcel.kind() == Kind.HOTEL) for (int x : new int[]{12, 14}) {
var counter = rotate(new BlockPos(x, 1, 14), c.parcel.facing());
require(c.block(counter).is(Blocks.BARREL)
&& c.block(counter.above()).is(Blocks.OAK_PLANKS)
&& c.block(counter.above(2)).is(Blocks.OAK_SLAB),
label(c.parcel) + " laundry shelf lacks its counter-mounted timber frame");
}
long smooth = 0, facingMaterial = 0;
for (int x = 1; x <= 15; x++) for (int z = 1; z <= 15; z++) {
if (x != 1 && x != 15 && z != 1 && z != 15) continue;
for (int y = 1; y < c.parcel.floors() * 5; y++) {
var at = rotate(new BlockPos(x, y, z), c.parcel.facing());
var state = c.block(at);
if (state.isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, at)) {
facingMaterial++;
if (state.is(Blocks.SMOOTH_STONE)) smooth++;
}
}
}
require(smooth * 2 >= facingMaterial, label(c.parcel) + " no longer has predominantly smooth-stone facades: " + smooth + "/" + facingMaterial);
if (c.parcel.kind() == Kind.APARTMENT || c.parcel.kind() == Kind.LIBRARY)
require(c.block(rotate(new BlockPos(1, 2, 1), c.parcel.facing())).isAir(), "Front corner was not physically cut back");
if (c.parcel.kind() == Kind.LIBRARY) {
int roof = c.parcel.floors() * 5;
for (int x = 11; x <= 12; x++) for (int z = 4; z <= 6; z++) {
for (int y = roof - 1; y <= roof; y++)
require(c.block(rotate(new BlockPos(x, y, z), c.parcel.facing())).isAir(),
label(c.parcel) + " skylight is blocked by the roof plate or last-storey ceiling");
require(c.block(rotate(new BlockPos(x, roof + 1, z), c.parcel.facing())).isAir(),
label(c.parcel) + " skylight contains a redundant glazed floor under its cap");
require(c.block(rotate(new BlockPos(x, roof + 2, z), c.parcel.facing())).is(Blocks.GLASS),
label(c.parcel) + " skylight lost its continuous transparent cap");
for (int y = roof + 3; y <= roof + 4; y++)
require(c.block(rotate(new BlockPos(x, y, z), c.parcel.facing())).isAir(),
label(c.parcel) + " another roof feature shades the light well");
}
for (int x = 10; x <= 13; x++) for (int z = 3; z <= 7; z++) {
if (x > 10 && x < 13 && z > 3 && z < 7) continue;
var base = rotate(new BlockPos(x, roof, z), c.parcel.facing());
require(c.block(base).isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, base)
&& c.block(base.above()).is(Blocks.STRIPPED_OAK_LOG),
label(c.parcel) + " skylight cut through its supporting perimeter or timber frame");
}
}
if (c.parcel.kind() == Kind.HOTEL || c.parcel.kind() == Kind.BARRACKS) {
int roof = c.parcel.floors() * 5;
int x = c.parcel.kind() == Kind.HOTEL ? 9 : 1;
int z = c.parcel.kind() == Kind.HOTEL ? 3 : 2;
var top = rotate(new BlockPos(x, roof + 4, z), c.parcel.facing());
require(c.block(top).is(Blocks.STONE_BRICK_SLAB)
&& c.block(top).getValue(BlockStateProperties.SLAB_TYPE)
== net.minecraft.world.level.block.state.properties.SlabType.BOTTOM,
label(c.parcel) + " pavilion roof floats half a block above its walls");
require(c.block(top.below()).isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, top.below()),
label(c.parcel) + " pavilion lost the wall carrying its roof");
}
// Resolve every glazing edge from final neighbors, not precomputed straight strips.
for (var e : c.blocks.entrySet()) if (e.getValue().is(Blocks.GLASS_PANE)) {
for (Direction d : HORIZONTAL) {
var neighbor = c.block(e.getKey().relative(d));
if (!neighbor.isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, e.getKey().relative(d))) continue;
var property = switch(d) { case NORTH -> BlockStateProperties.NORTH; case EAST -> BlockStateProperties.EAST;
case SOUTH -> BlockStateProperties.SOUTH; case WEST -> BlockStateProperties.WEST; default -> throw new AssertionError(); };
require(e.getValue().getValue(property), "Window does not meet its actual stone or timber jamb");
}
}
}
/** The construction palette is explicit; functional fixtures and planting retain vanilla materials. */
public static void verifyPalette(Map<BlockPos, BlockState> blocks, boolean building, String label) {
for (var e : blocks.entrySet()) {
String name = e.getValue().toString();
require(!name.matches(".*(?:birch|dark_oak|spruce|jungle|acacia|mangrove|cherry|pale_oak|bamboo)_.*"),
label + " introduced a second timber palette at " + e.getKey() + ": " + name);
require(!name.contains("granite") && !name.contains("diorite"),
label + " introduced contrasting granite/diorite at " + e.getKey());
require(!name.contains("tuff") || !building && e.getKey().getY() <= 0,
label + " used tuff above the ground/subsoil at " + e.getKey());
}
}
/** Contact uses native collision volumes: merely neighboring slabs can still leave a real gap.
* Every solid fixture, roof and facade must join the foundation through touching material. */
public static void verifyGroundedGeometry(Map<BlockPos, BlockState> blocks, int groundY, String label) {
Map<BlockState, java.util.List<AABB>> shapes = new HashMap<>();
Map<BlockPos, java.util.List<AABB>> geometry = new HashMap<>();
for (var entry : blocks.entrySet()) {
// A lantern's collision omits its thin hanging chain. Its explicit native support
// is checked below instead of pretending that invisible collision gap is floating masonry.
if (entry.getValue().is(Blocks.LANTERN)) continue;
var shape = shapes.computeIfAbsent(entry.getValue(), state ->
state.getCollisionShape(EmptyBlockGetter.INSTANCE, BlockPos.ZERO).toAabbs());
if (!shape.isEmpty()) geometry.put(entry.getKey(), shape);
}
Set<BlockPos> supported = new HashSet<>();
ArrayDeque<BlockPos> queue = new ArrayDeque<>();
for (BlockPos at : geometry.keySet()) if (at.getY() <= groundY) {
supported.add(at); queue.add(at);
}
while (!queue.isEmpty()) {
var at = queue.removeFirst();
for (Direction direction : Direction.values()) {
var next = at.relative(direction);
if (supported.contains(next) || !geometry.containsKey(next)) continue;
boolean touches = false;
for (AABB a : geometry.get(at)) for (AABB b : geometry.get(next)) {
double dx = Math.min(a.maxX, b.maxX + direction.getStepX()) - Math.max(a.minX, b.minX + direction.getStepX());
double dy = Math.min(a.maxY, b.maxY + direction.getStepY()) - Math.max(a.minY, b.minY + direction.getStepY());
double dz = Math.min(a.maxZ, b.maxZ + direction.getStepZ()) - Math.max(a.minZ, b.minZ + direction.getStepZ());
if (dx >= -1e-7 && dy >= -1e-7 && dz >= -1e-7
&& (dx > 1e-7 && dy > 1e-7 || dx > 1e-7 && dz > 1e-7 || dy > 1e-7 && dz > 1e-7)) touches = true;
}
if (touches) { supported.add(next); queue.addLast(next); }
}
}
for (var at : geometry.keySet()) require(supported.contains(at),
label + " has a floating structural/fixture island at " + at + ": " + blocks.get(at));
for (var entry : blocks.entrySet()) if (entry.getValue().is(Blocks.LANTERN)) {
var anchor = entry.getValue().getValue(BlockStateProperties.HANGING) ? entry.getKey().above() : entry.getKey().below();
var state = blocks.getOrDefault(anchor, Blocks.AIR.defaultBlockState());
require(state.isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, anchor) && supported.contains(anchor),
label + " lamp lacks a full, grounded native support at " + entry.getKey());
}
}
private static boolean adjacentReached(BlockPos pos, Set<BlockPos> reached) {
for (Direction direction : HORIZONTAL) if (reached.contains(pos.relative(direction))) return true;
return false;
}
private static BlockPos rotate(BlockPos pos, int facing) {
return switch (facing) {
case 0 -> pos; case 1 -> new BlockPos(16 - pos.getZ(), pos.getY(), pos.getX());
case 2 -> new BlockPos(16 - pos.getX(), pos.getY(), 16 - pos.getZ());
default -> new BlockPos(pos.getZ(), pos.getY(), 16 - pos.getX());
};
}
private record Loot(ResourceKey<LootTable> table, long seed) {}
private static final class Capture implements LostCityRenderer.BlockWriter {
final Parcel parcel;
final Map<BlockPos, BlockState> blocks = new HashMap<>();
final Map<BlockPos, Loot> loot = new HashMap<>();
Capture(Parcel parcel) { this.parcel = parcel; }
@Override public void set(int x, int y, int z, BlockState state) {
require(x >= 0 && x < 17 && z >= 0 && z < 17 && y >= parcel.minY() - parcel.floorY()
&& y <= parcel.maxY() - parcel.floorY(), label(parcel) + " writes outside its saved bounds at " + x + "," + y + "," + z);
blocks.put(new BlockPos(x, y, z), state);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
set(x, y, z, Blocks.CHEST.defaultBlockState());
loot.put(new BlockPos(x, y, z), new Loot(table, seed));
}
BlockState block(BlockPos pos) { return blocks.getOrDefault(pos, Blocks.AIR.defaultBlockState()); }
}
private static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); }
}
@@ -0,0 +1,92 @@
package fr.koka.sanctuary.worldgen.city;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.function.IntBinaryOperator;
/** Natural extensions publish only with a supported player route; the old core/fallback is retained verbatim. */
public final class LostCityPlanner21Smoke {
public static void main(String[] args) {
int extras = 0;
for (int size : new int[]{512, 724, 1024}) for (long seed : new long[]{0, 42}) {
var terrain = new Terrain((x, z) -> 220);
var result = LostCityPlanner21.plan(seed, 0, 0, size, true, terrain); verify(result, terrain);
require(!result.core().surface().isEmpty(), "New solver lost the old starter core");
require(result.surface().districts().size() == 1 + result.connections().size(), "An isolated district was published");
extras += result.connections().size();
var repeat = LostCityPlanner21.plan(seed, 0, 0, size, true, new Terrain((x, z) -> 220));
require(signature(result).equals(signature(repeat)), "Extension depends on invocation order");
}
require(extras >= 6, "Flat fixtures do not demonstrate modest connected neighborhoods: " + extras);
var baseline = LostCityPlanner21.plan(0, 0, 0, 724, true, new Terrain((x, z) -> 220));
var core = baseline.core().surface().districts().getFirst();
Terrain cutoff = new Terrain((x, z) -> 220);
cutoff.blocked = (x, y, z) -> x < core.minX() - 2 || x > core.maxX() + 2 || z < core.minZ() - 2 || z > core.maxZ() + 2;
var stopped = LostCityPlanner21.plan(0, 0, 0, 724, true, cutoff); verify(stopped, cutoff);
require(stopped.connections().isEmpty(), "Protected outskirts acquired fictional connecting roads");
int northSouthGrades = 0;
for (int sign : new int[]{-1, 1}) for (long seed : new long[]{0, 42}) {
Terrain slope = new Terrain((x, z) -> 220 + sign * Math.floorDiv(z, 15));
northSouthGrades += verify(LostCityPlanner21.plan(seed, 0, 0, 724, true, slope), slope);
}
require(northSouthGrades > 0, "Slope witnesses never exercise a real north/south stair inside a connecting road");
Terrain empty = new Terrain((x, z) -> 0);
var fallback = LostCityPlanner21.plan(0, 0, 0, 512, true, empty);
require(fallback.connections().isEmpty() && fallback.reservation() != null && fallback.reservation().width() == 49,
"Void expanded the architectural fallback into a larger platform");
var continent = LostCityPlanner21.plan(0, 0, 0, 256, false, new Terrain((x, z) -> 220));
require(continent.connections().isEmpty() && continent.surface() != null, "Root extension policy leaked onto a continent");
System.out.println("LostCityPlanner21Smoke: preserved core, modest connected neighborhoods, bounded routes, terrain gaps, native bounds and no enlarged fallback passed.");
}
private static int verify(LostCityPlanner21.Result result, Terrain terrain) {
int northSouthGrades = 0;
require(result.columns() <= LostCityPlanner21.MAX_COLUMNS && result.volumeChecks() <= LostCityPlanner21.MAX_VOLUME_CHECKS, "Budget exceeded");
require(result.surface().districts().size() <= 3, "Town sprawls into more than three blocks");
if (!result.core().surface().isEmpty()) require(result.surface().districts().getFirst() == result.core().surface().districts().getFirst(), "Core was rewritten");
var columns = new HashMap<Long, Integer>(); var kinds = new HashMap<Long, LostCityPlan.Kind>();
var owners = new HashMap<Long, LostCityPlan.Parcel>();
for (var district : result.surface().districts()) for (var p : district.parcels()) {
require(p.width() <= 25 && p.depth() <= 25, "Road escaped parcel limits");
for (int z = p.minZ(); z <= p.maxZ(); z++) for (int x = p.minX(); x <= p.maxX(); x++) {
long key = key(x, z); Integer old = columns.putIfAbsent(key, p.surfaceY(x - p.minX(), z - p.minZ()));
require(old == null, "A road and cluster paint the same column twice"); kinds.put(key, p.kind()); owners.put(key, p);
}
}
for (var link : result.connections()) {
require(link.districtIndex() > 0 && link.districtIndex() < result.surface().districts().size(), "Connection points to an absent district");
require(link.path().size() >= 14 && link.path().size() <= 192, "Route lacks the intended natural gap or exceeds reach");
for (int i = 0; i < link.path().size(); i++) {
var p = link.path().get(i); var key = key(p.x(), p.z());
require(columns.get(key) != null && columns.get(key) == p.floorY() && kinds.get(key) == LostCityPlan.Kind.STREET, "Connection centerline has no rendered floor");
require(!terrain.blocked.test(p.x(), p.floorY(), p.z()), "Connection crosses protected terrain");
if (i > 0) {
var a = link.path().get(i - 1);
require(Math.abs(p.x() - a.x()) + Math.abs(p.z() - a.z()) == 1 && Math.abs(p.floorY() - a.floorY()) <= 1, "Player route is discontinuous");
if (p.floorY() != a.floorY()) {
require(owners.get(key) == owners.get(key(a.x(), a.z())),
"A road grade crosses a parcel boundary, so the native renderer cannot build its connecting stair");
if (p.z() != a.z()) northSouthGrades++;
}
}
}
}
return northSouthGrades;
}
private static String signature(LostCityPlanner21.Result r) {
var result = new StringBuilder().append(r.connections()).append(r.columns()).append(r.volumeChecks()).append(r.diagnostics());
for (var d : r.surface().districts()) for (var p : d.parcels()) result.append(p.kind()).append(p.minX()).append(p.minZ()).append(p.floorY()).append(java.util.Arrays.toString(p.surfaceHeights()));
return result.toString();
}
private interface Protection { boolean test(int x, int y, int z); }
private static final class Terrain implements LostCityPlanner16.Terrain {
final IntBinaryOperator height; Protection blocked = (x, y, z) -> false;
Terrain(IntBinaryOperator height) { this.height = height; }
public boolean owns(int x, int z) { return Math.abs(x) < 600 && Math.abs(z) < 600; }
public LostCityPlanner.Ground ground(int x, int z) { int h = height.applyAsInt(x, z); return new LostCityPlanner.Ground(h, h > 0 ? 24 : 0, h > 0, blocked.test(x, h, z)); }
public boolean solid(int x, int y, int z) { return y > 0 && y <= height.applyAsInt(x, z); }
public boolean protectedAt(int x, int y, int z) { return blocked.test(x, y, z); }
}
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
}
@@ -0,0 +1,286 @@
package fr.koka.sanctuary.worldgen.city;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Kind;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Parcel;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.StairBlock;
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.storage.loot.LootTable;
/** Real block-state geometry, without creating a world: clipping, rotations and walkable floors. */
public final class LostCitySurface21Smoke {
public static void main(String[] args) {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
run();
System.out.println("LostCitySurface21Smoke: all exterior amenities, supported lamps, basements, contained pools, excavation access, carved park variants, grounded canopies, restrained oak palette and suspended tuff roads passed.");
}
/** Also callable from an already bootstrapped native GameTest. */
public static void run() {
var failures = new java.util.ArrayList<String>();
for (Kind kind : Kind.values()) {
if (kind.building()) continue;
for (int facing = 0; facing < 4; facing++) {
for (long seed : new long[] {0, 42, -61872}) {
try {
int floors = kind == Kind.HALL ? 2 : 3;
int foundation = kind == Kind.POOL || kind == Kind.EXCAVATION ? 8 : 4;
Parcel parcel = new Parcel(kind, 0, 0, 17, 17, 170, floors, facing, seed, foundation, new int[0]);
Capture result = render(parcel);
require(result.blocks.equals(render(parcel).blocks), "Renderer changed for identical saved parcel");
require(result.blocks.values().stream().noneMatch(state -> state.is(Blocks.SPAWNER)), "City introduced a spawner");
if (kind != Kind.STREET && kind != Kind.BRIDGE) {
LostCityInteriors21Smoke.verifyPalette(result.blocks, false, kind + " facing=" + facing);
LostCityInteriors21Smoke.verifyGroundedGeometry(result.blocks,
kind == Kind.EXCAVATION && foundation >= 8 ? -5 : kind == Kind.POOL && foundation >= 8 ? -4 : 0,
kind + " facing=" + facing + " seed=" + seed);
}
if (kind.building()) {
require(result.blocks.values().stream().anyMatch(state -> state.is(Blocks.STONE_BRICKS)), "Missing explicit stone palette");
require(result.blocks.values().stream().anyMatch(state -> state.is(Blocks.SMOOTH_STONE)), "Missing smooth stone palette");
Set<BlockPos> reached = walk(result, rotate(8, 1, 0, facing));
for (int floor = 1; floor <= floors; floor++) {
int elevation = floor * 5 + 1;
require(reached.stream().anyMatch(pos -> pos.getY() == elevation),
kind + " facing " + facing + " cannot reach floor/roof " + floor);
}
require(result.loot.size() == 1, kind + " must keep exactly one deferred, limited loot chest");
}
for (var entry : result.blocks.entrySet()) {
if (entry.getValue().is(Blocks.LANTERN)) {
var support = entry.getValue().getValue(BlockStateProperties.HANGING) ? entry.getKey().above() : entry.getKey().below();
require(result.block(support).isCollisionShapeFullBlock(net.minecraft.world.level.EmptyBlockGetter.INSTANCE, support), kind + " lantern lacks full support at " + entry.getKey());
}
BlockState state = entry.getValue();
if (state.is(Blocks.BED.white()) && state.getValue(BlockStateProperties.BED_PART) == BedPart.FOOT) {
Direction direction = state.getValue(BlockStateProperties.HORIZONTAL_FACING);
BlockState head = result.block(entry.getKey().relative(direction));
require(head.is(Blocks.BED.white()) && head.getValue(BlockStateProperties.BED_PART) == BedPart.HEAD,
"Rotated bedroom has an incomplete bed");
}
}
} catch (AssertionError | RuntimeException failure) {
failures.add(kind + " facing=" + facing + " seed=" + seed + ": " + failure.getMessage());
}
}
}
}
for (int facing = 0; facing < 4; facing++) {
try {
Parcel garage = new Parcel(Kind.PARKING, 0, 0, 17, 17, 170, 1, facing, 9, 8, new int[0]);
Capture result = render(garage);
Set<BlockPos> reached = walk(result, rotate(8, 1, 0, facing));
require(reached.contains(rotate(8, -4, 10, facing)), "Underground garage is not reachable from its street ramp");
require(result.block(rotate(8, -6, 10, facing)).is(Blocks.STONE_BRICKS), "Garage excavation removed the certified rock below its floor");
for (int z = 0; z <= 4; z++) for (int x = 6; x <= 10; x++) for (int y = -5; y < -z; y++) {
var below = rotate(x, y, z, facing);
require(result.block(below).isCollisionShapeFullBlock(net.minecraft.world.level.EmptyBlockGetter.INSTANCE, below),
"Garage ramp is suspended above its floor instead of resting on its masonry wedge");
}
LostCityInteriors21Smoke.verifyGroundedGeometry(result.blocks, -5, "Underground garage facing=" + facing);
var threshold = rotate(8, 0, 0, facing);
require(result.block(threshold).isCollisionShapeFullBlock(net.minecraft.world.level.EmptyBlockGetter.INSTANCE, threshold)
&& result.block(threshold.above()).isAir() && result.block(threshold.above(2)).isAir(),
"Garage entry lost its level, clear ramp threshold");
} catch (AssertionError | RuntimeException failure) {
failures.add("Underground garage facing=" + facing + ": " + failure.getMessage());
}
try {
Parcel pit = new Parcel(Kind.EXCAVATION, 0, 0, 17, 17, 170, 0, facing, 9, 8, new int[0]);
Capture excavation = render(pit);
require(walk(excavation, rotate(8, 1, 0, facing)).contains(rotate(8, -4, 10, facing)),
"Excavation has no walkable stairs from the street");
require(excavation.block(rotate(8, -6, 10, facing)).is(Blocks.STONE_BRICKS), "Excavation removed its certified bottom support");
LostCityInteriors21Smoke.verifyGroundedGeometry(excavation.blocks, -5, "Excavation frame facing=" + facing);
for (int x : new int[]{3, 13}) for (int z : new int[]{10, 13}) {
for (int y = -4; y <= 3; y++) require(!excavation.block(rotate(x, y, z, facing)).isAir(),
"Excavation roof lacks an unbroken post to its real floor at " + x + "," + y + "," + z);
require(excavation.block(rotate(x, 4, z, facing)).is(Blocks.OAK_SLAB)
|| excavation.block(rotate(x, 4, z, facing)).is(Blocks.OAK_PLANKS),
"Excavation's only canopy is detached from its post");
}
} catch (AssertionError | RuntimeException failure) {
failures.add("Excavation facing=" + facing + ": " + failure.getMessage());
}
try {
Parcel pool = new Parcel(Kind.POOL, 0, 0, 17, 17, 170, 0, facing, 9, 8, new int[0]);
Capture water = render(pool);
sealedPool(water, facing);
for (int step = 1; step <= 3; step++) for (int x = 7; x <= 9; x++) {
var tread = rotate(x, -step, step + 4, facing);
require(water.block(tread).getBlock() instanceof StairBlock
&& water.block(tread).getValue(BlockStateProperties.WATERLOGGED),
"Pool lost its submerged stair tread");
for (int y = -4; y < -step; y++) {
var below = rotate(x, y, step + 4, facing);
require(water.block(below).isCollisionShapeFullBlock(net.minecraft.world.level.EmptyBlockGetter.INSTANCE, below),
"Pool tread is not carried continuously by its sealed basin floor");
}
}
require(walk(water, rotate(8, 1, 0, facing)).contains(rotate(8, -2, 7, facing)),
"Pool submerged steps cannot be reached from the deck");
} catch (AssertionError | RuntimeException failure) {
failures.add("Pool facing=" + facing + ": " + failure.getMessage());
}
}
// Every public type keeps a usable north approach, and its furniture does not consume the street threshold.
for (Kind kind : Kind.values()) if (!kind.building() && kind != Kind.STREET && kind != Kind.BRIDGE)
for (int facing = 0; facing < 4; facing++) {
try {
Capture c = render(new Parcel(kind, 0, 0, 17, 17, 170, 0, facing, 42, 8, new int[0]));
var reached = walk(c, rotate(8, 1, 0, facing));
require(reached.size() >= 100, kind + " lost its public circulation");
for (var chest : c.loot.keySet()) require(java.util.Arrays.stream(new Direction[]{Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST})
.anyMatch(d -> reached.contains(chest.relative(d))), kind + " loot is not approachable");
} catch (AssertionError | RuntimeException failure) {
failures.add(kind + " public access facing=" + facing + ": " + failure.getMessage());
}
}
require(failures.isEmpty(), "City21 surface fixture failures (" + failures.size() + "):\n" + String.join("\n", failures));
exportPreviews();
Set<Map<BlockPos, BlockState>> parks = new HashSet<>();
for (Kind park : new Kind[] {Kind.PARK, Kind.PARK_GARDEN, Kind.PARK_PLAYGROUND, Kind.PARK_MEMORIAL}) {
parks.add(render(new Parcel(park, 0, 0, 17, 17, 170, 0, 0, 42, 4, new int[0])).blocks);
}
require(parks.size() == 4, "Park variants must have distinct geometry");
for (Kind kind : new Kind[] {Kind.POOL, Kind.EXCAVATION}) {
Capture fallback = render(new Parcel(kind, 0, 0, 17, 17, 170, 0, 0, 42, 4, new int[0]));
require(fallback.blocks.values().stream().noneMatch(state -> !state.getFluidState().isEmpty()),
"Uncertified ground produced a wet excavation");
require(fallback.block(new BlockPos(8, -1, 8)).is(Blocks.STONE_BRICKS), "Uncertified ground was excavated");
}
int[] heights = new int[17 * 5];
for (int z = 0; z < 17; z++) for (int x = 0; x < 5; x++) heights[z * 5 + x] = 170 + z / 4;
Parcel bridge = new Parcel(Kind.BRIDGE, 0, 0, 5, 17, 170, 0, 0, 33, 0, heights);
Capture result = render(bridge);
require(result.blocks.entrySet().stream().noneMatch(entry -> !entry.getValue().isAir()
&& entry.getKey().getY() < bridge.surfaceY(entry.getKey().getX(), entry.getKey().getZ()) - bridge.floorY()),
"Bridge grew a column below its suspended deck");
for (var e : result.blocks.entrySet()) if (e.getValue().is(Blocks.TUFF_BRICK_STAIRS)) {
var face = e.getValue().getValue(BlockStateProperties.HORIZONTAL_FACING);
var high = e.getKey().relative(face);
require(bridge.surfaceY(high.getX(), high.getZ()) - bridge.floorY() == e.getKey().getY(),
"Bridge stair fails to meet the higher deck");
}
Set<BlockPos> reached = walk(result, new BlockPos(2, 1, 0));
require(reached.contains(new BlockPos(2, 5, 16)), "Stepped bridge cannot be crossed");
}
private static void exportPreviews() {
String path = System.getProperty("sanctuary.city21.exteriorPreview");
if (path == null) return;
var examples = new java.util.ArrayList<Object>();
for (Kind kind : Kind.values()) if (!kind.building() && kind != Kind.STREET && kind != Kind.BRIDGE) {
var p = new Parcel(kind, 0, 0, 17, 17, 170, 0, 0, 42, 8, new int[0]); var c = render(p);
var blocks = new java.util.ArrayList<Object>();
c.blocks.forEach((at, state) -> { if (!state.isAir()) blocks.add(java.util.List.of(at.getX(), at.getY(), at.getZ(), state.toString())); });
examples.add(Map.of("kind", kind.name(), "seed", p.seed(), "facing", 0, "blocks", blocks));
}
try { var output = java.nio.file.Path.of(path); if (output.getParent() != null) java.nio.file.Files.createDirectories(output.getParent());
java.nio.file.Files.writeString(output, new com.google.gson.Gson().toJson(examples)); }
catch (java.io.IOException e) { throw new AssertionError("Cannot write requested geometry preview", e); }
}
private static Capture render(Parcel parcel) {
Capture result = new Capture(parcel);
LostCitySurface21.render(parcel, result);
return result;
}
/** Flood the cells a source can reach horizontally/downward, without permitting water to rise. */
private static void sealedPool(Capture result, int facing) {
Set<BlockPos> flooded = new HashSet<>();
ArrayDeque<BlockPos> queue = new ArrayDeque<>();
result.blocks.forEach((pos, state) -> {
if (!state.getFluidState().isEmpty()) { flooded.add(pos); queue.add(pos); }
});
require(flooded.size() > 150, "Pool must contain a real basin of water");
while (!queue.isEmpty()) {
BlockPos pos = queue.removeFirst();
require(pos.getY() >= -3 && pos.getY() <= -1, "Pool leaks vertically through its floor or coping");
BlockPos canonical = rotate(pos.getX(), pos.getY(), pos.getZ(), (4 - facing) % 4);
require(canonical.getX() >= 4 && canonical.getX() <= 12 && canonical.getZ() >= 5 && canonical.getZ() <= 12,
"Pool leaks outside its continuous side walls");
for (Direction direction : new Direction[] {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST, Direction.DOWN}) {
BlockPos next = pos.relative(direction);
BlockState state = result.block(next);
if ((state.isAir() || !state.getFluidState().isEmpty()) && flooded.add(next)) queue.addLast(next);
}
}
}
/** Conservative full-block walking with one-block steps and two blocks of clear headroom. */
private static Set<BlockPos> walk(Capture result, BlockPos start) {
require(standable(result, start), "Entrance is blocked: " + start);
Set<BlockPos> seen = new HashSet<>();
ArrayDeque<BlockPos> queue = new ArrayDeque<>();
queue.add(start);
seen.add(start);
while (!queue.isEmpty()) {
BlockPos current = queue.removeFirst();
for (Direction direction : new Direction[] {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}) {
for (int dy = -1; dy <= 1; dy++) {
BlockPos next = current.relative(direction).offset(0, dy, 0);
if (standable(result, next) && seen.add(next)) queue.addLast(next);
}
}
}
return seen;
}
private static boolean standable(Capture result, BlockPos pos) {
if (pos.getX() < 0 || pos.getZ() < 0 || pos.getX() >= result.parcel.width() || pos.getZ() >= result.parcel.depth()) return false;
BlockState support = result.block(pos.below());
return (support.isSolid() || support.getBlock() instanceof StairBlock)
&& clear(result.block(pos)) && clear(result.block(pos.above()));
}
private static boolean clear(BlockState state) { return state.isAir() || state.is(Blocks.WATER); }
private static BlockPos rotate(int x, int y, int z, int facing) {
return switch (facing) {
case 0 -> new BlockPos(x, y, z);
case 1 -> new BlockPos(16 - z, y, x);
case 2 -> new BlockPos(16 - x, y, 16 - z);
default -> new BlockPos(z, y, 16 - x);
};
}
private static final class Capture implements LostCityRenderer.BlockWriter {
private final Parcel parcel;
private final Map<BlockPos, BlockState> blocks = new HashMap<>();
private final Map<BlockPos, ResourceKey<LootTable>> loot = new HashMap<>();
Capture(Parcel parcel) { this.parcel = parcel; }
@Override public void set(int x, int y, int z, BlockState state) {
require(x >= 0 && z >= 0 && x < parcel.width() && z < parcel.depth()
&& y >= parcel.minY() - parcel.floorY() && y <= parcel.maxY() - parcel.floorY(),
parcel.kind() + " emitted a block outside its saved bounds at " + x + "," + y + "," + z);
blocks.put(new BlockPos(x, y, z), state);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long lootSeed) {
set(x, y, z, Blocks.CHEST.defaultBlockState());
loot.put(new BlockPos(x, y, z), table);
}
BlockState block(BlockPos pos) { return blocks.getOrDefault(pos, Blocks.AIR.defaultBlockState()); }
}
private static void require(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
}
@@ -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 schema21 persistence for catalogue sites and chunk-local abandoned routes. */
public final class HeritagePiece21Smoke {
public static void main(String[] args) {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); run();
System.out.println("HeritagePiece21Smoke: 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 = HeritagePiece21.encodeSite(site);
require(HeritagePiece21.decodeSite(saved).equals(site) && HeritagePiece21.encodeSite(HeritagePiece21.decodeSite(saved)).equals(saved), "Site roundtrip changed geometry");
require(site.supports().size() == 1 && HeritagePiece21.bounds(site).minY() == 150, "Support input is mutable or excluded from native bounds");
require(HeritagePiece21.locator(site).getX() == entry.x() && HeritagePiece21.locator(site).getY() == entry.y()
&& HeritagePiece21.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(HeritagePiece21.decodeSegment(HeritagePiece21.encodeSegment(landing)).equals(landing), "Standalone entry landing lost its site binding");
var saved = HeritagePiece21.encodeSegment(segment);
require(HeritagePiece21.decodeSegment(saved).equals(segment) && HeritagePiece21.encodeSegment(HeritagePiece21.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(HeritagePiece21.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 { HeritagePiece21.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 { HeritagePiece21.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,217 @@
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 HeritagePlanner21Smoke {
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 = HeritagePlanner21.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 = HeritagePlanner21.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 = HeritagePlanner21.plan(0, 0, 0, 724, new Fixture((x, z) -> 220));
var oldFlat = HeritagePlanner19.plan(0, 0, 0, 724, new Fixture((x, z) -> 220));
var reached21 = new HashSet<String>(); flat.links().forEach(l -> { reached21.add(l.fromId()); reached21.add(l.toId()); });
var reached19 = new HashSet<String>(); oldFlat.links().forEach(l -> { reached19.add(l.fromId()); reached19.add(l.toId()); });
require(reached21.size() >= reached19.size(), "Proximity graph connects fewer real destinations than19 on its flat witness");
// Expensive duplicate edges must not consume the search reserved for a still isolated destination.
var components = new HashMap<String, String>();
flat.sites().forEach(site -> components.put(site.id(), site.id()));
for (var link : flat.links()) {
String from = root(components, link.fromId()), to = root(components, link.toId());
require(!from.equals(to), "An already connected flat destination receives a redundant budget-consuming cycle");
components.put(to, from);
}
require(flat.links().size() <= 24 && flat.segments().size() <= 512, "Route graph exceeded saved model limits");
require(!flat.sites().equals(HeritagePlanner21.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 = HeritagePlanner21.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 = HeritagePlanner21.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 = HeritagePlanner21.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 = HeritagePlanner21.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 = HeritagePlanner21.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 = HeritagePlanner21.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 = HeritagePlanner21.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("HeritagePlanner21Smoke 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 String root(Map<String, String> components, String id) {
String parent = components.get(id);
while (!parent.equals(id)) { id = parent; parent = components.get(id); }
return parent;
}
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() <= HeritagePlanner21.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() <= HeritagePlanner21.MAX_ROUTE_CUT, "Route makes excessive terrain cuts");
require(cell.floorY() - terrain.height.applyAsInt(cell.x(), cell.z()) <= HeritagePlanner21.MAX_ROUTE_FILL,
"Route uses an unbounded fill to force a connection");
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 HeritagePlanner21.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,506 @@
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 HeritageRenderer21Smoke {
private HeritageRenderer21Smoke() {}
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("HeritageRenderer21Smoke passed: fourteen restrained generation21 palettes, four rotations, supported single roofs and daylight wells, reachable original targets, retained loot and connected block details");
}
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) {
export(canonical);
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 = HeritageRenderer21.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);
architecture21(c);
daylightAndCrown21(c);
roofBearings21(c);
}
}
require(silhouettes.size() == 14, "Catalogue does not contain fourteen physical architectures");
routes(); chunkPartition(); materials21();
}
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); HeritageRenderer21.render(site, c); return c; }
private static BlockPos at(HeritageRenderer21.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.OAK_STAIRS) || b.is(Blocks.BIRCH_STAIRS)) && p.getY() == 1) {
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.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.OAK_TRAPDOOR); }
}
}
private static void architecture21(Capture c) {
if (c.site.kind() != HeritagePlan19.Kind.GREENHOUSE) has(c, Blocks.SMOOTH_STONE);
has(c, Blocks.STONE_BRICKS);
for (var e : c.blocks.entrySet()) {
String name = net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(e.getValue().getBlock()).getPath();
require(e.getKey().getY() <= 0 || !name.contains("tuff"), label(c) + " uses tuff above ground: " + e.getKey());
boolean greenhouse = c.site.kind() == HeritagePlan19.Kind.GREENHOUSE;
for (String wood : List.of("dark_oak", "oak", "birch", "spruce", "jungle", "acacia", "mangrove", "cherry", "pale_oak")) {
boolean woodBlock = name.startsWith(wood + "_") || name.startsWith("stripped_" + wood + "_");
require(!woodBlock || wood.equals(greenhouse ? "birch" : "oak"),
label(c) + " mixes timber species with " + name + " at " + e.getKey());
}
}
// The working destinations keep exactly their historical entrances and service witnesses.
var previous = HeritageRenderer19.targets(c.site).stream().map(t -> t.name() + ":" + t.x() + ":" + t.y() + ":" + t.z()).toList();
var current = HeritageRenderer21.targets(c.site).stream().map(t -> t.name() + ":" + t.x() + ":" + t.y() + ":" + t.z()).toList();
require(previous.equals(current), label(c) + " changes the saved functional access contract");
for (var e : c.blocks.entrySet()) if (e.getKey().getY() < 0 && e.getKey().getY() >= -c.site.foundationDepth()) {
// Workshop/trench cuts are deliberate; the exterior foundation always remains brick masonry.
int x = e.getKey().getX(), z = e.getKey().getZ();
if (x == 0 || x == 16 || z == 0 || z == 16) require(e.getValue().is(Blocks.STONE_BRICKS), "Visible foundations lost the stone-brick construction");
}
}
private static void daylightAndCrown21(Capture c) {
if (c.site.kind() == HeritagePlan19.Kind.FREIGHT_STATION) {
for (int x : new int[] {7, 9}) for (int z : new int[] {6, 8}) {
require(c.block(rotate(new BlockPos(x, 9, z), c.site.facing())).is(Blocks.GLASS),
label(c) + " passenger skylight is open to the sky instead of glazed");
clearLightWell(c, x, z, 6, c.site.height());
}
}
if (c.site.kind() == HeritagePlan19.Kind.COMMUNAL_DEPOT) {
for (int x : new int[] {5, 9}) for (int z : new int[] {3, 7, 11}) {
require(c.block(rotate(new BlockPos(x, 11, z), c.site.facing())).is(Blocks.GLASS),
label(c) + " warehouse roof light is no longer glazed");
require(c.block(rotate(new BlockPos(x, 9, z), c.site.facing())).isAir(),
label(c) + " warehouse kept its obsolete flat ceiling beneath the new roof");
clearLightWell(c, x, z, 9, c.site.height());
}
}
if (c.site.kind() == HeritagePlan19.Kind.RAIL_WORKSHOP || c.site.kind() == HeritagePlan19.Kind.GREENHOUSE) {
int x = c.site.kind() == HeritagePlan19.Kind.GREENHOUSE ? 5 : 8, z = 3;
require(c.block(rotate(new BlockPos(x, 6, z), c.site.facing())).isAir(),
label(c) + " old ceiling remains beneath the vaulted roof");
require(c.block(rotate(new BlockPos(x, 9, z), c.site.facing())).is(Blocks.GLASS),
label(c) + " single vaulted roof lost its glazing");
clearLightWell(c, x, z, 6, c.site.height());
}
if (c.site.kind() == HeritagePlan19.Kind.RADIO_RELAY || c.site.kind() == HeritagePlan19.Kind.CONSTRUCTION_REFUGE) {
int x = c.site.kind() == HeritagePlan19.Kind.RADIO_RELAY ? 7 : 11;
// Sample between the radio's trusses (z6,7,10), not through its real middle tie beam.
int z = 9;
int former = c.site.kind() == HeritagePlan19.Kind.RADIO_RELAY ? 6 : 10;
require(c.block(rotate(new BlockPos(x, former, z), c.site.facing())).isAir(),
label(c) + " shelter kept two separate opaque roofs");
int runs = 0; boolean previous = false;
for (int y = former; y <= c.site.height(); y++) {
boolean occupied = !c.block(rotate(new BlockPos(x, y, z), c.site.facing())).isAir();
if (occupied && !previous) runs++;
previous = occupied;
}
require(runs == 1, label(c) + " shelter has an interrupted or duplicated roof profile");
}
if (c.site.kind() == HeritagePlan19.Kind.OBSERVATORY) {
// The inset crown must be joined through face-adjacent masonry to the tower roof.
// This detects a detached decorative ring even when every individual voxel is in bounds.
Set<BlockPos> connected = new HashSet<>(); var pending = new java.util.ArrayDeque<BlockPos>();
c.blocks.forEach((p, block) -> {
if (p.getY() == 12 && !block.isAir()) { connected.add(p); pending.add(p); }
});
while (!pending.isEmpty()) {
var p = pending.removeFirst();
for (Direction direction : Direction.values()) {
var next = p.relative(direction);
if (next.getX() < 0 || next.getX() > 16 || next.getZ() < 0 || next.getZ() > 16
|| next.getY() < 12 || next.getY() > 17 || c.block(next).isAir()) continue;
if (connected.add(next)) pending.addLast(next);
}
}
c.blocks.forEach((p, block) -> {
if (p.getY() == 17 && block.is(Blocks.SMOOTH_STONE_SLAB))
require(connected.contains(p), label(c) + " observatory crown floats above its buttresses at " + p);
});
require(c.block(rotate(new BlockPos(8, 17, 8), c.site.facing())).isAir(),
label(c) + " crown connection closes the sky-viewing slot");
}
}
private static void clearLightWell(Capture c, int x, int z, int low, int high) {
for (int y = low; y <= high; y++) {
var p = rotate(new BlockPos(x, y, z), c.site.facing());
var block = c.block(p);
require(block.isAir() || block.is(Blocks.GLASS) || block.is(Blocks.GLASS_PANE),
label(c) + " skylight is blocked by an opaque roof or ceiling at " + p + ": " + block);
}
}
private static void roofBearings21(Capture c) {
int roofLine = switch (c.site.kind()) {
case COMMUNAL_DEPOT -> 9;
case OBSERVATORY -> 12;
case CONSTRUCTION_REFUGE, IMPOSSIBLE_SERVICE -> 10;
case VOID_DOCK, SUBSTATION -> 5;
default -> 6;
};
Set<BlockPos> supported = new HashSet<>(); var queue = new ArrayDeque<BlockPos>();
for (int x = 0; x < 17; x++) for (int z = 0; z < 17; z++) {
var at = new BlockPos(x, 0, z);
if (bearing(c.block(at))) { supported.add(at); queue.addLast(at); }
}
while (!queue.isEmpty()) {
var at = queue.removeFirst();
for (var direction : Direction.values()) {
var next = at.relative(direction);
if (next.getX() < 0 || next.getX() > 16 || next.getZ() < 0 || next.getZ() > 16
|| next.getY() < 0 || next.getY() > c.site.height() || !bearing(c.block(next))) continue;
if (supported.add(next)) queue.addLast(next);
}
}
// Shutters and cabinet screens may hang from a wall or each other, but not hover
// over a counter. The playable floor hatch has its separately verified ladder/lip contract.
queue.addAll(supported);
while (!queue.isEmpty()) {
var at = queue.removeFirst();
for (var direction : Direction.values()) {
var next = at.relative(direction);
if (c.block(next).getBlock() instanceof TrapDoorBlock && supported.add(next)) queue.addLast(next);
}
}
for (var e : c.blocks.entrySet()) if (e.getKey().getY() > 0 && e.getValue().getBlock() instanceof TrapDoorBlock) {
boolean floorHatch = c.site.kind() == HeritagePlan19.Kind.CONSTRUCTION_REFUGE
&& e.getKey().equals(rotate(new BlockPos(10, 5, 11), c.site.facing()));
require(floorHatch || supported.contains(e.getKey()), label(c) + " decorative screen has no frame or counter at " + e.getKey());
}
long witnesses = 0;
for (var e : c.blocks.entrySet()) if (e.getKey().getY() > 0 && bearing(e.getValue())) {
if (e.getKey().getY() >= roofLine) witnesses++;
require(supported.contains(e.getKey()), label(c) + " construction or ornament is detached from all grounded walls/frames at " + e.getKey());
}
require(witnesses > 0, label(c) + " roof-bearing test has no physical roof witness");
}
private static boolean bearing(BlockState block) {
return !block.isAir() && block.getFluidState().isEmpty() && !block.is(Blocks.LANTERN)
&& !block.is(Blocks.LEVER) && !block.is(Blocks.LADDER) && !block.is(Blocks.RAIL)
&& !(block.getBlock() instanceof DoorBlock) && !(block.getBlock() instanceof TrapDoorBlock)
&& !block.hasProperty(BlockStateProperties.BED_PART);
}
private static void export(Capture c) {
String directory = System.getProperty("sanctuary.test.heritage21.export");
if (directory == null || directory.isBlank()) return;
var blocks = c.blocks.entrySet().stream().filter(e -> !e.getValue().isAir())
.sorted(java.util.Comparator.<Map.Entry<BlockPos, BlockState>>comparingInt(e -> e.getKey().getY())
.thenComparingInt(e -> e.getKey().getZ()).thenComparingInt(e -> e.getKey().getX()))
.map(e -> List.of(e.getKey().getX(), e.getKey().getY(), e.getKey().getZ(), e.getValue().toString())).toList();
var targets = HeritageRenderer21.targets(c.site).stream().map(t -> List.of(t.name(), t.x(), t.y(), t.z())).toList();
var document = Map.of("kind", c.site.kind().name(), "facing", c.site.facing(), "seed", c.site.seed(),
"width", 17, "depth", 17, "height", c.site.height(), "floor_y", 0, "blocks", blocks, "targets", targets);
try {
var root = java.nio.file.Path.of(directory); java.nio.file.Files.createDirectories(root);
java.nio.file.Files.writeString(root.resolve(c.site.kind().name().toLowerCase(java.util.Locale.ROOT) + ".json"),
new com.google.gson.GsonBuilder().setPrettyPrinting().create().toJson(document));
} catch (java.io.IOException error) { throw new AssertionError("Cannot export the requested architecture capture", error); }
}
private static void materials21() {
Map<Block, Long> counts = new HashMap<>();
for (var kind : HeritagePlan19.Kind.values()) render(site(kind, 0, 0)).blocks.forEach((p, b) -> {
if (p.getY() > 0 && !b.isAir()) counts.merge(b.getBlock(), 1L, Long::sum);
});
for (Block block : new Block[] {Blocks.SMOOTH_STONE, Blocks.STONE, Blocks.STONE_BRICKS,
Blocks.CRACKED_STONE_BRICKS, Blocks.MOSSY_STONE_BRICKS, Blocks.ANDESITE,
Blocks.OAK_WOOD, Blocks.BIRCH_WOOD, Blocks.OAK_PLANKS, Blocks.BIRCH_PLANKS})
require(counts.getOrDefault(block, 0L) > 0, "The restrained generation21 palette omits " + block);
require(counts.get(Blocks.SMOOTH_STONE) > counts.getOrDefault(Blocks.STONE_BRICKS, 0L)
&& counts.get(Blocks.SMOOTH_STONE) > counts.getOrDefault(Blocks.ANDESITE, 0L),
"Smooth stone is no longer the principal masonry of the catalogue");
}
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<>();
HeritageRenderer21.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;
HeritageRenderer21.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); }
}
@@ -0,0 +1,175 @@
package fr.koka.sanctuary.worldgen.transit;
import fr.koka.sanctuary.worldgen.city.LostCityInteriors21Smoke;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Module;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.HashSet;
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.nbt.CompoundTag;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.EmptyBlockGetter;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.loot.LootTable;
import net.minecraft.world.phys.AABB;
/** Native block-shape paths across the entire planned route, plus refusal and exact saved geometry. */
public final class Transit21Smoke {
public static void main(String[] args) {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); run();
System.out.println("Transit21Smoke: bounded planning, two surface stair exits, through-road and hall, supported roofs, strict saved modules and all interior targets passed.");
}
public static void run() {
var terrain = new Plateau();
for (int diameter : new int[]{512, 724, 1024}) {
var plan = TransitPlanner21.plan(0, 0, 0, diameter, terrain);
require(!plan.isEmpty(), "Continuous native rock rejected its through-route: " + plan.diagnostic());
var accesses = plan.modules().stream().filter(m -> m.kind() == Kind.ACCESS).toList();
require(accesses.size() == 2, "Route lacks two surface stair exits");
int span = Math.abs(accesses.get(0).centerX() - accesses.get(1).centerX())
+ Math.abs(accesses.get(0).centerZ() - accesses.get(1).centerZ());
require(span >= diameter * .4, "Route became a short unconnected cellar");
require(plan.modules().stream().anyMatch(m -> m.kind() == Kind.HALL), "Available space produced no industrial hall");
require(plan.equals(TransitPlanner21.plan(0, 0, 0, diameter, terrain)), "Identical input changed final saved geometry");
require(plan.samples() <= TransitPlan21.MAX_SAMPLES, "Planning exceeded its work bound");
for (var access : accesses) {
require(plan.protects(access.centerX(), access.surfaceY() + 2, access.minZ() - 1), "Outside access approach can be decorated shut");
require(!plan.contains(access.centerX(), access.surfaceY(), access.minZ() - 1), "Read-only approach became a painted platform");
}
if (diameter == 724) verifyWorld(plan);
for (var m : plan.modules()) {
var saved = TransitPiece21.encodeModule(m);
require(TransitPiece21.decodeModule(saved).equals(m), "Module changed its persisted geometry");
var foreign = saved.copy(); foreign.putInt("Foreign", 1); reject(foreign);
var wrongType = saved.copy(); wrongType.putLong("Width", m.width()); reject(wrongType);
var missing = saved.copy(); missing.remove("Supports"); reject(missing);
}
}
var blocked = TransitPlanner21.plan(0, 0, 0, 724, new Plateau() {
@Override public boolean protectedAt(int x, int y, int z) { return true; }
});
require(blocked.isEmpty(), "Transit destroyed protected construction/hydrology");
var voidFloor = TransitPlanner21.plan(0, 0, 0, 724, new Plateau() {
@Override public boolean solid(int x, int y, int z) { return y >= 245 && y <= 252; }
});
require(voidFloor.isEmpty(), "Transit built an unsupported island-wide bridge");
var shortGap = TransitPlanner21.plan(0, 0, 0, 512, new Plateau() {
@Override public boolean solid(int x, int y, int z) { return super.solid(x, y, z) && !(y >= 199 && y <= 214); }
});
require(!shortGap.isEmpty() && shortGap.modules().stream().flatMap(m -> m.supports().stream())
.anyMatch(s -> s.maxY() - s.minY() + 1 == 16), "Certified sixteen-block piers should bridge a bounded natural cavity");
boolean longPierRejected = false;
try { new TransitPlan21.Support(0, 200, 0, 216); } catch (IllegalArgumentException expected) { longPierRejected = true; }
require(longPierRejected, "A seventeen-block pier escaped the foundation contract");
var oneStepApproach = TransitPlanner21.plan(0, 0, 0, 512, new Plateau() {
@Override public int surfaceY(int x, int z) { return (x & 1) == 0 ? 251 : 252; }
});
require(!oneStepApproach.isEmpty() && oneStepApproach.modules().stream().filter(m -> m.kind() == Kind.ACCESS)
.allMatch(m -> m.surfaceY() == 252), "A one-step native approach should use its highest cell without levelling it");
// Different surface heights terminate a partial flight, not just multiples of five steps.
for (int height : new int[]{249, 250, 251, 252, 253, 254, 255, 256, 257, 258}) {
var p = TransitPlanner21.plan(3, 0, 0, 512, new Plateau() {
@Override public int surfaceY(int x, int z) { return height; }
});
require(!p.isEmpty(), "Admissible irregular stair rise was rejected");
for (var access : p.modules()) if (access.kind() == Kind.ACCESS) verifySingle(access);
}
}
private static void reject(CompoundTag tag) {
boolean rejected = false;
try { TransitPiece21.decodeModule(tag); } catch (IllegalArgumentException expected) { rejected = true; }
require(rejected, "Saved module accepted missing/foreign/wrong-typed geometry");
}
private static void verifySingle(Module m) {
var capture = new Capture(); renderInto(m, capture);
var entrance = world(m, TransitRenderer21.targets(m).stream().filter(t -> t.name().equals("surface_portico")).findFirst().orElseThrow());
var reached = walk(capture, entrance);
for (var target : TransitRenderer21.targets(m)) require(reached.contains(world(m, target)),
"Stair access rise=" + (m.surfaceY() - m.floorY()) + " cannot reach " + target.name() + " " + world(m, target));
}
private static void verifyWorld(TransitPlan21 plan) {
var capture = new Capture();
for (var module : plan.modules()) renderInto(module, capture);
var first = plan.modules().stream().filter(m -> m.kind() == Kind.ACCESS).findFirst().orElseThrow();
var entrance = new BlockPos(first.centerX(), first.surfaceY() + 1, first.minZ());
var reached = walk(capture, entrance);
for (var module : plan.modules()) for (var target : TransitRenderer21.targets(module))
require(reached.contains(world(module, target)), "Through-route cannot reach " + module.kind() + "/" + target.name() + " at " + world(module, target));
for (var chest : capture.loot) require(java.util.Arrays.stream(new Direction[]{Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST})
.anyMatch(d -> reached.contains(chest.relative(d))), "Tools chest has no reachable approach");
require(capture.loot.size() == 1, "Route must keep one modest shared tool chest");
}
private static void renderInto(Module m, Capture into) {
var local = new Capture();
TransitRenderer21.render(m, new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) {
require(x >= 0 && x < m.width() && z >= 0 && z < m.depth() && y >= m.boundsMinY() - m.minY() && y < m.height(),
"Transit renderer escaped its saved native bounds");
local.set(x, y, z, state);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { local.chest(x, y, z, table, seed); }
});
LostCityInteriors21Smoke.verifyGroundedGeometry(local.blocks, 0, "Transit " + m.kind() + " floor=" + m.floorY());
local.blocks.forEach((at, state) -> {
var position = at.offset(m.minX(), m.minY(), m.minZ());
require(into.blocks.putIfAbsent(position, state) == null, "Neighboring native modules overwrite each other");
require(!state.is(Blocks.SPAWNER) && state.getFluidState().isEmpty(), "Transit introduced a spawner or uncontrolled fluid");
});
for (var chest : local.loot) into.loot.add(chest.offset(m.minX(), m.minY(), m.minZ()));
}
private static BlockPos world(Module m, TransitRenderer21.Target t) { return new BlockPos(m.minX() + t.x(), m.minY() + t.y(), m.minZ() + t.z()); }
private static Set<BlockPos> walk(Capture capture, BlockPos first) {
require(standable(capture, first), "Blocked transit entrance " + first);
var seen = new HashSet<BlockPos>(); var queue = new ArrayDeque<BlockPos>(); seen.add(first); queue.add(first);
while (!queue.isEmpty()) {
var at = queue.removeFirst();
for (Direction direction : new Direction[]{Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST})
for (int dy = -1; dy <= 1; dy++) {
var next = at.relative(direction).offset(0, dy, 0);
if (dy > 0 && (!clear(capture, at.above()) || !ascending(capture, next.below(), direction))) continue;
if (dy < 0 && (!clear(capture, next.above()) || !ascending(capture, at.below(), direction.getOpposite()))) continue;
if (standable(capture, next) && seen.add(next)) queue.addLast(next);
}
}
return seen;
}
private static boolean ascending(Capture c, BlockPos at, Direction direction) {
var state = c.block(at);
return state.getBlock() instanceof net.minecraft.world.level.block.StairBlock
&& state.getValue(net.minecraft.world.level.block.state.properties.BlockStateProperties.HORIZONTAL_FACING) == direction
&& state.getValue(net.minecraft.world.level.block.state.properties.BlockStateProperties.HALF)
== net.minecraft.world.level.block.state.properties.Half.BOTTOM;
}
private static boolean standable(Capture c, BlockPos at) {
if (!c.blocks.containsKey(at) || !clear(c, at)) return false;
return c.block(at.below()).getCollisionShape(EmptyBlockGetter.INSTANCE, at.below()).toAabbs().stream()
.anyMatch(box -> box.maxY >= .99 && box.minX < .8 && box.maxX > .2 && box.minZ < .8 && box.maxZ > .2);
}
private static boolean clear(Capture c, BlockPos at) {
var body = new AABB(.2, 0, .2, .8, 1.8, .8);
for (int up = 0; up < 2; up++) for (var box : c.block(at.above(up)).getCollisionShape(EmptyBlockGetter.INSTANCE, at.above(up)).toAabbs())
if (body.intersects(box.move(0, up, 0))) return false;
return true;
}
private static class Plateau implements TransitPlanner21.Terrain {
@Override public boolean owns(int x, int z) { return Math.abs(x) < 600 && Math.abs(z) < 600; }
@Override public boolean solid(int x, int y, int z) { return y >= 80 && y <= 252; }
@Override public boolean protectedAt(int x, int y, int z) { return false; }
@Override public int surfaceY(int x, int z) { return 252; }
}
private static final class Capture implements LostCityRenderer.BlockWriter {
final Map<BlockPos, BlockState> blocks = new HashMap<>(); final Set<BlockPos> loot = new HashSet<>();
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(new BlockPos(x, y, z), state); }
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { set(x, y, z, Blocks.CHEST.defaultBlockState()); loot.add(new BlockPos(x, y, z)); }
BlockState block(BlockPos at) { return blocks.getOrDefault(at, Blocks.AIR.defaultBlockState()); }
}
private static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); }
}
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary" name = "Sanctuary"
author = "KOKA99CAB" author = "KOKA99CAB"
version = "0.1.0-alpha.20" version = "0.1.0-alpha.21"
pack-format = "packwiz:1.1.0" pack-format = "packwiz:1.1.0"
[index] [index]