Add alpha16 inhabited ruins and deep city infrastructure
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -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 l’exté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
|
||||
|
||||
@@ -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. **L’alpha.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. **L’alpha.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 s’ouvrent vers l’exté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’à l’alpha.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).
|
||||
|
||||
@@ -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 l’alpha.15 publiée et corriger les ruines pour que
|
||||
Sanctuary Island possède un quartier identifiable et des infrastructures
|
||||
profondes qui donnent l’impression d’une 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
|
||||
n’est 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 l’accè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 l’alpha.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.
|
||||
|
||||
@@ -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 l’alpha.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 n’est 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, d’une réservation
|
||||
architecturale locale 49 × 49 : rues et bâtiments reliés au-dessus d’un socle
|
||||
creux, avec appuis de maçonnerie certifiés. Cette réservation peut dégager le
|
||||
terrain et remplacer l’eau dans sa seule emprise, avant le placement des pièces.
|
||||
Elle ne modifie pas la densité de toute l’île et ne s’applique 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 d’orientation et d’appui.
|
||||
|
||||
## 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 d’or 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 d’eau** ; les deux égouts observés déversent de l’eau au-delà
|
||||
de leur bouche. Dix-neuf témoins temporaires indépendants prouvent l’activité
|
||||
effective du moteur des fluides, puis sont supprimés avant sauvegarde. Les
|
||||
cascades atteignent des corniches : elles ne sont pas toutes libres jusqu’au
|
||||
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 l’alpha.15
|
||||
publiée. Les fichiers de génération 15 restent inchangés. Le preset caché
|
||||
`sanctuary_v15.json` est identique à l’ancien 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 d’amorç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 s’arrête alors en conservant les équipements déjà certifiés.
|
||||
Une aile absente ne prouve donc pas l’absence 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 l’essai 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 d’un monde précédent.
|
||||
|
||||
| Lieu | Repère X / Y / Z |
|
||||
| --- | --- |
|
||||
| Entrée de l’immeuble | −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. L’expansion 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 n’a é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
|
||||
l’occupation passée par d’autres 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 d’attribution restent à définir ; aucun réacteur,
|
||||
nouveau spawner de donjon ou système de progression n’est ajouté ici.
|
||||
+22
-3
@@ -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. L’alpha.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 d’expansion, en laissant
|
||||
sans ajouter le mod The Lost Cities ni reprendre ses assets. L’alpha.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 d’expansion, 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. L’alpha.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 d’expansion** : 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 l’ensemble 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 l’alpha.15) :
|
||||
développer les trois familles ensemble : mines et ateliers, lieux étranges,
|
||||
infrastructures à réactiver. Les ruines doivent raconter une occupation
|
||||
antérieure par d’autres joueurs, à travers leurs usages et leurs circulations.
|
||||
Plusieurs réseaux distincts peuvent se rejoindre ponctuellement : découvrir
|
||||
ces connexions fait partie de l’exploration. 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 l’alpha.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
@@ -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
|
||||
|
||||
@@ -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') }
|
||||
|
||||
+266
@@ -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; }
|
||||
}
|
||||
+1022
File diff suppressed because it is too large
Load Diff
+5
-1
@@ -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,
|
||||
|
||||
+4
-4
@@ -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();
|
||||
|
||||
+5
@@ -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);
|
||||
|
||||
+86
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+45
@@ -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;
|
||||
}
|
||||
}
|
||||
+76
@@ -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());
|
||||
}
|
||||
}
|
||||
+584
@@ -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); }
|
||||
}
|
||||
}
|
||||
+81
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
-1
@@ -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() {}
|
||||
|
||||
+68
@@ -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(); }
|
||||
}
|
||||
+702
@@ -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); }
|
||||
}
|
||||
+88
@@ -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());
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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",
|
||||
|
||||
+81
@@ -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");
|
||||
|
||||
+72
@@ -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); }
|
||||
}
|
||||
+374
@@ -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); }
|
||||
}
|
||||
+225
@@ -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); }
|
||||
}
|
||||
+197
@@ -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);
|
||||
}
|
||||
}
|
||||
+230
@@ -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
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user