Add alpha16 inhabited ruins and deep city infrastructure
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-09 18:12:06 +02:00
parent 71d5cf7906
commit 98fc13cf0a
39 changed files with 5419 additions and 27 deletions
+22
View File
@@ -1,5 +1,27 @@
# Changelog
## 0.1.0-alpha.16 — 2026-09-09
- Ville compacte obligatoire sur la nouvelle île initiale, structures activées.
Recherche naturelle puis réservation architecturale locale si nécessaire ;
les villes des continents restent conditionnelles.
- Grande piscine couverte avec bassin de 50 × 25 × 4 blocs, promenades et
marches. Parkings profonds de 3 à 8 plateformes, grande salle sèche et égouts
ouverts vers lextérieur avec écoulements natifs. Ces équipements restent
conditionnés par le relief, leurs fondations et un budget de recherche borné.
- Plans d’étages alternés, sanitaires clos et lits contre les murs, avec
circulation, orientations et appuis du mobilier contrôlés.
- Nouveau codec `sanctuary:island_v16`, journal16 et pièces natives propres.
Les générations publiées conservent leurs calculs, rendus et futurs chunks.
Un nouveau monde est nécessaire pour les nouveautés ; aucune conversion.
`check build assemblePack` passe, ainsi que création/réouverture : 71 pièces,
5 coffres et 74 appuis conservés. Piscine retenue et deux écoulements extérieurs
vérifiés après 120 ticks réels. Six reliefs contrôlés, avec piscines dans trois
cas et villes dans les six. Compatibilité 15 vérifiée contre ses empreintes
publiées. Artefacts vérifiés ; publication en préparation. Voir
[les preuves et limites alpha.16](docs/testing-alpha16.md).
## 0.1.0-alpha.15 — 2026-09-09
- Six familles de bâtiments reprennent leurs intérieurs : vraies pièces et
+30 -6
View File
@@ -15,10 +15,34 @@ 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)
conserve les intentions ; le [backlog](docs/backlog.md) suit les
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.15 est publiée** :
Lost City devient une petite ville compacte, avec intérieurs détaillés et
réseaux souterrains. Le canal packwiz et la même instance Prism Mac sont à jour ;
voir [Distribution](docs/packwiz.md).
décrits dans la vision ne sont pas encore implémentés. **Lalpha.16 est vérifiée,
en préparation de publication** : une ville sur l’île initiale, de grandes
piscines, des parkings profonds et des égouts ouverts. Voir
[Distribution](docs/packwiz.md).
## Alpha.16 — Sanctuary habitée avant nous
Une ville compacte apparaît sur la nouvelle île de départ lorsque les structures
sont activées. Les étages varient leurs plans ; les sanitaires sont clos et les
lits adossés aux murs. Sous la ville, la génération recherche une piscine couverte
de **50 × 25 blocs**, des parkings de **3 à 8 plateformes**, une grande salle
sèche et des égouts qui souvrent vers lextérieur avec de vrais écoulements.
Chaque équipement dépend du relief et des appuis : les six îles testées ont une
ville, mais seules trois accueillent la grande piscine. Les continents conservent
leurs villes conditionnelles.
Créer un **nouveau monde Sanctuary** pour la génération 16. Les mondes publiés
jusqu’à lalpha.15 gardent leur génération, y compris dans leurs futurs chunks.
Le bouton natif **Personnaliser** conserve Petit 512, Moyen 724 par défaut et
Grand 1 024. Le [protocole alpha.16](docs/testing-alpha16.md) donne les preuves,
les limites et les coordonnées du monde **Moyen, graine 0**, qui contient la
piscine, huit niveaux de parking, la salle et l’égout ouvert.
`check build assemblePack`, la création et la réouverture natives passent.
La compatibilité 15 est vérifiée contre les empreintes de la version publiée.
Les futurs donjons formeront plusieurs réseaux parfois connectés, avec des
installations encore utilisables et des chantiers interrompus. Le réacteur,
les reliques et leurs récompenses restent à construire.
## Alpha.15 — ville compacte et réseaux
@@ -355,7 +379,7 @@ sera une mise à jour explicite, avec vérification des API et des sauvegardes.
| Fabric API | 0.160.0+26.3 |
| Fabric Loom | 1.17.20 |
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
| Sanctuary / pack | 0.1.0-alpha.15 |
| Sanctuary / pack | 0.1.0-alpha.16 |
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
@@ -384,7 +408,7 @@ décrits dans [Validation](docs/testing.md).
Résultats :
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.15.jar` : mod à installer avec
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.16.jar` : mod à installer avec
Fabric API sur la version Minecraft indiquée.
- `build/packwiz/` : pack de développement complet, avec le mod construit et
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
+38
View File
@@ -628,6 +628,44 @@ final passe. Release, canal packwiz et même instance Prism à jour ; deux
synchronisations réussies et 767 fichiers personnels/réglages suivis conservés.
Voir [les preuves](testing-alpha15.md) et [le reçu](packwiz.md).
### WG-19 — Sanctuary habitée avant nous — alpha.16
**Branche :** `codex/sanctuary-ruins-alpha16`.
**Contrat :** conserver lalpha.15 publiée et corriger les ruines pour que
Sanctuary Island possède un quartier identifiable et des infrastructures
profondes qui donnent limpression dune occupation antérieure.
- Une zone de ville est obligatoire sur la nouvelle île initiale ; les
continents restent soumis à leur profil et au terrain. Garder une ville
compacte, ses rues continues et la salle souterraine existante.
- Remplacer les minuscules bassins des pool rooms par de grandes piscines
souterraines, avec volumes lisibles, allées et accès découvrables.
- Prolonger des égouts jusqu’à de vraies ouvertures extérieures, avec sources
et écoulements natifs contrôlés ; les fuites vers le vide sont souhaitées.
- Ajouter sous la ville des plateformes de parking profondes, des ouvertures
et des circulations en descente. Les volumes, supports et coûts de génération
restent bornés ; ne pas fabriquer une masse cylindrique sous l’île.
- Varier les plans et usages entre étages ; sanitaires clos et raccordés aux
cloisons, lits adossés à un mur, orientations et passages cohérents.
- Nouvelle génération16, codecs et pièces propres : les mondes publiés jusqu’à15
gardent leurs plans, rendus, journaux et chunks futurs. Aucun monde personnel
nest converti ou régénéré. Tests dans de nouveaux mondes de développement.
- Le réacteur, les nouvelles familles de donjons, les reliques et leurs règles
de butin restent à concevoir par questions à partir de la vision. Ne pas
implémenter le réacteur ni activer la progression dans cet incrément.
- Valider la présence et laccès des piscines, les exutoires après ticks réels,
les parcours, la variation des étages, la persistance et la compatibilité15.
Exécuter `check build assemblePack`, publier lalpha.16 et synchroniser la même
instance Prism selon le canal packwiz et ses sauvegardes ciblées.
**État : code et artefacts vérifiés, publication en préparation.** Création et
réouverture natives réussies, 71 pièces conservées, piscine de 50 × 25 et
parking à huit niveaux démontrés. Les six reliefs contrôlés ont une ville ; les
équipements restent conditionnels. `check build assemblePack` passe et la
compatibilité15 conserve les empreintes publiées. Voir [les preuves](testing-alpha16.md).
Les choix des futurs réseaux et donjons sont consignés dans la vision.
## 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.
+134
View File
@@ -0,0 +1,134 @@
# Alpha.16 — Sanctuary habitée avant nous
**État : code et artefacts vérifiés ; publication en préparation.** Ticket WG-19,
branche `codex/sanctuary-ruins-alpha16`. Minecraft 26.3-pre-2, Fabric Loader 0.19.5,
Fabric API 0.160.0+26.3 et Java 25, comme lalpha.15.
## Contrat de génération
La génération 16 demande un **nouveau monde Sanctuary**. Le choix public reste
unique, avec le bouton natif Personnaliser : Petit 512, Moyen 724 par défaut,
Grand 1024. Les codecs `island_v13`, `island_v14`, `island_v15`, leurs pièces
natives, journaux, biomes et calculs de chunks futurs sont conservés. Aucun
monde personnel nest converti, ouvert par les tests ou régénéré.
Lorsque les structures sont activées, l’île de départ reçoit une ville
compacte. Une recherche naturelle est suivie, si nécessaire, dune réservation
architecturale locale 49 × 49 : rues et bâtiments reliés au-dessus dun socle
creux, avec appuis de maçonnerie certifiés. Cette réservation peut dégager le
terrain et remplacer leau dans sa seule emprise, avant le placement des pièces.
Elle ne modifie pas la densité de toute l’île et ne sapplique pas aux continents.
Les expansions conservent leur profil et leur placement soumis au terrain.
La passe souterraine comprend une piscine couverte avec bassin de 50 × 25
blocs et quatre blocs de profondeur, des égouts ouverts et des parkings de
3, 4, 6 ou 8 plateformes selon les appuis disponibles. Une grande salle sèche
peut compléter le réseau. Chaque équipement dépend des volumes et appuis
certifiés sur le terrain ; le tableau
ci-dessous distingue les témoins natifs des cas de modèle. Les anciens petits
bassins de la génération 15 restent inchangés.
Les plans des étages alternent réellement. Les sanitaires ont des parois
complètes, leur porte et leur plafond ; les lits sont adossés aux murs avec
une rive latérale accessible. Portes, sièges et lanternes conservent leurs
contrôles dorientation et dappui.
## Vérifications
- Les cinq smokes 16 passent : intérieurs, planification, fondation, protection
et sous-sols. Ils vérifient notamment les quatre orientations, les plans
d’étages, les sanitaires, les lits et les appuis.
- Matrice de planification moteur : les trois tailles 512/724/1024 × graines 0/1
passent. Une ville de 3 à 8 lots, avec rues et entrées reliées dans les six cas.
Ces six reliefs trouvent une implantation naturelle ; le recours architectural
est couvert séparément par les terrains adverses, le rendu borné et le NBT
des smokes. Rapport : `build/alpha16-evidence/planning-matrix.json`.
- Création native 16, graine 0, taille Moyenne, puis réouverture dans une seconde
JVM : **71 pièces, 5 coffres différés, 107 cibles de parcours et 74 appuis**
conservés entre l’île et une expansion de 256 blocs. Les hashes des starts,
le journal et le bloc dor témoin restent identiques. Processus 29685 puis 29853.
Rapports : `build/alpha16-evidence/final-city-create.json` et
`final-city-reload.json` ; logs `build/alpha16-third-city-create.log` et
`alpha16-city-reload.log`.
- Après **120 ticks du monde réellement observés**, la piscine garde ses
**4 976 cellules deau** ; les deux égouts observés déversent de leau au-delà
de leur bouche. Dix-neuf témoins temporaires indépendants prouvent lactivité
effective du moteur des fluides, puis sont supprimés avant sauvegarde. Les
cascades atteignent des corniches : elles ne sont pas toutes libres jusquau
vide inférieur. Aucun test ne présente une simple attente comme un écoulement.
- Compatibilité 15 : création puis réouverture sous le mod 16 réussies,
76 pièces natives, 5 coffres, 108 cibles de parcours et 5 appuis conservés.
Les SHA-256 des deux starts et du journal sont exactement ceux de lalpha.15
publiée. Les fichiers de génération 15 restent inchangés. Le preset caché
`sanctuary_v15.json` est identique à lancien preset public, SHA-256
`6c4a91eede4b9978526d6de6289dbc7394ed4114ebcae9f779fac3ce7e7d887e`.
Rapports : `build/alpha16-evidence/legacy15-create.json` et `legacy15-reload.json`.
- `./gradlew check build assemblePack` passe en 2 min 18 s : huit GameTests,
tous les smokes et index du pack vérifiés. Log :
`build/alpha16-final-check-build-pack.log`.
- JAR, MRpack et ZIP damorçage vérifiés : même version et même JAR, index et
hashes cohérents, aucun code de test embarqué. SHA-256 du JAR :
`5504018c46300accf01478dbc8bfb8e216395a5435cf7d7f501005cbc65b8e43`.
### Distribution des sous-sols sur six reliefs
Un second relevé utilise le vrai bruit Minecraft et les protections hydrologiques,
sans générer les chunks de la ville. Il contrôle les volumes, les fondations et
les limites de calcul ; les fluides et la persistance restent prouvés par le
scénario natif ci-dessus. Rapport : `build/alpha16-evidence/underground-matrix.json`.
| Diamètre | Graine | Piscine | Grande salle | Parking | Égout ouvert |
| --- | --- | --- | --- | --- | --- |
| 512 | 0 | Absente | Présente | 8 niveaux | Présent |
| 512 | 1 | Absente | Présente | 8 niveaux | Présent |
| 724 | 0 | Présente | Présente | 8 niveaux | Présent |
| 724 | 1 | Absente | Présente | 8 niveaux | Présent |
| 1024 | 0 | Présente | Absente | 8 niveaux | Présent |
| 1024 | 1 | Présente | Présente | 8 niveaux | Présent |
La piscine est donc présente dans trois cas sur six, la salle dans cinq cas.
Les six réseaux ont un accès depuis la surface. Ce petit échantillon ne donne
pas une fréquence statistique ni une garantie pour les autres graines. Le
budget souterrain atteint son plafond de 420 000 sondes dans quatre cas ;
la recherche sarrête alors en conservant les équipements déjà certifiés.
Une aile absente ne prouve donc pas labsence de tout emplacement compatible :
une autre solution peut se trouver au-delà du budget de recherche.
La passe souterraine prend 2,9 à 7,0 secondes après la planification de surface
sur cette machine ; cela ne mesure ni le rendu client ni le chargement complet.
## Repères pour lessai en jeu
Créer un nouveau monde **Sanctuary, Moyen, graine 0**, structures activées.
Les coordonnées suivantes proviennent des blocs réellement générés et relus.
Elles ne constituent pas une migration dun monde précédent.
| Lieu | Repère X / Y / Z |
| --- | --- |
| Entrée de limmeuble | 30 / 252 / 21 |
| Trappe depuis la rue | **19 / 252 / 41** |
| Promenade de la piscine | 45 / 230 / 11 |
| Grande salle sèche | 42 / 231 / 20 |
| Parking, niveau supérieur | 10 / 230 / 20 |
| Parking, niveau inférieur | 10 / 118 / 20 |
| Bouche de l’égout inférieur | **50 / 117 / 20** |
Le bassin remplit X 44 à 5, Y 225 à 228, Z 23 à 1. Les plateformes du
parking sont à Y 117, 133, 149, 165, 181, 197, 213 et 229. Le réseau est
accessible depuis la trappe de rue. Sur ce témoin, la cascade atteint une
corniche à Y 113. Lexpansion de référence `city16_0_2`, graine
9111000990819250332, possède aussi une bouche à 1958 / 202 / 651.
Les équipements restent conditionnés par leurs volumes et appuis ; seule la
zone de ville initiale est garantie par le recours architectural. Aucun essai Windows natif ni revue visuelle en client na été effectué pour
cet incrément ; les vérifications ci-dessus proviennent du moteur serveur Mac. Le pack doit encore être validé en jeu par Koka.
## Suite validée avec Koka, hors alpha.16
Développer les trois familles : mines et ateliers, lieux étranges,
infrastructures à réactiver. Plusieurs réseaux distincts se connectent parfois.
Les installations encore utilisables et les chantiers interrompus racontent
loccupation passée par dautres joueurs. Les back rooms de ces donjons sont
physiquement dans l’île et restent distinctes de la dimension des objets perdus.
Le butin combinera des reliques collectives uniques et des récompenses
personnelles. Les règles dattribution restent à définir ; aucun réacteur,
nouveau spawner de donjon ou système de progression nest ajouté ici.
+22 -3
View File
@@ -178,8 +178,9 @@ L'ambition est de reprendre puis d'étendre la centaine de biomes du catalogue T
- **Lost City** : des quartiers traités comme un biome, où les bâtiments
contemporains en ruine prennent la place principale. Lalpha.14 développe une
réalisation originale Sanctuary, inspirée du principe de ville abandonnée,
sans ajouter le mod The Lost Cities ni reprendre ses assets. Une petite ville compacte est
souhaitée sur l’île de départ, puis des quartiers dans les continents dexpansion, en laissant
sans ajouter le mod The Lost Cities ni reprendre ses assets. Lalpha.16 garantit une petite ville compacte sur la nouvelle île de départ,
lorsque les structures sont activées, puis admet des quartiers conditionnels
dans les continents dexpansion, en laissant
une majorité naturelle à l’île initiale. La pierre, la pierre lisse et les
briques de pierre, notamment fissurées et moussues, donnent une palette sobre.
- **Vie urbaine passée** : routes, ponts, places, marchés, appartements,
@@ -201,7 +202,12 @@ L'ambition est de reprendre puis d'étendre la centaine de biomes du catalogue T
rooms en réseau, procéduralement creusés sous la ville,
avec des passages et des accès de surface. Une section suspendue exige des
chaînes ou des pylônes ancrés. Leur présence reste liée au terrain ; le
[suivi de validation](testing-alpha15.md) distingue les intentions des preuves.
suivi de validation distingue les intentions des preuves. Lalpha.16 ajoute
un bassin couvert de 50 × 25 blocs, des plateformes de parking jusqu’à huit
niveaux et des exutoires ouverts avec écoulement natif. Les étages alternent
leurs plans, les sanitaires sont clos et les lits adossés aux murs. Seule la
zone urbaine initiale est garantie ; piscine et autres grands équipements
dépendent du terrain et des appuis. Voir [la validation alpha.16](testing-alpha16.md).
- **Future salle dexpansion** : une grande salle sèche reliée aux tunnels
pourrait accueillir une machine diégétique. Un socle et des cadres porteraient
des cartes déjà découvertes, présentant lensemble de l’île en vue du dessus.
@@ -212,6 +218,19 @@ L'ambition est de reprendre puis d'étendre la centaine de biomes du catalogue T
spitters, boomers, hunters, chargeurs et autres classes arrivent avec les
systèmes de la bêta. La présence de zombies dépend des règles du jeu.
- **Structures uniques et donjons** : d'abord sur Sanctuary Island, puis dans les continents. Ils peuvent contenir des clés de chunk et d'autres ressources rares.
- **Sanctuary était habitée avant nous** (orientation validée après lalpha.15) :
développer les trois familles ensemble : mines et ateliers, lieux étranges,
infrastructures à réactiver. Les ruines doivent raconter une occupation
antérieure par dautres joueurs, à travers leurs usages et leurs circulations.
Plusieurs réseaux distincts peuvent se rejoindre ponctuellement : découvrir
ces connexions fait partie de lexploration. Privilégier les installations
encore utilisables et les chantiers interrompus pour raconter cette histoire.
Les back rooms de ces donjons sont des lieux physiques dans l’île, distincts
de la dimension Backrooms recevant les objets perdus. Le futur butin combine
des reliques collectives uniques et des récompenses personnelles ; les
spawners pourront rester exploitables après exploration sans régénérer les
donjons. Les règles précises restent à concevoir. Le réacteur et ces nouveaux
systèmes ne font pas partie de lalpha.16.
- **Secret rooms et challenge rooms** : des accès présents dans le monde mènent à des indoors aménagés, avec des équipements, des épreuves ou des gisements de fer, d'or, d'argent ou de titane.
### Dimensions
+2 -2
View File
@@ -8,6 +8,6 @@ loader_version=0.19.5
loom_version=1.17.20
fabric_api_version=0.160.0+26.3
mod_version=0.1.0-alpha.15
pack_version=0.1.0-alpha.15
mod_version=0.1.0-alpha.16
pack_version=0.1.0-alpha.16
maven_group=fr.koka.sanctuary
+50 -2
View File
@@ -40,11 +40,14 @@ tasks.named('runGameTest') {
systemProperty('sanctuary.test.unified', providers.gradleProperty('sanctuaryUnifiedTests').getOrElse('true'))
systemProperty('sanctuary.test.diameter', providers.gradleProperty('sanctuaryTestDiameter').getOrElse('724'))
systemProperty('sanctuary.test.generation', providers.gradleProperty('sanctuaryTestGeneration').getOrElse(
providers.gradleProperty('sanctuaryLostCityTests').map { it.toBoolean() }.getOrElse(false) ? '14' : '15'))
providers.gradleProperty('sanctuaryLostCityTests').map { it.toBoolean() }.getOrElse(false) ? '14' : providers.gradleProperty('sanctuaryLostCity15Tests').map { it.toBoolean() }.getOrElse(false) ? '15' : '16'))
systemProperty('sanctuary.test.expansion', providers.gradleProperty('sanctuaryExpansionTests').getOrElse('false'))
systemProperty('sanctuary.test.expansionReload', providers.gradleProperty('sanctuaryExpansionReload').getOrElse('false'))
systemProperty('sanctuary.test.lostCity', providers.gradleProperty('sanctuaryLostCityTests').getOrElse('false'))
systemProperty('sanctuary.test.lostCity15', providers.gradleProperty('sanctuaryLostCity15Tests').getOrElse('false'))
systemProperty('sanctuary.test.lostCity16', providers.gradleProperty('sanctuaryLostCity16Tests').getOrElse('false'))
systemProperty('sanctuary.test.cityMatrix', providers.gradleProperty('sanctuaryCityMatrix').getOrElse('false'))
systemProperty('sanctuary.test.cityMatrixOnly', providers.gradleProperty('sanctuaryCityMatrixOnly').getOrElse('false'))
systemProperty('sanctuary.test.voidReuse', providers.gradleProperty('sanctuaryVoidReuseTests').getOrElse('false'))
systemProperty('sanctuary.test.optionalStructureWitness', providers.gradleProperty('sanctuaryOptionalStructureWitness').getOrElse('false'))
}
@@ -60,10 +63,12 @@ tasks.named('processGametestResources') {
inputs.property('lostCity', lostCity)
def lostCity15 = providers.gradleProperty('sanctuaryLostCity15Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('lostCity15', lostCity15)
def lostCity16 = providers.gradleProperty('sanctuaryLostCity16Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('lostCity16', lostCity16)
def voidReuse = providers.gradleProperty('sanctuaryVoidReuseTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('voidReuse', voidReuse)
filesMatching('fabric.mod.json') {
expand(gametest_entrypoint: voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark
expand(gametest_entrypoint: voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark
? 'fr.koka.sanctuary.gametest.WorldgenPerformanceGameTests'
: unified ? 'fr.koka.sanctuary.gametest.UnifiedWorldGameTests'
: 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests')
@@ -352,3 +357,46 @@ tasks.register('lostCityPlanner15Smoke', JavaExec) {
mainClass = 'fr.koka.sanctuary.worldgen.city.LostCityPlanner15Smoke'
}
tasks.named('check') { dependsOn('lostCityPlanner15Smoke') }
tasks.register('lostCityInteriors16Smoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.city.LostCityInteriors16Smoke'
}
tasks.named('check') { dependsOn('lostCityInteriors16Smoke') }
tasks.register('lostCityUnderground16Smoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.city.LostCityUnderground16Smoke'
}
tasks.named('check') { dependsOn('lostCityUnderground16Smoke') }
tasks.register('lostCityProtection16Smoke', JavaExec) {
group = 'verification'
description = 'Check bounded three-dimensional city protection and immutable snapshots.'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.city.LostCityProtection16Smoke'
}
tasks.named('check') { dependsOn('lostCityProtection16Smoke') }
tasks.register('lostCityPlanner16Smoke', JavaExec) {
group = 'verification'
description = 'Check terrain-certified city fragments, inset floors and anchored viaducts.'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.city.LostCityPlanner16Smoke'
}
tasks.named('check') { dependsOn('lostCityPlanner16Smoke') }
tasks.register('lostCityFoundation16Smoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.city.LostCityFoundation16Smoke'
}
tasks.named('check') { dependsOn('lostCityFoundation16Smoke') }
@@ -0,0 +1,266 @@
package fr.koka.sanctuary.gametest;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import fr.koka.sanctuary.worldgen.PopulationGenerationContext;
import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import fr.koka.sanctuary.worldgen.SanctuaryDecorationProtection;
import fr.koka.sanctuary.worldgen.city.LostCityPlan;
import fr.koka.sanctuary.worldgen.city.LostCityPlanner16;
import fr.koka.sanctuary.worldgen.city.LostCityProtection16;
import fr.koka.sanctuary.worldgen.city.LostCityTerrain16;
import fr.koka.sanctuary.worldgen.city.LostCityUnderground16;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.RandomState;
/** Six isolated, real starter-noise witnesses. This helper never requests a chunk, attaches a
* generator, mutates the active biome source or creates an expansion session. Hydrology is the
* production planner and its exact protection mask, rather than an all-dry testing substitute.
* Surface availability is asserted; underground coverage is reported as a survey, not a guarantee. */
public final class LostCity16PlanningDiagnostics {
private LostCity16PlanningDiagnostics() {}
public static List<Map<String, Object>> verify(GameTestHelper helper) {
var level = helper.getLevel();
helper.assertTrue(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator,
"Native city matrix requires the Sanctuary source");
var active = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
helper.assertTrue(active.rootGeneration() == 16 && level.getMinY() == 0 && level.getHeight() == 384,
"Native city matrix uses generation16 and its actual 384-block height");
var originalIslands = active.source().islands();
var access = level.registryAccess();
var settings = access.lookupOrThrow(Registries.NOISE_SETTINGS);
var noises = access.lookupOrThrow(Registries.NOISE);
var reports = new ArrayList<Map<String, Object>>();
for (int diameter : new int[] {512, 724, 1024}) for (long seed : new long[] {0, 1}) {
long started = System.nanoTime();
int capacity = diameter == 512 ? 5 : diameter == 724 ? 10 : 20;
var key = ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("unified_" + capacity));
var registered = settings.getOrThrow(key);
var island = ExpansionIsland.origin(diameter, seed, 16);
var source = active.source().copy();
helper.assertTrue(source != active.source() && source.generation() == 16, "Matrix shares a mutable biome source");
source.setIslands(List.of(island));
var generator = new NoiseBasedChunkGenerator(source, registered);
var random = RandomState.create(noises, seed, registered.value());
var binding = new PopulationGenerationContext.Binding(generator, random);
Map<Long, SanctuaryDecorationProtection> protection = new HashMap<>();
var terrain = new LostCityTerrain16(island, generator, random, level, position -> {
int cx = position.getX() >> 4, cz = position.getZ() >> 4;
return protection.computeIfAbsent(key(cx, cz), ignored -> SanctuaryDecorationProtection.create(binding, cx, cz))
.protects(position);
});
var plan = LostCityPlanner16.plan(seed, 0, 0, diameter, true, terrain);
int plannedColumns = terrain.columnsUsed();
helper.assertTrue(plan.columns() == plannedColumns && plan.columns() <= LostCityPlanner16.MAX_COLUMNS,
label(diameter, seed) + " exceeds or bypasses its surface column budget");
helper.assertTrue(plan.volumeChecks() <= LostCityPlanner16.MAX_VOLUME_CHECKS,
label(diameter, seed) + " exceeds its volume-check budget");
var report = verifyPlan(helper, diameter, seed, island, plan, terrain);
helper.assertTrue(terrain.columnsUsed() == plannedColumns,
label(diameter, seed) + " accepted a foundation that the planner never sampled");
report.put("surface_elapsed_ms", (System.nanoTime() - started) / 1_000_000);
var underground = surveyUnderground(seed, plan, terrain);
report.put("underground", underground);
report.put("noise_settings", key.identifier().toString());
report.put("columns", plan.columns());
report.put("total_columns", terrain.columnsUsed());
report.put("volume_checks", plan.volumeChecks());
report.put("hydrology_regions", PopulationHydrologyRuntime.cachedRegionCount(random));
report.put("hydrology_mask_chunks", protection.size());
report.put("native_noise", true);
report.put("chunk_requests", 0);
report.put("elapsed_ms", (System.nanoTime() - started) / 1_000_000);
reports.add(Map.copyOf(report));
SanctuaryMod.LOGGER.info("Lost City16 matrix seed {} size {}: {} lots, fallback {}, {} anchored piles, {} columns, {} ms",
seed, diameter, report.get("lots"), report.get("fallback"), report.get("foundation_anchors"),
plan.columns(), report.get("elapsed_ms"));
SanctuaryMod.LOGGER.info("Lost City16 utility survey seed {} size {}: types {}, missing {}, {} modules, {} garage levels, {} total columns, {} UG samples",
seed, diameter, underground.get("types"), underground.get("missing_requested_types"),
underground.get("modules"), underground.get("garage_levels"), terrain.columnsUsed(), underground.get("samples"));
}
helper.assertTrue(active.source().islands().equals(originalIslands), "Read-only city matrix changed the live island snapshot");
return List.copyOf(reports);
}
/** Mirrors SanctuaryChunkGenerator's generation16 adapter, sharing its sampled columns and
* reserving exactly the same piers/architectural foundation before underground prospecting. */
private static Map<String, Object> surveyUnderground(long seed, LostCityPlanner16.Result placement, LostCityTerrain16 sampled) {
long started = System.nanoTime();
int surfaceColumns = sampled.columnsUsed();
var supports = new LostCityProtection16.Builder();
for (var support : placement.supports()) supports.addViaductSupport(support);
if (placement.reservation() != null) supports.addReservation(placement.reservation());
var supportMask = supports.build();
var underground = LostCityUnderground16.plan(seed, placement.surface(), new 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);
}
});
Map<String, Integer> types = new TreeMap<>();
var details = new ArrayList<Map<String, Object>>();
int garageLevels = 0, largestGarage = 0;
for (var network : underground.networks()) for (var module : network.modules()) {
types.merge(module.kind().name(), 1, Integer::sum);
var floors = module.parkingFloors();
garageLevels += floors.size(); largestGarage = Math.max(largestGarage, floors.size());
Map<String, Object> detail = new LinkedHashMap<>();
detail.put("kind", module.kind().name());
detail.put("bounds", List.of(module.minX(), module.minY(), module.minZ(), module.maxX(), module.maxY(), module.maxZ()));
detail.put("floor_y", module.floorY()); detail.put("parking_floors", floors);
detail.put("supports", module.supports().size());
if (module.kind() == LostCityUnderground16.Kind.SEWER_OUTLET) {
var mouth = module.outletMouth();
detail.put("outlet_mouth", List.of(mouth.getX(), mouth.getY(), mouth.getZ()));
detail.put("outlet_direction", module.outletDirection());
}
details.add(Map.copyOf(detail));
}
var requested = List.of(LostCityUnderground16.Kind.SEWER, LostCityUnderground16.Kind.POOLROOM,
LostCityUnderground16.Kind.GARAGE, LostCityUnderground16.Kind.EXPANSION_HALL, LostCityUnderground16.Kind.SEWER_OUTLET);
Map<String, Object> report = new LinkedHashMap<>();
report.put("mode", "survey"); report.put("networks", underground.networks().size());
report.put("types", types); report.put("modules", details.size()); report.put("module_details", details);
report.put("missing_requested_types", requested.stream().map(Enum::name).filter(kind -> !types.containsKey(kind)).toList());
report.put("garage_levels", garageLevels); report.put("largest_garage_levels", largestGarage);
report.put("surface_columns", surfaceColumns); report.put("total_columns", sampled.columnsUsed());
report.put("additional_columns", sampled.columnsUsed() - surfaceColumns);
report.put("samples", underground.samples()); report.put("sample_limit", LostCityUnderground16.MAX_SAMPLES);
report.put("column_limit", LostCityTerrain16.MAX_COLUMNS);
report.put("elapsed_ms", (System.nanoTime() - started) / 1_000_000);
return Map.copyOf(report);
}
private static Map<String, Object> verifyPlan(GameTestHelper helper, int diameter, long seed, ExpansionIsland island,
LostCityPlanner16.Result result, LostCityTerrain16 terrain) {
String label = label(diameter, seed);
helper.assertTrue(result.surface().districts().size() == 1, label + " must contain exactly one starter town");
var district = result.surface().districts().getFirst();
helper.assertTrue(district.maxX() - district.minX() + 1 <= 71 && district.maxZ() - district.minZ() + 1 <= 71,
label + " escapes the compact starter envelope");
Map<Long, Street> streets = new HashMap<>();
Set<Long> written = new HashSet<>();
var kinds = new TreeMap<String, Integer>();
int lots = 0, buildings = 0;
for (var parcel : district.parcels()) {
kinds.merge(parcel.kind().name(), 1, Integer::sum);
boolean street = parcel.kind() == LostCityPlan.Kind.STREET || parcel.kind() == LostCityPlan.Kind.BRIDGE;
if (!street) lots++;
if (parcel.kind().building()) buildings++;
helper.assertTrue(parcel.minY() >= 0 && parcel.maxY() <= 383
&& Math.abs((parcel.minX() >> 4) - district.startChunkX()) <= 7
&& Math.abs((parcel.maxX() >> 4) - district.startChunkX()) <= 7
&& Math.abs((parcel.minZ() >> 4) - district.startChunkZ()) <= 7
&& Math.abs((parcel.maxZ() >> 4) - district.startChunkZ()) <= 7,
label + " has a parcel outside native height/reference bounds");
for (int z = parcel.minZ(); z <= parcel.maxZ(); z++) for (int x = parcel.minX(); x <= parcel.maxX(); x++) {
helper.assertTrue(written.add(key(x, z)) && island.contains(x, z),
label + " has overlapping parcels or an unowned column");
int floor = parcel.surfaceY(x - parcel.minX(), z - parcel.minZ());
if (result.reservation() == null) {
var ground = terrain.ground(x, z);
helper.assertTrue(ground.top() <= floor + LostCityPlanner16.MAX_CUT,
label + " requires an unapproved natural terrain cut");
if (parcel.kind() != LostCityPlan.Kind.BRIDGE) {
int bottom = floor - parcel.foundationDepth();
helper.assertTrue(ground.dry() && ground.top() >= bottom
&& ground.top() - bottom + 1 <= ground.solidDepth(), label + " has an unsupported natural floor");
}
}
if (street) {
boolean railing = parcel.kind() == LostCityPlan.Kind.BRIDGE && (parcel.width() > parcel.depth()
? z == parcel.minZ() || z == parcel.maxZ() : x == parcel.minX() || x == parcel.maxX());
streets.put(key(x, z), new Street(x, z, floor, parcel, !railing));
}
}
}
helper.assertTrue(lots >= 3 && buildings >= (lots * 3 + 4) / 5, label + " is not a populated starter town");
Set<Long> reached = walk(helper, label, streets);
for (var parcel : district.parcels()) if (parcel.kind() != LostCityPlan.Kind.STREET && parcel.kind() != LostCityPlan.Kind.BRIDGE) {
helper.assertTrue(parcel.facing() == 0, label + " rotates its entrance away from its certified road");
for (int dx = 7; dx <= 9; dx++) {
long threshold = key(parcel.minX() + dx, parcel.minZ() - 1);
var road = streets.get(threshold);
helper.assertTrue(road != null && road.walkable() && road.y() == parcel.floorY() && reached.contains(threshold),
label + " has an isolated or obstructed entrance");
}
for (int x = parcel.minX(); x <= parcel.maxX(); x++)
helper.assertTrue(streets.containsKey(key(x, parcel.minZ() - 1)) && streets.containsKey(key(x, parcel.maxZ() + 1)),
label + " leaves natural soil between facades and streets");
for (int z = parcel.minZ(); z <= parcel.maxZ(); z++)
helper.assertTrue(streets.containsKey(key(parcel.minX() - 1, z)) && streets.containsKey(key(parcel.maxX() + 1, z)),
label + " lacks a side street beside an admitted lot");
}
for (var support : result.supports()) for (int z = support.minZ(); z <= support.maxZ(); z++)
for (int x = support.minX(); x <= support.maxX(); x++) {
var deck = streets.get(key(x, z));
helper.assertTrue(deck != null && deck.parcel().kind() == LostCityPlan.Kind.BRIDGE && deck.y() == support.maxY() + 1,
label + " has a pier that misses its bridge deck");
helper.assertTrue(terrain.solid(x, support.minY(), z) && terrain.solid(x, support.minY() - 1, z),
label + " has a viaduct pier without native rock at its foot");
}
var reservation = result.reservation();
if (reservation != null) {
helper.assertTrue(reservation.width() == 49 && reservation.depth() == 49 && lots == 4
&& reservation.minX() == district.minX() && reservation.maxX() == district.maxX()
&& reservation.minZ() == district.minZ() && reservation.maxZ() == district.maxZ(),
label + " fallback is not the explicit compact four-lot reservation");
for (var anchor : reservation.anchors()) for (int z = anchor.minZ(); z <= anchor.maxZ(); z++)
for (int x = anchor.minX(); x <= anchor.maxX(); x++)
helper.assertTrue(terrain.solid(x, anchor.minY(), z) && terrain.solid(x, anchor.minY() + 1, z),
label + " fallback invents a two-block native rock footing");
}
Map<String, Object> report = new LinkedHashMap<>();
report.put("generation", 16); report.put("diameter", diameter); report.put("seed", seed);
report.put("districts", 1); report.put("lots", lots); report.put("buildings", buildings); report.put("kinds", kinds);
report.put("bounds", List.of(district.minX(), district.minZ(), district.maxX(), district.maxZ()));
report.put("start_chunk", List.of(district.startChunkX(), district.startChunkZ()));
report.put("road_columns", streets.size()); report.put("walkable_road_columns", reached.size());
report.put("connected_roads", true); report.put("flush_entrances", true);
report.put("fallback", reservation != null); report.put("viaduct_piers", result.supports().size());
report.put("foundation_anchors", reservation == null ? 0 : reservation.anchors().size());
report.put("passed", true);
return report;
}
private static Set<Long> walk(GameTestHelper helper, String label, Map<Long, Street> streets) {
Set<Long> walkable = new HashSet<>();
streets.forEach((position, street) -> { if (street.walkable()) walkable.add(position); });
helper.assertTrue(!walkable.isEmpty(), label + " has no walkable street");
Set<Long> reached = new HashSet<>(); var queue = new ArrayDeque<Long>();
long start = walkable.iterator().next(); reached.add(start); queue.add(start);
while (!queue.isEmpty()) {
var current = streets.get(queue.removeFirst());
for (int[] delta : new int[][] {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}) {
long position = key(current.x() + delta[0], current.z() + delta[1]);
var next = streets.get(position);
if (next == null || !next.walkable()) continue;
helper.assertTrue(Math.abs(current.y() - next.y()) <= 1, label + " contains a street cliff");
if (current.parcel() != next.parcel()) helper.assertTrue(current.y() == next.y(),
label + " requires an unpainted stair between two independent pieces");
if (reached.add(position)) queue.addLast(position);
}
}
helper.assertTrue(reached.equals(walkable), label + " contains disconnected streets");
return reached;
}
private record Street(int x, int z, int y, LostCityPlan.Parcel parcel, boolean walkable) {}
private static long key(int x, int z) { return ((long) x << 32) ^ (z & 0xffffffffL); }
private static String label(int diameter, long seed) { return "Native city size=" + diameter + " seed=" + seed; }
}
@@ -39,7 +39,11 @@ final class UnifiedWorldSettingsDiagnostics {
var historical14 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v14"))).value().createWorldDimensions();
helper.assertTrue(historical14.overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 14 && old.source().generation() == 14,
"Historical alpha.14 keeps its own biome codec and city generator");
var historical15 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v15"))).value().createWorldDimensions();
helper.assertTrue(historical15.overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 15 && old.source().generation() == 15,
"Historical alpha.15 keeps its own biome codec and city generator");
var generator = (SanctuaryChunkGenerator) standard.overworld();
helper.assertTrue(generator.rootGeneration() == 16 && generator.source().generation() == 16, "The public preset uses generation16");
helper.assertTrue(generator.initialDiameter() == 724, "The public preset defaults to Medium / 724 blocks");
for (var tag : List.of(WorldPresetTags.NORMAL, WorldPresetTags.EXTENDED)) {
var visible = presets.getOrThrow(tag).stream().map(holder -> holder.getRegisteredName())
@@ -49,7 +53,7 @@ final class UnifiedWorldSettingsDiagnostics {
}
var ops = RegistryOps.create(JsonOps.INSTANCE, access);
var activeCodec = generator.rootGeneration() == 15 ? SanctuaryChunkGenerator.CITY15_CODEC : generator.rootGeneration() == 14 ? SanctuaryChunkGenerator.CITY_CODEC : SanctuaryChunkGenerator.CODEC;
var activeCodec = generator.rootGeneration() == 16 ? SanctuaryChunkGenerator.CITY16_CODEC : generator.rootGeneration() == 15 ? SanctuaryChunkGenerator.CITY15_CODEC : generator.rootGeneration() == 14 ? SanctuaryChunkGenerator.CITY_CODEC : SanctuaryChunkGenerator.CODEC;
var serialized = activeCodec.codec().encodeStart(ops, generator).getOrThrow().getAsJsonObject();
serialized.remove("initial_diameter");
helper.assertTrue(activeCodec.codec().parse(ops, serialized).getOrThrow().initialDiameter() == 724,
@@ -22,7 +22,7 @@ public abstract class SanctuaryGameTestServerMixin {
@Inject(method = "waitUntilNextTick", at = @At("RETURN"))
private void sanctuary$paceExpansionWorkers(CallbackInfo ci) {
if (Boolean.getBoolean("sanctuary.test.expansion") || Boolean.getBoolean("sanctuary.test.lostCity")
|| 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 remaining;
// Worker completion unparks the server; keep the pacing deadline despite those signals.
@@ -46,9 +46,9 @@ public abstract class SanctuaryGameTestServerMixin {
"Lnet/minecraft/world/level/levelgen/presets/WorldPresets;FLAT_ALL_DIMENSIONS:Lnet/minecraft/resources/ResourceKey;"))
private static ResourceKey<WorldPreset> sanctuary$selectActualPreset() {
if (Boolean.getBoolean("sanctuary.test.unified")) {
int generation = Integer.getInteger("sanctuary.test.generation", 15);
if (generation != 13 && generation != 14 && generation != 15) throw new IllegalArgumentException("Unsupported test generation");
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 15 ? "sanctuary" : "sanctuary_v" + generation));
int generation = Integer.getInteger("sanctuary.test.generation", 16);
if (generation != 13 && generation != 14 && generation != 15 && generation != 16) throw new IllegalArgumentException("Unsupported test generation");
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 16 ? "sanctuary" : "sanctuary_v" + generation));
}
if (Boolean.getBoolean("sanctuary.test.expansion")) return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("expansion_lab"));
int players = Integer.getInteger("sanctuary.test.players", 10);
@@ -55,6 +55,8 @@ public final class SanctuaryMod implements ModInitializer {
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v14"), SanctuaryBiomeSource.CITY_CODEC);
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v15"), SanctuaryChunkGenerator.CITY15_CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v15"), SanctuaryBiomeSource.CITY15_CODEC);
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v16"), SanctuaryChunkGenerator.CITY16_CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v16"), SanctuaryBiomeSource.CITY16_CODEC);
fr.koka.sanctuary.worldgen.city.LostCityStructures.register();
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("population_island"), PopulationIslandDensity.CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("population_island"), PopulationIslandBiomeSource.CODEC);
@@ -41,7 +41,7 @@ public record ExpansionIsland(String id, String parent, String direction, String
if (!((spatialGeneration(generation) ? DIAMETERS : LEGACY_DIAMETERS).contains(diameter) || (id.equals("sanctuary") && (diameter == 724 || (generation == 13 || spatialGeneration(generation)) && diameter == 1024)))) throw new IllegalArgumentException("Unsupported nominal diameter");
if (Math.abs((long) centerX) > 100_000 || Math.abs((long) centerZ) > 100_000 || (centerX & 15) != 0 || (centerZ & 15) != 0) throw new IllegalArgumentException("Invalid island centre");
if (!List.of("reserved", "ready").contains(status) || (generation != GENERATION && !spatialGeneration(generation) && !(generation == 13 && id.equals("sanctuary")))) throw new IllegalArgumentException("Unsupported island state or generation");
if (climate.equals("lost_city") && (!spatialGeneration(generation) || id.equals("sanctuary"))) throw new IllegalArgumentException("Lost City requires a generation 14 or 15 expansion");
if (climate.equals("lost_city") && (!spatialGeneration(generation) || id.equals("sanctuary"))) throw new IllegalArgumentException("Lost City requires a generation 14, 15 or 16 expansion");
if (reuseToken == null || !reuseToken.isEmpty() && (!spatialGeneration(generation) || id.equals("sanctuary") || !reuseToken.matches("[0-9a-f]{64}")))
throw new IllegalArgumentException("Invalid void reuse certificate token");
}
@@ -53,7 +53,7 @@ public record ExpansionIsland(String id, String parent, String direction, String
return new ExpansionIsland("sanctuary", "sanctuary", "north", "temperate", diameter, 0, 0, seed, "natural", "ready", generation);
}
public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15; }
public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16; }
public boolean origin() { return id.equals("sanctuary"); }
public double referenceDiameter() { return origin() && diameter == 724 ? Math.sqrt(2) * 512 : 512; }
public double horizontalScale() { return origin() ? 1.0 : diameter / referenceDiameter(); }
@@ -127,8 +127,10 @@ public final class ExpansionStructures {
&& city.lostCityPlan(island).intersects(total.minX(), total.minZ(), total.maxX(), total.maxZ()))
return "occupied-city-district";
if (generator instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator city
&& city.intersectsCityConstruction15(total.minX() - 1, total.minY() - (temple ? 24 : 12), total.minZ() - 1,
total.maxX() + 1, total.maxY(), total.maxZ() + 1))
&& (city.intersectsCityConstruction15(total.minX() - 1, total.minY() - (temple ? 24 : 12), total.minZ() - 1,
total.maxX() + 1, total.maxY(), total.maxZ() + 1)
|| city.intersectsCityConstruction16(total.minX() - 1, total.minY() - (temple ? 24 : 12), total.minZ() - 1,
total.maxX() + 1, total.maxY(), total.maxZ() + 1)))
return "occupied-city-underground-or-support";
if (island.origin() && generator instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator unified) {
var binding = unified.starterContext();
@@ -37,6 +37,11 @@ public abstract class UnifiedDecorationProtectionMixin {
if (!region.isWithinWriteZone(pos)) return;
// The dedicated shoreline feature still grows cane on its naturally valid dry banks.
if (state.is(Blocks.SUGAR_CANE)) return;
// Generation16 reserves its architectural volume before decoration. Only its own
// native pieces may replace water there; published generations retain the old rule.
if (generator.rootGeneration() == 16
&& fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
&& generator.protectsCityDecoration(pos)) return;
if (generator.protectsDecoration(pos) && !region.getBlockState(pos).equals(state)) {
generator.rejectedDecorationWrite();
callback.setReturnValue(false);
@@ -34,6 +34,9 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
public static final MapCodec<SanctuaryBiomeSource> CITY15_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 15)));
public static final MapCodec<SanctuaryBiomeSource> CITY16_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 16)));
private final List<Holder<Biome>> allBiomes;
private final PopulationIslandBiomeSource population;
@@ -68,7 +71,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
cityPlans = java.util.Map.copyOf(plans);
}
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; }
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; }
@Override public BiomeResolver createResolver(Climate.Sampler sampler) {
BiomeResolver woodlands = population.createResolver(sampler);
@@ -8,6 +8,7 @@ import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import fr.koka.sanctuary.worldgen.city.LostCityPlanner15;
import fr.koka.sanctuary.worldgen.city.LostCityPlanner16;
import java.util.List;
import net.minecraft.core.registries.Registries;
import net.minecraft.server.level.ServerLevel;
@@ -33,6 +34,10 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
SanctuaryBiomeSource.CITY15_CODEC.fieldOf("biome_source").forGetter(SanctuaryChunkGenerator::source),
DIAMETER_CODEC.optionalFieldOf("initial_diameter", 724).forGetter(SanctuaryChunkGenerator::initialDiameter)
).apply(i, (source, diameter) -> new SanctuaryChunkGenerator(source, diameter, 15)));
public static final MapCodec<SanctuaryChunkGenerator> CITY16_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
SanctuaryBiomeSource.CITY16_CODEC.fieldOf("biome_source").forGetter(SanctuaryChunkGenerator::source),
DIAMETER_CODEC.optionalFieldOf("initial_diameter", 724).forGetter(SanctuaryChunkGenerator::initialDiameter)
).apply(i, (source, diameter) -> new SanctuaryChunkGenerator(source, diameter, 16)));
private volatile Context originContext;
private final int generation;
@@ -44,6 +49,10 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
private final java.util.Map<ExpansionIsland, fr.koka.sanctuary.worldgen.city.LostCityUnderground15.Plan> undergroundPlans = new java.util.concurrent.ConcurrentHashMap<>();
private final java.util.Map<ExpansionIsland, List<LostCityPlanner15.Support>> citySupports = new java.util.concurrent.ConcurrentHashMap<>();
private volatile fr.koka.sanctuary.worldgen.city.LostCityProtection15 cityProtection15 = fr.koka.sanctuary.worldgen.city.LostCityProtection15.EMPTY;
private final java.util.Map<ExpansionIsland, fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan> undergroundPlans16 = new java.util.concurrent.ConcurrentHashMap<>();
private final java.util.Map<ExpansionIsland, List<LostCityPlanner16.Support>> citySupports16 = new java.util.concurrent.ConcurrentHashMap<>();
private volatile fr.koka.sanctuary.worldgen.city.LostCityProtection16 cityProtection16 = fr.koka.sanctuary.worldgen.city.LostCityProtection16.EMPTY;
private final java.util.Map<ExpansionIsland, LostCityPlanner16.Fallback> cityReservations16 = new java.util.concurrent.ConcurrentHashMap<>();
private boolean cityStructuresEnabled;
// Root extent is finite (at most 68x68 chunks); each immutable mask is at most 12 KiB.
private final java.util.concurrent.ConcurrentHashMap<Long, SanctuaryDecorationProtection> protections = new java.util.concurrent.ConcurrentHashMap<>();
@@ -66,7 +75,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
if (generation != source.generation()) throw new IllegalArgumentException("Sanctuary generator and biome source versions must agree");
}
@Override public SanctuaryBiomeSource source() { return (SanctuaryBiomeSource) super.source(); }
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; }
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; }
public SanctuaryChunkGenerator withInitialDiameter(int diameter) {
return new SanctuaryChunkGenerator(source().copy(), diameter, generation);
}
@@ -117,6 +126,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
source().setCityPlans(java.util.Map.of());
return;
}
if (generation == 16 && !cityStructuresEnabled) {
cityProtection16 = fr.koka.sanctuary.worldgen.city.LostCityProtection16.EMPTY;
activeCityPlans = java.util.Map.of();
source().setCityPlans(java.util.Map.of());
return;
}
var published = new java.util.LinkedHashMap<String, fr.koka.sanctuary.worldgen.city.LostCityPlan>();
for (var island : islands) published.put(island.id(), lostCityPlan(island));
if (generation == 15) {
@@ -127,6 +142,16 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
}
cityProtection15 = protection.build();
}
if (generation == 16) {
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
for (var island : islands) {
protection.add(published.get(island.id()), undergroundPlan16(island));
for (var support : citySupports16(island)) protection.addViaductSupport(support);
var reservation = cityReservation16(island);
if (reservation != null) protection.addReservation(reservation);
}
cityProtection16 = protection.build();
}
// Rejected read-only prospects remain private and never enter the biome source.
activeCityPlans = java.util.Map.copyOf(published);
source().setCityPlans(published);
@@ -142,7 +167,10 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
if (source().islands().stream().noneMatch(saved -> cityIdentity(saved).equals(key))) {
cityPlans.remove(key);
undergroundPlans.remove(key);
undergroundPlans16.remove(key);
citySupports.remove(key);
citySupports16.remove(key);
cityReservations16.remove(key);
// City prospecting may have prepared the receipt-free terrain context too.
super.discardUnpublishedContext(key);
}
@@ -153,6 +181,33 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
return cityPlans.computeIfAbsent(cityIdentity(island), ignored -> {
long start = System.nanoTime();
var terrain = context(island);
if (generation == 16) {
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain16(island, terrain.generator(), terrain.random(),
heightAccessor, pos -> island.origin() && protectsDecoration(pos));
var placement = LostCityPlanner16.plan(island.seed(), island.centerX(), island.centerZ(),
island.diameter(), island.origin(), sampled);
var surface = placement.surface();
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 supportMask = supports.build();
var underground = fr.koka.sanctuary.worldgen.city.LostCityUnderground16.plan(island.seed(), 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 City16 {} 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);
return surface;
}
if (generation == 15) {
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain15(island, terrain.generator(), terrain.random(),
heightAccessor, pos -> island.origin() && protectsDecoration(pos));
@@ -197,6 +252,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
});
}
public boolean protectsCityDecoration(net.minecraft.core.BlockPos pos) {
if (generation == 16) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ());
if (generation == 15) return cityProtection15.contains(pos.getX(), pos.getY(), pos.getZ());
if (generation != 14) return false;
var island = islandAt(pos.getX(), pos.getZ());
@@ -217,6 +273,24 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
lostCityPlan(island);
return citySupports.getOrDefault(cityIdentity(island), List.of());
}
public boolean intersectsCityConstruction16(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
return generation == 16 && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
}
public fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan undergroundPlan16(ExpansionIsland island) {
if (generation != 16) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
lostCityPlan(island);
return undergroundPlans16.getOrDefault(cityIdentity(island), new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0));
}
public List<LostCityPlanner16.Support> citySupports16(ExpansionIsland island) {
if (generation != 16) return List.of();
lostCityPlan(island);
return citySupports16.getOrDefault(cityIdentity(island), List.of());
}
public LostCityPlanner16.Fallback cityReservation16(ExpansionIsland island) {
if (generation != 16) return null;
lostCityPlan(island);
return cityReservations16.get(cityIdentity(island));
}
/** Lost City handles exact, single-structure queries. Mixed sets retain the vanilla
* search unchanged: searching both engines with skipReferenced=true would consume
* two starts before choosing one result. Mixed-set city discovery is not added here. */
@@ -252,5 +326,16 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
@Override protected void afterTerrain(ExpansionIsland island, ChunkAccess chunk) {
// Root water/lava are already applied by the Population hook on the named delegate.
if (!island.origin()) super.afterTerrain(island, chunk);
if (generation == 16 && cityStructuresEnabled) {
var reserved = cityReservations16.get(cityIdentity(island));
if (reserved != null) {
var mutable = new net.minecraft.core.BlockPos.MutableBlockPos();
var at = chunk.getPos();
for (int x = Math.max(reserved.minX(), at.getMinBlockX()); x <= Math.min(reserved.maxX(), at.getMaxBlockX()); x++)
for (int z = Math.max(reserved.minZ(), at.getMinBlockZ()); z <= Math.min(reserved.maxZ(), at.getMaxBlockZ()); z++)
for (int y = reserved.minY(); y <= reserved.maxY(); y++)
chunk.setBlockState(mutable.set(x, y, z), net.minecraft.world.level.block.Blocks.AIR.defaultBlockState(), 0);
}
}
}
}
@@ -0,0 +1,45 @@
package fr.koka.sanctuary.worldgen.city;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
/** Explicit masonry for the mandatory starter's last-resort architectural foundation.
* The upper city owns every block from floorY-2 upward. This renderer writes only below that
* boundary, so loading native pieces in another order cannot erase streets or buildings. */
public final class LostCityFoundation16 {
private LostCityFoundation16() {}
@FunctionalInterface public interface Writer { void set(int x, int y, int z, BlockState state); }
/** World coordinates, with no terrain reads and no chunk requests. */
public static void render(LostCityPlanner16.Fallback reservation, long seed, Writer writer) {
int floor = reservation.floorY();
for (int z = reservation.minZ(); z <= reservation.maxZ(); z++)
for (int x = reservation.minX(); x <= reservation.maxX(); x++) {
int lx = x - reservation.minX(), lz = z - reservation.minZ();
boolean wall = lx == 0 || lx == 48 || lz == 0 || lz == 48;
boolean column = nearPier(lx) && nearPier(lz);
for (int y = reservation.minY(); y <= floor - 3; y++) {
BlockState state;
if (y >= floor - 5) state = y == floor - 3 ? Blocks.SMOOTH_STONE.defaultBlockState()
: LostCityPalette.masonry(seed, lx, y - floor, lz);
else if (y == reservation.minY()) state = Blocks.SMOOTH_STONE.defaultBlockState();
else if (wall || column || y == floor - 6 && (nearPier(lx) || nearPier(lz)))
state = LostCityPalette.masonry(seed, lx, y - floor, lz);
else state = Blocks.AIR.defaultBlockState();
writer.set(x, y, z, state);
}
}
for (var anchor : reservation.anchors())
for (int z = anchor.minZ(); z <= anchor.maxZ(); z++)
for (int x = anchor.minX(); x <= anchor.maxX(); x++)
for (int y = anchor.minY(); y <= anchor.maxY(); y++)
writer.set(x, y, z, y == anchor.minY() || y == anchor.maxY()
? Blocks.SMOOTH_STONE.defaultBlockState()
: LostCityPalette.masonry(seed, x - reservation.minX(), y - floor, z - reservation.minZ()));
}
private static boolean nearPier(int coordinate) {
for (int center = 2; center <= 46; center += 11) if (Math.abs(coordinate - center) <= 1) return true;
return false;
}
}
@@ -0,0 +1,76 @@
package fr.koka.sanctuary.worldgen.city;
import java.util.ArrayList;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
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.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;
/** Saved generation16 fallback masonry. The enclosing bounding box is NOT its write mask:
* below the basement only the explicitly saved three-by-three anchors are written. */
public final class LostCityFoundationPiece16 extends StructurePiece {
private final LostCityPlanner16.Fallback reservation;
private final long seed;
public LostCityFoundationPiece16(LostCityPlanner16.Fallback reservation, long seed) {
super(LostCityStructures.FOUNDATION16, 0, bounds(reservation));
this.reservation = reservation; this.seed = seed;
setOrientation(null);
}
public LostCityFoundationPiece16(StructurePieceSerializationContext context, CompoundTag tag) {
super(LostCityStructures.FOUNDATION16, tag);
reservation = readReservation(tag); seed = tag.getLong("FoundationSeed").orElseThrow();
if (!getBoundingBox().equals(bounds(reservation)))
throw new IllegalArgumentException("Saved city foundation bounds disagree with its geometry");
setOrientation(null);
}
public LostCityPlanner16.Fallback reservation() { return reservation; }
@Override public BlockPos getLocatorPosition() {
return new BlockPos(reservation.minX() + 24, reservation.floorY() + 1, reservation.minZ() + 24);
}
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
writeReservation(reservation, tag); tag.putLong("FoundationSeed", seed);
}
/** Public codec helpers are also used by round-trip tests without a live StructureStart. */
public static void writeReservation(LostCityPlanner16.Fallback r, CompoundTag tag) {
tag.putInt("SanctuarySchema", 1);
tag.putInt("MinX", r.minX()); tag.putInt("MinZ", r.minZ());
tag.putInt("Width", r.width()); tag.putInt("Depth", r.depth()); tag.putInt("FloorY", r.floorY());
tag.putInt("MinY", r.minY()); tag.putInt("MaxY", r.maxY());
int[] anchors = new int[r.anchors().size() * 6]; int i = 0;
for (var a : r.anchors()) {
anchors[i++] = a.minX(); anchors[i++] = a.minY(); anchors[i++] = a.minZ();
anchors[i++] = a.maxX(); anchors[i++] = a.maxY(); anchors[i++] = a.maxZ();
}
tag.putIntArray("Anchors", anchors);
}
public static LostCityPlanner16.Fallback readReservation(CompoundTag tag) {
if (tag.getInt("SanctuarySchema").orElseThrow() != 1)
throw new IllegalArgumentException("Unsupported generation16 foundation schema");
int[] saved = tag.getIntArray("Anchors").orElseThrow();
if (saved.length % 6 != 0 || saved.length > 150) throw new IllegalArgumentException("Invalid foundation anchors");
var anchors = new ArrayList<LostCityPlanner16.Anchor>();
for (int i = 0; i < saved.length; i += 6)
anchors.add(new LostCityPlanner16.Anchor(saved[i], saved[i + 1], saved[i + 2], saved[i + 3], saved[i + 4], saved[i + 5]));
return new LostCityPlanner16.Fallback(tag.getInt("MinX").orElseThrow(), tag.getInt("MinZ").orElseThrow(),
tag.getInt("Width").orElseThrow(), tag.getInt("Depth").orElseThrow(), tag.getInt("FloorY").orElseThrow(),
tag.getInt("MinY").orElseThrow(), tag.getInt("MaxY").orElseThrow(), anchors);
}
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
LostCityStructures.placing(() -> LostCityFoundation16.render(reservation, seed, (x, y, z, state) -> {
if (clip.isInside(x, y, z)) placeBlock(level, state, x, y, z, clip);
}));
}
public static BoundingBox bounds(LostCityPlanner16.Fallback r) {
int bottom = r.anchors().stream().mapToInt(LostCityPlanner16.Anchor::minY).min().orElse(r.minY());
return new BoundingBox(r.minX(), bottom, r.minZ(), r.maxX(), r.floorY() - 3, r.maxZ());
}
}
@@ -0,0 +1,584 @@
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 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-16 buildings. Generation 15 deliberately never calls this renderer.
* A north-facing plan is rotated once; all positions remain relative to the saved parcel. */
public final class LostCityInteriors16 {
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.DARK_OAK_PLANKS.defaultBlockState();
private static final BlockState TILE = Blocks.POLISHED_ANDESITE.defaultBlockState();
private static final int STOREY = 5;
private LostCityInteriors16() {}
/** 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);
new Building(parcel, output).render();
}
/** 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);
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-16 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); }
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, STONE);
box(0, 0, 0, 16, 0, 16, SMOOTH);
box(0, 1, 0, 16, roof + 4, 16, AIR);
shell(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() ^ 0x16c17a7eL ^ p.kind().ordinal());
}
void shell(int roof) {
for (int floor = 0; floor <= p.floors(); floor++) {
int base = floor * STOREY;
box(1, base, 1, 15, base, 15, floor == p.floors() ? SMOOTH : floorMaterial());
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.DARK_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() {
return switch (p.kind()) {
case LIBRARY, APARTMENT, HOTEL -> WOOD;
case RESTAURANT, BARRACKS -> TILE;
default -> SMOOTH;
};
}
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, p.kind() == Kind.BARRACKS ? brick(x, y, z) : STONE);
else if (window) {
if (roll(x, y / 5, z, 13) != 0)
set(x, y, z, p.kind() == Kind.BARRACKS ? rail(eastWest) : pane(eastWest));
} else set(x, y, z, withinFloor == 4 ? SMOOTH : brick(x, y, z));
}
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);
}
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, 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, WOOD);
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());
box(12, y + 3, 14, 14, y + 3, 14, Blocks.DARK_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.DARK_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.DARK_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.DARK_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++) 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) {
for (int n = 1; n <= 15; n++) {
if (roll(n, y, 1, 9) != 0) set(n, y + 1, 1, brick(n, y, 1));
if (roll(n, y, 15, 9) != 0) set(n, y + 1, 15, brick(n, y, 15));
set(1, y + 1, n, brick(1, y, n)); set(15, y + 1, n, brick(15, y, n));
}
box(11, y + 1, 11, 13, y + 1, 13, SMOOTH);
set(12, y + 2, 12, Blocks.IRON_TRAPDOOR);
set(6, y + 1, 13, Blocks.POTTED_DEAD_BUSH);
set(14, y + 2, 14, Blocks.COBWEB);
}
void door(int x, int y, int z, Direction facing, boolean rightHinge) {
BlockState state = facing(Blocks.DARK_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.DARK_OAK_FENCE); set(x, y + 2, z, Blocks.DARK_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.DARK_OAK_FENCE); set(x, y + 2, z, Blocks.DARK_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.DARK_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;
/** Generation16 surface parcel. Its distinct native type preserves the historical generation14 renderer. */
public final class LostCityPiece16 extends StructurePiece {
private static final int SCHEMA = 1;
private final LostCityPlan.Parcel parcel;
public LostCityPiece16(LostCityPlan.Parcel parcel) {
super(LostCityStructures.PIECE16, 0, bounds(parcel));
this.parcel = parcel;
setOrientation(null);
}
public LostCityPiece16(StructurePieceSerializationContext context, CompoundTag tag) {
super(LostCityStructures.PIECE16, 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) {
LostCitySurface16.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,590 @@
package fr.koka.sanctuary.worldgen.city;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.District;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Kind;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Parcel;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** Generation 16 only: one compact town, with an explicit architectural reservation as a last resort.
* A 22-block grid consists entirely of 17-block lots, five-block streets and junctions.
* Streets touch facades directly; no leftover strip of natural soil separates their entrances. */
public final class LostCityPlanner16 {
public static final int MAX_COLUMNS = 16_000;
public static final int MAX_VOLUME_CHECKS = 900_000;
public static final int MAX_CUT = 6;
private static final int CELL = 22, LOT = 17, MIN_FLOOR = 176;
private static final LostCityPlanner.Ground INVALID = new LostCityPlanner.Ground(0, 0, false, true);
private static final Kind[] BUILDINGS = {Kind.HOTEL, Kind.LIBRARY, Kind.RESTAURANT, Kind.BARRACKS, Kind.HALL};
private static final Kind[] PUBLIC_SPACES = {Kind.POOL, Kind.PARKING, Kind.EXCAVATION,
Kind.MARKET, Kind.PARK_GARDEN, Kind.PARK_PLAYGROUND, Kind.PARK_MEMORIAL, Kind.PARK};
private LostCityPlanner16() {}
public interface Terrain {
LostCityPlanner.Ground ground(int x, int z);
boolean solid(int x, int y, int z);
boolean protectedAt(int x, int y, int z);
boolean owns(int x, int z);
}
/** A short three-by-three pier with an embedded footing, never a column to the void. */
public record Support(int startChunkX, int startChunkZ, int minX, int minY, int minZ,
int maxX, int maxY, int maxZ, long seed) {
public Support {
if (maxX - minX != 2 || maxZ - minZ != 2 || minY < 1 || maxY > 382
|| maxY < minY || maxY - minY >= 16
|| Math.abs((minX >> 4) - startChunkX) > 7 || Math.abs((maxX >> 4) - startChunkX) > 7
|| Math.abs((minZ >> 4) - startChunkZ) > 7 || Math.abs((maxZ >> 4) - startChunkZ) > 7)
throw new IllegalArgumentException("Invalid generation-16 viaduct support");
}
}
/** A certified three-by-three foundation column; two blocks at its foot are native solid rock.
* Unlike a viaduct pier, an urban foundation may descend farther within its own narrow footprint. */
public record Anchor(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
public Anchor {
if (maxX - minX != 2 || maxZ - minZ != 2 || minY < 8 || maxY < minY || maxY > 267)
throw new IllegalArgumentException("Invalid generation-16 foundation anchor");
}
}
/** Explicit permission for NEW generation-16 starter terrain only. The main volume may be
* cleared before native pieces, and hydrology excluded within it. Anchors reserve only their
* narrow columns below the basement, never the enclosing cuboid down to the lowest footing.
* An all-air test fixture can still receive a self-supporting deck, but has no certified anchor. */
public record Fallback(int minX, int minZ, int width, int depth, int floorY, int minY, int maxY,
List<Anchor> anchors) {
public Fallback {
anchors = List.copyOf(anchors);
if (width != 49 || depth != 49 || floorY < MIN_FLOOR || floorY > 280
|| minY != floorY - 12 || maxY < floorY + 29 || maxY > 383 || anchors.size() > 25)
throw new IllegalArgumentException("Invalid generation-16 city reservation");
for (Anchor a : anchors) if (a.minX() < minX || a.maxX() >= minX + width
|| a.minZ() < minZ || a.maxZ() >= minZ + depth || a.maxY() != minY - 1)
throw new IllegalArgumentException("Foundation anchor escaped its reservation");
}
public int maxX() { return minX + width - 1; }
public int maxZ() { return minZ + depth - 1; }
public boolean contains(int x, int y, int z) {
if (x < minX || x > maxX() || z < minZ || z > maxZ()) return false;
if (y >= minY && y <= maxY) return true;
for (Anchor a : anchors) if (x >= a.minX() && x <= a.maxX() && z >= a.minZ() && z <= a.maxZ()
&& y >= a.minY() && y <= a.maxY()) return true;
return false;
}
}
public record Result(LostCityPlan surface, List<Support> supports, int columns, int volumeChecks,
Fallback reservation) {
public Result(LostCityPlan surface, List<Support> supports, int columns, int volumeChecks) {
this(surface, supports, columns, volumeChecks, null);
}
public Result {
java.util.Objects.requireNonNull(surface); supports = List.copyOf(supports);
if (columns < 0 || columns > MAX_COLUMNS || volumeChecks < 0 || volumeChecks > MAX_VOLUME_CHECKS)
throw new IllegalArgumentException("Invalid generation-16 planning budget");
}
}
public static Result plan(long seed, int centerX, int centerZ, int diameter, boolean origin, Terrain terrain) {
if (diameter < 64 || diameter > 1024 || origin && diameter < 512)
throw new IllegalArgumentException("Unsupported city island size");
int cells = origin ? 3 : diameter < 128 ? 1 : diameter < 256 ? 2 : diameter < 512 ? 3 : 4;
Probe probe = new Probe(terrain, centerX, centerZ, diameter);
Town best = null;
List<Integer> accepted = new ArrayList<>();
Candidate fallback = origin ? fallbackSite(seed, centerX, centerZ, probe) : null;
probe.columnLimit = origin ? 10_500 : MAX_COLUMNS;
probe.volumeLimit = origin ? 700_000 : MAX_VOLUME_CHECKS;
try {
var candidates = candidates(seed, centerX, centerZ, diameter, origin, cells, probe);
for (int i = 0; i < Math.min(4, candidates.size()); i++) {
Town town = build(seed, candidates.get(i), cells, probe);
accepted.add(town == null ? 0 : town.lots().size());
if (town != null && (best == null || town.lots().size() > best.lots().size())) best = town;
if (best != null && best.lots().size() >= Math.max(1, cells * cells - 1)) break;
}
} catch (BudgetExceeded exhausted) { probe.reject("natural-budget"); }
// A smaller complete block fits folds and narrow terraces which reject the 71x71 layout.
if (origin && (best == null || best.lots().size() < 3)) {
probe.columnLimit = 13_500; probe.volumeLimit = 750_000;
try {
var smaller = candidates(seed ^ 0x160249L, centerX, centerZ, diameter, true, 2, probe);
for (int i = 0; i < Math.min(4, smaller.size()); i++) {
Town town = build(seed, smaller.get(i), 2, probe);
accepted.add(town == null ? 0 : town.lots().size());
if (town != null && (best == null || town.lots().size() > best.lots().size())) best = town;
if (best != null && best.lots().size() >= 3) break;
}
} catch (BudgetExceeded exhausted) { probe.reject("small-natural-budget"); }
}
probe.columnLimit = MAX_COLUMNS; probe.volumeLimit = MAX_VOLUME_CHECKS;
int minimum = origin ? 3 : cells == 1 ? 1 : 2;
LostCityPlan surface = LostCityPlan.EMPTY;
List<Support> supports = List.of();
if (best != null && best.lots().size() >= minimum) {
var pieces = new ArrayList<Parcel>();
best.nodes().forEach(n -> pieces.add(n.parcel()));
best.edges().forEach(e -> pieces.addAll(e.pieces()));
pieces.addAll(assign(seed, best.lots()));
surface = new LostCityPlan(List.of(new District(best.startX(), best.startZ(), best.seed(), pieces)));
supports = best.edges().stream().flatMap(e -> e.supports().stream()).toList();
}
Fallback reservation = null;
if (origin && surface.isEmpty()) {
var reserved = reserve(seed, fallback, probe);
surface = reserved.surface(); reservation = reserved.reservation();
}
if (Boolean.getBoolean("sanctuary.test.lostCity") || Boolean.getBoolean("sanctuary.test.lostCity16")
|| Boolean.getBoolean("sanctuary.city.plannerDiagnostics"))
System.getLogger(LostCityPlanner16.class.getName()).log(System.Logger.Level.INFO,
"Lost City16 compact district seed=" + seed + " origin=" + origin + " diameter=" + diameter
+ " lots=" + (surface.isEmpty() ? 0 : reservation != null ? 4 : best.lots().size()) + " candidates=" + accepted
+ " fallback=" + (reservation != null) + " anchors=" + (reservation == null ? 0 : reservation.anchors().size())
+ " piers=" + supports.size() + " columns=" + probe.columns.size()
+ " volumeChecks=" + probe.volumeChecks + " rejected=" + probe.rejections);
return new Result(surface, supports, probe.columns.size(), probe.volumeChecks, reservation);
}
/** Coarse comparison is budgeted before the natural searches, so exhausting those searches
* cannot accidentally remove the starter's reserved city. This examines only nine local sites. */
private static Candidate fallbackSite(long seed, int cx, int cz, Probe probe) {
Candidate best = null;
double bearing = unit(mix(seed ^ 0x16FA11L)) * Math.PI * 2;
for (int i = 0; i < 9; i++) {
double angle = bearing + i * Math.PI / 4;
int x = cx - 22 + (i == 0 ? 0 : (int) Math.round(Math.cos(angle) * 48));
int z = cz - 22 + (i == 0 ? 0 : (int) Math.round(Math.sin(angle) * 48));
int valid = 0, low = 384, high = 0; boolean owned = true;
for (int dz = -2; dz <= 46; dz += 8) for (int dx = -2; dx <= 46; dx += 8) {
var g = probe.at(x + dx, z + dz);
if (g == INVALID) { owned = false; break; }
if (g.top() > 0) { low = Math.min(low, g.top()); high = Math.max(high, g.top()); }
if (g.dry() && g.solidDepth() >= 4 && g.top() >= MIN_FLOOR - 12) valid++;
}
if (!owned) continue;
Candidate site = new Candidate(x, z, valid, Math.max(0, high - low), mix(seed ^ i ^ 0xFA1116L));
if (best == null || site.score() > best.score() || site.score() == best.score() && site.spread() < best.spread()) best = site;
}
// A real starter owns its complete central envelope. This checks ownership, not terrain
// quality; no terrain-dependent throw is used as a substitute for the guarantee.
if (best == null) throw new IllegalArgumentException("Starter terrain does not own a central 49x49 city envelope");
return best;
}
private static Result reserve(long seed, Candidate site, Probe probe) {
int x = site.x(), z = site.z();
var elevations = new ArrayList<Integer>(); int highest = 0;
for (int dz = -2; dz <= 46; dz++) for (int dx = -2; dx <= 46; dx++) {
var g = probe.at(x + dx, z + dz);
if (g == INVALID) throw new IllegalArgumentException("Starter city reservation crosses another island's ownership");
if (g.top() > 0) { elevations.add(g.top()); highest = Math.max(highest, g.top()); }
}
elevations.sort(Integer::compareTo);
int floor = elevations.isEmpty() ? 220 : Math.clamp(elevations.get(elevations.size() / 2), MIN_FLOOR, 280);
int startX = (x + 22) >> 4, startZ = (z + 22) >> 4;
var pieces = new ArrayList<Parcel>();
var lots = new ArrayList<Lot>();
for (int row = 0; row <= 2; row++) for (int col = 0; col <= 2; col++) {
pieces.add(new Parcel(Kind.STREET, x + col * CELL - 2, z + row * CELL - 2,
5, 5, floor, 0, 0, mix(site.seed() ^ row * 13L ^ col), 2, new int[0]));
if (col < 2) pieces.add(new Parcel(Kind.STREET, x + col * CELL + 3, z + row * CELL - 2,
17, 5, floor, 0, 0, mix(site.seed() ^ row * 59L ^ col ^ 0x16EAL), 2, new int[0]));
if (row < 2) pieces.add(new Parcel(Kind.STREET, x + col * CELL - 2, z + row * CELL + 3,
5, 17, floor, 0, 0, mix(site.seed() ^ row * 71L ^ col ^ 0x16EEL), 2, new int[0]));
if (row < 2 && col < 2) lots.add(new Lot(row, col, x + col * CELL + 3, z + row * CELL + 3,
floor, 2, false, mix(site.seed() ^ row * 307L ^ col ^ 0x1607L)));
}
pieces.addAll(assign(seed, lots));
var anchors = new ArrayList<Anchor>();
try {
for (int dz = 0; dz <= 44; dz += 11) for (int dx = 0; dx <= 44; dx += 11) {
int top = floor - 13;
for (int y = top; y >= 9; y--) {
boolean rock = true;
for (int az = -1; az <= 1 && rock; az++) for (int ax = -1; ax <= 1; ax++)
if (!probe.solidAt(x + dx + ax, y, z + dz + az) || !probe.solidAt(x + dx + ax, y - 1, z + dz + az)) { rock = false; break; }
if (rock) { anchors.add(new Anchor(x + dx - 1, y - 1, z + dz - 1, x + dx + 1, top, z + dz + 1)); break; }
}
}
} catch (BudgetExceeded exhausted) { probe.reject("anchor-budget"); }
Fallback reservation = new Fallback(x - 2, z - 2, 49, 49, floor, floor - 12,
Math.min(383, Math.max(floor + 29, highest + 8)), anchors);
return new Result(new LostCityPlan(List.of(new District(startX, startZ, site.seed(), pieces))),
List.of(), probe.columns.size(), probe.volumeChecks, reservation);
}
private static List<Candidate> candidates(long seed, int cx, int cz, int diameter, boolean origin, int cells, Probe probe) {
var result = new ArrayList<Candidate>();
double bearing = unit(mix(seed ^ 0xC17517L)) * Math.PI * 2;
for (int attempt = 0; attempt < (origin ? 16 : diameter < 128 ? 5 : 12); attempt++) {
long salt = mix(seed ^ attempt * 0x9e3779b97f4a7c15L ^ 0x15C04A7L);
double radius = origin ? Math.min(150, diameter * .17) * (attempt < 8 ? 1 : .48)
: attempt == 0 ? 0 : Math.min(diameter * .15, 95);
double angle = bearing + attempt * Math.PI * 2 / 8;
int x = cx + (int) Math.round(Math.cos(angle) * radius) - cells * CELL / 2;
int z = cz + (int) Math.round(Math.sin(angle) * radius) - cells * CELL / 2;
int score = 0, spread = 0;
for (int row = 0; row < cells; row++) for (int col = 0; col < cells; col++) {
int low = Integer.MAX_VALUE, high = Integer.MIN_VALUE;
boolean valid = true;
for (int[] point : new int[][] {{0, 0}, {16, 0}, {0, 16}, {16, 16}, {8, 8}}) {
var g = probe.at(x + col * CELL + 3 + point[0], z + row * CELL + 3 + point[1]);
if (!land(g)) { valid = false; break; }
low = Math.min(low, g.top()); high = Math.max(high, g.top());
}
if (valid && high - MAX_CUT <= low + 9 && high >= MIN_FLOOR && low <= 350) {
score++; spread += high - low;
}
}
if (score > 0) result.add(new Candidate(x, z, score, spread, salt));
else probe.reject("coarse-no-lot");
}
result.sort(Comparator.comparingInt(Candidate::score).reversed().thenComparingInt(Candidate::spread).thenComparingLong(Candidate::seed));
return result;
}
private static Town build(long seed, Candidate site, int cells, Probe probe) {
int startX = Math.floorDiv(site.x() + cells * CELL / 2, 16), startZ = Math.floorDiv(site.z() + cells * CELL / 2, 16);
Node[][] nodes = new Node[cells + 1][cells + 1];
for (int row = 0; row <= cells; row++) for (int col = 0; col <= cells; col++) {
int x = site.x() + col * CELL, z = site.z() + row * CELL;
Footprint certificate = certify(x - 2, z - 2, 5, 5, MIN_FLOOR, 350, 8, probe);
if (certificate != null) nodes[row][col] = new Node(node(col, row, cells), x, z, certificate.floor(),
new Parcel(Kind.STREET, x - 2, z - 2, 5, 5, certificate.floor(), 0, 0,
mix(seed ^ site.seed() ^ node(col, row, cells)), certificate.foundation(), new int[0]));
}
Lot[][] lots = new Lot[cells][cells];
for (int row = 0; row < cells; row++) for (int col = 0; col < cells; col++) {
Node a = nodes[row][col], b = nodes[row][col + 1];
if (a == null || b == null) { probe.reject("lot-north-junction"); continue; }
int x = site.x() + col * CELL + 3, z = site.z() + row * CELL + 3;
int low = Math.max(MIN_FLOOR, Math.max(a.floor(), b.floor()) - 7);
int high = Math.min(350, Math.min(a.floor(), b.floor()) + 7);
Footprint certificate = certify(x, z, LOT, LOT, low, high, 29, probe);
if (certificate == null) continue;
boolean deep = solidTo(x, z, LOT, LOT, certificate.floor() - 8, probe)
&& minTop(x, z, LOT, LOT, probe) >= certificate.floor() - 1
&& unprotected(x, certificate.floor() - 8, z, x + 16, certificate.floor() - certificate.foundation() - 1, z + 16, probe);
lots[row][col] = new Lot(row, col, x, z, certificate.floor(), certificate.foundation(), deep,
mix(seed ^ site.seed() ^ col * 0xD1342543DE82EF95L ^ row * 0xC6BC279692B5CC83L));
}
Edge[][] horizontal = new Edge[cells + 1][cells], vertical = new Edge[cells][cells + 1];
var edges = new ArrayList<Edge>();
for (int row = 0; row <= cells; row++) for (int col = 0; col < cells; col++) {
Node a = nodes[row][col], b = nodes[row][col + 1];
if (a == null || b == null) continue;
Lot lot = row < cells ? lots[row][col] : null;
int mid = lot == null ? (a.floor() + b.floor()) / 2 : lot.floor();
Edge edge = shortRoad(a, b, mid, mix(seed ^ site.seed() ^ a.id() * 67L ^ b.id()), probe);
if (edge != null) { edges.add(edge); horizontal[row][col] = edge; }
}
for (int row = 0; row < cells; row++) for (int col = 0; col <= cells; col++) {
Node a = nodes[row][col], b = nodes[row + 1][col];
if (a == null || b == null) continue;
Edge edge = shortRoad(a, b, (a.floor() + b.floor()) / 2, mix(seed ^ site.seed() ^ a.id() * 163L ^ b.id()), probe);
if (edge != null) { edges.add(edge); vertical[row][col] = edge; }
}
// Long links may replace missing junctions across a fissure, never cross a lot or another street.
int bridges = 0;
for (boolean eastWest : new boolean[] {true, false}) for (int line = 0; line <= cells; line++)
for (int from = 0; from <= cells - 2; from++) for (int to = from + 2; to <= cells; to++) {
if (bridges >= 4) break;
Node a = eastWest ? nodes[line][from] : nodes[from][line];
Node b = eastWest ? nodes[line][to] : nodes[to][line];
if (a == null || b == null) continue;
boolean intermediate = false;
for (int n = from + 1; n < to; n++) if ((eastWest ? nodes[line][n] : nodes[n][line]) != null) intermediate = true;
if (intermediate) continue;
Edge bridge = viaduct(a, b, startX, startZ, mix(seed ^ site.seed() ^ a.id() * 479L ^ b.id()), probe);
if (bridge == null || edges.stream().anyMatch(e -> overlaps(e, bridge))) continue;
edges.add(bridge); bridges++;
}
var eligible = new ArrayList<Lot>();
for (int row = 0; row < cells; row++) for (int col = 0; col < cells; col++) {
if (lots[row][col] == null) continue;
if (horizontal[row][col] == null || horizontal[row + 1][col] == null
|| vertical[row][col] == null || vertical[row][col + 1] == null) {
probe.reject("lot-incomplete-streets"); continue;
}
// A bridge's intact side railing must never become a barrier in front of a doorway.
if (horizontal[row][col].pieces().stream().anyMatch(p -> p.kind() == Kind.BRIDGE)) {
probe.reject("lot-front-bridge-rail"); continue;
}
eligible.add(lots[row][col]);
}
return connected(startX, startZ, mix(seed ^ site.seed()), nodes, edges, eligible, cells);
}
private static Footprint certify(int x, int z, int width, int depth, int min, int max, int clearHeight, Probe probe) {
var elevations = new ArrayList<Integer>();
int low = Integer.MAX_VALUE, high = Integer.MIN_VALUE;
for (int dz = 0; dz < depth; dz++) for (int dx = 0; dx < width; dx++) {
var g = probe.at(x + dx, z + dz);
if (!land(g)) { probe.reject("footprint-support"); return null; }
low = Math.min(low, g.top()); high = Math.max(high, g.top()); elevations.add(g.top());
}
min = Math.max(min, high - MAX_CUT); max = Math.min(max, low + 9);
if (min > max) { probe.reject("footprint-grade"); return null; }
elevations.sort(Integer::compareTo);
int floor = Math.clamp(elevations.get(elevations.size() / 2), min, max);
int foundation = Math.max(2, floor - low + 1);
if (!solidTo(x, z, width, depth, floor - foundation, probe)) { probe.reject("footprint-rock"); return null; }
if (!unprotected(x, floor - foundation, z, x + width - 1, floor + clearHeight, z + depth - 1, probe)) {
probe.reject("footprint-protected"); return null;
}
return new Footprint(floor, foundation);
}
private static Edge shortRoad(Node a, Node b, int middle, long seed, Probe probe) {
if (Math.abs(a.floor() - middle) > 7 || Math.abs(b.floor() - middle) > 7) { probe.reject("street-grade"); return null; }
boolean alongX = a.z() == b.z();
int x = a.x() + (alongX ? 3 : -2), z = a.z() + (alongX ? -2 : 3);
int width = alongX ? 17 : 5, depth = alongX ? 5 : 17;
int[] heights = new int[width * depth];
int foundation = 2, lowRun = 0, longestLow = 0;
boolean fillable = true;
for (int step = 3; step <= 19; step++) {
int floor = streetY(a.floor(), middle, b.floor(), step);
boolean low = false;
for (int across = 0; across < 5; across++) {
int lx = alongX ? step - 3 : across, lz = alongX ? across : step - 3;
var g = probe.at(x + lx, z + lz);
if (g == INVALID || g.top() > floor + MAX_CUT) { probe.reject("street-cut"); return null; }
if (!land(g) || g.top() < floor - 9) fillable = false;
if (g.top() < floor - 3) low = true;
if (g.dry()) foundation = Math.max(foundation, floor - g.top() + 1);
heights[lz * width + lx] = floor;
if (!unprotected(x + lx, floor, z + lz, x + lx, floor + 8, z + lz, probe)) { probe.reject("street-water"); return null; }
}
lowRun = low ? lowRun + 1 : 0; longestLow = Math.max(longestLow, lowRun);
}
if (fillable && foundation <= 10) {
for (int dz = 0; dz < depth; dz++) for (int dx = 0; dx < width; dx++) {
int floor = heights[dz * width + dx];
var g = probe.at(x + dx, z + dz);
if (g.top() - (floor - foundation) + 1 > g.solidDepth()
|| !unprotected(x + dx, floor - foundation, z + dz, x + dx, floor - 1, z + dz, probe)) fillable = false;
}
} else fillable = false;
if (!fillable && longestLow < 4) { probe.reject("street-foundation"); return null; }
Kind kind = fillable ? Kind.STREET : Kind.BRIDGE;
return new Edge(a.id(), b.id(), List.of(new Parcel(kind, x, z, width, depth, middle, 0, 0, seed,
fillable ? foundation : 0, heights)), List.of());
}
/** Flat five-wide nodes and a three-column central landing make stair direction unambiguous. */
private static int streetY(int a, int middle, int b, int step) {
if (step <= 3) return a;
if (step < 10) return a + Math.round((middle - a) * ((step - 3) / 7.0f));
if (step <= 12) return middle;
if (step < 19) return middle + Math.round((b - middle) * ((step - 12) / 7.0f));
return b;
}
private static Edge viaduct(Node a, Node b, int startX, int startZ, long seed, Probe probe) {
if (Math.abs(a.floor() - b.floor()) > 6) { probe.reject("viaduct-grade"); return null; }
boolean alongX = a.z() == b.z();
int distance = alongX ? b.x() - a.x() : b.z() - a.z();
int middle = (a.floor() + b.floor()) / 2;
int lowRun = 0, longest = 0;
for (int step = 3; step <= distance - 3; step++) {
int floor = bridgeY(a.floor(), middle, b.floor(), step, distance);
boolean low = false;
for (int across = -2; across <= 2; across++) {
int x = a.x() + (alongX ? step : across), z = a.z() + (alongX ? across : step);
var g = probe.at(x, z);
if (g == INVALID || g.top() > floor + MAX_CUT) { probe.reject("viaduct-cut"); return null; }
if (g.top() < floor - 3) low = true;
if (!unprotected(x, floor, z, x, floor + 8, z, probe)) { probe.reject("viaduct-water"); return null; }
}
lowRun = low ? lowRun + 1 : 0; longest = Math.max(longest, lowRun);
}
if (longest < 4) { probe.reject("viaduct-no-gap"); return null; }
var supports = new ArrayList<Support>();
int last = 2;
while (distance - 2 - last > 24) {
int found = -1;
for (int offset : new int[] {0, -1, 1, -2, 2, -3, 3, -4, 4}) {
int step = Math.min(last + 20, distance - 12) + offset;
if (step < 12 || step > distance - 12 || step <= last || step - last > 24) continue;
int x = a.x() + (alongX ? step : 0), z = a.z() + (alongX ? 0 : step);
if (naturalPier(x, z, middle, probe)) { found = step; break; }
Support support = pier(startX, startZ, x, z, middle, mix(seed ^ step), probe);
if (support != null) { supports.add(support); found = step; break; }
}
if (found < 0) { probe.reject("viaduct-pier"); return null; }
last = found;
}
var pieces = new ArrayList<Parcel>();
int length = distance - 5, count = (length + 23) / 24, consumed = 0;
for (int i = 0; i < count; i++) {
int part = (length - consumed + count - i - 1) / (count - i);
int minX = a.x() + (alongX ? consumed + 3 : -2), minZ = a.z() + (alongX ? -2 : consumed + 3);
int width = alongX ? part : 5, depth = alongX ? 5 : part;
int[] heights = new int[width * depth];
for (int z = 0; z < depth; z++) for (int x = 0; x < width; x++)
heights[z * width + x] = bridgeY(a.floor(), middle, b.floor(), consumed + 3 + (alongX ? x : z), distance);
pieces.add(new Parcel(Kind.BRIDGE, minX, minZ, width, depth, middle, 0, 0, mix(seed ^ i), 0, heights));
consumed += part;
}
return new Edge(a.id(), b.id(), pieces, supports);
}
private static int bridgeY(int a, int mid, int b, int step, int distance) {
if (step <= 3) return a;
if (step < 10) return a + Math.round((mid - a) * ((step - 3) / 7.0f));
if (step <= distance - 10) return mid;
if (step < distance - 3) return mid + Math.round((b - mid) * ((step - (distance - 10)) / 7.0f));
return b;
}
private static Town connected(int startX, int startZ, long seed, Node[][] nodes, List<Edge> edges, List<Lot> lots, int cells) {
Map<Integer, List<Edge>> graph = new HashMap<>();
for (Edge e : edges) { graph.computeIfAbsent(e.a(), n -> new ArrayList<>()).add(e); graph.computeIfAbsent(e.b(), n -> new ArrayList<>()).add(e); }
Set<Integer> visited = new HashSet<>(); Town best = null;
for (Edge first : edges) {
if (!visited.add(first.a())) continue;
Set<Integer> component = new HashSet<>(); component.add(first.a());
ArrayDeque<Integer> queue = new ArrayDeque<>(); queue.add(first.a());
while (!queue.isEmpty()) {
int id = queue.removeFirst();
for (Edge e : graph.getOrDefault(id, List.of())) {
int next = e.a() == id ? e.b() : e.a();
if (visited.add(next)) { component.add(next); queue.addLast(next); }
}
}
List<Lot> admitted = lots.stream().filter(l -> component.contains(node(l.col(), l.row(), cells))).toList();
if (admitted.isEmpty() || best != null && admitted.size() <= best.lots().size()) continue;
var retainedNodes = new ArrayList<Node>();
for (Node[] row : nodes) for (Node n : row) if (n != null && component.contains(n.id())) retainedNodes.add(n);
best = new Town(startX, startZ, seed, retainedNodes,
edges.stream().filter(e -> component.contains(e.a()) && component.contains(e.b())).toList(), admitted);
}
return best;
}
private static List<Parcel> assign(long seed, List<Lot> lots) {
Kind[] kinds = new Kind[lots.size()];
int buildingCount = Math.max(1, (lots.size() * 3 + 4) / 5);
int buildingOffset = (int) Long.remainderUnsigned(mix(seed ^ 0x15B017DL), BUILDINGS.length);
for (int i = 0; i < buildingCount; i++) {
int archetype = i % 6;
Kind kind = archetype == 0 ? Kind.APARTMENT : BUILDINGS[(buildingOffset + archetype - 1) % BUILDINGS.length];
assign(kind, false, lots, kinds);
}
// A small town is predominantly buildings; its scarce public sites vary across seeds.
if (buildingCount < lots.size()) assign(Kind.PLAZA, false, lots, kinds);
int publicOffset = (int) Long.remainderUnsigned(mix(seed ^ 0x15A2C4L), PUBLIC_SPACES.length);
for (int i = 0; i < PUBLIC_SPACES.length; i++) {
Kind kind = PUBLIC_SPACES[(publicOffset + i) % PUBLIC_SPACES.length];
assign(kind, kind == Kind.POOL || kind == Kind.EXCAVATION, lots, kinds);
}
var result = new ArrayList<Parcel>();
for (int i = 0; i < lots.size(); i++) {
Lot lot = lots.get(i); if (kinds[i] == null) kinds[i] = Kind.PARK;
Kind kind = kinds[i];
int floors = switch (kind) {
case APARTMENT, HOTEL -> 3 + (int) Long.remainderUnsigned(lot.seed(), 3);
case LIBRARY, RESTAURANT, BARRACKS, HALL -> 2;
case PARKING, MARKET -> 1;
default -> 0;
};
int foundation = lot.deep() && (kind == Kind.POOL || kind == Kind.PARKING || kind == Kind.EXCAVATION) ? 8 : lot.foundation();
result.add(new Parcel(kind, lot.x(), lot.z(), LOT, LOT, lot.floor(), floors, 0, lot.seed(), foundation, new int[0]));
}
return result;
}
private static void assign(Kind kind, boolean deepOnly, List<Lot> lots, Kind[] kinds) {
int chosen = -1;
for (int i = 0; i < lots.size(); i++) {
Lot lot = lots.get(i);
if (kinds[i] != null || deepOnly && !lot.deep() || kind == Kind.PARKING && !lot.deep() && lot.foundation() >= 8) continue;
if (chosen < 0 || !deepOnly && kind != Kind.PARKING && lots.get(chosen).deep() && !lot.deep()
|| kind == Kind.PARKING && !lots.get(chosen).deep() && lot.deep()) chosen = i;
}
if (chosen >= 0) kinds[chosen] = kind;
}
private static boolean overlaps(Edge a, Edge b) {
for (Parcel p : a.pieces()) for (Parcel q : b.pieces()) if (p.minX() <= q.maxX() && p.maxX() >= q.minX()
&& p.minZ() <= q.maxZ() && p.maxZ() >= q.minZ()) return true;
return false;
}
private static boolean naturalPier(int x, int z, int floor, Probe probe) {
for (int dz = -1; dz <= 1; dz++) for (int dx = -1; dx <= 1; dx++) {
var g = probe.at(x + dx, z + dz);
if (!g.dry() || g.solidDepth() < 4 || g.top() < floor - 1 || g.top() > floor + 1) return false;
}
return unprotected(x - 1, floor - 2, z - 1, x + 1, floor - 1, z + 1, probe);
}
private static Support pier(int startX, int startZ, int x, int z, int floor, long seed, Probe probe) {
int low = Integer.MAX_VALUE, high = Integer.MIN_VALUE;
for (int dz = -1; dz <= 1; dz++) for (int dx = -1; dx <= 1; dx++) {
var g = probe.at(x + dx, z + dz);
if (!g.dry() || g.solidDepth() < 4 || g.top() < floor - 15 || g.top() > floor - 2) return null;
low = Math.min(low, g.top()); high = Math.max(high, g.top());
}
if (high - low > 2 || !solidTo(x - 1, z - 1, 3, 3, low - 2, probe)
|| !unprotected(x - 1, low - 2, z - 1, x + 1, floor - 1, z + 1, probe)) return null;
return new Support(startX, startZ, x - 1, low - 1, z - 1, x + 1, floor - 1, z + 1, seed);
}
private static boolean land(LostCityPlanner.Ground g) { return g.dry() && !g.protectedTerrain() && g.solidDepth() >= 4 && g.top() >= MIN_FLOOR - 10; }
private static boolean solidTo(int x, int z, int width, int depth, int bottom, Probe probe) {
for (int dz = 0; dz < depth; dz++) for (int dx = 0; dx < width; dx++) {
var g = probe.at(x + dx, z + dz);
if (!g.dry() || g.top() < bottom || g.top() - bottom + 1 > g.solidDepth()) return false;
}
return true;
}
private static int minTop(int x, int z, int width, int depth, Probe probe) {
int min = Integer.MAX_VALUE;
for (int dz = 0; dz < depth; dz++) for (int dx = 0; dx < width; dx++) min = Math.min(min, probe.at(x + dx, z + dz).top());
return min;
}
private static boolean unprotected(int x0, int y0, int z0, int x1, int y1, int z1, Probe probe) {
for (int x = x0; x <= x1; x++) for (int z = z0; z <= z1; z++)
for (int y = y0; y <= y1; y++) if (probe.protectedAt(x, y, z)) return false;
return true;
}
private static int node(int col, int row, int cells) { return row * (cells + 1) + col; }
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 v) { v = (v ^ (v >>> 30)) * 0xbf58476d1ce4e5b9L; v = (v ^ (v >>> 27)) * 0x94d049bb133111ebL; return v ^ (v >>> 31); }
private record Candidate(int x, int z, int score, int spread, long seed) {}
private record Footprint(int floor, int foundation) {}
private record Node(int id, int x, int z, int floor, Parcel parcel) {}
private record Lot(int row, int col, int x, int z, int floor, int foundation, boolean deep, long seed) {}
private record Edge(int a, int b, List<Parcel> pieces, List<Support> supports) {}
private record Town(int startX, int startZ, long seed, List<Node> nodes, List<Edge> edges, List<Lot> lots) {}
private static final class BudgetExceeded extends RuntimeException {}
private static final class Probe {
final Terrain terrain;
final int centerX, centerZ;
final double radius;
final Map<Long, LostCityPlanner.Ground> columns = new HashMap<>();
final Map<String, Integer> rejections = new java.util.TreeMap<>();
int volumeChecks, columnLimit = MAX_COLUMNS, volumeLimit = MAX_VOLUME_CHECKS;
Probe(Terrain terrain, int centerX, int centerZ, int diameter) {
this.terrain = java.util.Objects.requireNonNull(terrain); this.centerX = centerX; this.centerZ = centerZ; radius = diameter / 2.0 - 6;
}
LostCityPlanner.Ground at(int x, int z) {
if (Math.hypot((double) x - centerX, (double) z - centerZ) > 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();
var value = java.util.Objects.requireNonNull(terrain.ground(x, z)); columns.put(key, value); return value;
}
boolean protectedAt(int x, int y, int z) {
if (at(x, z) == INVALID || y < 0 || y >= 384) return true;
if (volumeChecks >= volumeLimit) throw new BudgetExceeded();
volumeChecks++; return terrain.protectedAt(x, y, z);
}
boolean solidAt(int x, int y, int z) {
if (at(x, z) == INVALID || y < 0 || y >= 384) return false;
if (volumeChecks >= volumeLimit) throw new BudgetExceeded();
volumeChecks++; return terrain.solid(x, y, z);
}
void reject(String reason) { rejections.merge(reason, 1, Integer::sum); }
}
}
@@ -0,0 +1,81 @@
package fr.koka.sanctuary.worldgen.city;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** Immutable three-dimensional construction mask; underground works never widen the surface biome. */
public final class LostCityProtection16 {
public static final LostCityProtection16 EMPTY = new Builder().build();
private final Map<Long, List<Box>> chunks;
private LostCityProtection16(Map<Long, List<Box>> chunks) { this.chunks = chunks; }
public boolean contains(int x, int y, int z) {
var boxes = chunks.get(key(x >> 4, z >> 4));
if (boxes == null) return false;
for (var box : boxes) if (x >= box.x0 && x <= box.x1 && y >= box.y0 && y <= box.y1 && z >= box.z0 && z <= box.z1) return true;
return false;
}
public boolean intersects(int x0, int y0, int z0, int x1, int y1, int z1) {
if (x0 > x1 || y0 > y1 || z0 > z1) throw new IllegalArgumentException("Reversed city query bounds");
if (y1 < 0 || y0 > 383) return false;
for (int cx = x0 >> 4; cx <= x1 >> 4; cx++) for (int cz = z0 >> 4; cz <= z1 >> 4; cz++) {
var boxes = chunks.get(key(cx, cz));
if (boxes == null) continue;
for (var box : boxes) if (x0 <= box.x1 && x1 >= box.x0 && y0 <= box.y1
&& y1 >= box.y0 && z0 <= box.z1 && z1 >= box.z0) return true;
}
return false;
}
private static long key(int x, int z) { return ((long) x << 32) ^ (z & 0xffffffffL); }
private record Box(int x0, int y0, int z0, int x1, int y1, int z1) {}
public static final class Builder {
private final Map<Long, List<Box>> chunks = new HashMap<>();
public Builder add(int x0, int y0, int z0, int x1, int y1, int z1) {
if (x0 > x1 || y0 > y1 || z0 > z1 || x1 - x0 > 256 || z1 - z0 > 256 || y0 < 0 || y1 > 383)
throw new IllegalArgumentException("Invalid city construction mask");
var box = new Box(x0, y0, z0, x1, y1, z1);
for (int x = x0 >> 4; x <= x1 >> 4; x++) for (int z = z0 >> 4; z <= z1 >> 4; z++)
chunks.computeIfAbsent(key(x, z), ignored -> new ArrayList<>()).add(box);
return this;
}
public Builder add(LostCityPlan surface, LostCityUnderground16.Plan underground) {
for (var district : surface.districts()) for (var parcel : district.parcels()) {
add(parcel.minX(), parcel.minY(), parcel.minZ(), parcel.maxX(), parcel.maxY(), parcel.maxZ());
if (parcel.kind() == LostCityPlan.Kind.BRIDGE) for (int x = 0; x < parcel.width(); x++) for (int z = 0; z < parcel.depth(); z++) {
int y = parcel.surfaceY(x, z);
// Abutments and natural intermediate bearings are retained, never filled in.
add(parcel.minX() + x, y - 2, parcel.minZ() + z, parcel.minX() + x, y - 1, parcel.minZ() + z);
}
}
for (var network : underground.networks()) for (var module : network.modules()) {
add(module.boundsMinX(), module.boundsMinY(), module.boundsMinZ(), module.boundsMaxX(), module.boundsMaxY(), module.boundsMaxZ());
if (module.kind() == LostCityUnderground16.Kind.EXPANSION_HALL)
add(module.minX(), module.minY() - 2, module.minZ(), module.maxX(), module.minY() - 1, module.maxZ());
for (var support : module.supports()) {
int y = support.type() == LostCityUnderground16.Type.PILLAR ? support.minY() - 1 : support.maxY() + 1;
// Retain the untouched rock that carries the painted anchor.
add(support.x(), y, support.z(), support.x(), y, support.z());
}
}
return this;
}
public Builder addReservation(LostCityPlanner16.Fallback r) {
add(r.minX(), r.minY(), r.minZ(), r.maxX(), r.maxY(), r.maxZ());
for (var a : r.anchors()) add(a.minX(), a.minY() - 2, a.minZ(), a.maxX(), a.maxY(), a.maxZ());
return this;
}
public Builder addViaductSupport(LostCityPlanner16.Support support) {
return add(support.minX(), support.minY() - 1, support.minZ(), support.maxX(), support.maxY(), support.maxZ());
}
public LostCityProtection16 build() {
var immutable = new HashMap<Long, List<Box>>();
chunks.forEach((key, value) -> immutable.put(key, List.copyOf(value)));
return new LostCityProtection16(Map.copyOf(immutable));
}
}
}
@@ -29,11 +29,17 @@ public final class LostCityStructure extends Structure {
throw new IllegalStateException("Lost City district exceeds native structure reference range");
}
return Optional.of(new GenerationStub(LostCityStructures.locator(district), pieces -> {
if (generator.rootGeneration() == 16 && generator.cityReservation16(island) != null)
pieces.addPiece(new LostCityFoundationPiece16(generator.cityReservation16(island), district.seed()));
if (generator.rootGeneration() == 15) for (var support : generator.citySupports(island)) {
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
pieces.addPiece(new LostCitySupportPiece15(support));
}
district.parcels().forEach(parcel -> pieces.addPiece(generator.rootGeneration() == 15
if (generator.rootGeneration() == 16) for (var support : generator.citySupports16(island)) {
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
pieces.addPiece(new LostCitySupportPiece16(support));
}
district.parcels().forEach(parcel -> pieces.addPiece(generator.rootGeneration() == 16 ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
? new LostCityPiece15(parcel) : new LostCityPiece(parcel)));
if (generator.rootGeneration() == 15) for (var network : generator.undergroundPlan(island).networks()) {
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
@@ -43,6 +49,14 @@ public final class LostCityStructure extends Structure {
network.modules().stream().filter(m -> m.kind() == LostCityUnderground15.Kind.ACCESS)
.forEach(m -> pieces.addPiece(new LostCityUndergroundPiece15(m)));
}
if (generator.rootGeneration() == 16) for (var network : generator.undergroundPlan16(island).networks()) {
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
// Surface comes first, rooms/corridors next, shafts last so every entrance remains open.
network.modules().stream().filter(m -> m.kind() != LostCityUnderground16.Kind.ACCESS)
.forEach(m -> pieces.addPiece(new LostCityUndergroundPiece16(m)));
network.modules().stream().filter(m -> m.kind() == LostCityUnderground16.Kind.ACCESS)
.forEach(m -> pieces.addPiece(new LostCityUndergroundPiece16(m)));
}
}));
}
return Optional.empty();
@@ -40,6 +40,14 @@ public final class LostCityStructures {
BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_underground_v15"), LostCityUndergroundPiece15::new);
public static final StructurePieceType SUPPORT15 = Registry.register(
BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_support_v15"), LostCitySupportPiece15::new);
public static final StructurePieceType PIECE16 = Registry.register(
BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_parcel_v16"), LostCityPiece16::new);
public static final StructurePieceType UNDERGROUND16 = Registry.register(
BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_underground_v16"), LostCityUndergroundPiece16::new);
public static final StructurePieceType SUPPORT16 = Registry.register(
BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_support_v16"), LostCitySupportPiece16::new);
public static final StructurePieceType FOUNDATION16 = Registry.register(
BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_foundation_v16"), LostCityFoundationPiece16::new);
private static final ThreadLocal<Integer> PLACEMENT_DEPTH = ThreadLocal.withInitial(() -> 0);
private LostCityStructures() {}
@@ -0,0 +1,68 @@
package fr.koka.sanctuary.worldgen.city;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
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.Blocks;
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;
/** A short, terrain-certified viaduct pier, saved separately from the road deck. */
public final class LostCitySupportPiece16 extends StructurePiece {
private final LostCityPlanner16.Support support;
public LostCitySupportPiece16(LostCityPlanner16.Support support) {
super(LostCityStructures.SUPPORT16, 0, bounds(support));
this.support = support;
setOrientation(null);
}
public LostCitySupportPiece16(StructurePieceSerializationContext context, CompoundTag tag) {
super(LostCityStructures.SUPPORT16, tag);
if (tag.getInt("SanctuarySchema").orElseThrow() != 1)
throw new IllegalArgumentException("Unsupported city support schema");
support = new LostCityPlanner16.Support(tag.getInt("StartX").orElseThrow(), tag.getInt("StartZ").orElseThrow(),
tag.getInt("MinX").orElseThrow(), tag.getInt("MinY").orElseThrow(), tag.getInt("MinZ").orElseThrow(),
tag.getInt("MaxX").orElseThrow(), tag.getInt("MaxY").orElseThrow(), tag.getInt("MaxZ").orElseThrow(),
tag.getLong("SupportSeed").orElseThrow());
if (!getBoundingBox().equals(bounds(support))) throw new IllegalArgumentException("City support bounds disagree with its saved geometry");
setOrientation(null);
}
public LostCityPlanner16.Support support() { return support; }
@Override public BlockPos getLocatorPosition() {
return new BlockPos((support.minX() + support.maxX()) / 2, support.maxY() + 2,
(support.minZ() + support.maxZ()) / 2);
}
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
tag.putInt("SanctuarySchema", 1);
tag.putInt("StartX", support.startChunkX()); tag.putInt("StartZ", support.startChunkZ());
tag.putInt("MinX", support.minX()); tag.putInt("MinY", support.minY()); tag.putInt("MinZ", support.minZ());
tag.putInt("MaxX", support.maxX()); tag.putInt("MaxY", support.maxY()); tag.putInt("MaxZ", support.maxZ());
tag.putLong("SupportSeed", support.seed());
}
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
LostCityStructures.placing(() -> {
for (int x = Math.max(clip.minX(), support.minX()); x <= Math.min(clip.maxX(), support.maxX()); x++)
for (int z = Math.max(clip.minZ(), support.minZ()); z <= Math.min(clip.maxZ(), support.maxZ()); z++)
for (int y = Math.max(clip.minY(), support.minY()); y <= Math.min(clip.maxY(), support.maxY()); y++) {
var block = y == support.minY() || y == support.maxY() ? Blocks.SMOOTH_STONE.defaultBlockState()
: LostCityPalette.masonry(support.seed(), x - support.minX(), y - support.minY(), z - support.minZ());
placeBlock(level, block, x, y, z, clip);
}
});
}
public static BoundingBox bounds(LostCityPlanner16.Support support) {
return new BoundingBox(support.minX(), support.minY(), support.minZ(), support.maxX(), support.maxY(), support.maxZ());
}
}
@@ -0,0 +1,90 @@
package fr.koka.sanctuary.worldgen.city;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceKey;
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.storage.loot.LootTable;
/** Generation16 surface corrections; the saved generation14 renderer remains unchanged. */
public final class LostCitySurface16 {
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
private LostCitySurface16() {}
public static void render(LostCityPlan.Parcel parcel, LostCityRenderer.BlockWriter output) {
if (parcel.kind().building()) {
LostCityInteriors16.render(parcel, output);
return;
}
LostCityRenderer.render(parcel, new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) {
if (state.is(Blocks.DARK_OAK_STAIRS)) state = seat(parcel, x, z, state);
output.set(x, y, z, state);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
output.chest(x, y, z, table, seed);
}
});
if (parcel.kind() == LostCityPlan.Kind.STREET || parcel.kind() == LostCityPlan.Kind.BRIDGE) {
// The compact planner certifies cuts of up to six blocks. Clear their whole
// headroom, including the surface of the cut, without widening the road.
for (int z = 0; z < parcel.depth(); z++) for (int x = 0; x < parcel.width(); x++) {
int y = parcel.surfaceY(x, z) - parcel.floorY();
for (int air = 6; air <= 8; air++) output.set(x, y + air, z, LostCityPalette.AIR);
boolean eastWest = parcel.width() > parcel.depth();
int across = eastWest ? z : x;
int breadth = eastWest ? parcel.depth() : parcel.width();
// Keep the intact bridge parapet on its flat stone curb.
if (parcel.kind() == LostCityPlan.Kind.BRIDGE && (across == 0 || across == breadth - 1)) continue;
for (Direction direction : HORIZONTAL) {
int nx = x + direction.getStepX(), nz = z + direction.getStepZ();
if (nx >= 0 && nx < parcel.width() && nz >= 0 && nz < parcel.depth()
&& parcel.surfaceY(nx, nz) == parcel.surfaceY(x, z) + 1) {
output.set(x, y + 1, z, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, direction));
break;
}
}
}
}
}
private static BlockState seat(LostCityPlan.Parcel p, int x, int z, BlockState state) {
boolean rotated = switch (p.kind()) {
case PARK_GARDEN, PARK_PLAYGROUND, PARK_MEMORIAL, EXCAVATION, POOL -> true;
default -> false;
};
int turn = rotated ? p.facing() : 0;
int localX = switch (turn) {
case 1 -> z;
case 2 -> p.width() - 1 - x;
case 3 -> p.depth() - 1 - z;
default -> x;
};
int localZ = switch (turn) {
case 1 -> p.width() - 1 - x;
case 2 -> p.depth() - 1 - z;
case 3 -> x;
default -> z;
};
// Stair FACING points toward the high half: it is the seat back, not its view.
Direction back = switch (p.kind()) {
case PARK, EXCAVATION, PLAZA, PARK_MEMORIAL -> localZ > p.depth() / 2 ? Direction.SOUTH : Direction.NORTH;
case PARK_GARDEN, PARK_PLAYGROUND -> Direction.SOUTH;
case POOL -> p.foundationDepth() < 8
? (localZ > p.depth() / 2 ? Direction.SOUTH : Direction.NORTH)
: (localX == 1 ? Direction.NORTH : Direction.EAST);
default -> null;
};
if (back == null) return state;
Rotation rotation = switch (turn) {
case 1 -> Rotation.CLOCKWISE_90;
case 2 -> Rotation.CLOCKWISE_180;
case 3 -> Rotation.COUNTERCLOCKWISE_90;
default -> Rotation.NONE;
};
return state.setValue(BlockStateProperties.HORIZONTAL_FACING, rotation.rotate(back));
}
}
@@ -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;
/** Short-lived noise-column cache shared by surface and underground planning, never loaded chunks. */
public final class LostCityTerrain16 implements LostCityUnderground16.Terrain, LostCityPlanner16.Terrain {
public static final int MAX_COLUMNS = 20_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 LostCityTerrain16(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(); }
}
@@ -0,0 +1,702 @@
package fr.koka.sanctuary.worldgen.city;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import net.minecraft.core.Direction;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
/** Generation 16 only: shared, terrain-certified utility networks. All rendering inputs are saved records. */
public final class LostCityUnderground16 {
public static final int MAX_SAMPLES = 420_000;
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
private static final int CELL = 20;
private LostCityUnderground16() {}
public interface Terrain {
boolean solid(int x, int y, int z);
boolean protectedAt(int x, int y, int z);
boolean owns(int x, int z);
boolean exteriorAir(int x, int y, int z, int dx, int dz);
}
public enum Kind { ACCESS, SEWER, POOLROOM, GARAGE, PASSAGE, EXPANSION_HALL, SEWER_OUTLET }
public enum Type { PILLAR, CHAIN }
public record WaterBounds(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {}
private static int floorOffset(Kind kind, int height) {
return kind == Kind.POOLROOM ? 5 : kind == Kind.GARAGE ? height - 9 : 1;
}
public record Support(Type type, int x, int minY, int z, int maxY) {
public Support {
if (type == null || minY < 1 || maxY > 382 || minY > maxY || maxY - minY >= 8)
throw new IllegalArgumentException("Invalid underground support");
}
}
/** openings bits: north=1, east=2, south=4, west=8. Body coordinates are absolute; writer coordinates are local. */
public record Module(Kind kind, int minX, int minY, int minZ, int width, int height, int depth,
int openings, long seed, List<Support> supports) {
public Module {
if (kind == null || width < 5 || depth < 5 || width > 63 || depth > 63 || height < 6 || height > 128
|| minY < 8 || minY + height > 383 || openings < 0 || openings > 15 || supports.size() > 64)
throw new IllegalArgumentException("Invalid underground module");
if (kind == Kind.SEWER_OUTLET && Integer.bitCount(openings) != 1)
throw new IllegalArgumentException("An outlet must retain its unique discharge direction");
if (kind == Kind.POOLROOM && (width != 57 || depth != 31 || height != 13)
|| kind == Kind.GARAGE && (width != 31 || depth != 25 || height != 122 && height != 90 && height != 58 && height != 42))
throw new IllegalArgumentException("Invalid saved large infrastructure dimensions");
supports = List.copyOf(supports);
for (var support : supports) if (support.x() < minX || support.x() >= minX + width
|| support.z() < minZ || support.z() >= minZ + depth)
throw new IllegalArgumentException("Support is outside its module footprint");
}
public int floorY() { return minY + floorOffset(kind, height); }
public WaterBounds poolWaterBounds() {
if (kind != Kind.POOLROOM) throw new IllegalStateException("Not a poolroom");
return new WaterBounds(minX + 3, minY + 1, minZ + 3, minX + 52, minY + 4, minZ + 27);
}
public int outletDirection() {
if (kind != Kind.SEWER_OUTLET) throw new IllegalStateException("Not a sewer outlet");
return Integer.numberOfTrailingZeros(openings);
}
public BlockPos outletMouth() {
int direction = outletDirection();
return new BlockPos(DX[direction] == 0 ? centerX() : direction == 1 ? maxX() : minX(), floorY(),
DZ[direction] == 0 ? centerZ() : direction == 2 ? maxZ() : minZ());
}
public List<Integer> parkingFloors() {
if (kind != Kind.GARAGE) return List.of();
var floors = new ArrayList<Integer>();
for (int y = minY + 1; y <= floorY(); y += 16) floors.add(y);
return List.copyOf(floors);
}
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 boundsMinX() { return minX; }
public int boundsMaxX() { return maxX(); }
public int boundsMinZ() { return minZ; }
public int boundsMaxZ() { return maxZ(); }
public int boundsMinY() { return Math.min(minY, supports.stream().mapToInt(Support::minY).min().orElse(minY)); }
public int boundsMaxY() { return Math.max(maxY(), supports.stream().mapToInt(Support::maxY).max().orElse(maxY())); }
public boolean contains(int x, int y, int z) {
if (x < minX || x > maxX() || z < minZ || z > maxZ()) return false;
if (y >= minY && y <= maxY()) return true;
return supports.stream().anyMatch(s -> s.x() == x && s.z() == z && y >= s.minY() && y <= s.maxY());
}
private Module opening(int direction) { return new Module(kind, minX, minY, minZ, width, height, depth,
openings | 1 << direction, seed, supports); }
private Module supported(List<Support> values) { return new Module(kind, minX, minY, minZ, width, height, depth, openings, seed, values); }
}
public record Network(int startChunkX, int startChunkZ, long seed, List<Module> modules) {
public Network {
modules = List.copyOf(modules);
if (modules.isEmpty() || modules.size() > 25) throw new IllegalArgumentException("Invalid underground network");
for (var module : modules) if (Math.abs(Math.floorDiv(module.boundsMinX(), 16) - startChunkX) > 7
|| Math.abs(Math.floorDiv(module.boundsMaxX(), 16) - startChunkX) > 7
|| Math.abs(Math.floorDiv(module.boundsMinZ(), 16) - startChunkZ) > 7
|| Math.abs(Math.floorDiv(module.boundsMaxZ(), 16) - startChunkZ) > 7)
throw new IllegalArgumentException("Underground network exceeds native reference range");
}
}
public record Plan(List<Network> networks, int samples) {
public Plan { networks = List.copyOf(networks); if (samples < 0 || samples > MAX_SAMPLES) throw new IllegalArgumentException("Invalid sampling budget"); }
public boolean isEmpty() { return networks.isEmpty(); }
public boolean contains(int x, int y, int z) {
return networks.stream().anyMatch(n -> n.modules().stream().anyMatch(m -> m.contains(x, y, z)));
}
}
public static Plan plan(long seed, LostCityPlan surface, Terrain terrain) {
var probe = new Probe(terrain); var networks = new ArrayList<Network>();
for (var district : surface.districts()) {
if (probe.samples >= MAX_SAMPLES) break;
int centerX = (district.minX() + district.maxX()) / 2, centerZ = (district.minZ() + district.maxZ()) / 2;
var anchors = district.parcels().stream().filter(LostCityUnderground16::accessParcel)
.flatMap(p -> anchors(p).stream())
.sorted(Comparator.<Anchor>comparingInt(a -> Math.abs(a.parcel().minX() + a.localX() - centerX)
+ Math.abs(a.parcel().minZ() + a.localZ() - centerZ)).thenComparingLong(a -> mix(seed ^ a.parcel().seed())))
.limit(24).toList();
probe.rejections.merge("usable-accesses", anchors.size(), Integer::sum);
Network accepted = null;
int bestScore = -1, attempts = 0;
try {
search: for (var anchor : anchors) {
int x = anchor.parcel().minX() + anchor.localX(), z = anchor.parcel().minZ() + anchor.localZ();
int top = anchor.top();
for (int drop : new int[] {22, 26, 18, 32, 38}) {
int floor = top - drop;
if (floor < 32) continue;
long networkSeed = mix(seed ^ district.seed() ^ anchor.parcel().seed() ^ drop);
var access = new Module(Kind.ACCESS, x - 2, floor - 1, z - 2, 5, drop + 4, 5, 0, networkSeed, List.of());
access = certify(access, district, surface, probe, false);
if (access == null) continue;
for (int attempt = 0; attempt < 4; attempt++) {
if (attempts++ >= 6 || probe.samples >= MAX_SAMPLES) break search;
int direction = (roll(networkSeed, 4) + attempt) & 3;
var modules = new ArrayList<Module>(); modules.add(access);
if (!connect(modules, 0, Kind.SEWER, direction, district, surface, probe, networkSeed)) continue;
try {
sharedNetwork(modules, direction, district, surface, probe, networkSeed);
} catch (BudgetExceeded exhausted) { /* Each appended branch is already connected and certified. */ }
var candidate = new Network(district.startChunkX(), district.startChunkZ(), networkSeed, modules);
int score = networkScore(candidate);
if (score > bestScore) { bestScore = score; accepted = candidate; }
if (score >= 23_009 || probe.samples >= MAX_SAMPLES) break search;
}
}
}
} catch (BudgetExceeded exhausted) { /* Keep the best previously completed, connected candidate. */ }
if (accepted != null) {
var candidate = accepted;
if (networks.stream().noneMatch(n -> n.modules().stream().anyMatch(a -> candidate.modules().stream()
.anyMatch(b -> boundsOverlap(a, b) || supportConflict(a, b)))))
networks.add(candidate);
}
}
System.getLogger(LostCityUnderground16.class.getName()).log(System.Logger.Level.INFO,
"Lost City16 utilities seed=" + seed + " networks=" + networks.size() + " samples=" + probe.samples
+ " kinds=" + networks.stream().flatMap(n -> n.modules().stream()).map(Module::kind).toList()
+ " admission=" + probe.rejections);
return new Plan(networks, probe.samples);
}
private static int networkScore(Network network) {
int score = network.modules().size();
for (var module : network.modules()) score += switch (module.kind()) {
case POOLROOM -> 10_000;
case EXPANSION_HALL -> 5_000;
case GARAGE -> 5_000 + module.parkingFloors().size() * 500;
case SEWER_OUTLET -> 1_000;
default -> 0;
};
return score;
}
private static void sharedNetwork(List<Module> modules, int direction, LostCityPlan.District district,
LostCityPlan surface, Probe probe, long seed) {
int side = (direction + (roll(seed + 2, 2) == 0 ? 1 : 3)) & 3;
int pool = attach(modules, 1, Kind.POOLROOM, direction, district, surface, probe, mix(seed + 10));
int hall = attach(modules, 1, Kind.EXPANSION_HALL, (side + 2) & 3, district, surface, probe, mix(seed + 11));
int garage = attach(modules, 1, Kind.GARAGE, side, district, surface, probe, mix(seed + 12));
// The lowest parking deck opens onto a deeper sewer. The mouth piece shares only that deck's
// wall and records its own elevation; no unsaved vertical-door state is added to the garage.
if (garage >= 0) for (int floor : modules.get(garage).parkingFloors()) {
if (floor == modules.get(garage).floorY()) continue;
for (int turn : new int[] {0, 1, 3, 2})
if (outlet(modules, garage, (side + turn) & 3, floor, district, surface, probe, mix(seed + 100 + floor + turn))) return;
}
// The outward side of an existing wing cannot cross the central access shaft.
if (pool >= 0 && outlet(modules, pool, direction, district, surface, probe, mix(seed + 13))) return;
if (garage >= 0 && outlet(modules, garage, side, district, surface, probe, mix(seed + 14))) return;
if (hall >= 0 && outlet(modules, hall, (side + 2) & 3, district, surface, probe, mix(seed + 15))) return;
for (int turn = 0; turn < 4; turn++)
if (outlet(modules, 1, turn, district, surface, probe, mix(seed + 16 + turn))) return;
}
private static int attach(List<Module> modules, int parent, Kind kind, int direction,
LostCityPlan.District district, LostCityPlan surface, Probe probe, long seed) {
if (modules.size() > 23) return -1;
int index = modules.size();
return connect(modules, parent, kind, direction, district, surface, probe, seed) ? index : -1;
}
/** The outlet direction is persisted as its single opening bit; the opposite end is its entrance. */
private static boolean outlet(List<Module> modules, int parentIndex, int direction,
LostCityPlan.District district, LostCityPlan surface, Probe probe, long seed) {
return outlet(modules, parentIndex, direction, modules.get(parentIndex).floorY(), district, surface, probe, seed);
}
private static boolean outlet(List<Module> modules, int parentIndex, int direction, int entryFloor,
LostCityPlan.District district, LostCityPlan surface, Probe probe, long seed) {
if (modules.size() >= 25) return false;
var parent = modules.get(parentIndex);
int startX = DX[direction] == 0 ? parent.centerX() : direction == 1 ? parent.maxX() : parent.minX();
int startZ = DZ[direction] == 0 ? parent.centerZ() : direction == 2 ? parent.maxZ() : parent.minZ();
// Three clear aperture columns, a two-block head space, and the supplied exterior ray distinguish
// a genuine island edge from a pocket cave. No ownership boundary is used as an air proof.
for (int length = 17; length <= 63; length++) {
int endX = startX + DX[direction] * (length - 1), endZ = startZ + DZ[direction] * (length - 1);
if (Math.abs(Math.floorDiv(endX, 16) - district.startChunkX()) > 7
|| Math.abs(Math.floorDiv(endZ, 16) - district.startChunkZ()) > 7) break;
int outsideX = endX + DX[direction], outsideZ = endZ + DZ[direction];
boolean exterior = true;
for (int across = -1; across <= 1 && exterior; across++) for (int y = entryFloor; y <= entryFloor + 3; y++)
if (!probe.exteriorAir(outsideX + DZ[direction] * across, y,
outsideZ + DX[direction] * across, DX[direction], DZ[direction])) { exterior = false; break; }
if (!exterior) continue;
int minX = DX[direction] == 0 ? startX - 3 : Math.min(startX, endX);
int minZ = DZ[direction] == 0 ? startZ - 3 : Math.min(startZ, endZ);
var draft = new Module(Kind.SEWER_OUTLET, minX, entryFloor - 1, minZ,
DX[direction] == 0 ? 7 : length, 7, DZ[direction] == 0 ? 7 : length,
1 << direction, seed, List.of());
boolean collides = false;
for (int i = 0; i < modules.size(); i++) if (i != parentIndex && overlap(draft, modules.get(i))) { collides = true; break; }
if (collides) return false;
var certified = certify(draft, district, surface, probe, true);
if (certified == null) continue;
for (var existing : modules) if (supportConflict(certified, existing)) { collides = true; break; }
if (collides) continue;
if (entryFloor == parent.floorY()) modules.set(parentIndex, parent.opening(direction));
modules.add(certified);
return true;
}
return false;
}
private static boolean accessParcel(LostCityPlan.Parcel p) {
return (p.kind() == LostCityPlan.Kind.STREET || p.kind() == LostCityPlan.Kind.PLAZA)
&& p.width() >= 5 && p.depth() >= 5 && p.floors() == 0;
}
private record Anchor(LostCityPlan.Parcel parcel, int localX, int localZ, int top) {}
private static List<Anchor> anchors(LostCityPlan.Parcel p) {
var values = new ArrayList<Anchor>();
if (p.kind() == LostCityPlan.Kind.PLAZA) {
// Left promenade: clear of the central fountain and the benches at z=3 and depth-4.
if (p.width() >= 17 && p.depth() >= 17) addAnchor(values, p, 3, p.depth() / 2);
} else if (p.width() >= p.depth()) {
for (int x = 2; x <= p.width() - 3; x++) addAnchor(values, p, x, p.depth() / 2);
} else {
for (int z = 2; z <= p.depth() - 3; z++) addAnchor(values, p, p.width() / 2, z);
}
values.sort(Comparator.comparingInt(a -> Math.abs(a.localX() - p.width() / 2) + Math.abs(a.localZ() - p.depth() / 2)));
return values.stream().limit(4).toList();
}
private static void addAnchor(List<Anchor> values, LostCityPlan.Parcel p, int cx, int cz) {
int top = p.surfaceY(cx, cz);
for (int z = cz - 2; z <= cz + 2; z++) for (int x = cx - 2; x <= cx + 2; x++)
if (p.surfaceY(x, z) != top) return;
values.add(new Anchor(p, cx, cz, top));
}
private static boolean connect(List<Module> modules, int parentIndex, Kind kind, int direction,
LostCityPlan.District district, LostCityPlan surface, Probe probe, long seed) {
var parent = modules.get(parentIndex);
int width = kind == Kind.SEWER ? 9 : kind == Kind.EXPANSION_HALL ? 25 : kind == Kind.POOLROOM ? 57 : 31;
int depth = kind == Kind.POOLROOM ? 31 : kind == Kind.GARAGE ? 25 : width;
int height = kind == Kind.EXPANSION_HALL ? 10 : kind == Kind.POOLROOM ? 13 : 7;
int parentHalf = DX[direction] == 0 ? parent.depth() / 2 : parent.width() / 2;
int roomHalf = DX[direction] == 0 ? depth / 2 : width / 2;
int distance = Math.max(CELL, parentHalf + roomHalf + (kind == Kind.POOLROOM ? 12 : 7));
int x = parent.centerX() + DX[direction] * distance, z = parent.centerZ() + DZ[direction] * distance;
Module room = null;
for (int candidateHeight : kind == Kind.GARAGE ? new int[] {122, 90, 58, 42} : new int[] {height}) {
int minY = parent.floorY() - floorOffset(kind, candidateHeight);
if (minY < 8 || minY + candidateHeight > 383) continue;
var draft = new Module(kind, x - width / 2, minY, z - depth / 2, width, candidateHeight, depth,
1 << ((direction + 2) & 3), seed, List.of());
for (var existing : modules) if (overlap(draft, existing)) return false;
room = certify(draft, district, surface, probe, false);
if (room != null) break;
}
if (room == null) return false;
for (var existing : modules) if (supportConflict(room, existing)) return false;
var passage = corridor(parent, room, direction, mix(seed));
if (passage == null) return false;
for (int i = 0; i < modules.size(); i++) if (i != parentIndex && overlap(passage, modules.get(i))) return false;
passage = certify(passage, district, surface, probe, roll(seed, 4) == 0);
if (passage == null) return false;
if (supportConflict(passage, room)) return false;
for (var existing : modules) if (supportConflict(passage, existing)) return false;
modules.set(parentIndex, parent.opening(direction)); modules.add(room); modules.add(passage);
return true;
}
private static Module corridor(Module parent, Module room, int direction, long seed) {
int x = room.centerX(), z = room.centerZ();
int minX, minZ, spanX, spanZ;
if (DX[direction] != 0) {
minX = Math.min(parent.maxX(), room.maxX());
int endX = Math.max(parent.minX(), room.minX());
minZ = z - 2; spanX = endX - minX + 1; spanZ = 5;
} else {
minX = x - 2; minZ = Math.min(parent.maxZ(), room.maxZ());
int endZ = Math.max(parent.minZ(), room.minZ());
spanX = 5; spanZ = endZ - minZ + 1;
}
if (spanX < 5 || spanZ < 5 || spanX > 31 || spanZ > 31) return null;
return new Module(Kind.PASSAGE, minX, parent.floorY() - 1, minZ, spanX, 6, spanZ,
(1 << direction) | (1 << ((direction + 2) & 3)), seed, List.of());
}
private static boolean link(List<Module> modules, int from, int to, LostCityPlan.District district,
LostCityPlan surface, Probe probe, long seed) {
if (modules.size() >= 25) return false;
var first = modules.get(from); var second = modules.get(to);
if (first.floorY() != second.floorY() || first.centerX() != second.centerX() && first.centerZ() != second.centerZ()) return false;
int direction = first.centerX() == second.centerX() ? second.centerZ() > first.centerZ() ? 2 : 0
: second.centerX() > first.centerX() ? 1 : 3;
var passage = corridor(first, second, direction, seed);
if (passage == null) return false;
for (int i = 0; i < modules.size(); i++) if (i != from && i != to && overlap(passage, modules.get(i))) return false;
passage = certify(passage, district, surface, probe, false);
if (passage == null) return false;
for (var existing : modules) if (supportConflict(passage, existing)) return false;
modules.set(from, first.opening(direction)); modules.set(to, second.opening((direction + 2) & 3)); modules.add(passage);
return true;
}
private static boolean overlap(Module a, Module b) {
return a.minX() < b.maxX() && a.maxX() > b.minX() && a.minZ() < b.maxZ() && a.maxZ() > b.minZ()
&& a.minY() <= b.maxY() && a.maxY() >= b.minY();
}
private static boolean boundsOverlap(Module a, Module b) {
return a.boundsMinX() <= b.boundsMaxX() && a.boundsMaxX() >= b.boundsMinX()
&& a.boundsMinZ() <= b.boundsMaxZ() && a.boundsMaxZ() >= b.boundsMinZ()
&& a.boundsMinY() <= b.boundsMaxY() && a.boundsMaxY() >= b.boundsMinY();
}
/** Read dependencies include the untouched rock beyond the written anchor. This is independent
* of body overlap: joined rooms deliberately share a doorway wall, but never another room's footing. */
static boolean supportConflict(Module a, Module b) {
for (var support : a.supports()) {
if (supportIntersectsBody(support, b)) return true;
for (var other : b.supports()) if (support.x() == other.x() && support.z() == other.z()
&& supportMinY(support) <= supportMaxY(other) && supportMaxY(support) >= supportMinY(other)) return true;
}
for (var support : b.supports()) if (supportIntersectsBody(support, a)) return true;
return false;
}
private static boolean supportIntersectsBody(Support support, Module module) {
return support.x() >= module.minX() && support.x() <= module.maxX()
&& support.z() >= module.minZ() && support.z() <= module.maxZ()
&& supportMinY(support) <= module.maxY() && supportMaxY(support) >= module.minY();
}
private static int supportMinY(Support support) { return support.minY() - (support.type() == Type.PILLAR ? 1 : 0); }
private static int supportMaxY(Support support) { return support.maxY() + (support.type() == Type.CHAIN ? 1 : 0); }
private static int dependencyMinY(Module module) {
int minY = module.minY();
for (var support : module.supports()) minY = Math.min(minY, supportMinY(support));
return minY;
}
private static int dependencyMaxY(Module module) {
int maxY = module.maxY();
for (var support : module.supports()) maxY = Math.max(maxY, supportMaxY(support));
return maxY;
}
private static Module certify(Module m, LostCityPlan.District district, LostCityPlan surface, Probe probe, boolean hanging) {
if (Math.abs(Math.floorDiv(m.minX(), 16) - district.startChunkX()) > 7
|| Math.abs(Math.floorDiv(m.maxX(), 16) - district.startChunkX()) > 7
|| Math.abs(Math.floorDiv(m.minZ(), 16) - district.startChunkZ()) > 7
|| Math.abs(Math.floorDiv(m.maxZ(), 16) - district.startChunkZ()) > 7 || surfaceCollision(m, surface)) return probe.reject(m, "geometry");
int solid = 0, volume = 0;
for (int z = m.minZ(); z <= m.maxZ(); z++) for (int x = m.minX(); x <= m.maxX(); x++) {
if (!probe.owns(x, z)) return probe.reject(m, "ownership");
for (int y = m.minY(); y <= m.maxY(); y++) {
if (probe.protectedAt(x, y, z)) return probe.reject(m, "protected");
// Only the visual density preference is sampled in the tall parking volume. Protection
// above and every load-bearing anchor below remain exhaustive, independent checks.
if (m.kind() == Kind.SEWER_OUTLET || m.kind() == Kind.GARAGE && (y - m.minY()) % 4 != 0) continue;
if (probe.solid(x, y, z)) solid++;
volume++;
}
}
int requiredRock = switch (m.kind()) {
case ACCESS -> 60;
case POOLROOM, EXPANSION_HALL -> 25;
case GARAGE -> 12;
case SEWER_OUTLET -> 0;
default -> hanging ? 0 : 48;
};
if (solid * 100L < volume * requiredRock) return probe.reject(m, "rock-ratio");
if (m.kind() == Kind.SEWER_OUTLET) {
var bearings = outletSupports(m, probe);
if (bearings == null) return probe.reject(m, "unanchored");
var result = m.supported(bearings);
return surfaceCollision(result, surface) ? probe.reject(m, "support-geometry") : result;
}
var supports = new ArrayList<Support>();
var xs = m.kind() == Kind.GARAGE ? List.of(m.minX() + 4, m.centerX(), m.maxX() - 4)
: lattice(m.minX(), m.maxX(), (m.kind() == Kind.POOLROOM || m.kind() == Kind.EXPANSION_HALL) ? 8 : 4);
var zs = m.kind() == Kind.GARAGE ? List.of(m.minZ() + 3, m.centerZ(), m.maxZ() - 3)
: lattice(m.minZ(), m.maxZ(), (m.kind() == Kind.POOLROOM || m.kind() == Kind.EXPANSION_HALL) ? 8 : 4);
for (int z : zs) for (int x : xs) {
if (probe.solid(x, m.minY() - 1, z) && probe.solid(x, m.minY() - 2, z)) {
if (probe.protectedAt(x, m.minY() - 1, z) || probe.protectedAt(x, m.minY() - 2, z)) return null;
if (m.kind() == Kind.GARAGE || m.kind() == Kind.POOLROOM || m.kind() == Kind.EXPANSION_HALL) supports.add(new Support(Type.PILLAR, x, m.minY() - 1, z, m.minY() - 1));
continue;
}
Support found = support(m, x, z, probe);
if (found == null || supports.size() >= 64) return probe.reject(m, "unanchored");
supports.add(found);
}
var result = m.supported(supports);
return surfaceCollision(result, surface) ? null : result;
}
/** A narrow concrete sewer can bridge up to twelve blocks between two anchored cross-beams.
* Every beam has two independent short rock anchors; the last lip cantilevers at most eight blocks.
* The first beam belongs to the already certified room where the sewer starts. */
private static List<Support> outletSupports(Module m, Probe probe) {
int direction = m.outletDirection(), length = Math.max(m.width(), m.depth());
var result = new ArrayList<Support>();
int lastBeam = 0;
while (lastBeam < length - 9) {
boolean found = false;
for (int along = Math.min(length - 6, lastBeam + 12); along >= lastBeam + 3; along--) {
int x = DX[direction] == 0 ? m.centerX() : direction == 1 ? m.minX() + along : m.maxX() - along;
int z = DZ[direction] == 0 ? m.centerZ() : direction == 2 ? m.minZ() + along : m.maxZ() - along;
var row = new ArrayList<Support>();
for (int side : new int[] {-2, 2}) {
int sx = x + DZ[direction] * side, sz = z + DX[direction] * side;
var anchor = anchorAt(m, sx, sz, probe);
if (anchor == null) { row.clear(); break; }
row.add(anchor);
}
if (row.size() != 2) continue;
result.addAll(row); lastBeam = along; found = true; break;
}
if (!found) return null;
}
return result;
}
private static Support anchorAt(Module m, int x, int z, Probe probe) {
if (probe.solid(x, m.minY() - 1, z) && probe.solid(x, m.minY() - 2, z)
&& !probe.protectedAt(x, m.minY() - 1, z) && !probe.protectedAt(x, m.minY() - 2, z))
return new Support(Type.PILLAR, x, m.minY() - 1, z, m.minY() - 1);
return support(m, x, z, probe);
}
private static List<Integer> lattice(int min, int max, int stride) {
var values = new ArrayList<Integer>(); for (int n = min; n <= max; n += stride) values.add(n);
if (values.getLast() != max) values.add(max); return values;
}
private static Support support(Module m, int x, int z, Probe probe) {
for (int distance = 1; distance <= 8; distance++) {
int y = m.minY() - distance;
if (y <= 1 || probe.protectedAt(x, y, z)) break;
if (probe.solid(x, y, z) && probe.solid(x, y - 1, z) && !probe.protectedAt(x, y - 1, z)) return new Support(Type.PILLAR, x, y, z, m.minY() - 1);
}
for (int distance = 1; distance <= 8; distance++) {
int y = m.maxY() + distance;
if (y >= 382 || probe.protectedAt(x, y, z)) break;
if (probe.solid(x, y, z) && probe.solid(x, y + 1, z) && !probe.protectedAt(x, y + 1, z)) return new Support(Type.CHAIN, x, m.maxY() + 1, z, y);
}
return null;
}
private static boolean surfaceCollision(Module m, LostCityPlan surface) {
for (var district : surface.districts()) for (var p : district.parcels()) {
if (m.maxX() < p.minX() || m.minX() > p.maxX() || m.maxZ() < p.minZ() || m.minZ() > p.maxZ()) continue;
if (m.kind() == Kind.ACCESS && accessParcel(p) && m.minX() >= p.minX() && m.maxX() <= p.maxX()
&& m.minZ() >= p.minZ() && m.maxZ() <= p.maxZ()) continue;
int footing = p.minY() - (p.kind() == LostCityPlan.Kind.BRIDGE ? 2 : 1);
if (dependencyMaxY(m) >= footing && dependencyMinY(m) <= p.maxY()) return true;
}
return false;
}
/** Render the entire saved module; the native piece clips writes to its chunk and bounds. */
public static void render(Module m, LostCityRenderer.BlockWriter writer) {
BlockState air = Blocks.AIR.defaultBlockState(), floor = Blocks.SMOOTH_STONE.defaultBlockState();
int deck = floorOffset(m.kind(), m.height());
for (int y = 0; y < m.height(); y++) for (int z = 0; z < m.depth(); z++) for (int x = 0; x < m.width(); x++) {
boolean wall = x == 0 || z == 0 || x == m.width() - 1 || z == m.depth() - 1;
BlockState block = m.kind() == Kind.GARAGE ? air : y == 0 ? Blocks.STONE.defaultBlockState()
: y <= deck ? floor : wall || y == m.height() - 1 ? LostCityPalette.masonry(m.seed(), x, y, z) : air;
writer.set(x, y, z, block);
}
switch (m.kind()) {
case ACCESS -> access(m, writer);
case POOLROOM -> pools(m, writer);
case GARAGE -> garage(m, writer);
case SEWER -> sewer(m, writer);
case SEWER_OUTLET -> sewerOutlet(m, writer);
case EXPANSION_HALL -> expansionHall(m, writer);
case PASSAGE -> { /* Narrow, unlit service passages. */ }
}
int openings = m.openings();
if (m.kind() == Kind.SEWER_OUTLET) openings |= 1 << ((Integer.numberOfTrailingZeros(openings) + 2) & 3);
for (int direction = 0; direction < 4; direction++) if ((openings & 1 << direction) != 0)
for (int side = -1; side <= 1; side++) for (int y = deck + 1; y <= deck + 3; y++)
writer.set(DX[direction] == 0 ? m.width() / 2 + side : direction == 1 ? m.width() - 1 : 0, y,
DZ[direction] == 0 ? m.depth() / 2 + side : direction == 2 ? m.depth() - 1 : 0, air);
for (var support : m.supports()) for (int y = support.minY(); y <= support.maxY(); y++) {
BlockState state = support.type() == Type.PILLAR ? Blocks.STONE_BRICKS.defaultBlockState()
: y == support.maxY() ? Blocks.POLISHED_ANDESITE.defaultBlockState() : Blocks.IRON_CHAIN.defaultBlockState();
writer.set(support.x() - m.minX(), y - m.minY(), support.z() - m.minZ(), state);
}
}
private static void access(Module m, LostCityRenderer.BlockWriter writer) {
int exit = Integer.numberOfTrailingZeros(m.openings() == 0 ? 1 : m.openings());
int wall = (exit + 2) & 3;
int x = wall == 1 ? m.width() - 2 : wall == 3 ? 1 : m.width() / 2;
int z = wall == 2 ? m.depth() - 2 : wall == 0 ? 1 : m.depth() / 2;
Direction facing = switch (wall) { case 0 -> Direction.SOUTH; case 1 -> Direction.WEST; case 2 -> Direction.NORTH; default -> Direction.EAST; };
for (int y = 2; y <= m.height() - 3; y++) writer.set(x, y, z,
Blocks.LADDER.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing));
// Restore the street plane around one accessible maintenance hatch, never leave a five-block-wide shaft open.
for (int zz = 0; zz < m.depth(); zz++) for (int xx = 0; xx < m.width(); xx++) {
writer.set(xx, m.height() - 3, zz, xx == x && zz == z
? Blocks.LADDER.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing) : Blocks.SMOOTH_STONE.defaultBlockState());
writer.set(xx, m.height() - 2, zz, Blocks.AIR.defaultBlockState());
writer.set(xx, m.height() - 1, zz, Blocks.AIR.defaultBlockState());
}
// A wooden hatch remains usable without redstone, including from below.
writer.set(x, m.height() - 2, z, Blocks.DARK_OAK_TRAPDOOR.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, facing));
}
private static void sewer(Module m, LostCityRenderer.BlockWriter writer) {
int z = m.depth() / 2;
for (int x = 2; x < m.width() - 2; x++) writer.set(x, 1, z, Blocks.WATER.defaultBlockState());
writer.set(1, 3, 1, Blocks.REDSTONE_WALL_TORCH.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST));
}
private static void sewerOutlet(Module m, LostCityRenderer.BlockWriter writer) {
int direction = Integer.numberOfTrailingZeros(m.openings()), length = DX[direction] == 0 ? m.depth() : m.width();
for (int along = 3; along < length; along++) {
int x = DX[direction] == 0 ? m.width() / 2 : direction == 1 ? along : m.width() - 1 - along;
int z = DZ[direction] == 0 ? m.depth() / 2 : direction == 2 ? along : m.depth() - 1 - along;
writer.set(x, 1, z, Blocks.AIR.defaultBlockState());
writer.set(x, 0, z, along % 7 == 0 ? Blocks.MOSSY_STONE_BRICKS.defaultBlockState() : Blocks.STONE_BRICKS.defaultBlockState());
if (along == length - 5) writer.set(x, 1, z, Blocks.SMOOTH_STONE.defaultBlockState());
if (along == length - 4) writer.set(x, 1, z, Blocks.WATER.defaultBlockState());
}
// Only the last three channel cells discharge. The upstream weir keeps the public walkways dry.
// The exit has no end wall at water level; its bed stops over certified exterior air.
int x = DX[direction] == 0 ? m.width() / 2 : direction == 1 ? m.width() - 1 : 0;
int z = DZ[direction] == 0 ? m.depth() / 2 : direction == 2 ? m.depth() - 1 : 0;
writer.set(x, 1, z, Blocks.AIR.defaultBlockState());
for (int along = 7; along < length - 5; along += 12) {
int px = DX[direction] == 0 ? 1 : direction == 1 ? along : m.width() - 1 - along;
int pz = DZ[direction] == 0 ? 1 : direction == 2 ? along : m.depth() - 1 - along;
writer.set(px, 2, pz, Blocks.IRON_BARS.defaultBlockState());
writer.set(px, 3, pz, Blocks.IRON_BARS.defaultBlockState());
}
}
private static void pools(Module m, LostCityRenderer.BlockWriter writer) {
// Fifty by twenty-five blocks of open water. The promenade is independent of the four-deep tank.
int x0 = 3, x1 = 52, z0 = 3, z1 = 27;
for (int x = x0; x <= x1; x++) for (int z = z0; z <= z1; z++) {
writer.set(x, 0, z, (z - z0) % 3 == 1 ? Blocks.POLISHED_ANDESITE.defaultBlockState() : Blocks.CALCITE.defaultBlockState());
for (int y = 1; y <= 4; y++) writer.set(x, y, z, Blocks.WATER.defaultBlockState());
writer.set(x, 5, z, Blocks.AIR.defaultBlockState());
}
// A dry half-step joins the promenade to the submerged flight without requiring a full-block jump.
for (int x = 5; x <= 8; x++) writer.set(x, 5, 2, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
// Broad steps descend into one end of the pool, leaving its full length open to swimming.
for (int x = 5; x <= 8; x++) for (int step = 0; step < 4; step++) {
int z = z0 + step, y = 4 - step;
for (int fill = 1; fill < y; fill++) writer.set(x, fill, z, Blocks.SMOOTH_STONE.defaultBlockState());
writer.set(x, y, z, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH)
.setValue(BlockStateProperties.WATERLOGGED, true));
}
for (int x : new int[] {8, 20, 34, 46}) for (int z : new int[] {1, m.depth() - 2}) {
for (int y = 6; y < m.height() - 1; y++) writer.set(x, y, z, Blocks.POLISHED_ANDESITE.defaultBlockState());
}
for (int x : new int[] {13, 27, 41}) {
for (int y = m.height() - 3; y < m.height() - 1; y++) writer.set(x, y, 15, Blocks.IRON_CHAIN.defaultBlockState());
writer.set(x, m.height() - 4, 15, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
}
// A few surviving lockers and benches imply previous use without filling the promenade.
for (int z : new int[] {6, 10, 20, 24}) {
writer.set(m.width() - 2, 6, z, Blocks.IRON_TRAPDOOR.defaultBlockState()
.setValue(BlockStateProperties.OPEN, true).setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST));
}
writer.set(1, 6, 5, Blocks.BARREL.defaultBlockState());
}
private static void garage(Module m, LostCityRenderer.BlockWriter writer) {
int top = floorOffset(m.kind(), m.height());
for (int deck = 1; deck <= top; deck += 16) {
for (int z = 0; z < m.depth(); z++) for (int x = 0; x < m.width(); x++) {
writer.set(x, deck - 1, z, Blocks.STONE.defaultBlockState());
writer.set(x, deck, z, (z == 5 || z == 10 || z == 16 || z == 21) && (x >= 5 && x <= 11 || x >= 20 && x <= 26)
? Blocks.CONCRETE.white().defaultBlockState() : Blocks.SMOOTH_STONE.defaultBlockState());
if (x == 0 || z == 0 || x == m.width() - 1 || z == m.depth() - 1)
writer.set(x, deck + 1, z, Blocks.IRON_BARS.defaultBlockState());
}
writer.set(m.width() - 3, deck + 1, 2, Blocks.BARREL.defaultBlockState());
writer.set(m.width() - 3, deck + 1, m.depth() - 3, Blocks.CAULDRON.defaultBlockState());
}
for (int x : new int[] {4, m.width() / 2, m.width() - 5}) for (int z : new int[] {3, m.depth() / 2, m.depth() - 4})
for (int y = 0; y <= m.height() - 1; y++) writer.set(x, y, z, Blocks.POLISHED_ANDESITE.defaultBlockState());
// Three two-wide sixteen-rise stair flights connect all four decks. Their upper slab openings
// are carved after the platforms, including headroom for a standing player on every stair.
for (int lower = 1; lower < top; lower += 16) for (int step = 0; step < 16; step++) {
int z = 3 + step, y = lower + step + 1;
for (int x = 1; x <= 2; x++) {
for (int clear = y + 1; clear <= y + 3; clear++) writer.set(x, clear, z, Blocks.AIR.defaultBlockState());
writer.set(x, y, z, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
writer.set(x, y - 1, z, Blocks.STONE_BRICKS.defaultBlockState());
}
}
for (int deck = 1; deck < top; deck += 16) {
writer.set(15, deck + 12, 12, Blocks.POLISHED_ANDESITE.defaultBlockState());
writer.set(16, deck + 12, 12, Blocks.POLISHED_ANDESITE.defaultBlockState());
writer.set(16, deck + 11, 12, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
}
}
private static void expansionHall(Module m, LostCityRenderer.BlockWriter writer) {
int middle = m.width() / 2;
// An empty architectural plinth reserves a future diegetic machine location. No machine,
// map entity, interaction or progression state is created by world generation.
for (int x = middle - 2; x <= middle + 2; x++) for (int z = middle - 2; z <= middle + 2; z++)
writer.set(x, 2, z, Blocks.SMOOTH_STONE.defaultBlockState());
for (int n = middle - 1; n <= middle + 1; n++) {
writer.set(n, 2, middle - 3, Blocks.STONE_BRICK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
writer.set(n, 2, middle + 3, Blocks.STONE_BRICK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
writer.set(middle - 3, 2, n, Blocks.STONE_BRICK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST));
writer.set(middle + 3, 2, n, Blocks.STONE_BRICK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST));
}
for (int x : new int[] {4, m.width() - 5}) for (int z : new int[] {4, m.depth() - 5}) {
for (int y = 2; y < m.height() - 1; y++) writer.set(x, y, z, Blocks.POLISHED_ANDESITE.defaultBlockState());
}
for (int x : new int[] {7, m.width() - 8}) for (int z : new int[] {7, m.depth() - 8}) {
// Every suspended lantern is linked to the complete masonry ceiling, not a slab or pane.
for (int y = m.height() - 3; y < m.height() - 1; y++) writer.set(x, y, z, Blocks.IRON_CHAIN.defaultBlockState());
writer.set(x, m.height() - 4, z, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
}
for (int z : new int[] {5, 8, m.depth() - 9, m.depth() - 6}) {
for (int y = 2; y <= 3; y++) {
writer.set(1, y, z, Blocks.BOOKSHELF.defaultBlockState());
writer.set(m.width() - 2, y, z, Blocks.BOOKSHELF.defaultBlockState());
}
writer.set(3, 2, z, Blocks.DARK_OAK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST));
writer.set(m.width() - 4, 2, z, Blocks.DARK_OAK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST));
}
for (int n = 2; n < m.width() - 2; n++) {
writer.set(n, 1, middle, Blocks.POLISHED_ANDESITE.defaultBlockState());
writer.set(middle, 1, n, Blocks.POLISHED_ANDESITE.defaultBlockState());
}
}
private record Point(int x, int y, int z) {}
private static final class Probe {
final Terrain terrain;
final Map<Point, Byte> blocks = new HashMap<>(); final Map<Long, Boolean> columns = new HashMap<>();
final Map<String, Integer> rejections = new java.util.TreeMap<>();
int samples;
Module reject(Module module, String reason) {
rejections.merge(module.kind() + ":" + reason, 1, Integer::sum); return null;
}
Probe(Terrain terrain) { this.terrain = terrain; }
void count() { if (samples >= MAX_SAMPLES) throw new BudgetExceeded(); samples++; }
boolean owns(int x, int z) {
long key = (long) x << 32 ^ (z & 0xffffffffL);
return columns.computeIfAbsent(key, ignored -> { count(); return terrain.owns(x, z); });
}
boolean exteriorAir(int x, int y, int z, int dx, int dz) {
count(); return terrain.exteriorAir(x, y, z, dx, dz);
}
boolean solid(int x, int y, int z) {
if (y < 0 || y >= 384) return false;
var key = new Point(x, y, z); byte value = blocks.getOrDefault(key, (byte) 0);
if ((value & 8) == 0) {
count(); value = (byte) (value | 8 | (terrain.solid(x, y, z) ? 1 : 0)); blocks.put(key, value);
}
return (value & 1) != 0;
}
boolean protectedAt(int x, int y, int z) {
if (y < 0 || y >= 384) return true;
var key = new Point(x, y, z); byte value = blocks.getOrDefault(key, (byte) 0);
if ((value & 4) == 0) {
count(); value = (byte) (value | 4 | (terrain.protectedAt(x, y, z) ? 2 : 0)); blocks.put(key, value);
}
return (value & 2) != 0;
}
}
private static final class BudgetExceeded extends RuntimeException {}
private static int roll(long value, int bound) { return (int) Long.remainderUnsigned(mix(value), bound); }
private static long mix(long value) { value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L; value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL; return value ^ (value >>> 31); }
}
@@ -0,0 +1,88 @@
package fr.koka.sanctuary.worldgen.city;
import java.util.ArrayList;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
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;
/** A saved underground module includes its openings and certified supports, including their anchors. */
public final class LostCityUndergroundPiece16 extends StructurePiece {
private final LostCityUnderground16.Module module;
public LostCityUndergroundPiece16(LostCityUnderground16.Module module) {
super(LostCityStructures.UNDERGROUND16, 0, bounds(module));
this.module = module;
setOrientation(null);
}
public LostCityUndergroundPiece16(StructurePieceSerializationContext context, CompoundTag tag) {
super(LostCityStructures.UNDERGROUND16, tag);
if (tag.getInt("SanctuarySchema").orElseThrow() != 1)
throw new IllegalArgumentException("Unsupported underground piece schema");
var supports = new ArrayList<LostCityUnderground16.Support>();
var saved = tag.getList("Supports").orElseThrow();
if (saved.size() > 128) throw new IllegalArgumentException("Too many underground supports");
for (var entry : saved) {
if (!(entry instanceof CompoundTag support)) throw new IllegalArgumentException("Invalid support");
supports.add(new LostCityUnderground16.Support(
LostCityUnderground16.Type.valueOf(support.getString("Type").orElseThrow()),
support.getInt("X").orElseThrow(), support.getInt("MinY").orElseThrow(),
support.getInt("Z").orElseThrow(), support.getInt("MaxY").orElseThrow()));
}
module = new LostCityUnderground16.Module(LostCityUnderground16.Kind.valueOf(tag.getString("Kind").orElseThrow()),
tag.getInt("MinX").orElseThrow(), tag.getInt("MinY").orElseThrow(), tag.getInt("MinZ").orElseThrow(),
tag.getInt("Width").orElseThrow(), tag.getInt("Height").orElseThrow(), tag.getInt("Depth").orElseThrow(),
tag.getInt("Openings").orElseThrow(), tag.getLong("ModuleSeed").orElseThrow(), supports);
if (!getBoundingBox().equals(bounds(module))) throw new IllegalArgumentException("Underground bounds disagree with saved geometry");
setOrientation(null);
}
public LostCityUnderground16.Module module() { return module; }
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
tag.putInt("SanctuarySchema", 1);
tag.putString("Kind", module.kind().name());
tag.putInt("MinX", module.minX()); tag.putInt("MinY", module.minY()); tag.putInt("MinZ", module.minZ());
tag.putInt("Width", module.width()); tag.putInt("Height", module.height()); tag.putInt("Depth", module.depth());
tag.putInt("Openings", module.openings()); tag.putLong("ModuleSeed", module.seed());
var supports = new ListTag();
for (var support : module.supports()) {
var value = new CompoundTag();
value.putString("Type", support.type().name()); 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);
}
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
LostCityStructures.placing(() -> LostCityUnderground16.render(module, new LostCityRenderer.BlockWriter() {
private BlockPos world(int x, int y, int z) { return new BlockPos(module.minX() + x, module.minY() + y, module.minZ() + z); }
@Override public void set(int x, int y, int z, BlockState state) {
var pos = world(x, y, z);
if (getBoundingBox().isInside(pos) && clip.isInside(pos)) placeBlock(level, state, pos.getX(), pos.getY(), pos.getZ(), clip);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
var pos = world(x, y, z);
if (getBoundingBox().isInside(pos) && clip.isInside(pos)) createChest(level, clip, RandomSource.create(seed), pos, table, null);
}
}));
}
public static BoundingBox bounds(LostCityUnderground16.Module module) {
return new BoundingBox(module.boundsMinX(), module.boundsMinY(), module.boundsMinZ(),
module.boundsMaxX(), module.boundsMaxY(), module.boundsMaxZ());
}
}
@@ -3,7 +3,7 @@
"minecraft:overworld": {
"type": "sanctuary:sanctuary_384",
"generator": {
"type": "sanctuary:island_v15",
"type": "sanctuary:island_v16",
"biome_source": {
"biomes": [
"minecraft:the_void",
@@ -0,0 +1,81 @@
{
"dimensions": {
"minecraft:overworld": {
"type": "sanctuary:sanctuary_384",
"generator": {
"type": "sanctuary:island_v15",
"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"
}
}
}
}
@@ -127,6 +127,22 @@ public final class ExpansionJournalSmoke {
require(Arrays.equals(detailBytes, Files.readAllBytes(detailDirectory.resolve(ExpansionJournal.FILE_NAME)))
&& Arrays.equals(cityBytes, Files.readAllBytes(cityDirectory.resolve(ExpansionJournal.FILE_NAME))),
"Cross-generation rejection preserves both city histories");
var ruinsRoot = ExpansionIsland.origin(724, SEED, 16);
var ruinsDirectory = scratch.resolve("city-ruins-16");
var ruinsJournal = new ExpansionJournal(ruinsDirectory, SEED, 724, 16);
var ruinsChild = ExpansionPlacement.candidates(SEED, List.of(ruinsRoot), "ruins", "sanctuary", "west", "lost_city", 512, "natural", 0).getFirst()
.withReuseToken("d".repeat(64));
require(ruinsChild.generation() == 16, "Generation16 child inherits its root identity");
ruinsJournal.commit(List.of(ruinsRoot, ruinsChild));
require(new ExpansionJournal(ruinsDirectory, SEED, 724, 16).islands().equals(List.of(ruinsRoot, ruinsChild)),
"Generation16 reservation and certificate survive reload");
byte[] ruinsBytes = Files.readAllBytes(ruinsDirectory.resolve(ExpansionJournal.FILE_NAME));
reject(() -> new ExpansionJournal(ruinsDirectory, SEED, 724, 15), "Reading generation16 as15");
reject(() -> new ExpansionJournal(detailDirectory, SEED, 724, 16), "Reading generation15 as16");
reject(() -> ruinsJournal.commit(List.of(ruinsRoot, detailChild)), "Generation15 child in generation16 journal");
require(Arrays.equals(ruinsBytes, Files.readAllBytes(ruinsDirectory.resolve(ExpansionJournal.FILE_NAME)))
&& Arrays.equals(detailBytes, Files.readAllBytes(detailDirectory.resolve(ExpansionJournal.FILE_NAME))),
"Rejected migrations preserve both published15 and16 bytes");
try {
ExpansionPlacement.candidates(SEED, List.of(ExpansionIsland.origin(724, SEED, 13)), "city", "sanctuary", "east", "lost_city", 256, "natural", 0);
throw new AssertionError("Lost City must not silently activate in a saved alpha.13 world");
@@ -0,0 +1,72 @@
package fr.koka.sanctuary.worldgen.city;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
/** Native material writes, chunk partitioning and the exact serialized fallback contract. */
public final class LostCityFoundation16Smoke {
public static void main(String[] args) {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); run();
System.out.println("LostCityFoundation16Smoke passed: hollow local masonry, narrow saved anchors, disjoint surface writes, chunk order and NBT round-trip");
}
public static void run() {
var anchors = List.of(new LostCityPlanner16.Anchor(-29, 80, 35, -27, 207, 37),
new LostCityPlanner16.Anchor(-7, 190, 57, -5, 207, 59));
var r = new LostCityPlanner16.Fallback(-30, 34, 49, 49, 220, 208, 262, anchors);
CompoundTag tag = new CompoundTag(); LostCityFoundationPiece16.writeReservation(r, tag);
var decoded = LostCityFoundationPiece16.readReservation(tag);
require(decoded.equals(r), "Foundation lost exact geometry in NBT");
Map<BlockPos, BlockState> result = render(r, 42), repeated = render(decoded, 42);
require(result.equals(repeated), "Reload changes foundation blocks");
require(!result.equals(render(r, 43)), "Foundation palette ignores its saved seed");
var bounds = LostCityFoundationPiece16.bounds(r);
int basementAir = 0;
for (var entry : result.entrySet()) {
BlockPos p = entry.getKey(); BlockState state = entry.getValue();
require(bounds.isInside(p), "Foundation painter escaped its native piece bounds");
require(r.contains(p.getX(), p.getY(), p.getZ()), "Foundation writes outside its exact reservation");
require(p.getY() <= r.floorY() - 3, "Foundation can erase a surface parcel written before it");
require(state.getFluidState().isEmpty() && !state.is(Blocks.SPAWNER), "Foundation adds a fluid or spawner");
if (p.getY() >= r.minY() && state.isAir()) basementAir++;
if (p.getY() < r.minY()) require(anchors.stream().anyMatch(a -> p.getX() >= a.minX() && p.getX() <= a.maxX()
&& p.getZ() >= a.minZ() && p.getZ() <= a.maxZ() && p.getY() >= a.minY() && p.getY() <= a.maxY()),
"Foundation fills the enclosing cuboid down to its deepest anchor");
}
require(basementAir > 8_000, "Fallback became a solid mass instead of hollow architecture");
require(!result.containsKey(new BlockPos(r.minX() + 6, 100, r.minZ() + 6)), "Natural rock between anchor columns was overwritten");
for (int z = r.minZ(); z <= r.maxZ(); z++) for (int x = r.minX(); x <= r.maxX(); x++) {
require(!result.get(new BlockPos(x, r.floorY() - 3, z)).isAir(), "Upper support slab is perforated");
require(!result.get(new BlockPos(x, r.minY(), z)).isAir(), "Basement slab is perforated");
}
// Reconstruct chunks in reverse order. The foundation renderer is pure and every write
// belongs to exactly one chunk, independent of which neighboring chunk generated first.
Map<BlockPos, BlockState> partitioned = new HashMap<>();
for (int cz = r.maxZ() >> 4; cz >= r.minZ() >> 4; cz--) for (int cx = r.maxX() >> 4; cx >= r.minX() >> 4; cx--) {
final int chunkX = cx, chunkZ = cz;
LostCityFoundation16.render(r, 42, (x, y, z, state) -> {
if ((x >> 4) == chunkX && (z >> 4) == chunkZ)
require(partitioned.put(new BlockPos(x, y, z), state) == null, "Two chunks wrote the same foundation block");
});
}
require(partitioned.equals(result), "Chunk clipping loses foundation blocks");
CompoundTag bad = new CompoundTag(); LostCityFoundationPiece16.writeReservation(r, bad);
bad.putIntArray("Anchors", new int[] {1, 2, 3});
boolean rejected = false;
try { LostCityFoundationPiece16.readReservation(bad); } catch (IllegalArgumentException expected) { rejected = true; }
require(rejected, "Truncated anchor NBT was accepted");
}
private static Map<BlockPos, BlockState> render(LostCityPlanner16.Fallback r, long seed) {
Map<BlockPos, BlockState> result = new HashMap<>();
LostCityFoundation16.render(r, seed, (x, y, z, state) ->
require(result.put(new BlockPos(x, y, z), state) == null, "Foundation writes overlap each other"));
return result;
}
private static void require(boolean valid, String message) { if (!valid) throw new AssertionError(message); }
}
@@ -0,0 +1,374 @@
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 LostCityInteriors16Smoke {
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("LostCityInteriors16Smoke: 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 and deferred loot passed.");
}
/** Also usable after the engine has already bootstrapped its registries. */
public static void run() {
Set<Map<BlockPos, BlockState>> distinct = new HashSet<>();
for (Kind kind : KINDS) for (int floors : new int[]{2, 3, 5}) for (long seed : new long[]{0, 43}) {
Capture canonical = render(parcel(kind, floors, seed, 0));
if (floors == 2 && seed == 0) distinct.add(canonical.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)), "Missing stone");
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 = LostCityInteriors16.rooms(parcel).getFirst();
Set<BlockPos> reached = walk(result, position(entrance));
for (var room : LostCityInteriors16.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);
}
}
require(distinct.size() == KINDS.length, "Building uses only labels to vary an identical interior");
boolean rejected = false;
try { LostCityInteriors16.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");
}
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);
LostCityInteriors16.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(LostCityInteriors16.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.DARK_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.DARK_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 : LostCityInteriors16.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.DARK_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.DARK_OAK_FENCE)
|| !result.block(entry.getKey().above()).is(Blocks.DARK_OAK_PRESSURE_PLATE)) continue;
int aroundThisTable = 0;
for (Direction side : HORIZONTAL) {
BlockPos chair = entry.getKey().relative(side);
if (!result.block(chair).is(Blocks.DARK_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 (LostCityInteriors16.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.DARK_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 : LostCityInteriors16.bathrooms(result.parcel)) {
BlockPos door = new BlockPos(bath.doorX(), bath.doorY(), bath.doorZ());
require(result.block(door).is(Blocks.DARK_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(LostCityInteriors16.layout(result.parcel, floor - 1) != LostCityInteriors16.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.DARK_OAK_FENCE)
|| state.is(Blocks.LECTERN) || state.is(Blocks.CRAFTING_TABLE) || state.is(Blocks.DARK_OAK_STAIRS)
|| state.is(Blocks.DARK_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 && !clear(result.block(current.above(2)))) 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;
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.DARK_OAK_DOOR); }
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,225 @@
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.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.IntBinaryOperator;
/** A road-only flood verifies the town's actual saved elevations, without walking over natural soil. */
public final class LostCityPlanner16Smoke {
public static void main(String[] args) {
Fixture flat = new Fixture((x, z) -> 220, 40);
var root = LostCityPlanner16.plan(42, 0, 0, 724, true, flat);
require(root.surface().districts().size() == 1 && lots(root).count() >= 6, "Starter must contain one compact, populated town");
check(root, flat);
require(fingerprint(root).equals(fingerprint(LostCityPlanner16.plan(42, 0, 0, 724, true, new Fixture((x, z) -> 220, 40)))), "Planning order changes the city");
require(!fingerprint(root).equals(fingerprint(LostCityPlanner16.plan(43, 0, 0, 724, true, new Fixture((x, z) -> 220, 40)))), "City ignores its seed");
require(lots(root).allMatch(p -> p.floorY() == 220), "Town remains raised above its natural ground");
var legacy = LostCityPlanner.plan(42, 0, 0, 724, true, (x, z) -> new LostCityPlanner.Ground(220, 40, true, false));
require(!legacy.isEmpty() && legacy.districts().stream().flatMap(d -> d.parcels().stream()).filter(p -> p.kind().building()).allMatch(p -> p.floorY() == 221), "Generation-14 floor changed");
var bounds = root.surface().districts().getFirst();
require(bounds.maxX() - bounds.minX() + 1 <= 71 && bounds.maxZ() - bounds.minZ() + 1 <= 71, "Starter town exceeds its compact envelope");
require(kinds(root).containsAll(Set.of(Kind.APARTMENT, Kind.PLAZA)), "Town lacks buildings or a public square");
int surface = bounds.parcels().stream().mapToInt(p -> p.width() * p.depth()).sum();
require(surface == (bounds.maxX() - bounds.minX() + 1) * (bounds.maxZ() - bounds.minZ() + 1), "Flat city leaves a mound or strip of natural dirt between facades");
for (int sign : new int[] {-1, 1}) {
Fixture slope = new Fixture((x, z) -> 220 + sign * Math.floorDiv(z, 6), 40);
var plan = LostCityPlanner16.plan(42, 0, 0, 724, true, slope);
require(!plan.surface().isEmpty(), "A modest slope eliminates the compact city"); check(plan, slope);
}
Fixture thin = new Fixture((x, z) -> 220, 4);
var shallow = LostCityPlanner16.plan(42, 0, 0, 724, true, thin); check(shallow, thin);
require(!shallow.surface().isEmpty() && !kinds(shallow).contains(Kind.POOL) && !kinds(shallow).contains(Kind.EXCAVATION), "A thin rock shelf lost ordinary buildings or admitted excavations");
Fixture protectedTerrain = new Fixture((x, z) -> 220, 40) {
@Override public boolean protectedAt(int x, int y, int z) { super.protectedAt(x, y, z); return y == 220; }
};
var protectedRoot = LostCityPlanner16.plan(42, 0, 0, 724, true, protectedTerrain);
check(protectedRoot, protectedTerrain); checkReservation(protectedRoot);
require(protectedRoot.reservation().anchors().size() == 25, "Fallback does not certify its native rock footings");
require(LostCityPlanner16.plan(42, 0, 0, 724, false, new Fixture((x, z) -> 220, 40) {
@Override public boolean protectedAt(int x, int y, int z) { super.protectedAt(x, y, z); return true; }
}).surface().isEmpty(), "An optional expansion silently acquires permission to overwrite protected terrain");
Fixture water = new Fixture((x, z) -> 220, 0);
var unsupported = LostCityPlanner16.plan(42, 0, 0, 724, true, water);
check(unsupported, water); checkReservation(unsupported);
require(unsupported.reservation().anchors().isEmpty(), "All-air terrain received a false rock footing certificate");
// Mandatory starter availability is tested independently from a natural support certificate.
// Replacing water or terrain is permitted ONLY inside the explicit fallback reservation.
for (int diameter : new int[] {512, 724, 1024}) for (int seed : new int[] {0, 1, 42, 31415, -1, Integer.MIN_VALUE}) {
for (int adversary = 0; adversary < 3; adversary++) {
final int mode = adversary;
Fixture hostile = new Fixture((x, z) -> mode == 0 ? 0 : mode == 1 ? 220
: Math.floorMod(x + z, 2) == 0 ? 100 : 360, mode == 0 ? 0 : 40) {
@Override public boolean protectedAt(int x, int y, int z) { super.protectedAt(x, y, z); return mode == 1; }
};
var mandatory = LostCityPlanner16.plan(seed, 0, 0, diameter, true, hostile);
check(mandatory, hostile); checkReservation(mandatory);
require(!mandatory.surface().isEmpty(), "Mandatory city absent for size/seed " + diameter + "/" + seed);
}
}
var all = EnumSet.noneOf(Kind.class);
LostCityPlanner16.Result large = null;
for (int diameter : new int[] {64, 128, 256, 512, 1024}) {
Fixture terrain = new Fixture((x, z) -> 220, 40);
var plan = LostCityPlanner16.plan(42, 0, 0, diameter, false, terrain);
require(plan.surface().districts().size() == 1, "Flat expansion has no compact town: " + diameter);
check(plan, terrain); all.addAll(kinds(plan)); if (diameter == 1024) large = plan;
}
for (int seed = 0; seed < 12; seed++) {
Fixture terrain = new Fixture((x, z) -> 220, 40);
var plan = LostCityPlanner16.plan(seed, 0, 0, 1024, false, terrain);
check(plan, terrain); all.addAll(kinds(plan));
}
var expected = EnumSet.allOf(Kind.class); expected.remove(Kind.BRIDGE);
require(all.containsAll(expected), "A compatible function is never selected: " + difference(expected, all));
// Keep the same finite candidate envelope and replace one junction by a fissure.
// The surviving street graph must cross it through a real, integrated viaduct.
var reference = large.surface().districts().getFirst();
int midX = reference.minX() + 2 + 44, midZ = reference.minZ() + 2 + 44;
Fixture fissure = new Fixture((x, z) -> Math.abs(x - midX) <= 14 ? 210 : 220, 40) {
@Override int depthAt(int x, int z) { return Math.abs(x - midX) <= 2 && Math.abs(z - midZ) <= 2 ? 0 : depth; }
@Override public boolean owns(int x, int z) { return x >= reference.minX() && x <= reference.maxX() && z >= reference.minZ() && z <= reference.maxZ(); }
};
var bridged = LostCityPlanner16.plan(42, 0, 0, 1024, false, fissure);
check(bridged, fissure);
require(!bridged.surface().isEmpty() && kinds(bridged).contains(Kind.BRIDGE), "A supported fissure cannot be crossed by the town's own streets");
require(!bridged.supports().isEmpty(), "The integrated viaduct lacks short rock-anchored piers");
require(bridged.surface().districts().size() == 1, "A viaduct became an isolated structure");
System.out.println("LostCityPlanner16Smoke passed: mandatory starter across all three sizes, explicit local fallback, honest anchor certificates, optional expansions, connected streets and bounded native queries");
}
private static void check(LostCityPlanner16.Result result, Fixture terrain) {
require(result.columns() == terrain.queries && result.columns() <= LostCityPlanner16.MAX_COLUMNS, "Column budget drifted");
require(result.volumeChecks() == terrain.volumeQueries && result.volumeChecks() <= LostCityPlanner16.MAX_VOLUME_CHECKS, "Volume budget drifted");
require(result.surface().districts().size() <= 1, "City is dispersed among independent fragments");
if (result.surface().isEmpty()) return;
var district = result.surface().districts().getFirst();
long lotCount = lots(result).count(), buildings = lots(result).filter(p -> p.kind().building()).count();
require(buildings >= (lotCount * 3 + 4) / 5, "Town is mostly empty public lots instead of buildings");
require(lots(result).filter(p -> p.kind().building()).map(Parcel::kind).distinct().count() == Math.min(buildings, 6),
"Buildings repeat before all six archetypes have been used");
Map<Long, Cell> roads = new HashMap<>();
Set<Long> painted = new HashSet<>();
for (Parcel p : district.parcels()) for (int x = p.minX(); x <= p.maxX(); x++) for (int z = p.minZ(); z <= p.maxZ(); z++) {
require(painted.add(key(x, z)), "Two pieces write the same surface column");
require(terrain.owns(x, z), "City escaped the island");
int floor = p.surfaceY(x - p.minX(), z - p.minZ()), top = terrain.height.applyAsInt(x, z);
if (result.reservation() == null) require(top <= floor + LostCityPlanner16.MAX_CUT, "Natural urban cut exceeds the explicit six-block limit");
if (result.reservation() == null && p.kind() != Kind.BRIDGE) require(top >= floor - p.foundationDepth()
&& top - (floor - p.foundationDepth()) + 1 <= terrain.depthAt(x, z), "Foundation hangs over a cavity");
if (p.kind() == Kind.POOL || p.kind() == Kind.EXCAVATION || p.kind() == Kind.PARKING && p.foundationDepth() >= 8)
require(top >= floor - 1 && top - (floor - 8) + 1 <= terrain.depthAt(x, z), "Deep lot lacks continuous rock");
if (p.kind() == Kind.STREET || p.kind() == Kind.BRIDGE) {
boolean rail = p.kind() == Kind.BRIDGE && (p.width() > p.depth()
? z == p.minZ() || z == p.maxZ() : x == p.minX() || x == p.maxX());
roads.put(key(x, z), new Cell(x, z, floor, p, !rail));
}
}
Set<Long> walkable = new HashSet<>();
roads.forEach((position, cell) -> { if (cell.walkable()) walkable.add(position); });
require(!walkable.isEmpty(), "Town has no walkable street");
Set<Long> visited = new HashSet<>(); ArrayDeque<Long> queue = new ArrayDeque<>();
long start = walkable.iterator().next(); visited.add(start); queue.add(start);
while (!queue.isEmpty()) {
Cell cell = roads.get(queue.removeFirst());
for (int[] offset : new int[][] {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}) {
long position = key(cell.x() + offset[0], cell.z() + offset[1]);
Cell next = roads.get(position);
if (next == null || !next.walkable()) continue;
require(Math.abs(cell.floor() - next.floor()) <= 1, "Street has a cliff");
if (cell.parcel() != next.parcel()) require(cell.floor() == next.floor(), "A step straddles two parcels and has no stair block");
if (visited.add(position)) queue.addLast(position);
}
}
require(visited.equals(walkable), "A street or bridge is disconnected from the town");
for (Parcel p : district.parcels()) if (p.kind() != Kind.STREET && p.kind() != Kind.BRIDGE) {
require(p.facing() == 0, "The saved doorway no longer faces its certified frontage");
for (int x = p.minX(); x <= p.maxX(); x++) {
require(roads.containsKey(key(x, p.minZ() - 1)) && roads.containsKey(key(x, p.maxZ() + 1)), "Natural dirt separates a facade from the street");
}
for (int z = p.minZ(); z <= p.maxZ(); z++) {
require(roads.containsKey(key(p.minX() - 1, z)) && roads.containsKey(key(p.maxX() + 1, z)), "A side street is absent beside an accepted lot");
}
for (int dx = 7; dx <= 9; dx++) {
Cell threshold = roads.get(key(p.minX() + dx, p.minZ() - 1));
require(threshold != null && threshold.walkable() && threshold.floor() == p.floorY()
&& visited.contains(key(threshold.x(), threshold.z())), "A doorway is blocked by a rail, mound or disconnected road");
}
}
for (var support : result.supports()) {
require(support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ(), "Pier uses another native start");
for (int x = support.minX(); x <= support.maxX(); x++) for (int z = support.minZ(); z <= support.maxZ(); z++) {
Cell deck = roads.get(key(x, z));
require(deck != null && deck.parcel().kind() == Kind.BRIDGE && support.maxY() == deck.floor() - 1, "Pier does not meet its actual tablier");
int top = terrain.height.applyAsInt(x, z);
require(top >= support.minY() && top - (support.minY() - 1) + 1 <= terrain.depthAt(x, z), "Pier footing lacks solid rock");
require(support.maxY() - support.minY() < 16, "Pier extends towards the void");
}
}
if (result.reservation() != null) for (var anchor : result.reservation().anchors()) {
for (int z = anchor.minZ(); z <= anchor.maxZ(); z++) for (int x = anchor.minX(); x <= anchor.maxX(); x++) {
int top = terrain.height.applyAsInt(x, z);
require(top >= anchor.minY() + 1 && top - anchor.minY() < terrain.depthAt(x, z),
"Architectural foundation falsely claims a two-block native rock footing");
}
}
}
private static void checkReservation(LostCityPlanner16.Result result) {
var r = result.reservation();
require(r != null, "Adversarial terrain did not use the explicit architectural contract");
require(r.width() == 49 && r.depth() == 49 && r.minY() == r.floorY() - 12, "Fallback expands beyond its local 49x49 basement");
require(result.surface().districts().size() == 1 && lots(result).count() == 4, "Fallback is not one compact four-lot town");
var district = result.surface().districts().getFirst();
require(district.minX() == r.minX() && district.maxX() == r.maxX()
&& district.minZ() == r.minZ() && district.maxZ() == r.maxZ(), "Foundation is not aligned with the whole street grid");
require(district.parcels().stream().mapToInt(p -> p.width() * p.depth()).sum() == 49 * 49,
"Fallback leaves dirt between facades");
require(kinds(result).contains(Kind.PLAZA) && lots(result).filter(p -> p.kind().building()).count() == 3,
"Fallback lacks three distinct buildings and a public entrance square");
require(district.parcels().stream().allMatch(p -> p.floorY() == r.floorY() && p.foundationDepth() == 2),
"Fallback grade is not covered by its two-block surface foundation");
require(!r.contains(r.minX() - 1, r.floorY(), r.minZ())
&& !r.contains(r.minX(), r.maxY() + 1, r.minZ())
&& !r.contains(r.minX(), r.minY() - 1, r.minZ()), "Reservation consumes unconstructed terrain outside its boxes");
require(r.anchors().size() <= 25, "Unbounded pile forest below the city");
}
private static java.util.stream.Stream<Parcel> lots(LostCityPlanner16.Result result) {
return result.surface().districts().stream().flatMap(d -> d.parcels().stream()).filter(p -> p.kind() != Kind.STREET && p.kind() != Kind.BRIDGE);
}
private static Set<Kind> kinds(LostCityPlanner16.Result result) {
var set = EnumSet.noneOf(Kind.class); result.surface().districts().forEach(d -> d.parcels().forEach(p -> set.add(p.kind()))); return set;
}
private static Set<Kind> difference(Set<Kind> a, Set<Kind> b) { Set<Kind> result = new HashSet<>(a); result.removeAll(b); return result; }
private static String fingerprint(LostCityPlanner16.Result result) {
StringBuilder text = new StringBuilder();
result.surface().districts().forEach(d -> {
text.append(d.startChunkX()).append(':').append(d.startChunkZ()).append(':').append(d.seed());
d.parcels().forEach(p -> text.append('|').append(p.kind()).append(':').append(p.minX()).append(':').append(p.minZ())
.append(':').append(p.floorY()).append(':').append(p.facing()).append(':').append(p.seed())
.append(':').append(p.foundationDepth()).append(':').append(Arrays.toString(p.surfaceHeights())));
}); return text.append(result.supports()).append(result.reservation()).toString();
}
private record Cell(int x, int z, int floor, Parcel parcel, boolean walkable) {}
private static long key(int x, int z) { return ((long) x << 32) ^ (z & 0xffffffffL); }
private static class Fixture implements LostCityPlanner16.Terrain {
final IntBinaryOperator height; final int depth; int queries, volumeQueries;
Fixture(IntBinaryOperator height, int depth) { this.height = height; this.depth = depth; }
int depthAt(int x, int z) { return depth; }
@Override public LostCityPlanner.Ground ground(int x, int z) { queries++; return new LostCityPlanner.Ground(height.applyAsInt(x, z), depthAt(x, z), true, false); }
@Override public boolean solid(int x, int y, int z) { volumeQueries++; int top = height.applyAsInt(x, z); return y <= top && y > top - depthAt(x, z); }
@Override public boolean protectedAt(int x, int y, int z) { volumeQueries++; return false; }
@Override public boolean owns(int x, int z) { return true; }
}
private static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); }
}
@@ -0,0 +1,197 @@
package fr.koka.sanctuary.worldgen.city;
import java.util.List;
import java.util.HashMap;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.SharedConstants;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.storage.loot.LootTable;
/** Construction volumes stay local in all three axes, including negative chunks and support anchors. */
public final class LostCityProtection16Smoke {
public static void main(String[] args) {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap();
run();
System.out.println("LostCityProtection16Smoke: surface, ramps, underground, shafts, supports, chunk boundaries and immutable snapshots passed.");
}
public static void run() {
var hall = new LostCityPlan.Parcel(LostCityPlan.Kind.HALL, -9, -9, 17, 17,
240, 2, 0, 42, 8, new int[0]);
int[] heights = new int[25 * 5];
for (int z = 0; z < 5; z++) for (int x = 0; x < 25; x++) heights[z * 25 + x] = 180 + x / 5;
var ramp = new LostCityPlan.Parcel(LostCityPlan.Kind.STREET, -24, 24, 25, 5,
180, 0, 0, 7, 3, heights);
var surface = new LostCityPlan(List.of(new LostCityPlan.District(0, 0, 42, List.of(hall, ramp))));
var passage = new LostCityUnderground16.Module(LostCityUnderground16.Kind.PASSAGE,
14, 120, -18, 5, 6, 23, 5, 42, List.of(
new LostCityUnderground16.Support(LostCityUnderground16.Type.PILLAR, 14, 114, -18, 119),
new LostCityUnderground16.Support(LostCityUnderground16.Type.CHAIN, 18, 126, 4, 132)));
var access = new LostCityUnderground16.Module(LostCityUnderground16.Kind.ACCESS,
-20, 170, -20, 5, 26, 5, 1, 7, List.of());
var garage = new LostCityUnderground16.Module(LostCityUnderground16.Kind.PASSAGE,
55, 150, -10, 17, 8, 17, 1, 137, List.of());
var expansionHall = new LostCityUnderground16.Module(LostCityUnderground16.Kind.EXPANSION_HALL,
45, 110, 40, 25, 10, 25, 5, 1515, List.of());
var underground = new LostCityUnderground16.Plan(List.of(
new LostCityUnderground16.Network(0, 0, 42, List.of(passage, access, garage, expansionHall))), 0);
var builder = new LostCityProtection16.Builder().add(surface, underground);
var mask = builder.build();
require(mask.contains(-9, 232, -9) && mask.contains(7, 255, 7), "Foundation or highest surface parcel boundary is unprotected");
require(!mask.contains(-9, 231, -9) && !mask.contains(7, 256, 7), "Surface mask escaped vertically into unrelated terrain");
require(mask.contains(-24, 177, 24) && mask.contains(0, 192, 28), "Ramp protection lost its lowest foundation or raised end");
require(!mask.contains(-25, 180, 24) && !mask.contains(1, 184, 28), "Ramp protection widened into neighboring land");
for (int x : new int[] {14, 15, 16, 18}) for (int z : new int[] {-18, -17, -16, -1, 0, 4})
require(mask.contains(x, 122, z), "Underground body has a hole at a signed chunk boundary");
require(mask.contains(14, 114, -18) && mask.contains(18, 132, 4), "Certified support anchor is outside the construction mask");
require(mask.contains(14, 113, -18) && mask.contains(18, 133, 4), "Untouched rock beside a support anchor is unprotected");
require(!mask.contains(14, 112, -18) && !mask.contains(18, 134, 4), "Support bounds protect an unrelated height");
require(!mask.contains(14, 113, -17) && !mask.contains(17, 133, 4), "Single rock dependency widened into an unrelated horizontal column");
require(!passage.contains(14, 113, -18) && !passage.contains(18, 133, 4), "Protecting rock dependencies changed the native module's write volume");
require(mask.contains(-20, 170, -20) && mask.contains(-16, 195, -16), "Surface access or its hatch is unprotected");
require(!mask.contains(-16, 196, -16) && !mask.contains(-21, 180, -20), "Access protection expands beyond the saved shaft");
// Rooms below the island do not create protected columns all the way up to its surface.
require(!mask.contains(16, 220, -16) && !mask.contains(0, 170, 0), "Underground mask incorrectly widened into a vertical biome column");
require(!mask.contains(-18, 122, 16) && !mask.contains(16, 122, 16), "Signed chunk keys alias a different quadrant");
require(!LostCityProtection16.EMPTY.contains(0, 0, 0), "Empty snapshot unexpectedly protects terrain");
// A neighboring temple fits above the garage, until its real foundation is included.
require(!surface.intersects(54, -11, 72, 7), "Underground-only collision fixture accidentally overlaps surface parcels");
require(!mask.intersects(54, 180, -11, 72, 195, 7), "A separated temple above the network was rejected");
require(!mask.intersects(54, 180 - 12, -11, 72, 195, 7), "A shallow foundation cannot reach the garage");
require(mask.intersects(54, 180 - 24, -11, 72, 195, 7), "A temple foundation entering the garage was not rejected");
require(mask.intersects(55, 150, -10, 55, 150, -10)
&& mask.intersects(71, 157, 6, 71, 157, 6), "An exact corner collision was missed at the garage bounds");
require(!mask.intersects(55, 158, -10, 71, 179, 6), "A box just above the garage intersects its body");
require(mask.intersects(14, 114, -18, 14, 114, -18)
&& mask.intersects(18, 132, 4, 18, 132, 4), "A collision through a support anchor was ignored");
require(mask.intersects(14, 113, -18, 14, 113, -18)
&& mask.intersects(18, 133, 4, 18, 133, 4), "A native structure can replace the rock carrying a support anchor");
require(mask.intersects(-16, 195, -16, -16, 195, -16), "A native start can overwrite the surface hatch");
require(!mask.intersects(-15, 170, -20, -15, 195, -16), "A box alongside the access was falsely rejected");
require(mask.contains(45, 108, 40) && mask.contains(69, 109, 64), "Large hall lost its two untouched bearing layers");
require(mask.intersects(45, 108, 40, 69, 109, 64), "A neighboring structure can carve out the large hall foundation");
require(!mask.contains(45, 107, 40) && !mask.contains(44, 108, 40), "Large hall bearing protection escaped its footprint");
require(!expansionHall.contains(45, 108, 40), "Hall bearing protection enlarged the painted native volume");
require(!mask.intersects(-18, 120, 14, 4, 125, 18), "Intersection query aliases transposed signed chunk coordinates");
require(!mask.intersects(-32, -64, -32, 80, -1, 32)
&& !mask.intersects(-32, 384, -32, 80, 400, 32), "Queries outside world height intersect city volumes");
require(!LostCityProtection16.EMPTY.intersects(-32, 0, -32, 80, 383, 32), "Empty mask rejects a native structure");
// A later support publication must not mutate the snapshot already used by chunk workers.
builder.add(32, 100, -33, 33, 107, -32);
require(!mask.contains(32, 100, -33), "A published mask changed when its builder was reused");
var expanded = builder.build();
require(expanded.contains(32, 100, -33) && expanded.contains(33, 107, -32), "Explicit support volume lost a chunk-edge anchor");
require(!expanded.contains(34, 107, -32), "Explicit support protects a neighboring block");
require(!mask.intersects(31, 99, -34, 34, 108, -31)
&& expanded.intersects(31, 99, -34, 34, 108, -31), "Intersection queries do not respect immutable support publication");
require(!expanded.intersects(34, 100, -33, 35, 107, -32), "Box beside an explicit pier was falsely rejected");
var pier = new LostCityPlanner16.Support(0, 0, 64, 100, -48, 66, 107, -46, 137);
var pierMask = new LostCityProtection16.Builder().addViaductSupport(pier).build();
require(pierMask.contains(64, 99, -48) && pierMask.contains(66, 99, -46), "Viaduct's entire untouched rock footing is unprotected");
require(pierMask.intersects(63, 98, -49, 67, 99, -45), "A new excavation can remove the rock under a viaduct");
require(!pierMask.intersects(63, 97, -49, 67, 98, -45), "Viaduct footing protection extends unnecessarily downward");
require(pier.minY() == 100, "Protecting a viaduct footing changed its saved paint bounds");
slopingBridge();
streetCutsAndSeats();
rejects(() -> new LostCityProtection16.Builder().add(2, 1, 0, 1, 2, 0));
rejects(() -> new LostCityProtection16.Builder().add(0, -1, 0, 1, 2, 1));
rejects(() -> new LostCityProtection16.Builder().add(0, 0, 0, 1, 384, 1));
rejects(() -> new LostCityProtection16.Builder().add(0, 0, 0, 257, 2, 1));
rejects(() -> mask.intersects(1, 0, 0, 0, 1, 1));
rejects(() -> mask.intersects(0, 1, 0, 1, 0, 1));
rejects(() -> mask.intersects(0, 0, 1, 1, 1, 0));
}
private static void slopingBridge() {
int[] elevations = new int[17 * 5];
for (int z = 0; z < 5; z++) for (int x = 0; x < 17; x++) elevations[z * 17 + x] = 210 + x / 6;
var bridge = new LostCityPlan.Parcel(LostCityPlan.Kind.BRIDGE, -24, -2, 17, 5,
210, 0, 0, 1515, 0, elevations);
var surface = new LostCityPlan(List.of(new LostCityPlan.District(-1, 0, 1515, List.of(bridge))));
var mask = new LostCityProtection16.Builder().add(surface, new LostCityUnderground16.Plan(List.of(), 0)).build();
for (int z = 0; z < bridge.depth(); z++) for (int x = 0; x < bridge.width(); x++) {
int wx = bridge.minX() + x, wz = bridge.minZ() + z, y = bridge.surfaceY(x, z);
require(mask.contains(wx, y - 1, wz) && mask.contains(wx, y - 2, wz), "Sloping bridge lost its two local bearing layers");
require(!mask.contains(wx, y - 3, wz), "Sloping bridge protects a third rock layer below its bearings");
}
require(bridge.foundationDepth() == 0 && bridge.minY() == 210,
"Protecting bridge bearings changed the saved geometry or added a painted foundation");
int[] deckWrites = {0};
LostCitySurface16.render(bridge, new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) {
int worldY = bridge.floorY() + y;
require(worldY >= bridge.surfaceY(x, z), "The bridge renderer painted an added block in protected natural rock");
require(mask.contains(bridge.minX() + x, worldY, bridge.minZ() + z), "Sloping deck write escaped its mask");
if (worldY == bridge.surfaceY(x, z) && !state.isAir()) deckWrites[0]++;
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
throw new AssertionError("A bridge must not add unsolicited loot");
}
});
require(deckWrites[0] >= bridge.width() * bridge.depth(), "The sloping bridge fixture never painted a complete deck");
}
private static void streetCutsAndSeats() {
int[] elevations = new int[17 * 5];
for (int z = 0; z < 5; z++) for (int x = 0; x < 17; x++) elevations[z * 17 + x] = 210 + x / 6;
var road = new LostCityPlan.Parcel(LostCityPlan.Kind.STREET, 0, 0, 17, 5,
210, 0, 0, 42, 3, elevations);
var writes = render(road);
for (int z = 0; z < 5; z++) for (int x = 0; x < 17; x++) {
int floor = road.surfaceY(x, z) - road.floorY();
for (int y = 2; y <= 8; y++) require(writes.get(new BlockPos(x, floor + y, z)).isAir(),
"A certified street cut still contains a terrain obstruction");
}
var step = writes.get(new BlockPos(5, 1, 2));
require(step.is(Blocks.STONE_BRICK_STAIRS)
&& step.getValue(BlockStateProperties.HORIZONTAL_FACING) == Direction.EAST,
"Street stair does not rise toward its uphill neighboring block");
var collision = step.getCollisionShape(net.minecraft.world.level.EmptyBlockGetter.INSTANCE, BlockPos.ZERO);
require(collision.max(Direction.Axis.Y) + 211 == road.surfaceY(6, 2) + 1,
"Street stair top does not meet the top of its uphill neighbor");
require(!writes.get(new BlockPos(5, 0, 2)).isAir(), "Street ramp removed its underlying floor");
for (int turn = 0; turn < 4; turn++) {
var garden = new LostCityPlan.Parcel(LostCityPlan.Kind.PARK_GARDEN, 0, 0, 17, 17,
210, 0, turn, 42, 3, new int[0]);
var gardenWrites = render(garden);
int bx = switch (turn) { case 1 -> 1; case 2 -> 8; case 3 -> 15; default -> 8; };
int bz = switch (turn) { case 1 -> 8; case 2 -> 1; case 3 -> 8; default -> 15; };
Direction back = switch (turn) { case 1 -> Direction.WEST; case 2 -> Direction.NORTH;
case 3 -> Direction.EAST; default -> Direction.SOUTH; };
var bench = gardenWrites.get(new BlockPos(bx, 1, bz));
require(bench.is(Blocks.DARK_OAK_STAIRS)
&& bench.getValue(BlockStateProperties.HORIZONTAL_FACING) == back,
"A public garden bench faces away from its garden after rotation " + turn);
}
}
private static HashMap<BlockPos, BlockState> render(LostCityPlan.Parcel p) {
var result = new HashMap<BlockPos, BlockState>();
LostCitySurface16.render(p, new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) { result.put(new BlockPos(x, y, z), state); }
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
throw new AssertionError("Unexpected loot in road/park fixture");
}
});
return result;
}
private static void rejects(Runnable action) {
try { action.run(); } catch (IllegalArgumentException expected) { return; }
throw new AssertionError("Invalid or unbounded construction volume was accepted");
}
private static void require(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
}
@@ -0,0 +1,230 @@
package fr.koka.sanctuary.worldgen.city;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.StairBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.storage.loot.LootTable;
/** Actual renderer topology on disposable, finite terrain fixtures. No Minecraft save is changed. */
public final class LostCityUnderground16Smoke {
public static void main(String[] args) {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); run();
System.out.println("LostCityUnderground16Smoke: Olympic pool, deep connected parking, exterior discharge, dry hall, bounds, rock anchors and protected terrain passed.");
}
public static void run() {
var terrain = new RockTerrain(); var surface = surface();
for (long seed : new long[] {0, 42}) {
var plan = LostCityUnderground16.plan(seed, surface, terrain);
require(!plan.isEmpty(), "Finite rock island has no accessible utilities");
require(plan.equals(LostCityUnderground16.plan(seed, surface, terrain)), "Same input changed saved module parameters");
var network = plan.networks().getFirst(); var capture = render(plan, terrain);
require(capture.blocks.equals(render(plan, terrain).blocks), "Saved parameters do not reconstruct the same blocks");
for (int a = 0; a < network.modules().size(); a++) for (int b = a + 1; b < network.modules().size(); b++)
require(!LostCityUnderground16.supportConflict(network.modules().get(a), network.modules().get(b)),
"One connected room destroys another room's bearing rock");
var access = module(network, LostCityUnderground16.Kind.ACCESS);
var hatch = capture.blocks.entrySet().stream().filter(e -> e.getValue().is(Blocks.DARK_OAK_TRAPDOOR)
&& access.contains(e.getKey().getX(), e.getKey().getY(), e.getKey().getZ())).map(Map.Entry::getKey).findFirst().orElseThrow();
var reached = walk(capture, hatch.above());
for (var m : network.modules()) {
require(reached.stream().anyMatch(p -> p.getY() == m.floorY() + 1 && p.getX() > m.minX()
&& p.getX() < m.maxX() && p.getZ() > m.minZ() && p.getZ() < m.maxZ()), "No two-block-high route from the hatch to " + m.kind());
for (var support : m.supports()) {
int anchor = support.type() == LostCityUnderground16.Type.PILLAR ? support.minY() : support.maxY();
int neighbor = anchor + (support.type() == LostCityUnderground16.Type.PILLAR ? -1 : 1);
require(support.maxY() - support.minY() < 8 && terrain.solid(support.x(), anchor, support.z())
&& terrain.solid(support.x(), neighbor, support.z()), "A support has no short real rock anchor");
for (int y = support.minY(); y <= support.maxY(); y++)
require(!capture.block(new BlockPos(support.x(), y, support.z())).isAir(), "Interrupted saved support");
}
verifyLights(m, capture);
}
verifyPool(module(network, LostCityUnderground16.Kind.POOLROOM), capture, reached);
verifyGarage(module(network, LostCityUnderground16.Kind.GARAGE), capture, reached);
verifyOutlet(module(network, LostCityUnderground16.Kind.SEWER_OUTLET), capture, terrain, reached);
var hall = module(network, LostCityUnderground16.Kind.EXPANSION_HALL);
require(reached.contains(new BlockPos(hall.centerX(), hall.floorY() + 2, hall.centerZ())), "Future expansion plinth is disconnected");
capture.blocks.forEach((p, state) -> {
if (hall.contains(p.getX(), p.getY(), p.getZ())) require(state.getFluidState().isEmpty(), "Architectural expansion hall is wet");
});
require(plan.samples() <= LostCityUnderground16.MAX_SAMPLES, "Terrain sampling exceeded its cap");
}
var unsupported = new RockTerrain() { @Override public boolean solid(int x, int y, int z) { return false; } };
require(LostCityUnderground16.plan(0, surface, unsupported).isEmpty(), "Unsupported void acquired an underground city");
var protectedTerrain = new RockTerrain() { @Override public boolean protectedAt(int x, int y, int z) { return true; } };
require(LostCityUnderground16.plan(0, surface, protectedTerrain).isEmpty(), "Protected natural hydrology was overwritten");
var foreign = new RockTerrain() { @Override public boolean owns(int x, int z) { return false; } };
require(LostCityUnderground16.plan(0, surface, foreign).isEmpty(), "A network escaped chunk ownership");
var caveOnly = new RockTerrain() { @Override public boolean exteriorAir(int x, int y, int z, int dx, int dz) { return false; } };
require(LostCityUnderground16.plan(0, surface, caveOnly).networks().stream().flatMap(n -> n.modules().stream())
.noneMatch(m -> m.kind() == LostCityUnderground16.Kind.SEWER_OUTLET), "A pocket cave was treated as the island exterior");
foundationDependencies();
}
private static LostCityPlan surface() {
var street = new LostCityPlan.Parcel(LostCityPlan.Kind.STREET, -2, -12, 5, 25, 220, 0, 0, 7, 3, new int[0]);
return new LostCityPlan(List.of(new LostCityPlan.District(0, 0, 3, List.of(street))));
}
private static LostCityUnderground16.Module module(LostCityUnderground16.Network n, LostCityUnderground16.Kind kind) {
return n.modules().stream().filter(m -> m.kind() == kind).findFirst()
.orElseThrow(() -> new AssertionError("Reference island never generated " + kind + "; kinds=" + n.modules().stream().map(LostCityUnderground16.Module::kind).toList()));
}
private static void verifyPool(LostCityUnderground16.Module pool, Capture c, Set<BlockPos> reached) {
var bounds = pool.poolWaterBounds();
require(bounds.maxX() - bounds.minX() + 1 == 50 && bounds.maxZ() - bounds.minZ() + 1 == 25
&& bounds.maxY() - bounds.minY() + 1 == 4, "Pool lost its fifty-by-twenty-five four-deep tank");
int wet = 0, steps = 0;
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) for (int x = bounds.minX(); x <= bounds.maxX(); x++)
for (int y = bounds.minY(); y <= bounds.maxY(); y++) {
var p = new BlockPos(x, y, z); var state = c.block(p);
if (!state.getFluidState().isEmpty()) {
wet++;
for (var direction : new Direction[] {Direction.DOWN, Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}) {
var q = p.relative(direction); var next = c.block(q);
require(!next.isAir(), "Pool water has an unsealed horizontal or downward neighbor: " + q);
}
}
if (state.getBlock() instanceof StairBlock) {
steps++;
require(state.getValue(BlockStateProperties.WATERLOGGED)
&& state.getValue(BlockStateProperties.HORIZONTAL_FACING) == Direction.NORTH, "Pool access step cannot descend from its north promenade");
}
}
require(wet >= 4_950 && steps == 16, "Large pool was replaced by tiny basins or lost its submerged stairs: " + wet);
for (int[] point : new int[][] {{2, 2}, {2, 28}, {54, 2}, {54, 28}})
require(reached.contains(new BlockPos(pool.minX() + point[0], pool.floorY() + 1, pool.minZ() + point[1])),
"The dry promenade cannot be walked around the pool");
}
private static void verifyGarage(LostCityUnderground16.Module garage, Capture c, Set<BlockPos> reached) {
var decks = garage.parkingFloors();
require(decks.size() == 8 && decks.getLast() - decks.getFirst() == 112, "Garage did not descend through eight widely separated platforms");
require(garage.supports().size() == 9, "Open parking is missing its nine certified load-bearing columns");
for (int y : decks) {
require(reached.contains(new BlockPos(garage.centerX() + 3, y + 1, garage.centerZ())), "Garage deck is disconnected from the public stair: " + y);
require(c.block(new BlockPos(garage.minX(), y + 5, garage.centerZ())).isAir(), "Parking exterior was closed by a solid wall");
}
for (int lower : decks.subList(0, decks.size() - 1)) for (int step = 0; step < 16; step++)
for (int x = 1; x <= 2; x++) {
var p = new BlockPos(garage.minX() + x, lower + step + 1, garage.minZ() + 3 + step);
require(c.block(p).getBlock() instanceof StairBlock && c.block(p).getValue(BlockStateProperties.HORIZONTAL_FACING) == Direction.SOUTH,
"Parking stair flight has a gap or faces downhill");
require(c.block(p.above()).isAir() && c.block(p.above(2)).isAir(), "Parking stair lacks player headroom");
}
}
private static void verifyOutlet(LostCityUnderground16.Module outlet, Capture c, RockTerrain terrain, Set<BlockPos> reached) {
int[] dx = {0, 1, 0, -1}, dz = {-1, 0, 1, 0}; int direction = outlet.outletDirection();
var mouth = outlet.outletMouth(); var beyond = mouth.offset(dx[direction], 0, dz[direction]);
require(terrain.exteriorAir(beyond.getX(), beyond.getY(), beyond.getZ(), dx[direction], dz[direction]), "Sewer terminates in a cave or rock face");
require(c.block(mouth).isAir() && c.block(beyond).isAir(), "Outlet water level has a closed terminal wall");
var source = mouth.offset(-dx[direction] * 3, 0, -dz[direction] * 3);
require(c.block(source).is(Blocks.WATER), "Outflow has no real water source");
for (int distance = 1; distance <= 4; distance++) {
var p = source.offset(dx[direction] * distance, 0, dz[direction] * distance);
require(c.block(p).isAir(), "Sewer source cannot flow along the trough to the exterior");
}
require(solid(c.block(source.offset(-dx[direction], 0, -dz[direction]))), "Outlet weir allows water to flood back into the public network");
int length = Math.max(outlet.width(), outlet.depth());
var rows = new java.util.TreeMap<Integer, Integer>();
for (var support : outlet.supports()) {
int distance = dx[direction] == 0 ? direction == 0 ? outlet.maxZ() - support.z() : support.z() - outlet.minZ()
: direction == 1 ? support.x() - outlet.minX() : outlet.maxX() - support.x();
rows.merge(distance, 1, Integer::sum);
}
int prior = 0;
for (var row : rows.entrySet()) {
require(row.getValue() == 2 && row.getKey() - prior <= 12, "Sewer has a bridge span without its two short rock anchors");
prior = row.getKey();
}
require(length - 1 - prior <= 8, "Sewer outlet lip exceeds its eight-block cantilever");
require(reached.stream().anyMatch(p -> Math.abs(p.getX() - mouth.getX()) + Math.abs(p.getZ() - mouth.getZ()) <= 2
&& p.getY() == mouth.getY() + 1), "Dry sewer banks do not reach the outlet");
}
private static void verifyLights(LostCityUnderground16.Module m, Capture c) {
c.blocks.forEach((p, state) -> {
if (state.is(Blocks.LANTERN) && m.contains(p.getX(), p.getY(), p.getZ())) {
require(state.getValue(BlockStateProperties.HANGING), "Utility lantern has no suspension");
var above = p.above();
while (c.block(above).is(Blocks.IRON_CHAIN) && above.getY() < m.maxY()) above = above.above();
require(solid(c.block(above)), "A suspended lantern is not anchored to a full block");
}
});
}
private static void foundationDependencies() {
var pool = new LostCityUnderground16.Module(LostCityUnderground16.Kind.POOLROOM, 0, 150, 0, 57, 13, 31, 1, 42, List.of(new LostCityUnderground16.Support(LostCityUnderground16.Type.PILLAR, 8, 149, 8, 149)));
var below = new LostCityUnderground16.Module(LostCityUnderground16.Kind.PASSAGE, 8, 143, 8, 5, 6, 5, 5, 1, List.of());
require(LostCityUnderground16.supportConflict(pool, below), "Another tunnel can remove the pool support's rock-bearing neighbor");
var deeper = new LostCityUnderground16.Module(LostCityUnderground16.Kind.PASSAGE, 8, 142, 8, 5, 6, 5, 5, 1, List.of());
require(!LostCityUnderground16.supportConflict(pool, deeper), "Pool bearing protection escaped its certified depth");
}
/** Rock with a sloping roof, irregular deep caverns and a finite, exposed island edge. */
private static class RockTerrain implements LostCityUnderground16.Terrain {
@Override public boolean solid(int x, int y, int z) {
int top = 220 + (int) Math.floor(Math.sin(x / 37.0) * 3 * Math.min(1, Math.abs(x) / 8.0));
return Math.abs(x) <= 104 && Math.abs(z) <= 104 && y <= top && y >= 70
&& !(y < 100 && Math.floorMod(x * x + z * z, 71) < 14);
}
@Override public boolean protectedAt(int x, int y, int z) { return false; }
@Override public boolean owns(int x, int z) { return true; }
@Override public boolean exteriorAir(int x, int y, int z, int dx, int dz) {
if (Math.abs(x) <= 104 && Math.abs(z) <= 104) return false;
for (int n = 0; n <= 32; n++) if (solid(x + dx * n, y, z + dz * n)) return false;
return true;
}
}
private static Capture render(LostCityUnderground16.Plan plan, LostCityUnderground16.Terrain terrain) {
var result = new Capture(terrain);
for (var network : plan.networks()) for (var m : network.modules()) LostCityUnderground16.render(m, new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) {
var p = new BlockPos(m.minX() + x, m.minY() + y, m.minZ() + z);
require(p.getX() >= m.boundsMinX() && p.getX() <= m.boundsMaxX() && p.getY() >= m.boundsMinY()
&& p.getY() <= m.boundsMaxY() && p.getZ() >= m.boundsMinZ() && p.getZ() <= m.boundsMaxZ(), "Renderer exceeded saved bounds");
require(plan.contains(p.getX(), p.getY(), p.getZ()), "An actual block write is missing from the 3D mask");
require(!terrain.protectedAt(p.getX(), p.getY(), p.getZ()) && terrain.owns(p.getX(), p.getZ()), "Renderer entered protected or foreign terrain");
result.blocks.put(p, state);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) { throw new AssertionError("Unrequested dungeon loot"); }
});
return result;
}
private static Set<BlockPos> walk(Capture c, BlockPos start) {
var seen = new HashSet<BlockPos>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
while (!queue.isEmpty()) {
var p = queue.removeFirst();
for (Direction direction : Direction.Plane.HORIZONTAL) for (int dy : new int[] {0, 1, -1}) {
var q = p.relative(direction).offset(0, dy, 0);
if (Math.abs(q.getX() - start.getX()) > 120 || Math.abs(q.getZ() - start.getZ()) > 120 || Math.abs(q.getY() - start.getY()) > 160) 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 (Math.abs(q.getY() - start.getY()) > 160) continue;
if ((climb(c.block(p)) || climb(c.block(q)) || climb(c.block(p.below()))) && stand(c, q) && seen.add(q)) queue.add(q);
}
}
return seen;
}
private static boolean stand(Capture c, BlockPos p) {
return passable(c.block(p)) && passable(c.block(p.above()))
&& (solid(c.block(p.below())) || c.block(p.below()).getBlock() instanceof StairBlock || climb(c.block(p)) || climb(c.block(p.below())));
}
private static boolean climb(BlockState b) { return b.is(Blocks.LADDER) || b.is(Blocks.DARK_OAK_TRAPDOOR); }
private static boolean passable(BlockState b) { return b.isAir() || climb(b) || b.is(Blocks.REDSTONE_WALL_TORCH); }
private static boolean solid(BlockState b) { return b.canOcclude() && b.getFluidState().isEmpty(); }
private static final class Capture {
final Map<BlockPos, BlockState> blocks = new HashMap<>(); final LostCityUnderground16.Terrain terrain;
Capture(LostCityUnderground16.Terrain terrain) { this.terrain = terrain; }
BlockState block(BlockPos p) { return blocks.getOrDefault(p, terrain.solid(p.getX(), p.getY(), p.getZ()) ? Blocks.STONE.defaultBlockState() : 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"
author = "KOKA99CAB"
version = "0.1.0-alpha.15"
version = "0.1.0-alpha.16"
pack-format = "packwiz:1.1.0"
[index]