Compare commits

..
Author SHA1 Message Date
koka bb63fab388 worldgen: sober palettes and connected industrial galleries for alpha21
Build Sanctuary / build (push) Canceled after 0s
2026-09-10 10:03:30 +02:00
koka ae4c6200db docs: record verified alpha20 release and Prism update
Build Sanctuary / build (push) Canceled after 0s
2026-09-10 01:21:33 +02:00
koka 0edac29dd8 feat: add detailed architecture for Sanctuary alpha20
Build Sanctuary / build (push) Canceled after 0s
2026-09-10 01:18:31 +02:00
koka 2facf6f722 docs: record verified alpha19 release and Prism update
Build Sanctuary / build (push) Canceled after 0s
2026-09-09 22:49:20 +02:00
koka c23e72b8e6 feat(worldgen): add reversible alpha19 heritage structures and routes
Build Sanctuary / build (push) Canceled after 0s
2026-09-09 22:42:32 +02:00
112 changed files with 19398 additions and 62 deletions
+52
View File
@@ -1,5 +1,57 @@
# Changelog
## 0.1.0-alpha.20 — 2026-09-10
- Passe architecturale sur les six bâtiments urbains, les quatorze anciennes
infrastructures, les espaces publics et les réseaux souterrains. Palettes
propres aux usages : chêne, bouleau, granit, diorite, andésite et tuf ;
fondations maçonnées en briques de pierre conservées.
- Angles coupés, baies en saillie, portiques, retraits, loggias, toitures
nervurées et verrières avec passage lumineux. Sols et plafonds détaillés,
corniches, encadrements, murets et blocs ciselés ; bordures de routes en
briques de tuf et accents ciselés.
- Vitres, barreaux, clôtures, murs bas et angles descaliers raccordés depuis
la géométrie complète de chaque pièce avant son découpage entre chunks.
- Nouveau codec `sanctuary:island_v20`, journal et pièces distincts. Les mondes
19 et antérieurs gardent leurs rendus et futurs chunks. Les plans de terrain,
dhydrologie et dimplantation restent ceux de la génération précédente.
Personnaliser conserve les trois tailles et les structures expérimentales.
`check build assemblePack` passe, avec huit GameTests généraux. Création et
réouverture de la ville et des infrastructures passent ; les rapports 19 restent
identiques aux témoins publiés. Option désactivée et Petit/graine42 passent.
JAR, MRpack et ZIP Prism vérifiés ; release et canal packwiz publiés. Deux
synchronisations isolées puis deux dans la même instance Prism passent, avec
925 fichiers personnels et réglages suivis conservés.
Voir le [catalogue illustré](docs/architecture-alpha20.md) et les
[vérifications et limites](docs/testing-alpha20.md). Les fonctions de la bêta
restent à construire ; aucun nouveau système économique nest activé.
## 0.1.0-alpha.19 — 2026-09-09
- Quatorze nouvelles infrastructures abandonnées sur l’île initiale, avec pièces,
accès, mobilier, secrets et outils vanilla associés. La présence varie selon
la graine et le relief ; les quatorze types ont été rencontrés en tests natifs.
- Voies de fret aux rails incomplets et chemins entre destinations réelles.
Le témoin Moyen, graine 0, contient treize lieux et sept liaisons, dont un
fret de 499 blocs entre dépôt et pompage. Tracés enregistrés par chunk.
- Option FR/EN « Structures expérimentales » dans le bouton natif Personnaliser,
trois tailles conservées. Codec `sanctuary:island_v19` et journal 19 ; les
anciens mondes gardent leurs plans et futurs chunks. Un nouveau monde avec
loption désactivée conserve le socle urbain sans les expériences 17/19.
- Commande opérateur `/sanctuary structures` pour relever les entrées.
La ville et ses sous-sols sont conservés ; les systèmes de la bêta restent
à construire.
`check build assemblePack` passe, dont les huit GameTests généraux. Les trois
formats sont vérifiés nativement ; le témoin moyen se rouvre avec ses 112 pièces,
13 coffres, 68 cibles accessibles et la modification témoin. Les identités 17
restent identiques à la release publiée. JAR, MRpack et ZIP Prism vérifiés ;
release et canal packwiz publiés. Deux synchronisations isolées puis deux dans
la même instance Prism passent, avec 767 fichiers personnels et réglages suivis
conservés. Voir [les preuves et limites](docs/testing-alpha19.md).
## 0.1.0-alpha.17 — 2026-09-09
- Prototype de trois réseaux oubliés sur l’île initiale : mines et ateliers,
+55 -6
View File
@@ -15,10 +15,59 @@ infrastructures qui permettront progressivement d'ouvrir de nouveaux continents.
Ce dépôt démarre la nouvelle base Fabric. La [vision complète](docs/vision.md)
conserve les intentions ; le [backlog](docs/backlog.md) suit les
tickets. Les systèmes d’économie, de progression collective et les dimensions
décrits dans la vision ne sont pas encore implémentés. **Lalpha.17 est publiée** :
trois familles de réseaux oubliés expérimentaux rejoignent la ville et ses grands
sous-sols sur les nouvelles îles de départ. Le canal packwiz et la même instance
Prism sont à jour ; voir [Distribution](docs/packwiz.md).
décrits dans la vision ne sont pas encore implémentés. **Lalpha.21 est construite et
vérifiée** : palettes sobres, couvertures portées, petits quartiers reliés et
premières galeries industrielles. Voir [le catalogue alpha.21](docs/architecture-alpha21.md),
[les essais alpha.21](docs/testing-alpha21.md)
et l’état du canal packwiz dans [Distribution](docs/packwiz.md).
## Alpha.21 — matériaux, quartiers et traversées
La ville revient à la pierre lisse, pierre, briques de pierre, andésite et chêne.
Le tuf accompagne surtout les chemins et les sous-sols. Les toits et ornements
sont raccordés à leurs appuis ; les pièces gardent leurs aménagements. La ville
recherche des petits quartiers séparés par du terrain naturel, avec des chemins
vers le cœur. Les lieux patrimoniaux recherchent davantage de raccords.
Loption expérimentale ajoute une recherche de galeries industrielles : chaussée
cinq blocs, deux trottoirs, salles et escaliers de surface. Les placements sur
pente sont assouplis et les destinations isolées sont prioritaires pour les chemins.
Créer un **nouveau monde Sanctuary** pour ce générateur21. Les anciens mondes
conservent leur génération. Les vérifications et coordonnées du
témoin sont consignées dans [les essais](docs/testing-alpha21.md).
## Alpha.20 — identité des bâtiments
La passe reprend les six bâtiments de ville, les quatorze infrastructures,
les parcs et les sous-sols. Bois de chêne et de bouleau, granit, diorite et tuf
identifient les usages ; biseaux, baies, corniches, portiques et verrières
donnent du relief aux volumes. Les routes reçoivent des bordures de briques
de tuf et des blocs ciselés. Les vitres et grilles se raccordent entre chunks.
Créer un **nouveau monde Sanctuary, Moyen, graine 0**, avec **Structures
expérimentales** activées pour retrouver le témoin. Les anciens mondes gardent
leur génération. Le bouton natif **Personnaliser** et ses trois tailles restent
identiques. Voir le [catalogue illustré](docs/architecture-alpha20.md) et les
[essais alpha.20](docs/testing-alpha20.md). `check build assemblePack`, les essais natifs et les artefacts sont vérifiés ;
la [release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.20)
et le canal packwiz sont publiés. La même instance Prism est synchronisée
deux fois, avec 925 fichiers personnels et réglages suivis conservés.
## Alpha.19 — anciennes infrastructures
Le chantier ajoute une première passe des quatorze lieux du
[catalogue](docs/structures-audit.md), avec leurs accès, des petits butins vanilla
et des voies de fret ou chemins abandonnés entre destinations réelles.
Loption **Structures expérimentales** reste enregistrée à la création du monde.
La nouvelle génération garde les anciennes sauvegardes intactes. Aucun système
de progression, mailbox, caméra ou réacteur nest activé.
Les trois tailles sont vérifiées en génération native ; création et réouverture
conservent les structures et les coffres. `check build assemblePack` passe.
Pour voir le grand fret : **Moyen, graine 0**, option activée ;
`/sanctuary structures` liste les entrées. Voir [les preuves et la carte](docs/testing-alpha19.md).
La [release alpha.19](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.19)
et le canal packwiz sont publiés. La même instance Prism est synchronisée deux
fois, avec 767 fichiers personnels et réglages suivis conservés.
## Alpha.17 — prototype des réseaux oubliés
@@ -409,7 +458,7 @@ sera une mise à jour explicite, avec vérification des API et des sauvegardes.
| Fabric API | 0.160.0+26.3 |
| Fabric Loom | 1.17.20 |
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
| Sanctuary / pack | 0.1.0-alpha.17 |
| Sanctuary / pack | 0.1.0-alpha.21 |
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
uniquement la bibliothèque standard et repère aussi une installation Python
@@ -438,7 +487,7 @@ décrits dans [Validation](docs/testing.md).
Résultats :
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.17.jar` : mod à installer avec
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.21.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).
+115
View File
@@ -0,0 +1,115 @@
# Architecture de lalpha.20
Cette passe donne une identité visuelle aux lieux déjà planifiés : volumes,
façades, sols, plafonds et mobilier. Elle réutilise les emprises et les accès
certifiés des générations précédentes. Les bâtiments de ville et les sites
expérimentaux restent inscrits dans leurs parcelles de 17 × 17 blocs ; leurs
corniches et leurs retraits restent à lintérieur de cette limite.
Les nouveaux rendus demandent un **nouveau monde de génération 20**. Les mondes
19 et antérieurs conservent leurs bâtiments, y compris dans les futurs chunks.
Le bouton **Personnaliser**, les trois tailles et loption **Structures
expérimentales** restent disponibles. Le catalogue nimpose pas que chaque lieu
apparaisse sur chaque île.
![Sélection darchitectures produites par les rendus de lalpha.20](images/architecture20-overview.png)
*Vues des blocs réellement émis par les générateurs, affichés avec les modèles
Minecraft, sans terrain. Ce montage est une prévisualisation géométrique, pas
une capture du client en jeu ; il montre une sélection du catalogue.*
## Les six bâtiments de Lost City
Les étages conservent leurs plans alternés, leurs pièces et leurs escaliers.
Les sanitaires sont clos, les lits adossés à de vrais murs, et les lampes
reposent sur des blocs complets. Les aménagements de toit décrits ci-dessous
possèdent des points daccès vérifiés par le smoke du renderer.
| Fonction | Forme et façade | Espaces et détails construits |
| --- | --- | --- |
| Immeuble | Angles avant coupés, baies en saillie, montants de chêne écorcé et bandeaux de tuf. | Parquets alternant chêne et bouleau, logements avec cuisine et chambre ; jardinières, portique de bois et table sur le toit. |
| Hôtel | Portique et travées encadrées de diorite polie, garde-corps et bandeaux en bouleau ; pavillon de toit en retrait. | Réception, chambres, salons, sanitaires et buanderie ; petit local de linge et bancs sur le toit. |
| Bibliothèque | Grandes baies, angles coupés et lames verticales en bouleau écorcé ; lanterneau vitré avec ouverture à travers le toit et le plafond du dernier étage. | Rayonnages, archives, tables et lutrins ; sols délimitant les espaces de lecture et terrasse de lecture sur le toit. |
| Restaurant | Encadrements de granit poli, vitrines et auvents associant chêne et bouleau. | Salles à manger et cuisines réparties par étage, garde-manger et sanitaires ; tables sur le toit et sorties dextraction maçonnées. |
| Caserne | Piles épaisses de briques de tuf, soubassements ciselés et fenêtres grillagées. | Dortoirs, bureau de service, réserves d’équipement et sanitaires ; cage descalier coiffée dun pavillon accessible. |
| Hall | Travées monumentales, piliers ciselés, corniches de diorite et fronton de toit en gradins. | Billetterie, bureaux, salle avec scène sur deux niveaux, galerie protégée, salles de réunion et archives. |
## Les quatorze anciennes infrastructures
Chaque site conserve ses seuils, son accès de service et son coffre de découverte.
Les fonctions ci-dessous sont exprimées par les pièces et les blocs vanilla
présents ; elles ne constituent pas de nouveaux systèmes de progression.
| Lieu | Architecture et usage visibles |
| --- | --- |
| Gare de fret | Halle aux angles coupés, lanterneau vitré et long auvent de quai en chêne ; voie, salle dattente et petit bureau de signalisation. |
| Poste et centre de tri | Façade de granit, accents de diorite et toiture en retrait ; guichet, rangées de tonneaux, passage de tri et bureau. |
| Dépôt communal | Grand entrepôt nervuré de pierre, toiture industrielle à trois travées vitrées en dents de scie ; rails, chargement, caisses et mezzanine dinventaire. |
| Cartothèque dexpédition | Corps en bouleau, petites ailes différenciées et balcon arrière abrité ; table de cartographie, lutrin, archives et réserve. |
| Observatoire et météo | Tour à pans coupés, contreforts de diorite et couronne ouverte portée par des nervures ; deux volées descalier, salle dinstruments et plateforme dobservation. |
| Atelier ferroviaire | Large couverture à nervures et vitrages, piles de granit ; deux voies, fosse dentretien avec escalier, établi, fourneau, meule et petite réserve. |
| Station de pompage et château deau | Bâtiment bas, conduites figurées par des grilles et murets, cuve haute sur quatre piles ciselées ; galerie dentretien et réserve sèche. Leau reste dans la cuve. |
| Sous-station | Cour clôturée, supports et grilles autour des équipements, locaux bas à toiture nervurée ; commandes vanilla et tranchée dentretien accessible. |
| Serre et graineterie | Verrière haute rythmée par des nervures de bouleau ; cultures, deux points dirrigation contenus, allée de tuf et remise de rempotage. |
| Quai sur le vide | Platelage en chêne, garde-corps à piliers sculptés, abris bas et grue de bois à chaîne ; attente, débarcadère et réserve. |
| Relais radio et maison du gardien | Petite maison de chêne avec toiture à deux matières et cheminée ; antenne haute à traverses, plateforme accessible et chambre du gardien. |
| Club photo et chambre noire | Façade claire de diorite, corniche sculptée et toiture arrière en retrait ; salle du club, chambre noire, bacs et espace de séchage à chaînes. |
| Vestiaires et salles étranges | Entrée basse et étroite suivie dune salle haute à colonnes répétées ; casiers, sols carrelés, alcôve et petite pièce de service. |
| Refuge de chantier | Cabane surélevée sur piles ciselées, toiture inclinée et ossature inachevée ; couchage, établi, escalier extérieur et trappe avec échelle vers la réserve inférieure. |
Les voies de fret gardent leur ballast, leurs traverses, leurs courbes et leurs
rails manquants. Les chemins reçoivent des bordures de briques de tuf et des
accents ciselés ; leurs pentes et leurs destinations restent celles du plan
sauvegardé.
## Espaces publics et équipements extérieurs
| Espace | Détails présents en génération 20 |
| --- | --- |
| Rues et ponts | Chaussée dandésite polie, bordures de tuf poli, bandes de briques de tuf ponctuées de blocs ciselés ; marches sur les pentes, piles de garde-corps et grilles sur les ponts. Les tabliers najoutent pas de colonnes vers le vide. |
| Parc arboré | Allées en croix, bancs de chêne à accoudoirs minéraux, lampadaires et petite couronne de bouleau à côté des arbres existants. |
| Jardin | Quatre parterres bordés de diorite, angles de tuf sculpté, portique de bois et promenade centrale. |
| Aire de jeux | Bac à sable bordé de bouleau, portique de balançoire, plateforme de jeu avec marches et garde-corps, bancs et éclairage. |
| Place | Sol organisé en couronnes de diorite, granit et tuf ; fontaine sèche à coupe basse et piédestal sculpté. |
| Mémorial | Sol minéral à lignes de tuf, marches autour du socle, colonne mêlant tuf et briques de pierre sculptées. |
| Marché | Croisement piéton, quatre étals avec pieds ciselés, poteaux de chêne et couvertures alternant dalles de chêne et de bouleau. |
| Parking | Rampe et stationnements conservés ; piliers dandésite, assises ciselées, bandeau en tuf, grilles de ventilation et lampes de plafond. Il reste enterré seulement lorsque le sol est certifié. |
| Piscine extérieure | Bassin et marches immergées conservés, margelles de diorite, chaises longues et auvent de bouleau près des bacs de douche. |
| Excavation | Escalier central vers la fosse, barrières, piles sculptées, poutre sur appuis, établi et meule ; le chantier reste limité à la roche certifiée. |
## Sous la ville et réseaux oubliés
| Ensemble | Traitement architectural |
| --- | --- |
| Grande piscine couverte | Bassin, escaliers et colonnade conservés ; parois de calcite, bandeaux de diorite, chapiteaux ciselés et nervures hautes répétées. |
| Parkings souterrains | Plateformes et rampes conservées ; fondations maçonnées et bandeaux de tuf ponctués de motifs ciselés. |
| Grande salle sèche | Boiseries de chêne, assises de tuf, murs clairs, corniches et bancs de chêne ; le socle central reste sans machine de progression. |
| Égouts et sorties deau | Briques de tuf, motifs ciselés et corniches hautes ; les canaux, passerelles et écoulements existants sont conservés. |
| Passages et accès | Liserés de granit ou de tuf, seuils et accès verticaux conservés. |
| Mines, ateliers et réserves oubliés | Poutres de chêne, tuf, sols bordés de granit, établi et outillage vanilla ; rails, réserves et plateforme de travaux interrompus. |
| Archives et salles étranges | Parquets de bouleau, boiseries, rayonnages ; ailleurs, carrelages de calcite et diorite, colonnes et niches répétées. |
| Salles techniques oubliées | Nervures minérales, corniches et encadrements ciselés autour des seuils ; pompes figurées, grilles et postes de commande vanilla. |
## Matériaux et raccords
Les fondations des nouveaux bâtiments et chemins sont en **briques de pierre**.
La stone et la smooth stone restent présentes ; landésite, la diorite et le
granit différencient les sols et les usages. Le tuf existe brut, poli, en
briques et sous ses deux formes ciselées. Les dalles, escaliers inversés,
murets, grilles et trappes donnent de l’épaisseur aux corniches, aux appuis de
fenêtre et aux équipements. Le chêne et le bouleau apparaissent comme poteaux,
boiseries, parquets, charpentes et couvertures selon les lieux.
Le rendu 20 calcule les raccords des vitres, barreaux, clôtures, murets et
angles descaliers à partir de tous les blocs de la pièce, après sa rotation
et avant son découpage entre chunks. Une fenêtre peut ainsi rejoindre son
véritable encadrement, même lorsque le chargement découpe le bâtiment.
Les smokes des bâtiments de ville et des espaces extérieurs passent, notamment
pour les accès, les sanitaires, les lits, les lampes, les rampes et les bassins.
Les essais natifs de création et de réouverture de la ville et des infrastructures
passent ; leurs résultats et les contrôles de compatibilité sont suivis dans [les essais alpha.20](testing-alpha20.md).
Aucune caméra, mailbox, machine dexpansion ou production électrique dédiée
nest ajoutée par cette passe. Les meubles utilisables restent les blocs
vanilla, et les butins de découverte restent partagés et non renouvelés.
+24
View File
@@ -0,0 +1,24 @@
# Alpha.21 — palettes et couvertures
La ville revient à une base commune : pierre lisse, pierre, briques de pierre,
andésite et chêne. Les façades, seuils, corniches et verrières gardent leurs
formes ; les pièces conservent leurs fonctions et leur circulation. La serre
emploie le bouleau comme bois propre. Le tuf accompagne les routes et les
parties souterraines.
![Modèles des bâtiments alpha.21](images/architecture-alpha21-overview.png)
Ces vues sont calculées depuis les blocs produits par les renderers21 et les
modèles/textures de Minecraft 26.3-pre-2. Elles montrent des variantes choisies,
sans terrain, éclairage ni végétation de la partie. Ce ne sont pas des captures
du client et elles ne prouvent pas les raccords avec le relief.
Les couvertures de latelier, du dépôt, de la serre, du relais radio et du refuge
reposent sur des murs ou charpentes. Lancien plafond opaque sous leur nouvelle
couverture est retiré. Les vérifications contrôlent aussi les petits appuis des
excavations, piscines, rampes de garage et ornements.
![Coupe des couvertures](images/architecture-alpha21-roofs.png)
La seconde planche coupe volontairement les murs pour voir les dessous des toits.
Les détails de validation et la graine dessai sont dans [le protocole](testing-alpha21.md).
+94
View File
@@ -717,6 +717,100 @@ Aucune structure, règle de butin, version binaire ou instance Prism modifiée.
La sélection des lieux et la réalisation des axes feront lobjet du chantier
suivant ; ce catalogue nest pas une liste dajouts tous validés.
### WG-22 — Anciennes infrastructures et liaisons — alpha.19
**Branche :** `codex/heritage-worldgen-alpha19`.
**Demande validée :** première passe de toutes les nouvelles structures du
catalogue WG-21 et de leurs liaisons avant la bêta, en conservant la possibilité
de revenir à une génération sans ce chantier.
- Ajouter les quatorze lieux avec architectures, entrées, pièces, mobilier et
petits butins vanilla associés à leur usage. Présence selon le terrain ;
relever les lieux réellement générés et donner les coordonnées des témoins.
- Planifier des voies de fret et chemins entre destinations réelles, avec
sections abandonnées lisibles, appuis et passages vérifiés. Rechercher les
raccords aux anciennes installations dans un budget borné.
- Garder une majorité naturelle à l’île ; conserver le terrain, lhydrologie,
la ville et les réseaux existants comme base. Première île uniquement.
- Nouveau codec 19, option expérimentale sauvegardée à la création du monde.
Option désactivée : socle urbain seul. Les mondes 17 et antérieurs gardent
leur génération, leurs pièces et leurs futurs chunks. Aucune conversion,
suppression de structures ou rétrogradation dune sauvegarde 19.
- Les bâtiments préparent les futurs services ; mailbox, deposit boxes,
caméra, réacteur, progression et récompenses personnelles restent pour la
bêta. Les coffres de cette passe sont partagés et utilisent du butin vanilla.
- Vérifier créations/réouvertures, option désactivée, anciens mondes, supports,
circulations et vrais placements. `check build assemblePack`, artefacts,
publication packwiz et mise à jour de la même instance Prism.
**État : livré en alpha.19 ; code, artefacts et synchronisation Prism vérifiés.** Voir
[les essais natifs et la carte des lieux](testing-alpha19.md). Version 19 demandée
explicitement ; aucune version 18 publiée
dans ce dépôt à louverture du chantier. Le catalogue est maintenant accepté
pour une première passe, sans imposer sa présence exhaustive sur chaque graine.
### WG-23 — Identité architecturale et blocs détaillés — alpha.20
**Branche :** `codex/architecture-alpha20`.
**Demande :** donner aux bâtiments une identité contemporaine lisible selon
leur fonction, corriger les vitres isolées et détailler formes, façades et
circulations. Employer du tuf, ses briques et variantes ciselées, du chêne,
du bouleau, du granit, de la diorite et de landésite ; conserver les fondations
en briques de pierre. Ajouter des blocs sculptés et du relief architectural.
- Reprendre les quatorze lieux expérimentaux, les bâtiments de Lost City,
leurs espaces extérieurs et leurs sous-sols ; palettes par usage, volumes
en retrait, biseaux, baies, corniches, liserés et mobilier adapté.
- Résoudre les connexions des vitres, grilles, clôtures et murs ainsi que les
angles descaliers depuis la géométrie complète, avant le découpage par chunk.
- Dessiner les bordures de routes avec les briques de tuf et le tuf ciselé,
en conservant la circulation et les tracés sauvegardés.
- Garder les plans de placement et les emprises certifiées ; génération 20
et types de pièces distincts. Les mondes 19 et antérieurs gardent leurs
rendus, y compris dans les futurs chunks. Aucune rénovation rétroactive.
- Garder le bouton natif Personnaliser, les trois tailles et loption des
structures expérimentales pour les nouveaux mondes.
- Vérifier les états réels des blocs, les accès, les supports, les coffres,
la création/réouverture et la compatibilité avec la version 19 publiée.
Livrer le pack 20 par le canal stable et synchroniser la même instance Prism.
**État : livré en alpha.20 ; code, artefacts et même instance Prism vérifiés.** Voir
[le contrat et les essais alpha.20](testing-alpha20.md).
### WG-24 — Palettes sobres, toits portés et galeries — alpha.21
**Branche :** `codex/palettes-reseaux-alpha21`.
Reprendre les extérieurs sans changer les plans intérieurs approuvés : smooth
stone, stone brick, stone, andésite et chêne dominants en ville ; un bois cohérent
par bâtiment unique et tuf surtout au sol et sous terre. Corriger les toits
suspendus et les anciens plafonds superposés, avec appuis vérifiables.
Étendre la ville par au plus deux petits quartiers séparés de terrain naturel,
reliés par des chemins praticables ; rapprocher les destinations patrimoniales
par un graphe de chemins. Ajouter des galeries industrielles abandonnées :
chaussée de cinq blocs, deux trottoirs, quelques grandes salles et deux accès
par escaliers depuis la surface, en respectant les eaux et structures existantes.
Ajouter des fronts rocheux et de petits filons intacts dans les galeries et
leurs salles de maintenance : ressources vanilla réellement minables, sans
régénération de butin ni remplacement des blocs déjà extraits. Fournir un MRpack
pour lessai séparé demandé.
Assouplir les refus de placement : admettre les pentes, des volumes partiellement
enfouis et des appuis adaptés ; rechercher des détours et prioriser les lieux
isolés pour les chemins. Garder une ville fragmentée sans socle géant.
Nouvelle génération21 réservée aux nouveaux mondes ; tous les codecs et rendus
20 et antérieurs restent disponibles. Tailles512/724/1024, Moyen par défaut ;
option expérimentale enregistrée. Aucun monde personnel modifié. Vérifier les
pièces natives, accès, connexions, supports, création/réouverture, ancien monde20,
puis livrer le pack stable21 et synchroniser la même instance Prism.
**État : implémenté et vérifié ; MRpack fourni, publication et synchronisation en cours.**
## Réserve de thèmes futurs
Ces thèmes servent à retrouver la vision, pas à demander leur implémentation immédiate. On en extrait un ticket seulement lorsqu'il devient utile au prochain incrément jouable.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

+91 -1
View File
@@ -13,10 +13,100 @@ JAR Sanctuary sont des pièces jointes de releases Gitea. Aucun JAR de mod ou
dépendance téléchargée n'entre dans l'historique Git. Le canal Beta est distinct
de l'ancien pack 26.2.
## Alpha.20 publiée — 10 septembre 2026
[Sanctuary 0.1.0-alpha.20](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.20)
est publiée depuis le commit source `0edac29`. Le canal stable utilise cette
version au commit `4e0967d`. Le JAR, le
[MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.20/Sanctuary-0.1.0-alpha.20.mrpack)
et le ZIP damorçage Prism ont été vérifiés par téléchargement public.
SHA-256 du JAR :
```text
9ea10576e641d83c13e1277960a375448a22d416a98a6b327c4d6d2a204c423a
```
Passe de détails sur six bâtiments de ville, quatorze infrastructures,
espaces publics et réseaux souterrains : palettes propres aux usages,
boiseries, tuf ciselé, corniches, biseaux, verrières et raccords des vitres.
Voir le [catalogue illustré](architecture-alpha20.md).
`check build assemblePack` passe, avec huit GameTests généraux. Les créations
et réouvertures natives conservent les structures, accès, supports et coffres.
Le bassin et les sorties d’égout restent fonctionnels. Les rapports de la
version19 restent identiques à ceux de la release publiée. Le mode désactivé
et une petite île passent ; les quatorze infrastructures sont rencontrées sur
les deux témoins. Les limites des raccords au terrain sont détaillées dans
[les preuves alpha.20](testing-alpha20.md).
Deux synchronisations isolées puis deux dans la même instance Prism Mac
réussissent. Un seul JAR Sanctuary alpha.20 est actif ; les **925 fichiers
personnels et réglages suivis** conservent leurs hashes. Le dossier reste
`Sanctuary-0.1.0-alpha.1`, affiché « Sanctuary Beta ». Le JAR19 et le suivi
packwiz sont sauvegardés dans `sanctuary-backups/before-0.1.0-alpha.20/`.
Reçus locaux : `build/alpha20-isolated-validation.json` et
`build/alpha20-prism-validation.json`.
Pour voir cette architecture, créer un **nouveau monde Sanctuary, Moyen,
graine 0**, avec **Structures expérimentales** activées dans **Personnaliser**.
`/sanctuary structures` liste les entrées. Petit, graine42, possède la
cartothèque à `-101 251 -25`. Loption désactivée dun autre nouveau monde
conserve la ville sans les réseaux expérimentaux. Les anciens mondes gardent
leur génération ; aucune sauvegarde personnelle na été ouverte, convertie ou
régénérée. Une sauvegarde20 ne se rétrograde pas vers le binaire19.
Les vues du catalogue viennent des modèles et ne sont pas des captures de
partie. Aucun essai client visuel ni essai Windows natif na été effectué.
Les nouvelles fonctions de la bêta restent à construire.
## Alpha.19 publiée — 9 septembre 2026
[Sanctuary 0.1.0-alpha.19](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.19)
est publiée depuis le commit source `c23e72b`. Le canal stable utilisait alors
cette version, au commit `ea883b7`. JAR, [MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.19/Sanctuary-0.1.0-alpha.19.mrpack)
et ZIP damorçage Prism ont été vérifiés par téléchargement public.
SHA-256 du JAR :
```text
a159b00a2778020800e81381e0608495a4d7909f1f853f0b8812cd4017865b0b
```
Première passe de quatorze architectures et de leurs voies de fret ou chemins
abandonnés sur l’île initiale. Les trois tailles sont vérifiées nativement ;
les quatorze types ont chacun été rencontrés et parcourus sur ces témoins.
Créer un **nouveau monde Sanctuary, Moyen, graine 0**, puis garder
**Structures expérimentales** activées dans **Personnaliser** : ce témoin
contient treize lieux et sept liaisons, dont un fret de 499 blocs.
`/sanctuary structures` relève les entrées. La présence des bâtiments et les
raccords dépendent du relief ; voir [les preuves et la carte](testing-alpha19.md).
`check build assemblePack` et les huit GameTests généraux passent.
Création et réouverture natives conservent 112 pièces et 13 coffres ; 68 cibles
sont accessibles. Le mode désactivé passe. Les empreintes de la génération 17
restent identiques aux preuves publiées, y compris après réouverture.
Deux synchronisations isolées puis deux dans la même instance Prism Mac
réussissent. Un seul JAR Sanctuary alpha.19 est actif ; les **767 fichiers
personnels et réglages suivis** conservent leurs hashes. Aucune sauvegarde
personnelle na été ouverte, convertie ou régénérée. Le dossier reste
`Sanctuary-0.1.0-alpha.1`, affiché « Sanctuary Beta ».
Le JAR 17 et le suivi packwiz sont sauvegardés dans
`sanctuary-backups/before-0.1.0-alpha.19/`. Reçus locaux ignorés :
`build/alpha19-isolated-validation.json` et `build/alpha19-prism-validation.json`.
Pour comparer, créer un autre nouveau monde avec loption désactivée : le socle
urbain demeure, sans les expériences 17/19. Cette option ne supprime pas de
structures dans une sauvegarde existante. Les anciens mondes conservent leur
génération ; une sauvegarde 19 ne doit pas être ouverte avec lancien binaire 17.
Les bâtiments préparent les futurs usages, mais mailbox, caméra, réacteur et
progression restent pour la bêta. Aucun essai visuel dans le client ni essai
Windows natif na été effectué.
## Alpha.17 publiée — 9 septembre 2026
[Sanctuary 0.1.0-alpha.17](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.17)
est publiée depuis le commit source `0969ed5`. Le canal stable utilise cette
est publiée depuis le commit source `0969ed5`. Le canal stable utilisait alors cette
version, au commit `6e0c297`. JAR, [MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.17/Sanctuary-0.1.0-alpha.17.mrpack)
et ZIP damorçage Prism ont été vérifiés par téléchargement public.
SHA-256 du JAR :
+173
View File
@@ -0,0 +1,173 @@
# Alpha.19 — anciennes infrastructures de Sanctuary
**État : alpha.19 publiée, même instance Prism synchronisée et vérifiée.** Ticket WG-22, branche
`codex/heritage-worldgen-alpha19`. Minecraft 26.3-pre-2, mêmes dépendances que
lalpha.17. La numérotation 19 est demandée explicitement ; aucune 18 nest
intercalée dans ce dépôt.
## Périmètre et réversibilité
Première passe des quatorze lieux du [catalogue](structures-audit.md) : gare de
marchandises, centre postal, dépôt communal, cartothèque, observatoire,
maintenance ferroviaire, pompage, sous-station, serre, quai dexpédition,
relais radio, club photo, vestiaires/salles étranges et refuge de chantier.
Les plans recherchent des sites admissibles et des voies de fret ou chemins
entre destinations réelles ; ils ne garantissent pas tous les lieux sur chaque
graine. Les mesures et coordonnées des essais figurent ci-dessous.
Créer un **nouveau monde Sanctuary** et utiliser **Personnaliser** :
Petit 512, Moyen 724 par défaut ou Grand 1 024, avec **Structures expérimentales**.
Loption est activée par défaut et enregistrée avec le générateur 19. La désactiver
pour un autre nouveau monde conserve le socle urbain sans les réseaux
expérimentaux 17 et les infrastructures 19. Elle ne retire aucun bloc ni ne change
les règles dune sauvegarde existante.
Les mondes 17 et antérieurs gardent leurs codecs, plans, pièces et futurs chunks.
Le preset 17 public est conservé octet par octet sous un identifiant historique
caché à la création. Le générateur 19 et son journal sont distincts. Il nexiste
aucune conversion ou rétrogradation de sauvegarde 19 vers un ancien binaire.
Lancienne release reste disponible pour ses propres mondes, et loption dun
nouveau monde permet de comparer les deux configurations du nouveau binaire.
Les nouveaux lieux sont réservés à l’île initiale. La ville et les anciens
réseaux servent de base ; leurs protections et celles de lhydrologie restent
respectées. Les liaisons vers de futurs continents seront construites après
leur apparition, sans présumer leurs emplacements dans cette génération.
## Objets et systèmes
Les bâtiments suggèrent leurs usages futurs par leurs pièces et aménagements.
Mailbox, deposit boxes, caméra, réacteur, progression et récompenses
personnelles ne sont pas activés. Chaque nouveau lieu dispose dune table
de coffre propre à la génération 19, avec un outil vanilla lié à son usage et
quelques fournitures. Les coffres sont partagés et ne se renouvellent pas.
La valeur des rails, équipements et blocs récupérables du décor sajoute au
butin. Leur distribution doit être constatée sur les témoins, sans transformer
cette première passe en une garantie d’équilibrage économique pour la bêta.
## Vérifications effectuées
- Placement des quatorze architectures, ouvertures, circulation, orientation
et appuis du mobilier ; accès réel au coffre et retour possible.
- Plans bornés sur les trois tailles, terrain non occupé, supports, continuité
des tracés et distinction entre ballast praticable et rails retirés.
- Pièces natives et références locales, y compris pour les longues voies ;
géométrie, orientation et rails sauvegardés dans un schéma strict.
- Création et réouverture dun monde 19 avec modification témoin ; conservation
des starts, butins différés, accès, routes et supports.
- Option désactivée, paramètres sauvegardés, génération 17 comparée aux preuves
de la version publiée, refus des migrations implicites de journaux.
- `./gradlew check build assemblePack`, artefacts et index, publication immuable
puis canal packwiz et deux synchronisations de la même instance Prism.
Les smokes des quatorze architectures, de leurs quatre orientations, des
plans sur les trois tailles et des pièces natives passent. Le mode désactivé
est vérifié dans un monde 19 neuf : aucun site ni liaison expérimental.
`./gradlew check build assemblePack` passe, avec les huit GameTests généraux,
125 chunks FULL, 5 923 blocs deau conservés et 1 800 ticks de fluides.
Le JAR, le MRpack, les hashes du pack et le ZIP Prism sont vérifiés.
La release et le canal packwiz sont publiés. Deux synchronisations isolées puis
deux dans la même instance Prism passent ; 767 fichiers personnels et réglages
suivis conservent leurs hashes. Le JAR précédent et le suivi packwiz sont
sauvegardés dans `sanctuary-backups/before-0.1.0-alpha.19/`.
Voir [le reçu de distribution](packwiz.md#alpha19-publiée--9-septembre-2026).
Aucun essai visuel dans le client ni essai Windows natif na été effectué.
## Témoin natif Moyen, graine 0
Créer un nouveau monde avec **Structures expérimentales activées**.
`/sanctuary structures` liste les entrées prévues sans charger de chunks ;
`/locate structure sanctuary:heritage_networks` retrouve lentrée dun site.
Le relevé ci-dessous provient des placements et parcours vérifiés par le moteur.
![Entrées et tracés du témoin alpha.19](images/heritage19-seed0.svg)
Treize des quatorze lieux apparaissent. **La cartothèque est absente de cette
graine** : la recherche na pas trouvé demplacement admissible. La ville et les
trois réseaux souterrains 17 sont conservés. Les raccords recherchés vers la
ville et lancienne mine ne sont pas admis sur ce témoin.
Les sept liaisons comprennent un tracé de fret **dépôt → pompage de 499 blocs**,
dune étendue de 245 blocs sur X et 181 sur Z. Son ballast est praticable ;
478 rails sont présents et 21 sont volontairement manquants. Les autres voies
et chemins relient gare, dépôt, atelier, poste, club photo, serre et vestiaires.
Le quai, lobservatoire, le relais, la sous-station et le refuge gardent leurs
propres accès au terrain, même sans liaison avec ce réseau.
La création et la réouverture dans deux JVM distinctes passent chacune les deux
GameTests : **112 pièces natives, 111 chunks FULL, 80 487 blocs contrôlés,
2 882 contrôles de portance, 13 coffres différés et 68 cibles accessibles**.
Trois tirages de chacune des quatorze tables de butin sont vérifiés
séparément, sans ouvrir les coffres persistants. Une modification témoin à
`-204 / 255 / -13` est conservée à la réouverture.
Empreinte stable du plan :
`7f476c2ecca703a05bf88a550c61cc5cbeff2594024c43ccd9f3620bbc2ad3de`.
JVM de création `36366`, de réouverture `36481`. Reçus locaux ignorés :
`build/alpha19-evidence/heritage19-create.json` et `heritage19-reload.json`.
La planification supplémentaire utilise 15 144 colonnes natives, dans un plafond
de 16 000, en 20,7 s sur le Mac de développement. Ce temps concerne ce plan,
pas le chargement total du jeu. Les routes sont découpées par chunk ; aucun
start ne couvre toute leur longueur ni ne charge une destination lointaine.
La prospection 19 consulte les plans hydrologiques de toute lenveloppe de
départ, avant de publier les sites. Le contrôle général utilise donc pour 19
une limite géographique calculée : 5 régions possibles pour Petit, 9 pour
Moyen/Grand, avec les budgets de colonnes et de sondes contrôlés séparément.
Le témoin Moyen en consulte 5 ; les quatre régions périphériques représentent
3,38 s de calcul hydrologique supplémentaire dans le relevé du build.
Ces consultations ne matérialisent aucun chunk. Les générations historiques
conservent le contrôle initial de 4 régions maximum autour du spawn.
```sh
./gradlew :sanctuary:runGameTest -PsanctuaryHeritage19Tests=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false
./gradlew :sanctuary:runGameTest -PsanctuaryHeritage19Tests=true -PsanctuaryExpansionReload=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false
```
## Autres tailles et couverture du catalogue
Les deux GameTests natifs passent aussi sur Petit et Grand, graine 42.
Les quatorze architectures ont ainsi chacune été rencontrées et parcourues
sur le terrain réel, en complément des rotations vérifiées par les smokes.
| Taille et graine | Lieux | Liaisons | Pièces natives | Chunks FULL |
| --- | ---: | ---: | ---: | ---: |
| Petit 512, 42 | 10 | 2 | 35 | 44 |
| Moyen 724, 0 | 13 | 7 | 112 | 111 |
| Grand 1 024, 42 | 13 | 4 | 47 | 61 |
Pour visiter la cartothèque : **Petit, graine 42**, entrée
`-101 / 251 / -25`, ou **Grand, graine 42**, entrée `-252 / 249 / 146`.
Le Petit testé na ni poste, ni observatoire, ni atelier ferroviaire, ni quai.
Le Grand testé na pas de quai. Aucun grand axe nest admis sur ces deux témoins :
les portées maximales de leurs liaisons sont respectivement de 32 et 55 blocs.
Le témoin Moyen, graine 0, reste celui conseillé pour découvrir le grand fret.
Ces résultats rendent explicite la variation de cette première passe ; ils ne
constituent pas une garantie de présence ou de connectivité pour chaque graine.
Reçus locaux ignorés : `build/alpha19-evidence/small42-create.json` (JVM 36591)
et `large42-create.json` (JVM 36669).
## Compatibilité 17 vérifiée
Deux JVM de développement distinctes créent puis rouvrent le témoin 17
**Moyen, graine 0**, avec le nouveau binaire. Les deux GameTests passent dans
chaque exécution. Le codec, les starts natifs, les butins différés et les
parcours restent identiques aux preuves de lalpha.17 publiée. Le bloc dor
placé par le test est conservé à la réouverture.
Empreinte du plan 17 :
`7358ecd7841c03be1977a7f45f37c0c3ef83a32dc27e8d3ec4f0d85d6ee5d3ef`.
Les trois starts restent ceux documentés dans [le témoin 17](testing-alpha17.md).
Reçus locaux ignorés : `build/alpha19-evidence/legacy17-create.json` et
`legacy17-reload.json`.
```sh
./gradlew :sanctuary:runGameTest -PsanctuaryRuins17Tests=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false
./gradlew :sanctuary:runGameTest -PsanctuaryRuins17Tests=true -PsanctuaryExpansionReload=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false
```
Ces commandes visent uniquement le monde de développement ignoré. La première
le recrée ; la seconde conserve ce même monde et lit ses preuves précédentes.
+327
View File
@@ -0,0 +1,327 @@
# Alpha.20 — identité architecturale
**État : alpha.20 publiée, même instance Prism synchronisée et vérifiée.**
Ticket WG-23, branche `codex/architecture-alpha20`.
Minecraft 26.3-pre-2, dépendances identiques à la version 19 publiée.
## Intention
Chaque fonction reçoit une palette et une composition propre : bois clairs et
grandes baies des lieux publics, pierre et tuf des installations techniques,
loggias et retraits des logements. Les détails utilisent les blocs vanilla :
tuf ciselé, briques de tuf ciselées, briques de pierre sculptées, bois et
planches de chêne/bouleau, granit, diorite et andésite. Moulures, liserés,
encadrements, murs bas et corniches utilisent escaliers, dalles et trappes.
Les fondations restent en briques de pierre. Le [catalogue architectural](architecture-alpha20.md)
décrit les formes et les matériaux effectivement construits pour chaque usage.
La passe concerne les quatorze infrastructures, la ville et ses sous-sols,
ainsi que les petits réseaux oubliés. Elle ne déplace pas les sites ni les
routes pour obtenir ses silhouettes. Les plans 19/17/16 servent demprises
certifiées aux nouveaux rendus 20.
## Connexions des blocs
La géométrie complète dune pièce est assemblée avant le découpage par chunk.
Les états des vitres, grilles, clôtures et murets sont calculés à partir des voisins
réellement présents dans ce plan ; les escaliers reçoivent leurs angles.
Cela évite d’écrire des vitres sans raccord malgré leur juxtaposition, ou de
dépendre de lordre de chargement des chunks voisins. Aucun chunk extérieur
nest chargé pour compléter ces états.
## Sauvegardes et réversibilité
Créer un **nouveau monde Sanctuary** pour voir la génération 20. Le bouton
natif **Personnaliser** garde Petit 512, Moyen 724 et Grand 1 024.
**Structures expérimentales** reste activé par défaut, enregistré avec le
monde ; le désactiver dans un autre nouveau monde garde la ville sans les
réseaux expérimentaux. Les détails urbains de la version 20 restent présents.
Le nouveau codec `sanctuary:island_v20`, son journal et ses types de pièces
distinguent les nouveaux rendus des précédents. Les mondes 19 et antérieurs
gardent leurs codecs, leurs pièces et leurs futurs chunks. Aucun monde
personnel nest ouvert ni rénové par la mise à jour. Une sauvegarde 20 ne se
rétrograde pas vers un ancien binaire ; la release 19 reste disponible pour
ses propres mondes.
## Vérification avant livraison
- Parcours des entrées, pièces et coffres, quatre orientations, marches,
supports et limites d’écriture pour chaque type.
- Connexions des vitres et grilles, angles descaliers, comparaison du rendu
complet et du rendu découpé selon plusieurs ordres de chunks.
- Création et réouverture natives, option désactivée, ancien témoin 19.
- `check build assemblePack`, intégrité des artefacts, publication immuable,
deux synchronisations isolées puis deux dans la même instance Prism.
## Reçus natifs : infrastructures et réseaux oubliés
**Création et réouverture réussies**, dans deux JVM distinctes, pour un monde
jetable **Moyen 724, graine 0**, structures et structures expérimentales activées,
codec `sanctuary:island_v20`. Les rapports locaux conservés sont
`build/alpha20-evidence/heritage20-create.json` (processus 39381) et
`build/alpha20-evidence/heritage20-reload.json` (processus 39736).
Les journaux correspondants sont `build/alpha20-heritage-create-2.log` et
`build/alpha20-heritage-reload.log`. Ces fichiers de développement restent
ignorés par Git ; les valeurs utiles sont transcrites ci-dessous.
| Vérification | Création | Réouverture |
| --- | --- | --- |
| Sites expérimentaux naturels | 13 types présents sur 14 | Mêmes 13 sites |
| Structures natives des sites et voies | 85 starts, 112 pièces | Mêmes empreintes NBT et références |
| Positions de blocs contrôlées pour sites et voies | 80 487 | 80 487, hors modification témoin autorisée |
| Chunks `FULL` contrôlés pour sites, voies et réseaux oubliés | 120 | 120 |
| Contrôles de roche porteuse | 2 882 | 2 882 |
| Coffres des sites | 13 butins différés, non ouverts | Tables et graines conservées |
| Réseaux oubliés | 3 réseaux, 15 pièces, 7 639 positions contrôlées | Mêmes plans et structures natives |
| Coffres des réseaux oubliés | 3 butins différés, non ouverts | Tables et graines conservées |
| Parcours depuis les entrées | 68 cibles dans les sites et 49 dans les réseaux oubliés | Parcours revérifiés |
| Modification témoin | Bloc dor placé en `-203 255 -13` | Bloc dor conservé |
La cartothèque dexpédition (`EXPEDITION_OFFICE`) na pas trouvé demplacement
sur cette graine. Elle fait partie du catalogue et de ses smokes, mais ce reçu
ne constitue pas une preuve de sa génération naturelle. Les quatorze tables
de butin sont échantillonnées séparément sans consommer les coffres sauvegardés.
Les portes et trappes ouvertes pendant le parcours retrouvent leur état initial.
Aucune expansion ni sauvegarde personnelle nest ouverte dans ces essais.
Le plan des infrastructures conserve lempreinte
`7f476c2ecca703a05bf88a550c61cc5cbeff2594024c43ccd9f3620bbc2ad3de`
entre création et réouverture. Celui des réseaux oubliés conserve
`7358ecd7841c03be1977a7f45f37c0c3ef83a32dc27e8d3ec4f0d85d6ee5d3ef`.
Les pièces passent aussi un aller-retour NBT exact et refusent un numéro de
schéma inconnu.
### Portée des vérifications de connexion
Les états complets de **594 blocs détaillés** des infrastructures sont comparés
au rendu : 362 vitres ou barreaux, 49 clôtures, 18 murets et 165 escaliers.
Sur ce total, **565** sont aussi confrontés aux règles natives de mise à jour
des voisins (`updateShape`), avec les tags du serveur chargés. **Onze raccords
de vitres ou barreaux traversent une frontière de chunk** et restent connectés.
Les **29 autres blocs détaillés** ont un voisinage extérieur au plan : leur
état sauvegardé est bien comparé au rendu, mais ils sont exclus de cet oracle
natif. Le compteur `externalNeighbors=29` désigne ces 29 blocs contrôlés,
non un nombre de voisins uniques ; aucune stabilité complète vis-à-vis de ce
voisinage extérieur nest revendiquée. Les réseaux oubliés passent pour leur
part **211 comparaisons d’états et 211 vérifications natives** : 6 vitres ou
barreaux, 1 clôture et 204 escaliers. Ces résultats sont identiques à la
réouverture.
### Coordonnées du monde témoin
Créer un nouveau monde **Moyen, graine 0**, avec les deux options de structures
activées. Les coordonnées indiquent les pieds à lentrée, pas le centre dun
toit. `/sanctuary structures` liste les lieux ;
`/locate structure sanctuary:heritage_networks`, recherché depuis `0 250 0`
dans le test, désigne lentrée du refuge de chantier.
| Lieu | Entrée X Y Z |
| --- | --- |
| Gare de fret | `-110 252 -144` |
| Dépôt communal | `-75 254 -184` |
| Atelier ferroviaire | `-83 253 -83` |
| Château deau | `164 252 -3` |
| Serre | `173 252 -42` |
| Observatoire | `51 251 210` |
| Quai sur le vide | `184 181 218` |
| Refuge de chantier | `-10 251 -15` |
| Réseau minier oublié | `78 252 179` |
| Salles étranges oubliées | `-194 254 -22` |
| Réseau technique oublié | `116 253 -157` |
Sept liaisons sont parcourues dans le monde natif. La voie dépôtchâteau deau
compte **499 positions**, 478 rails présents et 21 rails volontairement absents,
avec une emprise de 245 blocs en X et 181 en Z. Le test prouve un parcours à pied,
le ballast et les formes de rails sauvegardées ; il ne prouve pas un trajet
ininterrompu en wagonnet sur ces voies abandonnées.
## Reçu natif : ville, expansion et grands sous-sols
**Création réussie ; réouverture encore en cours.** Le rapport
`build/alpha20-evidence/city20-create.json` (processus 40221), associé au journal
`build/alpha20-city-create.log`, couvre deux villes de génération 20 dans un
monde de développement jetable. L’île initiale est **Moyen 724, graine 0**.
Après trois prospections en lecture seule, une expansion de diamètre **256**
est effectivement préparée : `city16_0_2`, graine `9111000990819250332`, centre
`2000 -608`, climat `lost_city`. Les deux premiers prospects sont restés sans
ville et nont pas été activés. Lidentifiant de test historique ne change pas
la version 20 enregistrée dans cette expansion.
| Mesure | Ville initiale | Ville dexpansion |
| --- | --- | --- |
| Pièces natives | 48 | 23 |
| Chunks `FULL` inspectés autour des districts | 43 | 19 |
| Bâtiments parcourus | Immeuble, hôtel, bibliothèque | Immeuble, hall |
| Cibles intérieures et de toit atteintes | 65 | 46 |
| Entrées reliées par les rues et les porches seuls | 5 | 2 |
| Pièces de rue ou pont traversées | 33 | 17, dont un pont |
| Appuis contrôlés | 71 | 3 |
| Coffres gardant leur butin différé, non ouvert | 3 | 2 |
| Témoins de biome urbain | 38 | 19 |
Les **71 pièces** sont issues des plans naturels des deux îles. Le parcours
urbain ne prend aucun raccourci par le terrain naturel, les intérieurs ou les
toits : il confirme la continuité des entrées par les rues dessinées.
La table native dapparition des monstres est vérifiée comme limitée aux
zombies vanilla ; cela ne mesure pas un nombre de zombies effectivement apparus.
Le pont de lexpansion se traverse de `2000 226 -613` à `2000 224 -597`.
Aucune pile de viaduc nest présente dans ces deux districts.
Vingt-quatre pièces de test supplémentaires couvrent la sauvegarde des six
modèles de bâtiments dans quatre orientations. Ce sont des témoins de
sérialisation, pas vingt-quatre bâtiments naturels. Restaurant et caserne,
ainsi que la piscine extérieure, ne sont pas présents dans ces deux villes ;
leurs smokes ne constituent pas une preuve dapparition naturelle ici.
### Grands équipements du témoin initial
| Repère | Géométrie ou coordonnées constatées |
| --- | --- |
| Entrée de limmeuble | `-30 252 21` |
| Entrée de lhôtel | `-8 252 21` |
| Entrée de la bibliothèque | `-52 252 43` |
| Trappe daccès aux sous-sols, position du bloc | `-19 252 41` |
| Grande piscine, emprise du bâtiment | De `-47 224 -26` à `9 236 4` |
| Volume deau du bassin | 50 × 25 blocs, quatre couches ; de `-44 225 -23` à `5 228 1`, avec les marches dans cette emprise |
| Grande salle sèche, emprise | De `-54 228 8` à `-30 237 32`, niveau dentrée 229 |
| Parking, emprise | De `-8 116 8` à `22 237 32` |
| Huit planchers du parking, niveaux Y | `117, 133, 149, 165, 181, 197, 213, 229` |
| Bouche de sortie de l’égout | `-50 117 20` |
La piscine conserve ses 40 appuis, la grande salle 16, le parking 9 et la
sortie d’égout 6. Ces présences sont constatées sur cette graine ; elles ne
sont pas garanties sur toutes les îles.
### Fluides et connexions en création
Lessai observe **120 ticks du monde** après la mise en activité des chunks.
Dix-neuf petits témoins temporaires fermés, placés dans le ciel, démontrent
lexécution réelle des fluides par une descente deau observée dans chaque
chunk concerné. Ils sont retirés avant la sauvegarde et exclus des quantités
deau des structures.
Après cette simulation, le grand bassin garde **4 976 cellules deau** dans
son enceinte, et chaque égout conserve ses cinq cellules contrôlées. Les deux
bouches gardent leur source et produisent de leau à lextérieur. Leur écoulement
rencontre cependant du terrain : premier bloc solide sous les sorties aux
niveaux **113** et **191**. Ce témoin prouve des sorties deau actives, pas une
chute libre continue jusquau vide.
Les chiffres de connexion suivants sont ceux de linspection initiale des
pièces, avant la simulation des fluides. Le contrôle de réouverture ci-dessous complète ces premières mesures.
| États détaillés | Ville initiale | Expansion | Total |
| --- | --- | --- | --- |
| Vitres ou barreaux | 1 457 | 577 | 2 034 |
| Clôtures | 36 | 14 | 50 |
| Escaliers | 1 451 | 699 | 2 150 |
| Comparaisons complètes au rendu | 2 944 | 1 290 | 4 234 |
| Vérifications supplémentaires par règles natives | 2 277 | 1 276 | 3 553 |
| Blocs exclus de cet oracle pour voisinage extérieur | 667 | 14 | 681 |
| Raccords de vitres/barreaux à travers des frontières de chunks | 82 | 22 | 104 |
Les **681 exclusions** gardent leur comparaison d’état au rendu. Elles ne
prouvent pas la stabilité de la connexion face au voisinage extérieur, comme
pour les infrastructures. Aucun muret nest représenté dans cette partie du
contrôle urbain. Le bloc dor témoin est placé en `-38 252 21` et le journal
est sauvegardé avec lempreinte
`e776dcab879458b3268ad34f44a003cb53b8b03cfcb67378559d40732f8ce73f` ;
ils sont conservés à la réouverture.
### Réouverture de la ville et raccords avec le terrain
La réouverture passe dans une seconde JVM (40687), avec **71 pièces natives**,
**5 coffres différés**, **74 appuis**, **111 cibles accessibles**, le bloc dor et
le journal conservés. Reçu : `build/alpha20-evidence/city20-reload.json` ; journal
`build/alpha20-city-reload-2.log`. La piscine et les sorties deau sont revérifiées.
Le test confronte les raccords au voisinage réellement présent. Au coin du
parking `-8 118 8`, les grilles touchent de la deepslate au nord et à louest :
Minecraft ajoute légitimement ces raccords après une mise à jour de blocs.
Le contrôle refuse tout changement de matériau, orientation, demi-bloc ou
waterlogging ; seules les propriétés de raccord peuvent suivre les règles
natives. À lintérieur dune géométrie connue, l’état du rendu reste exigé.
Sur les **4 234 blocs détaillés**, **4 178** sont alors des états stables selon
loracle Minecraft ; **640** de ces états ont un voisinage extérieur ou un
voisin ayant lui-même évolué. **56** autres gardent exactement leur état de
plan et attendent une mise à jour native extérieure : ils sont comptés
séparément, sans revendiquer leur stabilisation. Les **104 raccords entre
chunks** restent présents. `externalNeighbors=696` compte les blocs dont le
voisinage nest plus entièrement identique au plan, dont les 640 + 56 cas ;
ce compteur a donc une portée plus large que les 681 exclusions initiales.
## Compatibilité avec la version 19 publiée
Avec le binaire 20, les deux rapports natifs 19 sont **identiques intégralement**
aux reçus publiés, en retirant seulement le PID et le chemin du monde de test.
La comparaison porte sur plans, pièces, paramètres, parcours, supports, butins
et modification témoin, à la création comme à la réouverture.
- Création canonique SHA-256 : `3b055a3bc0fc2ddf4146aef035f5103bb7fe20d55a86fa1d4b87aeedc02f7879`.
- Réouverture canonique SHA-256 : `aed0962f8dfbfbbf69da1b2153861801c58a3ea2376e6bbec2ef44b90d555d63`.
- Référence source publiée : `c23e72b8e6fd8308855459f26c08082a68e81b61`.
- Reçus : `build/alpha20-evidence/legacy19-create.json` et `legacy19-reload.json`.
Les renderers, modèles et pièces historiques restent inchangés ; le preset 19
caché est identique octet par octet au preset public de la version publiée.
Les journaux refusent les chargements croisés 19/20 au lieu de convertir un monde.
## Option désactivée et petite île
Le scénario **Moyen, graine 0, option désactivée** passe : plan expérimental
vide, aucun start natif expérimental et aucune entrée localisable. Le paramètre
est enregistré. Reçu : `build/alpha20-evidence/experimental-off.json`.
Le témoin **Petit 512, graine 42, option activée** passe avec dix types de lieux,
35 pièces de sites/voies et 50 chunks `FULL` contrôlés, réseaux oubliés compris.
La cartothèque est présente, entrée **`-101 251 -25`**, et ses cinq cibles sont
accessibles. Les **436 états détaillés** des infrastructures passent loracle
natif, dont un avec voisin extérieur ; vingt raccords traversent des frontières
de chunks. Reçu : `build/alpha20-evidence/small42-create.json`.
Les deux témoins 20 réunis rencontrent et parcourent les **quatorze types
dinfrastructures**. Ils ne garantissent pas leur présence sur toutes les îles.
La génération native 20 Grand nest pas rejouée ici : ses réglages sont couverts
par les contrôles de codec et les plans de placement conservés. Les six modèles
urbains sont vérifiés dans les quatre orientations ; leur apparition naturelle
est distinguée des fixtures ci-dessus.
## Prévisualisation et état de livraison
Le [montage du catalogue](architecture-alpha20.md) provient des exports de
blocs des renderers et des modèles Minecraft. Il permet de comparer les
silhouettes, matières, escaliers, vitres et corniches. Il ne montre ni le
terrain final ni le rendu dun client avec ses shaders ; ce nest pas une
capture de partie et il ne remplace pas les essais de circulation ou de lumière.
| Étape | État de ce reçu |
| --- | --- |
| Infrastructures et petits réseaux : création/réouverture natives | Réussi, dans le périmètre détaillé ci-dessus |
| Ville 20, espaces publics et grands sous-sols : essais natifs | Création et réouverture réussies, 71 pièces et 5 coffres conservés |
| Compatibilité avec le témoin 19 publié | Rapports complets identiques à la création et à la réouverture |
| Autres configurations | Petit 512/graine 42 et option désactivée passent ; Grand 20 non rejoué nativement |
| `check build assemblePack` final et intégrité des artefacts | Réussi ; huit GameTests généraux, JAR/MRpack/ZIP et hashes vérifiés |
| Publication, canal packwiz et synchronisations Prism | Réussi ; deux passages isolés puis deux dans la même instance, 925 fichiers personnels et réglages préservés |
`./gradlew check build assemblePack --max-workers=1` passe en 4 min 39 s sur
la machine de développement : 62 tâches, dont 55 exécutées, huit GameTests,
125 chunks `FULL`, 5 923 blocs deau conservés et 1 800 ticks de fluides.
Les tests des sept nouveaux composants 20 et des anciennes générations passent.
Les réglages des trois tailles et leur sauvegarde passent également.
Journal : `build/alpha20-check-build-pack.log`.
Le JAR de production ne contient aucune classe de test. Son identité est
vérifiée dans le MRpack et le pack local ; les index et le bootstrap officiel
Prism sont contrôlés. SHA-256 du JAR :
`9ea10576e641d83c13e1277960a375448a22d416a98a6b327c4d6d2a204c423a`.
Reçu : `build/alpha20-artifact-validation.log`.
Release immuable depuis `0edac29`, canal packwiz `4e0967d` et deux
synchronisations isolées vérifiés. Les deux passages dans la même instance
Prism conservent les hashes de 925 fichiers personnels et réglages. Un seul
JAR20 est actif ; JAR19 et suivi packwiz sont sauvegardés avant remplacement.
Voir le [reçu de distribution](packwiz.md#alpha20-publiée--10-septembre-2026).
Aucun essai client visuel ni essai Windows natif na été effectué.
+138
View File
@@ -0,0 +1,138 @@
# Alpha.21 — contrat et vérification
Ticket WG-24, branche `codex/palettes-reseaux-alpha21`.
Minecraft 26.3-pre-2, Fabric Loader 0.19.5, Fabric API 0.160.0+26.3,
Java 25. Les dépendances restent celles de lalpha.20.
## Résultat recherché
La ville reprend une palette de pierre lisse, pierre, briques de pierre,
andésite et chêne. Les plans intérieurs sont conservés ; les corrections
portent sur les matériaux, les appuis et les couvertures. Chaque infrastructure
emploie un bois cohérent. Le tuf reste surtout au sol et dans les sous-sols.
La ville peut accueillir deux petits quartiers supplémentaires reliés à son
cœur, avec du terrain naturel entre eux. La réserve centrale existante nest
pas agrandie. Les infrastructures reçoivent davantage de chemins entre des
entrées réelles. Les recherches respectent des budgets de colonnes et refusent
un quartier si sa liaison entière ne peut pas être construite.
Les galeries industrielles expérimentales ont cinq blocs de chaussée et un
trottoir de chaque côté, des salles de maintenance et deux accès par escaliers.
Le plan est recherché après les structures précédentes et évite leurs volumes,
lhydrologie. Un tablier continu peut franchir une cavité entre ses appuis ;
les travées sans appuis complets sont limitées à 34 blocs, avec des piliers
locaux dau plus 16 blocs et deux couches de roche intacte sous les appuis. Chaque tronçon a une pièce native
locale ; aucun démarrage de structure ne charge toute l’île.
## Sauvegardes et comparaison
Créer un **nouveau monde Sanctuary**, taille **Moyen**, graine **0**, avec
**Structures expérimentales** activées. Petit 512, Moyen 724 et Grand 1024 restent
les tailles proposées par le bouton natif **Personnaliser**.
Le nouveau codec est `sanctuary:island_v21`. Les codecs et pièces 20 et
antérieurs restent disponibles, sans modifier leurs rendus. Le journal 21
réutilise le schéma de persistance existant avec une génération distincte ;
les ouvertures entre versions incompatibles sont refusées. Aucune conversion
ni rénovation automatique des mondes précédents nest effectuée.
Pour comparer sans les infrastructures, créer un autre monde avec loption
expérimentale désactivée. Une sauvegarde 21 nécessite le binaire 21 ou un futur
binaire compatible ; elle ne se rétrograde pas en remplaçant simplement le JAR.
## Compatibilité vérifiée
373 fichiers de production historiques sont identiques au commit de publication
20 (`0edac29`). Le preset caché `sanctuary_v20` est une copie exacte de son
ancien preset public. Création et réouverture natives de la génération 20 donnent
les mêmes plans, structures, appuis, accès et coffres que les rapports publiés.
La comparaison ignore seulement lidentifiant du processus, le chemin du monde
et deux nouveaux compteurs de détail ajoutés à loracle de connexion des tests.
## Galeries vérifiées dans Minecraft
Moyen, graine 0 : 23 modules dont deux accès et une salle, sur un axe de 340 blocs.
Les entrées sont `-135 253 -178` et `-135 249 162` ; chaussée à Y 216. La portée
sans appuis complets la plus longue du témoin est 17 blocs. Le plan emploie
8 686 colonnes de bruit, avec un budget maximal de 12 000 ; cette recherche prend
environ 11 secondes sur la machine de développement, hors reste du monde.
La création et la réouverture dans deux JVM distinctes passent : 34 chunks
FULL, 57 635 blocs vérifiés, mêmes pièces natives, mêmes coffres différés et
modification témoin conservée. Loracle de collision de Minecraft vérifie un
corps de 0,6×1,8 bloc, 74 cibles et 520 pas entre les deux portiques ; il utilise
les vrais escaliers et les demi-marches des trottoirs.
Une alcôve dexcavation inachevée ouvre le parement de la salle sur une paroi
irrégulière de tuf, pierre et deepslate. Certains tronçons exposent de petits
filons ordinaires. Le témoin contient cinq minerais de charbon, dix-neuf de
fer, cinq de redstone, un de diamant et un d’émeraude ajoutés dans ces fronts.
Cuivre, or et lapis font aussi partie des variantes possibles. Ce sont des
blocs finis, sans mécanisme de réapparition après minage. Ces quantités excluent
les minerais naturels de l’île et ne constituent pas une garantie par graine.
## Autres vérifications
La création et la réouverture natives de la ville passent. Le témoin possède
un cœur et deux quartiers reliés, 65 pièces de rue, trois immeubles de logements,
un hôtel, une bibliothèque, un jardin et une place. La réserve centrale nest
pas étendue. Les deux nouveaux chemins sont longs de 51 et 38 cellules. Une
expansion de test distincte est également vérifiée ; aucune expansion personnelle
nest activée.
La piscine couverte, le garage, la salle dexpansion, leurs accès et les égouts
restent présents. Les contrôles natifs vérifient les bassins et sorties après
exécution observée de la simulation des fluides.
Les quatorze types de lieux patrimoniaux apparaissent sur Moyen, graine 0,
avec douze liaisons et 127 segments. Leur création native passe : 141 pièces,
91 271 blocs, 140 chunks FULL et quatorze coffres différés. Les trois réseaux
oubliés sont également parcourus. Les chemins rejoignent deux entrées de ville.
Lobservatoire et le quai du vide restent isolés sur ce témoin : leurs chemins
nont pas trouvé de tracé admissible dans le budget. La commande `/sanctuary structures` indique leurs entrées ; aucun raccord
construit complet nest revendiqué.
Les smokes des renderers, plans, pièces enregistrées et raccords passent.
La réouverture des lieux patrimoniaux conserve également le plan, les pièces,
les coffres et la modification témoin. Loption expérimentale désactivée passe :
elle ne publie aucun module de galerie. `check build assemblePack` passe, avec les huit GameTests généraux. Le
MRpack, son index et le JAR embarqué sont vérifiés ; la publication et la
synchronisation restent à consigner.
Les chemins de quartier regroupent leurs pentes à lintérieur de rectangles ;
leurs frontières et raccords sont des paliers plats. Les structures restent
conditionnées par le terrain admissible, dans un budget borné. La traversée
industrielle ne relie pas encore tous les réseaux souterrains historiques.
Les vues de modèles éventuelles ne remplacent pas une inspection dans le client.
Aucune validation Windows native ni charge multijoueur nest revendiquée.
## Reproduire les contrôles
Depuis ce dépôt, avec Java 25, les mondes de test restent sous `mods/sanctuary/build/`.
Pour chaque scénario, exécuter la création puis sa réouverture avant tout autre
scénario qui nettoie ce monde de développement :
```sh
./gradlew :sanctuary:runGameTest -PsanctuaryTransit21Tests=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --max-workers=1
./gradlew :sanctuary:runGameTest -PsanctuaryTransit21Tests=true -PsanctuaryExpansionReload=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --max-workers=1
```
Remplacer `sanctuaryTransit21Tests` par `sanctuaryLostCity21Tests` pour la ville
ou `sanctuaryHeritage21Tests` pour les infrastructures. Le cas OFF ajoute
`-PsanctuaryExperimentalStructures=false` à une création du scénario transit.
Les rapports de cette livraison sont conservés localement dans
`build/alpha21-evidence/`. `./gradlew check build assemblePack` constitue la
vérification complète avant export.
## Artefacts vérifiés
MRpack : `Sanctuary-0.1.0-alpha.21.mrpack`. Son JAR est identique au JAR du build.
Les versions et hashes de lindex packwiz sont contrôlés, ainsi que le ZIP
damorçage Prism. SHA-256 du JAR :
```text
e528e42d99f9e1ea221f780ea089b49b42a1a4e9e24e4ddd001d045183202c21
```
+2 -2
View File
@@ -8,6 +8,6 @@ loader_version=0.19.5
loom_version=1.17.20
fabric_api_version=0.160.0+26.3
mod_version=0.1.0-alpha.17
pack_version=0.1.0-alpha.17
mod_version=0.1.0-alpha.21
pack_version=0.1.0-alpha.21
maven_group=fr.koka.sanctuary
+95 -2
View File
@@ -40,13 +40,19 @@ tasks.named('runGameTest') {
systemProperty('sanctuary.test.unified', providers.gradleProperty('sanctuaryUnifiedTests').getOrElse('true'))
systemProperty('sanctuary.test.diameter', providers.gradleProperty('sanctuaryTestDiameter').getOrElse('724'))
systemProperty('sanctuary.test.generation', providers.gradleProperty('sanctuaryTestGeneration').getOrElse(
providers.gradleProperty('sanctuaryLostCityTests').map { it.toBoolean() }.getOrElse(false) ? '14' : providers.gradleProperty('sanctuaryLostCity15Tests').map { it.toBoolean() }.getOrElse(false) ? '15' : providers.gradleProperty('sanctuaryLostCity16Tests').map { it.toBoolean() }.getOrElse(false) ? '16' : '17'))
providers.gradleProperty('sanctuaryLostCityTests').map { it.toBoolean() }.getOrElse(false) ? '14' : providers.gradleProperty('sanctuaryLostCity15Tests').map { it.toBoolean() }.getOrElse(false) ? '15' : providers.gradleProperty('sanctuaryLostCity16Tests').map { it.toBoolean() }.getOrElse(false) ? '16' : providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false) ? '17' : providers.gradleProperty('sanctuaryHeritage19Tests').map { it.toBoolean() }.getOrElse(false) ? '19' : (providers.gradleProperty('sanctuaryHeritage20Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryLostCity20Tests').map { it.toBoolean() }.getOrElse(false)) ? '20' : '21'))
systemProperty('sanctuary.test.heritage21', providers.gradleProperty('sanctuaryHeritage21Tests').getOrElse('false'))
systemProperty('sanctuary.test.lostCity21', providers.gradleProperty('sanctuaryLostCity21Tests').getOrElse('false'))
systemProperty('sanctuary.test.transit21', providers.gradleProperty('sanctuaryTransit21Tests').getOrElse('false'))
systemProperty('sanctuary.test.expansion', providers.gradleProperty('sanctuaryExpansionTests').getOrElse('false'))
systemProperty('sanctuary.test.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.lostCity20', providers.gradleProperty('sanctuaryLostCity20Tests').getOrElse('false'))
systemProperty('sanctuary.test.lostCity16', providers.gradleProperty('sanctuaryLostCity16Tests').getOrElse('false'))
systemProperty('sanctuary.test.ruins17', providers.gradleProperty('sanctuaryRuins17Tests').getOrElse('false'))
systemProperty('sanctuary.test.heritage20', providers.gradleProperty('sanctuaryHeritage20Tests').getOrElse('false'))
systemProperty('sanctuary.test.heritage19', providers.gradleProperty('sanctuaryHeritage19Tests').getOrElse('false'))
systemProperty('sanctuary.test.experimentalStructures', providers.gradleProperty('sanctuaryExperimentalStructures').getOrElse('true'))
systemProperty('sanctuary.test.cityMatrix', providers.gradleProperty('sanctuaryCityMatrix').getOrElse('false'))
systemProperty('sanctuary.test.cityMatrixOnly', providers.gradleProperty('sanctuaryCityMatrixOnly').getOrElse('false'))
@@ -69,10 +75,22 @@ tasks.named('processGametestResources') {
inputs.property('lostCity16', lostCity16)
def ruins17 = providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('ruins17', ruins17)
def lostCity20 = providers.gradleProperty('sanctuaryLostCity20Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('lostCity20', lostCity20)
def heritage20 = providers.gradleProperty('sanctuaryHeritage20Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('heritage20', heritage20)
def heritage21 = providers.gradleProperty('sanctuaryHeritage21Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('heritage21', heritage21)
def lostCity21 = providers.gradleProperty('sanctuaryLostCity21Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('lostCity21', lostCity21)
def transit21 = providers.gradleProperty('sanctuaryTransit21Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('transit21', transit21)
def heritage19 = providers.gradleProperty('sanctuaryHeritage19Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('heritage19', heritage19)
def voidReuse = providers.gradleProperty('sanctuaryVoidReuseTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('voidReuse', voidReuse)
filesMatching('fabric.mod.json') {
expand(gametest_entrypoint: ruins17 ? 'fr.koka.sanctuary.gametest.Ruins17WorldGameTests' : voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark
expand(gametest_entrypoint: transit21 ? 'fr.koka.sanctuary.gametest.Transit21WorldGameTests' : lostCity21 ? 'fr.koka.sanctuary.gametest.LostCity21WorldGameTests' : heritage21 ? 'fr.koka.sanctuary.gametest.Heritage21WorldGameTests' : lostCity20 ? 'fr.koka.sanctuary.gametest.LostCity20WorldGameTests' : heritage20 ? 'fr.koka.sanctuary.gametest.Heritage20WorldGameTests' : heritage19 ? 'fr.koka.sanctuary.gametest.Heritage19WorldGameTests' : ruins17 ? 'fr.koka.sanctuary.gametest.Ruins17WorldGameTests' : voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark
? 'fr.koka.sanctuary.gametest.WorldgenPerformanceGameTests'
: unified ? 'fr.koka.sanctuary.gametest.UnifiedWorldGameTests'
: 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests')
@@ -415,3 +433,78 @@ tasks.named('check') { dependsOn('lostCityFoundation16Smoke') }
}
tasks.named('check') { dependsOn(smoke) }
}
// Local native starts keep the long heritage routes bounded by Minecraft's reference range.
['Planner', 'Renderer', 'Piece'].each { component ->
def smoke = tasks.register("heritage19${component}Smoke", JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = "fr.koka.sanctuary.worldgen.heritage.Heritage${component}19Smoke"
}
tasks.named('check') { dependsOn(smoke) }
}
// Generation20 architecture retains the previous renderers and verifies connected block states.
[
connectedBlocks20Smoke: 'detail.ConnectedBlocks20Smoke',
heritage20RendererSmoke: 'heritage.HeritageRenderer20Smoke',
heritage20PieceSmoke: 'heritage.HeritagePiece20Smoke',
ruins20RendererSmoke: 'ruins.RuinsRenderer20Smoke',
lostCityUnderground20Smoke: 'city.LostCityUnderground20Smoke',
lostCitySurface20Smoke: 'city.LostCitySurface20Smoke',
lostCityInteriors20Smoke: 'city.LostCityInteriors20Smoke'
].each { name, harness ->
def smoke = tasks.register(name, JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = "fr.koka.sanctuary.worldgen.${harness}"
}
tasks.named('check') { dependsOn(smoke) }
}
// Optional actual block-state exports for architectural review; never enabled in ordinary checks.
tasks.named('heritage20RendererSmoke') {
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
systemProperty('sanctuary.test.heritage20.export', rootProject.layout.buildDirectory.dir('alpha20-preview/heritage').get().asFile.absolutePath)
}
tasks.named('lostCityInteriors20Smoke') {
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
systemProperty('sanctuary.city20.preview', rootProject.layout.buildDirectory.file('alpha20-preview/city.json').get().asFile.absolutePath)
}
tasks.named('lostCitySurface20Smoke') {
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
systemProperty('sanctuary.city20.exteriorPreview', rootProject.layout.buildDirectory.file('alpha20-preview/exterior.json').get().asFile.absolutePath)
}
// Generation21 keeps published20 renderers immutable.
[
heritage21RendererSmoke: 'heritage.HeritageRenderer21Smoke',
heritage21PieceSmoke: 'heritage.HeritagePiece21Smoke',
heritage21PlannerSmoke: 'heritage.HeritagePlanner21Smoke',
lostCity21PlannerSmoke: 'city.LostCityPlanner21Smoke',
lostCitySurface21Smoke: 'city.LostCitySurface21Smoke',
lostCityInteriors21Smoke: 'city.LostCityInteriors21Smoke',
transit21Smoke: 'transit.Transit21Smoke'
].each { name, harness ->
def smoke = tasks.register(name, JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = "fr.koka.sanctuary.worldgen.${harness}"
}
tasks.named('check') { dependsOn(smoke) }
}
tasks.named('heritage21RendererSmoke') {
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
systemProperty('sanctuary.test.heritage21.export', rootProject.layout.buildDirectory.dir('alpha21-preview/heritage').get().asFile.absolutePath)
}
tasks.named('lostCityInteriors21Smoke') {
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
systemProperty('sanctuary.city21.preview', rootProject.layout.buildDirectory.file('alpha21-preview/city.json').get().asFile.absolutePath)
}
tasks.named('lostCitySurface21Smoke') {
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
systemProperty('sanctuary.city21.exteriorPreview', rootProject.layout.buildDirectory.file('alpha21-preview/exterior.json').get().asFile.absolutePath)
}
@@ -0,0 +1,452 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mojang.serialization.JsonOps;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import fr.koka.sanctuary.worldgen.heritage.HeritagePiece19;
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19;
import fr.koka.sanctuary.worldgen.heritage.HeritageRenderer19;
import fr.koka.sanctuary.worldgen.heritage.HeritageStructures19;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderSet;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.RandomSource;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.DoorBlock;
import net.minecraft.world.level.block.TrapDoorBlock;
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.LevelResource;
import net.minecraft.world.level.storage.loot.LootParams;
import net.minecraft.world.level.storage.loot.LootTable;
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
/** Native creation/reload proof confined to a disposable generation19 starter island. */
public final class Heritage19WorldGameTests {
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
@GameTest(maxTicks = 12000)
public void heritagePersistsWithRealDestinations(GameTestHelper helper) {
helper.assertTrue(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator,
"The heritage proof requires a Sanctuary generator");
var generator = (SanctuaryChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
helper.assertTrue(generator.rootGeneration() == 19, "Use a fresh generation19 test world; do not convert an old save");
new Scenario(helper, generator, Boolean.getBoolean("sanctuary.test.expansionReload")).advance();
}
private static final class Scenario {
final GameTestHelper helper;
final ServerLevel level;
final SanctuaryChunkGenerator generator;
final HeritagePlan19 plan;
final boolean reload;
final Map<String, Object> report = new LinkedHashMap<>();
final List<ChunkPos> chunks = new ArrayList<>();
final List<StructureStart> starts = new ArrayList<>();
JsonObject previous;
BlockPos gold;
int nextChunk;
boolean prepared;
Scenario(GameTestHelper helper, SanctuaryChunkGenerator generator, boolean reload) {
this.helper = helper; this.level = helper.getLevel(); this.generator = generator; this.reload = reload;
this.plan = generator.heritagePlan19();
report.put("seed", level.getSeed()); report.put("size", generator.initialDiameter());
report.put("process_id", ProcessHandle.current().pid()); report.put("world_directory", world(level).toString());
report.put("experimental_structures", generator.experimentalStructures());
report.put("structures_enabled", level.structureManager().shouldGenerateStructures());
report.put("scope", "Native local starts, saved sites and routes, player-sized entrances, supports and unopened chests on the starter island. "
+ "All14 tool tables are sampled separately without consuming a saved chest. Door states are restored after traversal. "
+ "No personal save or expansion is opened; one disposable outer-wall edit is checked in a second JVM. "
+ "Missing rails are intentional; this proof checks walking and saved rail shapes, not an uninterrupted minecart journey.");
}
void advance() {
try {
if (!prepared) { report.put("stage", "prepare"); prepare(); prepared = true; if (plan.isEmpty()) { finishDisabled(); return; } }
if (nextChunk < chunks.size()) {
report.put("stage", "load_full_chunks"); var pos = chunks.get(nextChunk++);
helper.assertTrue(level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true) instanceof LevelChunk,
"Heritage footprint must reach native FULL: " + pos);
if (nextChunk % 20 == 0) SanctuaryMod.LOGGER.info("Heritage19 loaded {}/{} footprint chunks", nextChunk, chunks.size());
helper.runAtTickTime(helper.getTick() + 1, this::advance); return;
}
report.put("stage", "verify_native_geometry"); verifyPlaced(); report.put("passed", true);
if (!reload) {
helper.assertTrue(gold != null && level.getBlockState(gold).isCollisionShapeFullBlock(level, gold), "Gold witness must replace a solid wall away from circulation");
level.setBlock(gold, Blocks.GOLD_BLOCK.defaultBlockState(), 3); report.put("gold_witness", coords(gold));
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush native starts, unopened loot and the wall edit");
report.put("world_flushed_before_shutdown", true);
} else {
helper.assertTrue(level.getBlockState(gold).is(Blocks.GOLD_BLOCK), "Reload replaced the player's wall edit");
report.put("gold_witness", coords(gold)); report.put("gold_witness_preserved", true);
}
write(level, report, reload); helper.succeed();
} catch (Exception failure) {
report.put("passed", false); report.put("failure", failure.toString());
try { write(level, report, reload); } catch (IOException ignored) { }
throw new IllegalStateException("Heritage19 native " + (reload ? "reload" : "creation") + " failed", failure);
}
}
void prepare() throws IOException {
var ops = RegistryOps.create(JsonOps.INSTANCE, level.registryAccess());
var encoded = ChunkGenerator.CODEC.encodeStart(ops, generator).getOrThrow();
var decoded = ChunkGenerator.CODEC.parse(ops, encoded).getOrThrow();
helper.assertTrue(decoded instanceof SanctuaryChunkGenerator copy && copy.rootGeneration() == 19
&& copy.experimentalStructures() == generator.experimentalStructures(), "Generation19 codec lost its experimental switch");
report.put("generator_codec", encoded); report.put("plan_sha256", planHash(plan));
report.put("planned_sites", plan.sites().stream().map(site -> Map.of("id", site.id(), "kind", site.kind().name(),
"entry", coords(HeritagePiece19.locator(site)), "floor_y", site.floorY(), "foundation_depth", site.foundationDepth(), "footings", site.supports().size())).toList());
report.put("planned_links", plan.links().stream().map(link -> Map.of("id", link.id(), "from", link.fromId(), "to", link.toId(), "kind", link.kind().name(), "length", link.path().size(), "path", pathCoords(link))).toList());
report.put("planning_samples", plan.samples()); report.put("planning_columns", plan.columns()); report.put("planning_diagnostics", plan.diagnostics());
helper.assertTrue(plan.samples() <= HeritagePlan19.MAX_SAMPLES && plan.columns() <= HeritagePlan19.MAX_COLUMNS, "Heritage planning exceeded its budget");
verifyLootTables(helper, report);
if (reload) {
previous = JsonParser.parseString(Files.readString(proof(level))).getAsJsonObject();
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("world_flushed_before_shutdown").getAsBoolean(), "Reload needs a successful flushed creation proof");
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload must use a second JVM");
helper.assertTrue(previous.get("seed").getAsLong() == level.getSeed() && previous.get("size").getAsInt() == generator.initialDiameter()
&& previous.get("world_directory").getAsString().equals(world(level).toString()), "Reload selected another world");
helper.assertTrue(previous.get("generator_codec").equals(encoded) && previous.get("plan_sha256").getAsString().equals(planHash(plan)), "Saved settings or planned identities changed on reload");
if (previous.has("gold_witness")) gold = readPos(previous.getAsJsonArray("gold_witness"));
}
if (!generator.experimentalStructures() || !level.structureManager().shouldGenerateStructures()) {
helper.assertTrue(plan.isEmpty() && plan.segments().isEmpty() && plan.links().isEmpty(), "Disabled generation published heritage geometry"); return;
}
helper.assertTrue(!plan.isEmpty(), "Enabled reference island has no heritage site");
report.put("present_site_kinds", plan.sites().stream().map(s -> s.kind().name()).distinct().sorted().toList());
report.put("missing_site_kinds", java.util.Arrays.stream(HeritagePlan19.Kind.values()).filter(k -> plan.sites().stream().noneMatch(s -> s.kind() == k)).map(Enum::name).toList());
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
var origins = new HashSet<ChunkPos>(); var uniqueChunks = new HashSet<ChunkPos>();
plan.sites().forEach(s -> origins.add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
plan.segments().forEach(s -> origins.add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
for (var origin : origins.stream().sorted(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList()) {
var chunk = level.getChunk(origin.x(), origin.z(), ChunkStatus.STRUCTURE_STARTS, true);
var start = chunk.getStartForStructure(structure.value());
helper.assertTrue(start != null && start.isValid(), "Accepted site/route has no native start at " + origin); starts.add(start);
for (var piece : start.getPieces()) {
helper.assertTrue(piece instanceof HeritagePiece19, "Heritage loaded a foreign piece type");
var box = piece.getBoundingBox();
helper.assertTrue(Math.abs((long) (box.minX() >> 4) - origin.x()) <= 7 && Math.abs((long) (box.maxX() >> 4) - origin.x()) <= 7
&& Math.abs((long) (box.minZ() >> 4) - origin.z()) <= 7 && Math.abs((long) (box.maxZ() >> 4) - origin.z()) <= 7, "Native start references a distant route endpoint");
box.intersectingChunks().forEach(uniqueChunks::add);
}
}
for (var site : plan.sites()) uniqueChunks.add(ChunkPos.containing(HeritagePiece19.locator(site)));
for (var link : plan.links()) for (var point : link.path()) uniqueChunks.add(new ChunkPos(point.x() >> 4, point.z() >> 4));
helper.assertTrue(uniqueChunks.size() <= 1024, "Bounded proof footprint exceeds1024 chunks");
chunks.addAll(uniqueChunks.stream().sorted(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList());
}
void finishDisabled() throws IOException {
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
var start = level.getChunk(0, 0, ChunkStatus.STRUCTURE_STARTS, true).getStartForStructure(structure.value());
helper.assertTrue(start == null || !start.isValid(), "Disabled heritage emitted a native start");
helper.assertTrue(generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false) == null, "Disabled heritage is locatable");
report.put("empty_plan_verified", true); report.put("passed", true);
if (!reload) { helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save disabled experiment setting"); report.put("world_flushed_before_shutdown", true); }
write(level, report, reload); helper.succeed();
}
void verifyPlaced() {
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
var context = StructurePieceSerializationContext.fromLevel(level); var summaries = new ArrayList<Map<String, Object>>();
int blocks = 0, chests = 0, pieces = 0, supports = 0;
for (var start : starts) {
var origin = start.getChunkPos(); var saved = start.createTag(context, origin); String nativeHash = hash(saved.toString());
var copy = StructureStart.loadStaticStart(context, saved, level.getSeed());
helper.assertTrue(copy != null && copy.isValid() && copy.createTag(context, origin).equals(saved), "Native start failed an exact NBT roundtrip");
var expectedPayloads = new HashSet<net.minecraft.nbt.CompoundTag>();
plan.sites().stream().filter(s -> s.startChunkX() == origin.x() && s.startChunkZ() == origin.z()).forEach(s -> expectedPayloads.add(HeritagePiece19.encodeSite(s)));
plan.segments().stream().filter(s -> s.startChunkX() == origin.x() && s.startChunkZ() == origin.z()).forEach(s -> expectedPayloads.add(HeritagePiece19.encodeSegment(s)));
var actualPayloads = new HashSet<net.minecraft.nbt.CompoundTag>();
if (reload) {
JsonObject prior = null;
for (var value : previous.getAsJsonArray("starts")) { var item = value.getAsJsonObject(); var at = item.getAsJsonArray("start_chunk"); if (at.get(0).getAsInt() == origin.x() && at.get(1).getAsInt() == origin.z()) prior = item; }
helper.assertTrue(prior != null && prior.get("native_start_sha256").getAsString().equals(nativeHash), "Reload changed saved geometry or native reference count");
}
var loot = new ArrayList<Map<String, Object>>();
for (var value : start.getPieces()) {
var piece = (HeritagePiece19) value; pieces++;
var tag = piece.createTag(context); var restored = new HeritagePiece19(context, tag);
helper.assertTrue(restored.createTag(context).equals(tag), "Saved piece parameters changed on native load");
actualPayloads.add(tag.getCompound("SanctuaryHeritage").orElseThrow());
var corrupt = tag.copy(); corrupt.getCompound("SanctuaryHeritage").orElseThrow().putInt("Schema", 20);
boolean rejected = false; try { new HeritagePiece19(context, corrupt); } catch (IllegalArgumentException expected) { rejected = true; }
helper.assertTrue(rejected, "Unknown schema must fail closed");
piece.getBoundingBox().intersectingChunks().forEach(at -> helper.assertTrue(level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true)
.getReferencesForStructure(structure.value()).contains(origin.pack()), "FULL chunk lost its native structure reference"));
var expected = rendered(piece); blocks += expected.blocks().size();
for (var entry : expected.blocks().entrySet()) {
var at = entry.getKey(); var wanted = entry.getValue(); helper.assertTrue(piece.getBoundingBox().isInside(at), "Renderer escaped saved bounds");
if (reload && at.equals(gold)) continue;
helper.assertTrue(level.getBlockState(at).is(wanted.getBlock()), "Clipping or decoration changed heritage at " + at + ": expected " + wanted + " got " + level.getBlockState(at));
helper.assertTrue(!wanted.is(Blocks.SPAWNER), "Heritage must not create spawners");
}
if (piece.isSite()) {
supports += verifySiteBearing(helper, generator, plan, piece.site());
if (!reload && gold == null) {
gold = expected.blocks().entrySet().stream().filter(e -> isOuterWall(piece.site(), e.getKey()) && e.getValue().isCollisionShapeFullBlock(level, e.getKey()))
.map(Map.Entry::getKey).sorted(Comparator.<BlockPos>comparingInt(p -> p.getY()).thenComparingInt(p -> p.getX()).thenComparingInt(p -> p.getZ())).findFirst().orElse(null);
}
} else for (var cell : piece.segment().cells()) { supports++; assertRock(helper, new BlockPos(cell.x(), cell.minY() - 1, cell.z())); }
for (var chest : expected.chests()) {
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Deferred chest is missing");
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
helper.assertTrue(entity.getLootTable() != null && entity.getLootTable().equals(chest.table()) && entity.getLootTableSeed() == chest.seed(), "Deferred loot table/seed was lost or consumed");
if (piece.isSite()) helper.assertTrue(chest.table().equals(lootKey(piece.site().kind())), "Site uses another kind's tool table");
loot.add(Map.of("position", coords(chest.position()), "table", chest.table().identifier().toString(), "seed", chest.seed())); chests++;
}
}
helper.assertTrue(actualPayloads.equals(expectedPayloads) && start.getPieces().size() == expectedPayloads.size(), "Native start lost or duplicated planned pieces");
summaries.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", nativeHash, "pieces", start.getPieces().size(), "unopened_chests", loot));
}
report.put("starts", summaries); report.put("native_pieces", pieces); report.put("native_blocks_checked", blocks);
report.put("full_chunks_checked", chunks.size()); report.put("rock_supports_checked", supports); report.put("unopened_deferred_chests", chests);
report.put("stage", "verify_site_routes"); var sites = new ArrayList<Map<String, Object>>(); for (var site : plan.sites()) sites.add(verifyAccess(helper, site)); report.put("native_sites", sites);
report.put("stage", "verify_read_only_landings");
report.put("read_only_landmarks", verifyReadOnlyLandings(helper, generator, plan));
report.put("stage", "verify_island_routes"); report.put("native_links", verifyLinks(helper, plan));
int greatestSpan = plan.links().stream().mapToInt(Heritage19WorldGameTests::routeSpan).max().orElse(0);
report.put("greatest_single_route_span", greatestSpan);
if (level.getSeed() == 0 && generator.initialDiameter() == 724) {
int requiredSpan = generator.initialDiameter() / 4;
report.put("reference_minimum_route_span", requiredSpan);
helper.assertTrue(greatestSpan >= requiredSpan, "Seed0 Medium must demonstrate an island-scale route spanning at least "
+ requiredSpan + " blocks on X or Z; got " + greatestSpan);
}
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
helper.assertTrue(located != null && located.getSecond().is(HeritageStructures19.KEY) && plan.sites().stream().anyMatch(s -> HeritagePiece19.locator(s).equals(located.getFirst())), "Locate must lead to a real exterior site entry");
report.put("locate", coords(located.getFirst()));
}
}
private record Chest(BlockPos position, ResourceKey<LootTable> table, long seed) {}
private record Rendered(Map<BlockPos, BlockState> blocks, List<Chest> chests) {}
private static Rendered rendered(HeritagePiece19 piece) {
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
int x0 = piece.isSite() ? piece.site().minX() : piece.segment().minX();
int y0 = piece.isSite() ? piece.site().floorY() : piece.segment().minY();
int z0 = piece.isSite() ? piece.site().minZ() : piece.segment().minZ();
var writer = new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(new BlockPos(x0 + x, y0 + y, z0 + z), state); }
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
var at = new BlockPos(x0 + x, y0 + y, z0 + z); blocks.put(at, Blocks.CHEST.defaultBlockState()); chests.add(new Chest(at, table, RandomSource.create(seed).nextLong()));
}
};
if (piece.isSite()) HeritageRenderer19.render(piece.site(), writer); else HeritageRenderer19.renderSegment(piece.segment(), writer);
return new Rendered(blocks, chests);
}
private static boolean isOuterWall(HeritagePlan19.Site site, BlockPos pos) {
return pos.getY() >= site.floorY() + 2 && pos.getY() <= site.floorY() + 3
&& (pos.getX() == site.minX() + 2 || pos.getX() == site.maxX() - 2 || pos.getZ() == site.minZ() + 2 || pos.getZ() == site.maxZ() - 2)
&& Math.abs(pos.getX() - site.entry().x()) + Math.abs(pos.getZ() - site.entry().z()) > 6;
}
private static Map<String, Object> verifyAccess(GameTestHelper helper, HeritagePlan19.Site site) {
var level = helper.getLevel(); var expected = rendered(new HeritagePiece19(site)); var doors = new HashMap<BlockPos, BlockState>();
expected.blocks().forEach((at, state) -> {
if ((state.getBlock() instanceof DoorBlock || state.getBlock() instanceof TrapDoorBlock) && state.hasProperty(BlockStateProperties.OPEN)) doors.put(at, level.getBlockState(at));
});
doors.forEach((at, state) -> level.setBlock(at, state.setValue(BlockStateProperties.OPEN, true), 2));
try {
var entry = HeritagePiece19.locator(site);
Predicate<BlockPos> inside = p -> p.equals(entry) || p.getX() >= site.minX() && p.getX() <= site.maxX()
&& p.getZ() >= site.minZ() && p.getZ() <= site.maxZ() && p.getY() >= site.floorY() - 4 && p.getY() < site.maxY();
var reached = walk(helper, entry, inside); int targets = 0;
for (var target : HeritageRenderer19.targets(site)) {
var at = new BlockPos(site.minX() + target.x(), site.floorY() + target.y(), site.minZ() + target.z());
helper.assertTrue(reached.contains(at), "Site " + site.id() + " cannot reach " + target.name() + " at " + at); targets++;
}
return Map.of("id", site.id(), "kind", site.kind().name(), "entry", coords(entry), "standing_cells", reached.size(), "walkable_targets", targets);
} finally { doors.forEach((at, state) -> level.setBlock(at, state, 2)); }
}
private static List<Map<String, Object>> verifyLinks(GameTestHelper helper, HeritagePlan19 plan) {
helper.assertTrue(!plan.links().isEmpty(), "Reference island needs a real route between accepted destinations");
var entries = new HashMap<String, HeritagePlan19.Entry>(); plan.sites().forEach(s -> entries.put(s.id(), s.entry())); plan.landmarks().forEach(s -> entries.put(s.id(), s.entry()));
var cells = new HashMap<Long, HeritagePlan19.Cell>(); plan.segments().forEach(s -> s.cells().forEach(c -> cells.put(column(c.x(), c.z()), c)));
var result = new ArrayList<Map<String, Object>>();
for (var link : plan.links()) {
var from = entries.get(link.fromId()); var to = entries.get(link.toId());
helper.assertTrue(from != null && to != null, "Route points to an absent destination");
var first = link.path().getFirst(); var last = link.path().getLast();
helper.assertTrue(first.x() == from.x() && first.z() == from.z() && first.floorY() + 1 == from.y()
&& last.x() == to.x() && last.z() == to.z() && last.floorY() + 1 == to.y(), "Route does not meet its real entrances");
int present = 0, missing = 0;
for (int i = 0; i < link.path().size(); i++) {
var point = link.path().get(i); var at = new BlockPos(point.x(), point.floorY() + 1, point.z());
helper.assertTrue(standable(helper.getLevel(), at), "Ballast route " + link.id() + " is blocked at " + at);
if (i > 0) { var prior = link.path().get(i - 1); if (point.floorY() > prior.floorY()) helper.assertTrue(clearBody(helper.getLevel(), new BlockPos(prior.x(), prior.floorY() + 2, prior.z())), "Route slope cannot be climbed by a player"); }
var cell = cells.get(column(point.x(), point.z()));
if (cell != null && cell.rail() != HeritagePlan19.Rail.NONE) {
var actual = helper.getLevel().getBlockState(at);
if (cell.railPresent()) {
helper.assertTrue(actual.is(Blocks.RAIL), "Saved existing rail disappeared");
helper.assertTrue(actual.getValue(BlockStateProperties.RAIL_SHAPE).name().equals(cell.rail().name()), "Native rail update changed the intended slope or curve at " + at); present++;
} else { helper.assertTrue(!actual.is(Blocks.RAIL), "An intentionally missing rail regenerated"); missing++; }
}
}
int spanX = link.path().stream().mapToInt(HeritagePlan19.Point::x).max().orElseThrow() - link.path().stream().mapToInt(HeritagePlan19.Point::x).min().orElseThrow();
int spanZ = link.path().stream().mapToInt(HeritagePlan19.Point::z).max().orElseThrow() - link.path().stream().mapToInt(HeritagePlan19.Point::z).min().orElseThrow();
result.add(Map.of("id", link.id(), "from", link.fromId(), "to", link.toId(), "kind", link.kind().name(), "length", link.path().size(),
"span_x", spanX, "span_z", spanZ, "existing_rails", present, "deliberately_missing_rails", missing, "path", pathCoords(link)));
}
return result;
}
private static int routeSpan(HeritagePlan19.Link link) {
int spanX = link.path().stream().mapToInt(HeritagePlan19.Point::x).max().orElseThrow()
- link.path().stream().mapToInt(HeritagePlan19.Point::x).min().orElseThrow();
int spanZ = link.path().stream().mapToInt(HeritagePlan19.Point::z).max().orElseThrow()
- link.path().stream().mapToInt(HeritagePlan19.Point::z).min().orElseThrow();
return Math.max(spanX, spanZ);
}
private static List<List<Integer>> pathCoords(HeritagePlan19.Link link) {
return link.path().stream().map(point -> List.of(point.x(), point.floorY(), point.z())).toList();
}
private static int verifySiteBearing(GameTestHelper helper, SanctuaryChunkGenerator generator, HeritagePlan19 plan, HeritagePlan19.Site site) {
int checked = 0;
// The planner certifies a nine-point bearing grid, either directly below its slab or below a saved short footing.
for (int dx : new int[] {1, 8, 15}) for (int dz : new int[] {1, 8, 15}) {
int x = site.minX() + dx, z = site.minZ() + dz;
var support = site.supports().stream().filter(s -> s.x() == x && s.z() == z).findFirst();
int bottom = support.map(HeritagePlan19.Support::minY).orElse(site.minY());
for (int below = 1; below <= 2; below++) {
var at = new BlockPos(x, bottom - below, z); assertRock(helper, at);
helper.assertTrue(plan.protects(at.getX(), at.getY(), at.getZ()) && generator.protectsCityDecoration(at),
"Site's certified bearing layer is not published in the decoration mask: " + site.id() + " at " + at);
helper.assertTrue(!plan.contains(at.getX(), at.getY(), at.getZ()), "Original bearing rock became a writable heritage volume: " + at);
checked++;
}
}
return checked;
}
private static List<Map<String, Object>> verifyReadOnlyLandings(GameTestHelper helper, SanctuaryChunkGenerator generator, HeritagePlan19 plan) {
var used = new HashSet<String>(); plan.links().forEach(link -> { used.add(link.fromId()); used.add(link.toId()); });
var result = new ArrayList<Map<String, Object>>();
for (var landmark : plan.landmarks()) if (used.contains(landmark.id())) {
var e = landmark.entry(); int checked = 0;
for (int side = -1; side <= 1; side++) {
int x = e.x() + ((e.facing() & 1) == 0 ? side : 0), z = e.z() + ((e.facing() & 1) == 1 ? side : 0);
for (int y = e.y() - 2; y <= e.y() + 2; y++) {
var at = new BlockPos(x, y, z);
helper.assertTrue(plan.protects(x, y, z) && generator.protectsCityDecoration(at), "Read-only arrival lost its decoration mask: " + landmark.id() + " at " + at);
helper.assertTrue(!plan.contains(x, y, z), "Heritage gained write ownership of a historical arrival: " + landmark.id() + " at " + at);
if (y == e.y() - 1) assertRock(helper, at);
if (y >= e.y()) helper.assertTrue(helper.getLevel().getBlockState(at).isAir(), "Decoration obstructed a certified historical arrival: " + at);
checked++;
}
}
result.add(Map.of("id", landmark.id(), "entry", List.of(e.x(), e.y(), e.z()), "read_only_cells", checked));
}
return result;
}
private static void assertRock(GameTestHelper helper, BlockPos at) {
helper.assertTrue(helper.getLevel().getBlockState(at).isSolid() && helper.getLevel().getFluidState(at).isEmpty(), "Lost original rock bearing at " + at);
}
private static void verifyLootTables(GameTestHelper helper, Map<String, Object> report) {
var result = new ArrayList<Map<String, Object>>(); var level = helper.getLevel();
var params = new LootParams.Builder(level).withParameter(LootContextParams.ORIGIN, new Vec3(0, 160, 0)).create(LootContextParamSets.CHEST);
for (var kind : HeritagePlan19.Kind.values()) {
var table = level.getServer().reloadableRegistries().getLootTable(lootKey(kind));
helper.assertTrue(table != LootTable.EMPTY, "Heritage tool table did not load: " + kind);
for (long seed : new long[] {0, 42, -1}) helper.assertTrue(table.getRandomItems(params, seed).stream().anyMatch(stack -> stack.is(tool(kind))), "Tool table is empty or lacks its expected primary tool: " + kind);
result.add(Map.of("kind", kind.name(), "table", lootKey(kind).identifier().toString(), "sampled_seeds", List.of(0, 42, -1)));
}
report.put("tool_tables_sampled_separately", result);
}
private static ResourceKey<LootTable> lootKey(HeritagePlan19.Kind kind) { return ResourceKey.create(Registries.LOOT_TABLE, SanctuaryMod.id("chests/heritage19/" + kind.name().toLowerCase(Locale.ROOT))); }
private static Item tool(HeritagePlan19.Kind kind) {
return switch (kind) {
case OBSERVATORY -> Items.SPYGLASS;
case POST_OFFICE, EXPEDITION_OFFICE, RADIO_RELAY -> Items.COMPASS;
case SUBSTATION, IMPOSSIBLE_SERVICE -> Items.CLOCK;
case FREIGHT_STATION -> Items.MINECART;
case COMMUNAL_DEPOT -> Items.IRON_AXE;
case RAIL_WORKSHOP -> Items.IRON_PICKAXE;
case PUMP_STATION -> Items.BUCKET;
case GREENHOUSE -> Items.IRON_HOE;
case VOID_DOCK -> Items.LEAD;
case PHOTO_CLUB -> Items.BOOK;
case CONSTRUCTION_REFUGE -> Items.IRON_SHOVEL;
};
}
private static Set<BlockPos> walk(GameTestHelper helper, BlockPos start, Predicate<BlockPos> inside) {
var level = helper.getLevel(); helper.assertTrue(inside.test(start) && standable(level, start), "Blocked or unsupported exterior entrance: " + start);
var seen = new HashSet<BlockPos>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
while (!queue.isEmpty()) {
var pos = queue.removeFirst();
for (var direction : HORIZONTAL) for (int dy = -1; dy <= 1; dy++) {
var next = pos.relative(direction).offset(0, dy, 0);
if (!inside.test(next) || dy > 0 && !clearBody(level, pos.above())) continue;
if (standable(level, next) && seen.add(next)) queue.add(next);
}
for (int dy : new int[] {-1, 1}) {
var next = pos.offset(0, dy, 0); if (!inside.test(next)) continue;
if ((level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(next).is(Blocks.LADDER) || level.getBlockState(pos.below()).is(Blocks.LADDER))
&& standable(level, next) && seen.add(next)) queue.add(next);
}
helper.assertTrue(seen.size() <= 50_000, "Native site traversal escaped its bounds");
}
return seen;
}
private static boolean standable(ServerLevel level, BlockPos pos) {
if (!clearBody(level, pos)) return false;
if (level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(pos.below()).is(Blocks.LADDER)) return true;
var below = pos.below(); return level.getBlockState(below).getCollisionShape(level, below).toAabbs().stream()
.anyMatch(b -> b.maxY >= .5 && b.minX <= .5 && b.maxX >= .5 && b.minZ <= .5 && b.maxZ >= .5);
}
private static boolean clearBody(ServerLevel level, BlockPos pos) {
var body = new AABB(pos.getX() + .2, pos.getY(), pos.getZ() + .2, pos.getX() + .8, pos.getY() + 1.8, pos.getZ() + .8);
for (var at : new BlockPos[] {pos, pos.above()}) {
if (!level.getFluidState(at).isEmpty()) return false;
for (var shape : level.getBlockState(at).getCollisionShape(level, at).toAabbs()) if (shape.move(at.getX(), at.getY(), at.getZ()).intersects(body)) return false;
}
return true;
}
private static long column(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
private static List<Integer> coords(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
private static BlockPos readPos(com.google.gson.JsonArray value) { return new BlockPos(value.get(0).getAsInt(), value.get(1).getAsInt(), value.get(2).getAsInt()); }
private static Path world(ServerLevel level) { return level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize(); }
private static Path proof(ServerLevel level) { return world(level).resolve("heritage19-create-proof.json"); }
private static String planHash(HeritagePlan19 plan) { return hash(plan.sites().toString() + plan.landmarks() + plan.links() + plan.segments()); }
private static void write(ServerLevel level, Map<String, Object> report, boolean reload) throws IOException {
var path = Path.of("diagnostics", "heritage19-" + (reload ? "reload" : "create") + ".json"); Files.createDirectories(path.getParent());
var value = new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n"; Files.writeString(path, value); if (!reload) Files.writeString(proof(level), value);
}
private static String hash(String value) {
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8))); }
catch (NoSuchAlgorithmException impossible) { throw new AssertionError(impossible); }
}
}
@@ -0,0 +1,556 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mojang.serialization.JsonOps;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import fr.koka.sanctuary.worldgen.heritage.HeritagePiece20;
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19;
import fr.koka.sanctuary.worldgen.heritage.HeritageRenderer20;
import fr.koka.sanctuary.worldgen.heritage.HeritageStructures19;
import fr.koka.sanctuary.worldgen.ruins.RuinsPiece20;
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17;
import fr.koka.sanctuary.worldgen.ruins.RuinsRenderer20;
import fr.koka.sanctuary.worldgen.ruins.RuinsStructures17;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderSet;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.RandomSource;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.DoorBlock;
import net.minecraft.world.level.block.TrapDoorBlock;
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.LevelResource;
import net.minecraft.world.level.storage.loot.LootParams;
import net.minecraft.world.level.storage.loot.LootTable;
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
/** Native creation/reload proof confined to a disposable generation20 starter island. */
public final class Heritage20WorldGameTests {
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
@GameTest(maxTicks = 12000)
public void heritagePersistsWithRealDestinations(GameTestHelper helper) {
helper.assertTrue(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator,
"The heritage proof requires a Sanctuary generator");
var generator = (SanctuaryChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
helper.assertTrue(generator.rootGeneration() == 20, "Use a fresh generation20 test world; do not convert an old save");
new Scenario(helper, generator, Boolean.getBoolean("sanctuary.test.expansionReload")).advance();
}
private static final class Scenario {
final GameTestHelper helper;
final ServerLevel level;
final SanctuaryChunkGenerator generator;
final HeritagePlan19 plan;
final boolean reload;
final Map<String, Object> report = new LinkedHashMap<>();
final List<ChunkPos> chunks = new ArrayList<>();
final List<StructureStart> starts = new ArrayList<>();
final List<StructureStart> ruinsStarts = new ArrayList<>();
JsonObject previous;
BlockPos gold;
int nextChunk;
boolean prepared;
Scenario(GameTestHelper helper, SanctuaryChunkGenerator generator, boolean reload) {
this.helper = helper; this.level = helper.getLevel(); this.generator = generator; this.reload = reload;
this.plan = generator.heritagePlan19();
report.put("seed", level.getSeed()); report.put("size", generator.initialDiameter());
report.put("process_id", ProcessHandle.current().pid()); report.put("world_directory", world(level).toString());
report.put("experimental_structures", generator.experimentalStructures());
report.put("structures_enabled", level.structureManager().shouldGenerateStructures());
report.put("scope", "Native local starts, saved sites and routes, player-sized entrances, supports and unopened chests on the starter island. "
+ "All14 tool tables are sampled separately without consuming a saved chest. Door states are restored after traversal. "
+ "No personal save or expansion is opened; one disposable outer-wall edit is checked in a second JVM. "
+ "Missing rails are intentional; this proof checks walking and saved rail shapes, not an uninterrupted minecart journey.");
}
void advance() {
try {
if (!prepared) { report.put("stage", "prepare"); prepare(); prepared = true; if (plan.isEmpty()) { finishDisabled(); return; } }
if (nextChunk < chunks.size()) {
report.put("stage", "load_full_chunks"); var pos = chunks.get(nextChunk++);
helper.assertTrue(level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true) instanceof LevelChunk,
"Heritage footprint must reach native FULL: " + pos);
if (nextChunk % 20 == 0) SanctuaryMod.LOGGER.info("Heritage20 loaded {}/{} footprint chunks", nextChunk, chunks.size());
helper.runAtTickTime(helper.getTick() + 1, this::advance); return;
}
report.put("stage", "verify_native_geometry"); verifyPlaced(); report.put("passed", true);
if (!reload) {
helper.assertTrue(gold != null && level.getBlockState(gold).isCollisionShapeFullBlock(level, gold), "Gold witness must replace a solid wall away from circulation");
level.setBlock(gold, Blocks.GOLD_BLOCK.defaultBlockState(), 3); report.put("gold_witness", coords(gold));
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush native starts, unopened loot and the wall edit");
report.put("world_flushed_before_shutdown", true);
} else {
helper.assertTrue(level.getBlockState(gold).is(Blocks.GOLD_BLOCK), "Reload replaced the player's wall edit");
report.put("gold_witness", coords(gold)); report.put("gold_witness_preserved", true);
}
write(level, report, reload); helper.succeed();
} catch (Exception failure) {
report.put("passed", false); report.put("failure", failure.toString());
try { write(level, report, reload); } catch (IOException ignored) { }
throw new IllegalStateException("Heritage20 native " + (reload ? "reload" : "creation") + " failed", failure);
}
}
void prepare() throws IOException {
var ops = RegistryOps.create(JsonOps.INSTANCE, level.registryAccess());
var encoded = ChunkGenerator.CODEC.encodeStart(ops, generator).getOrThrow();
var decoded = ChunkGenerator.CODEC.parse(ops, encoded).getOrThrow();
helper.assertTrue(decoded instanceof SanctuaryChunkGenerator copy && copy.rootGeneration() == 20
&& copy.experimentalStructures() == generator.experimentalStructures(), "Generation20 codec lost its experimental switch");
report.put("generator_codec", encoded); report.put("plan_sha256", planHash(plan));
report.put("planned_sites", plan.sites().stream().map(site -> Map.of("id", site.id(), "kind", site.kind().name(),
"entry", coords(HeritagePiece20.locator(site)), "floor_y", site.floorY(), "foundation_depth", site.foundationDepth(), "footings", site.supports().size())).toList());
report.put("planned_links", plan.links().stream().map(link -> Map.of("id", link.id(), "from", link.fromId(), "to", link.toId(), "kind", link.kind().name(), "length", link.path().size(), "path", pathCoords(link))).toList());
report.put("planning_samples", plan.samples()); report.put("planning_columns", plan.columns()); report.put("planning_diagnostics", plan.diagnostics());
helper.assertTrue(plan.samples() <= HeritagePlan19.MAX_SAMPLES && plan.columns() <= HeritagePlan19.MAX_COLUMNS, "Heritage planning exceeded its budget");
verifyLootTables(helper, report);
if (reload) {
previous = JsonParser.parseString(Files.readString(proof(level))).getAsJsonObject();
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("world_flushed_before_shutdown").getAsBoolean(), "Reload needs a successful flushed creation proof");
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload must use a second JVM");
helper.assertTrue(previous.get("seed").getAsLong() == level.getSeed() && previous.get("size").getAsInt() == generator.initialDiameter()
&& previous.get("world_directory").getAsString().equals(world(level).toString()), "Reload selected another world");
helper.assertTrue(previous.get("generator_codec").equals(encoded) && previous.get("plan_sha256").getAsString().equals(planHash(plan)), "Saved settings or planned identities changed on reload");
if (previous.has("gold_witness")) gold = readPos(previous.getAsJsonArray("gold_witness"));
}
if (!generator.experimentalStructures() || !level.structureManager().shouldGenerateStructures()) {
helper.assertTrue(plan.isEmpty() && plan.segments().isEmpty() && plan.links().isEmpty(), "Disabled generation published heritage geometry"); return;
}
helper.assertTrue(!plan.isEmpty(), "Enabled reference island has no heritage site");
report.put("present_site_kinds", plan.sites().stream().map(s -> s.kind().name()).distinct().sorted().toList());
report.put("missing_site_kinds", java.util.Arrays.stream(HeritagePlan19.Kind.values()).filter(k -> plan.sites().stream().noneMatch(s -> s.kind() == k)).map(Enum::name).toList());
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
var origins = new HashSet<ChunkPos>(); var uniqueChunks = new HashSet<ChunkPos>();
plan.sites().forEach(s -> origins.add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
plan.segments().forEach(s -> origins.add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
for (var origin : origins.stream().sorted(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList()) {
var chunk = level.getChunk(origin.x(), origin.z(), ChunkStatus.STRUCTURE_STARTS, true);
var start = chunk.getStartForStructure(structure.value());
helper.assertTrue(start != null && start.isValid(), "Accepted site/route has no native start at " + origin); starts.add(start);
for (var piece : start.getPieces()) {
helper.assertTrue(piece instanceof HeritagePiece20, "Heritage loaded a foreign piece type");
var box = piece.getBoundingBox();
helper.assertTrue(Math.abs((long) (box.minX() >> 4) - origin.x()) <= 7 && Math.abs((long) (box.maxX() >> 4) - origin.x()) <= 7
&& Math.abs((long) (box.minZ() >> 4) - origin.z()) <= 7 && Math.abs((long) (box.maxZ() >> 4) - origin.z()) <= 7, "Native start references a distant route endpoint");
box.intersectingChunks().forEach(uniqueChunks::add);
}
}
for (var site : plan.sites()) uniqueChunks.add(ChunkPos.containing(HeritagePiece20.locator(site)));
for (var link : plan.links()) for (var point : link.path()) uniqueChunks.add(new ChunkPos(point.x() >> 4, point.z() >> 4));
helper.assertTrue(uniqueChunks.size() <= 1024, "Bounded proof footprint exceeds1024 chunks");
var ruins = generator.ruinsPlan17();
report.put("ruins_plan_sha256", hash(ruins.networks().toString()));
if (reload) helper.assertTrue(previous.get("ruins_plan_sha256").getAsString().equals(hash(ruins.networks().toString())), "Reload changed forgotten room planning");
helper.assertTrue(!ruins.isEmpty(), "Reference20 must demonstrate native experimental room architecture");
var ruinsType = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(RuinsStructures17.KEY);
var ruinsOrigins = new HashSet<ChunkPos>();
for (var network : ruins.networks()) {
var origin = new ChunkPos(network.startChunkX(), network.startChunkZ()); if (!ruinsOrigins.add(origin)) continue;
var nativeStart = level.getChunk(origin.x(), origin.z(), ChunkStatus.STRUCTURE_STARTS, true).getStartForStructure(ruinsType.value());
helper.assertTrue(nativeStart != null && nativeStart.isValid(), "Missing native20 forgotten network: " + network.id());
ruinsStarts.add(nativeStart);
for (var piece : nativeStart.getPieces()) {
helper.assertTrue(piece instanceof RuinsPiece20, "Generation20 loaded a historical forgotten piece");
piece.getBoundingBox().intersectingChunks().forEach(uniqueChunks::add);
}
}
chunks.addAll(uniqueChunks.stream().sorted(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList());
}
void finishDisabled() throws IOException {
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
var start = level.getChunk(0, 0, ChunkStatus.STRUCTURE_STARTS, true).getStartForStructure(structure.value());
helper.assertTrue(start == null || !start.isValid(), "Disabled heritage emitted a native start");
helper.assertTrue(generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false) == null, "Disabled heritage is locatable");
report.put("empty_plan_verified", true); report.put("passed", true);
if (!reload) { helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save disabled experiment setting"); report.put("world_flushed_before_shutdown", true); }
write(level, report, reload); helper.succeed();
}
void verifyPlaced() {
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
var context = StructurePieceSerializationContext.fromLevel(level); var summaries = new ArrayList<Map<String, Object>>();
int blocks = 0, chests = 0, pieces = 0, supports = 0;
var connections = NativeConnections20.Stats.empty();
for (var start : starts) {
var origin = start.getChunkPos(); var saved = start.createTag(context, origin); String nativeHash = hash(saved.toString());
var copy = StructureStart.loadStaticStart(context, saved, level.getSeed());
helper.assertTrue(copy != null && copy.isValid() && copy.createTag(context, origin).equals(saved), "Native start failed an exact NBT roundtrip");
var expectedPayloads = new HashSet<net.minecraft.nbt.CompoundTag>();
plan.sites().stream().filter(s -> s.startChunkX() == origin.x() && s.startChunkZ() == origin.z()).forEach(s -> expectedPayloads.add(HeritagePiece20.encodeSite(s)));
plan.segments().stream().filter(s -> s.startChunkX() == origin.x() && s.startChunkZ() == origin.z()).forEach(s -> expectedPayloads.add(HeritagePiece20.encodeSegment(s)));
var actualPayloads = new HashSet<net.minecraft.nbt.CompoundTag>();
if (reload) {
JsonObject prior = null;
for (var value : previous.getAsJsonArray("starts")) { var item = value.getAsJsonObject(); var at = item.getAsJsonArray("start_chunk"); if (at.get(0).getAsInt() == origin.x() && at.get(1).getAsInt() == origin.z()) prior = item; }
helper.assertTrue(prior != null && prior.get("native_start_sha256").getAsString().equals(nativeHash), "Reload changed saved geometry or native reference count");
}
var loot = new ArrayList<Map<String, Object>>();
for (var value : start.getPieces()) {
var piece = (HeritagePiece20) value; pieces++;
var tag = piece.createTag(context); var restored = new HeritagePiece20(context, tag);
helper.assertTrue(restored.createTag(context).equals(tag), "Saved piece parameters changed on native load");
actualPayloads.add(tag.getCompound("SanctuaryHeritage").orElseThrow());
var corrupt = tag.copy(); corrupt.getCompound("SanctuaryHeritage").orElseThrow().putInt("Schema", 21);
boolean rejected = false; try { new HeritagePiece20(context, corrupt); } catch (IllegalArgumentException expected) { rejected = true; }
helper.assertTrue(rejected, "Unknown schema must fail closed");
piece.getBoundingBox().intersectingChunks().forEach(at -> helper.assertTrue(level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true)
.getReferencesForStructure(structure.value()).contains(origin.pack()), "FULL chunk lost its native structure reference"));
var expected = rendered(piece); blocks += expected.blocks().size();
for (var entry : expected.blocks().entrySet()) {
var at = entry.getKey(); var wanted = entry.getValue(); helper.assertTrue(piece.getBoundingBox().isInside(at), "Renderer escaped saved bounds");
if (reload && at.equals(gold)) continue;
helper.assertTrue(level.getBlockState(at).is(wanted.getBlock()), "Clipping or decoration changed heritage at " + at + ": expected " + wanted + " got " + level.getBlockState(at));
helper.assertTrue(!wanted.is(Blocks.SPAWNER), "Heritage must not create spawners");
}
connections = connections.add(NativeConnections20.verify(helper, expected.blocks(), reload ? Set.of(gold) : Set.of()));
if (piece.isSite()) {
supports += verifySiteBearing(helper, generator, plan, piece.site());
if (!reload && gold == null) {
gold = expected.blocks().entrySet().stream().filter(e -> isOuterWall(piece.site(), e.getKey()) && e.getValue().isCollisionShapeFullBlock(level, e.getKey()))
.map(Map.Entry::getKey).sorted(Comparator.<BlockPos>comparingInt(p -> p.getY()).thenComparingInt(p -> p.getX()).thenComparingInt(p -> p.getZ())).findFirst().orElse(null);
}
} else for (var cell : piece.segment().cells()) { supports++; assertRock(helper, new BlockPos(cell.x(), cell.minY() - 1, cell.z())); }
for (var chest : expected.chests()) {
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Deferred chest is missing");
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
helper.assertTrue(entity.getLootTable() != null && entity.getLootTable().equals(chest.table()) && entity.getLootTableSeed() == chest.seed(), "Deferred loot table/seed was lost or consumed");
if (piece.isSite()) helper.assertTrue(chest.table().equals(lootKey(piece.site().kind())), "Site uses another kind's tool table");
loot.add(Map.of("position", coords(chest.position()), "table", chest.table().identifier().toString(), "seed", chest.seed())); chests++;
}
}
helper.assertTrue(actualPayloads.equals(expectedPayloads) && start.getPieces().size() == expectedPayloads.size(), "Native start lost or duplicated planned pieces");
summaries.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", nativeHash, "pieces", start.getPieces().size(), "unopened_chests", loot));
}
report.put("connected_blocks", connections);
helper.assertTrue(connections.panes() > 0 && connections.stairs() > 0 && connections.nativeOracles() > 0,
"Reference20 needs real panes/stairs validated by vanilla neighbor rules");
if (level.getSeed() == 0 && generator.initialDiameter() == 724)
helper.assertTrue(connections.crossChunkPaneLinks() > 0, "Reference20 must demonstrate a real pane join across a chunk boundary");
verifyRuins();
report.put("starts", summaries); report.put("native_pieces", pieces); report.put("native_blocks_checked", blocks);
report.put("full_chunks_checked", chunks.size()); report.put("rock_supports_checked", supports); report.put("unopened_deferred_chests", chests);
report.put("stage", "verify_site_routes"); var sites = new ArrayList<Map<String, Object>>(); for (var site : plan.sites()) sites.add(verifyAccess(helper, site)); report.put("native_sites", sites);
report.put("stage", "verify_read_only_landings");
report.put("read_only_landmarks", verifyReadOnlyLandings(helper, generator, plan));
report.put("stage", "verify_island_routes"); report.put("native_links", verifyLinks(helper, plan));
int greatestSpan = plan.links().stream().mapToInt(Heritage20WorldGameTests::routeSpan).max().orElse(0);
report.put("greatest_single_route_span", greatestSpan);
if (level.getSeed() == 0 && generator.initialDiameter() == 724) {
int requiredSpan = generator.initialDiameter() / 4;
report.put("reference_minimum_route_span", requiredSpan);
helper.assertTrue(greatestSpan >= requiredSpan, "Seed0 Medium must demonstrate an island-scale route spanning at least "
+ requiredSpan + " blocks on X or Z; got " + greatestSpan);
}
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
helper.assertTrue(located != null && located.getSecond().is(HeritageStructures19.KEY) && plan.sites().stream().anyMatch(s -> HeritagePiece20.locator(s).equals(located.getFirst())), "Locate must lead to a real exterior site entry");
report.put("locate", coords(located.getFirst()));
}
void verifyRuins() {
var context = StructurePieceSerializationContext.fromLevel(level);
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(RuinsStructures17.KEY);
var summaries = new ArrayList<Map<String, Object>>(); int count = 0, blockCount = 0, lootCount = 0;
var connections = NativeConnections20.Stats.empty();
for (var start : ruinsStarts) {
var origin = start.getChunkPos(); var saved = start.createTag(context, origin); var digest = hash(saved.toString());
var loaded = StructureStart.loadStaticStart(context, saved, level.getSeed());
helper.assertTrue(loaded != null && loaded.createTag(context, origin).equals(saved), "Forgotten20 native start NBT changed");
var expectedRooms = generator.ruinsPlan17().networks().stream().filter(n -> n.startChunkX() == origin.x() && n.startChunkZ() == origin.z()).flatMap(n -> n.rooms().stream()).toList();
var actualRooms = start.getPieces().stream().map(RuinsPiece20.class::cast).map(RuinsPiece20::room).toList();
helper.assertTrue(expectedRooms.size() == actualRooms.size() && new HashSet<>(expectedRooms).equals(new HashSet<>(actualRooms)), "Forgotten20 native rooms differ from plan");
if (reload) {
JsonObject prior = null;
for (var value : previous.getAsJsonArray("ruins_starts")) { var item = value.getAsJsonObject(); var at = item.getAsJsonArray("start_chunk"); if (at.get(0).getAsInt() == origin.x() && at.get(1).getAsInt() == origin.z()) prior = item; }
helper.assertTrue(prior != null && prior.get("native_start_sha256").getAsString().equals(digest), "Forgotten20 native start changed on a second JVM load");
}
for (var raw : start.getPieces()) {
var piece = (RuinsPiece20) raw; var room = piece.room(); count++;
var tag = piece.createTag(context); var roundtrip = new RuinsPiece20(context, tag);
helper.assertTrue(roundtrip.room().equals(room) && roundtrip.createTag(context).equals(tag), "Forgotten20 piece lost saved parameters");
piece.getBoundingBox().intersectingChunks().forEach(at -> helper.assertTrue(level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true).getReferencesForStructure(registry.value()).contains(origin.pack()), "Forgotten20 chunk lost native references"));
var expected = rendered(room); blockCount += expected.blocks().size();
for (var entry : expected.blocks().entrySet()) {
helper.assertTrue(piece.getBoundingBox().isInside(entry.getKey()), "Forgotten20 renderer escaped saved bounds");
helper.assertTrue(level.getBlockState(entry.getKey()).is(entry.getValue().getBlock()), "Forgotten20 native geometry changed at " + entry.getKey());
}
connections = connections.add(NativeConnections20.verify(helper, expected.blocks(), Set.of()));
for (var support : room.supports()) assertRock(helper, new BlockPos(support.x(), support.minY() - 1, support.z()));
for (var chest : expected.chests()) {
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Forgotten20 missing deferred chest");
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
helper.assertTrue(chest.table().equals(entity.getLootTable()) && chest.seed() == entity.getLootTableSeed(), "Forgotten20 consumed/changed saved loot"); lootCount++;
}
}
summaries.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", digest, "pieces", start.getPieces().size()));
}
report.put("ruins_starts", summaries); report.put("ruins_native_pieces", count); report.put("ruins_native_blocks_checked", blockCount);
report.put("ruins_unopened_deferred_chests", lootCount); report.put("ruins_connections", connections);
report.put("ruins_access", generator.ruinsPlan17().networks().stream().map(n -> verifyAccess(helper, n)).toList());
}
}
private record Chest(BlockPos position, ResourceKey<LootTable> table, long seed) {}
private record Rendered(Map<BlockPos, BlockState> blocks, List<Chest> chests) {}
private static Rendered rendered(HeritagePiece20 piece) {
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
int x0 = piece.isSite() ? piece.site().minX() : piece.segment().minX();
int y0 = piece.isSite() ? piece.site().floorY() : piece.segment().minY();
int z0 = piece.isSite() ? piece.site().minZ() : piece.segment().minZ();
var writer = new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(new BlockPos(x0 + x, y0 + y, z0 + z), state); }
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
var at = new BlockPos(x0 + x, y0 + y, z0 + z); blocks.put(at, Blocks.CHEST.defaultBlockState()); chests.add(new Chest(at, table, RandomSource.create(seed).nextLong()));
}
};
if (piece.isSite()) HeritageRenderer20.render(piece.site(), writer); else HeritageRenderer20.renderSegment(piece.segment(), writer);
return new Rendered(blocks, chests);
}
private static Rendered rendered(RuinsPlan17.Room room) {
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
RuinsRenderer20.render(room, new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(at(room, x, y, z), state); }
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
var position = at(room, x, y, z); blocks.put(position, Blocks.CHEST.defaultBlockState()); chests.add(new Chest(position, table, RandomSource.create(seed).nextLong()));
}
});
return new Rendered(blocks, chests);
}
private static Map<String, Object> verifyAccess(GameTestHelper helper, RuinsPlan17.Network network) {
var level = helper.getLevel(); var access = network.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS).findFirst().orElseThrow();
var hatches = new HashMap<BlockPos, BlockState>();
rendered(access).blocks().forEach((at, state) -> { if (state.is(Blocks.DARK_OAK_TRAPDOOR)) hatches.put(at, level.getBlockState(at)); });
helper.assertTrue(hatches.size() == 1, "Each network must have one usable wooden surface hatch");
hatches.forEach((at, state) -> {
helper.assertTrue(state.is(Blocks.DARK_OAK_TRAPDOOR) && state.canSurvive(level, at), "Surface hatch is missing its native support");
level.setBlock(at, state.setValue(BlockStateProperties.OPEN, true), 2);
});
try {
Predicate<BlockPos> inside = pos -> network.rooms().stream().anyMatch(r -> r.contains(pos.getX(), pos.getY(), pos.getZ()));
var start = RuinsPiece20.locator(access); var reached = walk(helper, start, inside);
int targets = 0;
for (var room : network.rooms()) for (var target : RuinsRenderer20.targets(room)) {
var point = at(room, target.x(), target.y(), target.z());
helper.assertTrue(reached.contains(point), "Native network " + network.id() + " cannot reach " + room.kind() + "/" + target.name() + " at " + point);
targets++;
}
for (var hatch : hatches.keySet()) helper.assertTrue(reached.contains(hatch), "Surface approach cannot enter its ladder shaft");
return Map.of("id", network.id(), "family", network.family().name(), "rooms", network.rooms().size(),
"entrance", coords(start), "walkable_targets", targets, "standing_cells", reached.size());
} finally { hatches.forEach((at, state) -> level.setBlock(at, state, 2)); }
}
private static BlockPos at(RuinsPlan17.Room room, int x, int y, int z) { return new BlockPos(room.minX() + x, room.minY() + y, room.minZ() + z); }
private static boolean isOuterWall(HeritagePlan19.Site site, BlockPos pos) {
return pos.getY() >= site.floorY() + 2 && pos.getY() <= site.floorY() + 3
&& (pos.getX() == site.minX() + 2 || pos.getX() == site.maxX() - 2 || pos.getZ() == site.minZ() + 2 || pos.getZ() == site.maxZ() - 2)
&& Math.abs(pos.getX() - site.entry().x()) + Math.abs(pos.getZ() - site.entry().z()) > 6;
}
private static Map<String, Object> verifyAccess(GameTestHelper helper, HeritagePlan19.Site site) {
var level = helper.getLevel(); var expected = rendered(new HeritagePiece20(site)); var doors = new HashMap<BlockPos, BlockState>();
expected.blocks().forEach((at, state) -> {
if ((state.getBlock() instanceof DoorBlock || state.getBlock() instanceof TrapDoorBlock) && state.hasProperty(BlockStateProperties.OPEN)) doors.put(at, level.getBlockState(at));
});
doors.forEach((at, state) -> level.setBlock(at, state.setValue(BlockStateProperties.OPEN, true), 2));
try {
var entry = HeritagePiece20.locator(site);
Predicate<BlockPos> inside = p -> p.equals(entry) || p.getX() >= site.minX() && p.getX() <= site.maxX()
&& p.getZ() >= site.minZ() && p.getZ() <= site.maxZ() && p.getY() >= site.floorY() - 4 && p.getY() < site.maxY();
var reached = walk(helper, entry, inside); int targets = 0;
for (var target : HeritageRenderer20.targets(site)) {
var at = new BlockPos(site.minX() + target.x(), site.floorY() + target.y(), site.minZ() + target.z());
helper.assertTrue(reached.contains(at), "Site " + site.id() + " cannot reach " + target.name() + " at " + at); targets++;
}
return Map.of("id", site.id(), "kind", site.kind().name(), "entry", coords(entry), "standing_cells", reached.size(), "walkable_targets", targets);
} finally { doors.forEach((at, state) -> level.setBlock(at, state, 2)); }
}
private static List<Map<String, Object>> verifyLinks(GameTestHelper helper, HeritagePlan19 plan) {
helper.assertTrue(!plan.links().isEmpty(), "Reference island needs a real route between accepted destinations");
var entries = new HashMap<String, HeritagePlan19.Entry>(); plan.sites().forEach(s -> entries.put(s.id(), s.entry())); plan.landmarks().forEach(s -> entries.put(s.id(), s.entry()));
var cells = new HashMap<Long, HeritagePlan19.Cell>(); plan.segments().forEach(s -> s.cells().forEach(c -> cells.put(column(c.x(), c.z()), c)));
var result = new ArrayList<Map<String, Object>>();
for (var link : plan.links()) {
var from = entries.get(link.fromId()); var to = entries.get(link.toId());
helper.assertTrue(from != null && to != null, "Route points to an absent destination");
var first = link.path().getFirst(); var last = link.path().getLast();
helper.assertTrue(first.x() == from.x() && first.z() == from.z() && first.floorY() + 1 == from.y()
&& last.x() == to.x() && last.z() == to.z() && last.floorY() + 1 == to.y(), "Route does not meet its real entrances");
int present = 0, missing = 0;
for (int i = 0; i < link.path().size(); i++) {
var point = link.path().get(i); var at = new BlockPos(point.x(), point.floorY() + 1, point.z());
helper.assertTrue(standable(helper.getLevel(), at), "Ballast route " + link.id() + " is blocked at " + at);
if (i > 0) { var prior = link.path().get(i - 1); if (point.floorY() > prior.floorY()) helper.assertTrue(clearBody(helper.getLevel(), new BlockPos(prior.x(), prior.floorY() + 2, prior.z())), "Route slope cannot be climbed by a player"); }
var cell = cells.get(column(point.x(), point.z()));
if (cell != null && cell.rail() != HeritagePlan19.Rail.NONE) {
var actual = helper.getLevel().getBlockState(at);
if (cell.railPresent()) {
helper.assertTrue(actual.is(Blocks.RAIL), "Saved existing rail disappeared");
helper.assertTrue(actual.getValue(BlockStateProperties.RAIL_SHAPE).name().equals(cell.rail().name()), "Native rail update changed the intended slope or curve at " + at); present++;
} else { helper.assertTrue(!actual.is(Blocks.RAIL), "An intentionally missing rail regenerated"); missing++; }
}
}
int spanX = link.path().stream().mapToInt(HeritagePlan19.Point::x).max().orElseThrow() - link.path().stream().mapToInt(HeritagePlan19.Point::x).min().orElseThrow();
int spanZ = link.path().stream().mapToInt(HeritagePlan19.Point::z).max().orElseThrow() - link.path().stream().mapToInt(HeritagePlan19.Point::z).min().orElseThrow();
result.add(Map.of("id", link.id(), "from", link.fromId(), "to", link.toId(), "kind", link.kind().name(), "length", link.path().size(),
"span_x", spanX, "span_z", spanZ, "existing_rails", present, "deliberately_missing_rails", missing, "path", pathCoords(link)));
}
return result;
}
private static int routeSpan(HeritagePlan19.Link link) {
int spanX = link.path().stream().mapToInt(HeritagePlan19.Point::x).max().orElseThrow()
- link.path().stream().mapToInt(HeritagePlan19.Point::x).min().orElseThrow();
int spanZ = link.path().stream().mapToInt(HeritagePlan19.Point::z).max().orElseThrow()
- link.path().stream().mapToInt(HeritagePlan19.Point::z).min().orElseThrow();
return Math.max(spanX, spanZ);
}
private static List<List<Integer>> pathCoords(HeritagePlan19.Link link) {
return link.path().stream().map(point -> List.of(point.x(), point.floorY(), point.z())).toList();
}
private static int verifySiteBearing(GameTestHelper helper, SanctuaryChunkGenerator generator, HeritagePlan19 plan, HeritagePlan19.Site site) {
int checked = 0;
// The planner certifies a nine-point bearing grid, either directly below its slab or below a saved short footing.
for (int dx : new int[] {1, 8, 15}) for (int dz : new int[] {1, 8, 15}) {
int x = site.minX() + dx, z = site.minZ() + dz;
var support = site.supports().stream().filter(s -> s.x() == x && s.z() == z).findFirst();
int bottom = support.map(HeritagePlan19.Support::minY).orElse(site.minY());
for (int below = 1; below <= 2; below++) {
var at = new BlockPos(x, bottom - below, z); assertRock(helper, at);
helper.assertTrue(plan.protects(at.getX(), at.getY(), at.getZ()) && generator.protectsCityDecoration(at),
"Site's certified bearing layer is not published in the decoration mask: " + site.id() + " at " + at);
helper.assertTrue(!plan.contains(at.getX(), at.getY(), at.getZ()), "Original bearing rock became a writable heritage volume: " + at);
checked++;
}
}
return checked;
}
private static List<Map<String, Object>> verifyReadOnlyLandings(GameTestHelper helper, SanctuaryChunkGenerator generator, HeritagePlan19 plan) {
var used = new HashSet<String>(); plan.links().forEach(link -> { used.add(link.fromId()); used.add(link.toId()); });
var result = new ArrayList<Map<String, Object>>();
for (var landmark : plan.landmarks()) if (used.contains(landmark.id())) {
var e = landmark.entry(); int checked = 0;
for (int side = -1; side <= 1; side++) {
int x = e.x() + ((e.facing() & 1) == 0 ? side : 0), z = e.z() + ((e.facing() & 1) == 1 ? side : 0);
for (int y = e.y() - 2; y <= e.y() + 2; y++) {
var at = new BlockPos(x, y, z);
helper.assertTrue(plan.protects(x, y, z) && generator.protectsCityDecoration(at), "Read-only arrival lost its decoration mask: " + landmark.id() + " at " + at);
helper.assertTrue(!plan.contains(x, y, z), "Heritage gained write ownership of a historical arrival: " + landmark.id() + " at " + at);
if (y == e.y() - 1) assertRock(helper, at);
if (y >= e.y()) helper.assertTrue(helper.getLevel().getBlockState(at).isAir(), "Decoration obstructed a certified historical arrival: " + at);
checked++;
}
}
result.add(Map.of("id", landmark.id(), "entry", List.of(e.x(), e.y(), e.z()), "read_only_cells", checked));
}
return result;
}
private static void assertRock(GameTestHelper helper, BlockPos at) {
helper.assertTrue(helper.getLevel().getBlockState(at).isSolid() && helper.getLevel().getFluidState(at).isEmpty(), "Lost original rock bearing at " + at);
}
private static void verifyLootTables(GameTestHelper helper, Map<String, Object> report) {
var result = new ArrayList<Map<String, Object>>(); var level = helper.getLevel();
var params = new LootParams.Builder(level).withParameter(LootContextParams.ORIGIN, new Vec3(0, 160, 0)).create(LootContextParamSets.CHEST);
for (var kind : HeritagePlan19.Kind.values()) {
var table = level.getServer().reloadableRegistries().getLootTable(lootKey(kind));
helper.assertTrue(table != LootTable.EMPTY, "Heritage tool table did not load: " + kind);
for (long seed : new long[] {0, 42, -1}) helper.assertTrue(table.getRandomItems(params, seed).stream().anyMatch(stack -> stack.is(tool(kind))), "Tool table is empty or lacks its expected primary tool: " + kind);
result.add(Map.of("kind", kind.name(), "table", lootKey(kind).identifier().toString(), "sampled_seeds", List.of(0, 42, -1)));
}
report.put("tool_tables_sampled_separately", result);
}
private static ResourceKey<LootTable> lootKey(HeritagePlan19.Kind kind) { return ResourceKey.create(Registries.LOOT_TABLE, SanctuaryMod.id("chests/heritage19/" + kind.name().toLowerCase(Locale.ROOT))); }
private static Item tool(HeritagePlan19.Kind kind) {
return switch (kind) {
case OBSERVATORY -> Items.SPYGLASS;
case POST_OFFICE, EXPEDITION_OFFICE, RADIO_RELAY -> Items.COMPASS;
case SUBSTATION, IMPOSSIBLE_SERVICE -> Items.CLOCK;
case FREIGHT_STATION -> Items.MINECART;
case COMMUNAL_DEPOT -> Items.IRON_AXE;
case RAIL_WORKSHOP -> Items.IRON_PICKAXE;
case PUMP_STATION -> Items.BUCKET;
case GREENHOUSE -> Items.IRON_HOE;
case VOID_DOCK -> Items.LEAD;
case PHOTO_CLUB -> Items.BOOK;
case CONSTRUCTION_REFUGE -> Items.IRON_SHOVEL;
};
}
private static Set<BlockPos> walk(GameTestHelper helper, BlockPos start, Predicate<BlockPos> inside) {
var level = helper.getLevel(); helper.assertTrue(inside.test(start) && standable(level, start), "Blocked or unsupported exterior entrance: " + start);
var seen = new HashSet<BlockPos>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
while (!queue.isEmpty()) {
var pos = queue.removeFirst();
for (var direction : HORIZONTAL) for (int dy = -1; dy <= 1; dy++) {
var next = pos.relative(direction).offset(0, dy, 0);
if (!inside.test(next) || dy > 0 && !clearBody(level, pos.above())) continue;
if (standable(level, next) && seen.add(next)) queue.add(next);
}
for (int dy : new int[] {-1, 1}) {
var next = pos.offset(0, dy, 0); if (!inside.test(next)) continue;
if ((level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(next).is(Blocks.LADDER) || level.getBlockState(pos.below()).is(Blocks.LADDER))
&& standable(level, next) && seen.add(next)) queue.add(next);
}
helper.assertTrue(seen.size() <= 50_000, "Native site traversal escaped its bounds");
}
return seen;
}
private static boolean standable(ServerLevel level, BlockPos pos) {
if (!clearBody(level, pos)) return false;
if (level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(pos.below()).is(Blocks.LADDER)) return true;
var below = pos.below(); return level.getBlockState(below).getCollisionShape(level, below).toAabbs().stream()
.anyMatch(b -> b.maxY >= .5 && b.minX <= .5 && b.maxX >= .5 && b.minZ <= .5 && b.maxZ >= .5);
}
private static boolean clearBody(ServerLevel level, BlockPos pos) {
var body = new AABB(pos.getX() + .2, pos.getY(), pos.getZ() + .2, pos.getX() + .8, pos.getY() + 1.8, pos.getZ() + .8);
for (var at : new BlockPos[] {pos, pos.above()}) {
if (!level.getFluidState(at).isEmpty()) return false;
for (var shape : level.getBlockState(at).getCollisionShape(level, at).toAabbs()) if (shape.move(at.getX(), at.getY(), at.getZ()).intersects(body)) return false;
}
return true;
}
private static long column(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
private static List<Integer> coords(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
private static BlockPos readPos(com.google.gson.JsonArray value) { return new BlockPos(value.get(0).getAsInt(), value.get(1).getAsInt(), value.get(2).getAsInt()); }
private static Path world(ServerLevel level) { return level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize(); }
private static Path proof(ServerLevel level) { return world(level).resolve("heritage20-create-proof.json"); }
private static String planHash(HeritagePlan19 plan) { return hash(plan.sites().toString() + plan.landmarks() + plan.links() + plan.segments()); }
private static void write(ServerLevel level, Map<String, Object> report, boolean reload) throws IOException {
var path = Path.of("diagnostics", "heritage20-" + (reload ? "reload" : "create") + ".json"); Files.createDirectories(path.getParent());
var value = new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n"; Files.writeString(path, value); if (!reload) Files.writeString(proof(level), value);
}
private static String hash(String value) {
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8))); }
catch (NoSuchAlgorithmException impossible) { throw new AssertionError(impossible); }
}
}
@@ -0,0 +1,554 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mojang.serialization.JsonOps;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import fr.koka.sanctuary.worldgen.heritage.HeritagePiece21;
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19;
import fr.koka.sanctuary.worldgen.heritage.HeritageRenderer21;
import fr.koka.sanctuary.worldgen.heritage.HeritageStructures19;
import fr.koka.sanctuary.worldgen.ruins.RuinsPiece20;
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17;
import fr.koka.sanctuary.worldgen.ruins.RuinsRenderer20;
import fr.koka.sanctuary.worldgen.ruins.RuinsStructures17;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderSet;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.RandomSource;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.DoorBlock;
import net.minecraft.world.level.block.TrapDoorBlock;
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.LevelResource;
import net.minecraft.world.level.storage.loot.LootParams;
import net.minecraft.world.level.storage.loot.LootTable;
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
/** Native creation/reload proof confined to a disposable generation21 starter island. */
public final class Heritage21WorldGameTests {
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
@GameTest(maxTicks = 12000)
public void heritagePersistsWithRealDestinations(GameTestHelper helper) {
helper.assertTrue(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator,
"The heritage proof requires a Sanctuary generator");
var generator = (SanctuaryChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
helper.assertTrue(generator.rootGeneration() == 21, "Use a fresh generation21 test world; do not convert an old save");
new Scenario(helper, generator, Boolean.getBoolean("sanctuary.test.expansionReload")).advance();
}
private static final class Scenario {
final GameTestHelper helper;
final ServerLevel level;
final SanctuaryChunkGenerator generator;
final HeritagePlan19 plan;
final boolean reload;
final Map<String, Object> report = new LinkedHashMap<>();
final List<ChunkPos> chunks = new ArrayList<>();
final List<StructureStart> starts = new ArrayList<>();
final List<StructureStart> ruinsStarts = new ArrayList<>();
JsonObject previous;
BlockPos gold;
int nextChunk;
boolean prepared;
Scenario(GameTestHelper helper, SanctuaryChunkGenerator generator, boolean reload) {
this.helper = helper; this.level = helper.getLevel(); this.generator = generator; this.reload = reload;
this.plan = generator.heritagePlan19();
report.put("seed", level.getSeed()); report.put("size", generator.initialDiameter());
report.put("process_id", ProcessHandle.current().pid()); report.put("world_directory", world(level).toString());
report.put("experimental_structures", generator.experimentalStructures());
report.put("structures_enabled", level.structureManager().shouldGenerateStructures());
report.put("scope", "Native local starts, saved sites and routes, player-sized entrances, supports and unopened chests on the starter island. "
+ "All14 tool tables are sampled separately without consuming a saved chest. Door states are restored after traversal. "
+ "No personal save or expansion is opened; one disposable outer-wall edit is checked in a second JVM. "
+ "Missing rails are intentional; this proof checks walking and saved rail shapes, not an uninterrupted minecart journey.");
}
void advance() {
try {
if (!prepared) { report.put("stage", "prepare"); prepare(); prepared = true; if (plan.isEmpty()) { finishDisabled(); return; } }
if (nextChunk < chunks.size()) {
report.put("stage", "load_full_chunks"); var pos = chunks.get(nextChunk++);
helper.assertTrue(level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true) instanceof LevelChunk,
"Heritage footprint must reach native FULL: " + pos);
if (nextChunk % 20 == 0) SanctuaryMod.LOGGER.info("Heritage21 loaded {}/{} footprint chunks", nextChunk, chunks.size());
helper.runAtTickTime(helper.getTick() + 1, this::advance); return;
}
report.put("stage", "verify_native_geometry"); verifyPlaced(); report.put("passed", true);
if (!reload) {
helper.assertTrue(gold != null && level.getBlockState(gold).isCollisionShapeFullBlock(level, gold), "Gold witness must replace a solid wall away from circulation");
level.setBlock(gold, Blocks.GOLD_BLOCK.defaultBlockState(), 3); report.put("gold_witness", coords(gold));
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush native starts, unopened loot and the wall edit");
report.put("world_flushed_before_shutdown", true);
} else {
helper.assertTrue(level.getBlockState(gold).is(Blocks.GOLD_BLOCK), "Reload replaced the player's wall edit");
report.put("gold_witness", coords(gold)); report.put("gold_witness_preserved", true);
}
write(level, report, reload); helper.succeed();
} catch (Exception failure) {
report.put("passed", false); report.put("failure", failure.toString());
try { write(level, report, reload); } catch (IOException ignored) { }
throw new IllegalStateException("Heritage21 native " + (reload ? "reload" : "creation") + " failed", failure);
}
}
void prepare() throws IOException {
var ops = RegistryOps.create(JsonOps.INSTANCE, level.registryAccess());
var encoded = ChunkGenerator.CODEC.encodeStart(ops, generator).getOrThrow();
var decoded = ChunkGenerator.CODEC.parse(ops, encoded).getOrThrow();
helper.assertTrue(decoded instanceof SanctuaryChunkGenerator copy && copy.rootGeneration() == 21
&& copy.experimentalStructures() == generator.experimentalStructures(), "Generation21 codec lost its experimental switch");
report.put("generator_codec", encoded); report.put("plan_sha256", planHash(plan));
report.put("planned_sites", plan.sites().stream().map(site -> Map.of("id", site.id(), "kind", site.kind().name(),
"entry", coords(HeritagePiece21.locator(site)), "floor_y", site.floorY(), "foundation_depth", site.foundationDepth(), "footings", site.supports().size())).toList());
report.put("planned_links", plan.links().stream().map(link -> Map.of("id", link.id(), "from", link.fromId(), "to", link.toId(), "kind", link.kind().name(), "length", link.path().size(), "path", pathCoords(link))).toList());
report.put("planning_samples", plan.samples()); report.put("planning_columns", plan.columns()); report.put("planning_diagnostics", plan.diagnostics());
helper.assertTrue(plan.samples() <= HeritagePlan19.MAX_SAMPLES && plan.columns() <= HeritagePlan19.MAX_COLUMNS, "Heritage planning exceeded its budget");
verifyLootTables(helper, report);
if (reload) {
previous = JsonParser.parseString(Files.readString(proof(level))).getAsJsonObject();
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("world_flushed_before_shutdown").getAsBoolean(), "Reload needs a successful flushed creation proof");
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload must use a second JVM");
helper.assertTrue(previous.get("seed").getAsLong() == level.getSeed() && previous.get("size").getAsInt() == generator.initialDiameter()
&& previous.get("world_directory").getAsString().equals(world(level).toString()), "Reload selected another world");
helper.assertTrue(previous.get("generator_codec").equals(encoded) && previous.get("plan_sha256").getAsString().equals(planHash(plan)), "Saved settings or planned identities changed on reload");
if (previous.has("gold_witness")) gold = readPos(previous.getAsJsonArray("gold_witness"));
}
if (!generator.experimentalStructures() || !level.structureManager().shouldGenerateStructures()) {
helper.assertTrue(plan.isEmpty() && plan.segments().isEmpty() && plan.links().isEmpty(), "Disabled generation published heritage geometry"); return;
}
helper.assertTrue(!plan.isEmpty(), "Enabled reference island has no heritage site");
report.put("present_site_kinds", plan.sites().stream().map(s -> s.kind().name()).distinct().sorted().toList());
report.put("missing_site_kinds", java.util.Arrays.stream(HeritagePlan19.Kind.values()).filter(k -> plan.sites().stream().noneMatch(s -> s.kind() == k)).map(Enum::name).toList());
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
var origins = new HashSet<ChunkPos>(); var uniqueChunks = new HashSet<ChunkPos>();
plan.sites().forEach(s -> origins.add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
plan.segments().forEach(s -> origins.add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
for (var origin : origins.stream().sorted(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList()) {
var chunk = level.getChunk(origin.x(), origin.z(), ChunkStatus.STRUCTURE_STARTS, true);
var start = chunk.getStartForStructure(structure.value());
helper.assertTrue(start != null && start.isValid(), "Accepted site/route has no native start at " + origin); starts.add(start);
for (var piece : start.getPieces()) {
helper.assertTrue(piece instanceof HeritagePiece21, "Heritage loaded a foreign piece type");
var box = piece.getBoundingBox();
helper.assertTrue(Math.abs((long) (box.minX() >> 4) - origin.x()) <= 7 && Math.abs((long) (box.maxX() >> 4) - origin.x()) <= 7
&& Math.abs((long) (box.minZ() >> 4) - origin.z()) <= 7 && Math.abs((long) (box.maxZ() >> 4) - origin.z()) <= 7, "Native start references a distant route endpoint");
box.intersectingChunks().forEach(uniqueChunks::add);
}
}
for (var site : plan.sites()) uniqueChunks.add(ChunkPos.containing(HeritagePiece21.locator(site)));
for (var link : plan.links()) for (var point : link.path()) uniqueChunks.add(new ChunkPos(point.x() >> 4, point.z() >> 4));
helper.assertTrue(uniqueChunks.size() <= 1024, "Bounded proof footprint exceeds1024 chunks");
var ruins = generator.ruinsPlan17();
report.put("ruins_plan_sha256", hash(ruins.networks().toString()));
if (reload) helper.assertTrue(previous.get("ruins_plan_sha256").getAsString().equals(hash(ruins.networks().toString())), "Reload changed forgotten room planning");
helper.assertTrue(!ruins.isEmpty(), "Reference20 must demonstrate native experimental room architecture");
var ruinsType = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(RuinsStructures17.KEY);
var ruinsOrigins = new HashSet<ChunkPos>();
for (var network : ruins.networks()) {
var origin = new ChunkPos(network.startChunkX(), network.startChunkZ()); if (!ruinsOrigins.add(origin)) continue;
var nativeStart = level.getChunk(origin.x(), origin.z(), ChunkStatus.STRUCTURE_STARTS, true).getStartForStructure(ruinsType.value());
helper.assertTrue(nativeStart != null && nativeStart.isValid(), "Missing native20 forgotten network: " + network.id());
ruinsStarts.add(nativeStart);
for (var piece : nativeStart.getPieces()) {
helper.assertTrue(piece instanceof RuinsPiece20, "Generation21 loaded a historical forgotten piece");
piece.getBoundingBox().intersectingChunks().forEach(uniqueChunks::add);
}
}
chunks.addAll(uniqueChunks.stream().sorted(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList());
}
void finishDisabled() throws IOException {
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
var start = level.getChunk(0, 0, ChunkStatus.STRUCTURE_STARTS, true).getStartForStructure(structure.value());
helper.assertTrue(start == null || !start.isValid(), "Disabled heritage emitted a native start");
helper.assertTrue(generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false) == null, "Disabled heritage is locatable");
report.put("empty_plan_verified", true); report.put("passed", true);
if (!reload) { helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save disabled experiment setting"); report.put("world_flushed_before_shutdown", true); }
write(level, report, reload); helper.succeed();
}
void verifyPlaced() {
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
var context = StructurePieceSerializationContext.fromLevel(level); var summaries = new ArrayList<Map<String, Object>>();
int blocks = 0, chests = 0, pieces = 0, supports = 0;
var connections = NativeConnections20.Stats.empty();
for (var start : starts) {
var origin = start.getChunkPos(); var saved = start.createTag(context, origin); String nativeHash = hash(saved.toString());
var copy = StructureStart.loadStaticStart(context, saved, level.getSeed());
helper.assertTrue(copy != null && copy.isValid() && copy.createTag(context, origin).equals(saved), "Native start failed an exact NBT roundtrip");
var expectedPayloads = new HashSet<net.minecraft.nbt.CompoundTag>();
plan.sites().stream().filter(s -> s.startChunkX() == origin.x() && s.startChunkZ() == origin.z()).forEach(s -> expectedPayloads.add(HeritagePiece21.encodeSite(s)));
plan.segments().stream().filter(s -> s.startChunkX() == origin.x() && s.startChunkZ() == origin.z()).forEach(s -> expectedPayloads.add(HeritagePiece21.encodeSegment(s)));
var actualPayloads = new HashSet<net.minecraft.nbt.CompoundTag>();
if (reload) {
JsonObject prior = null;
for (var value : previous.getAsJsonArray("starts")) { var item = value.getAsJsonObject(); var at = item.getAsJsonArray("start_chunk"); if (at.get(0).getAsInt() == origin.x() && at.get(1).getAsInt() == origin.z()) prior = item; }
helper.assertTrue(prior != null && prior.get("native_start_sha256").getAsString().equals(nativeHash), "Reload changed saved geometry or native reference count");
}
var loot = new ArrayList<Map<String, Object>>();
for (var value : start.getPieces()) {
var piece = (HeritagePiece21) value; pieces++;
var tag = piece.createTag(context); var restored = new HeritagePiece21(context, tag);
helper.assertTrue(restored.createTag(context).equals(tag), "Saved piece parameters changed on native load");
actualPayloads.add(tag.getCompound("SanctuaryHeritage").orElseThrow());
var corrupt = tag.copy(); corrupt.getCompound("SanctuaryHeritage").orElseThrow().putInt("Schema", 22);
boolean rejected = false; try { new HeritagePiece21(context, corrupt); } catch (IllegalArgumentException expected) { rejected = true; }
helper.assertTrue(rejected, "Unknown schema must fail closed");
piece.getBoundingBox().intersectingChunks().forEach(at -> helper.assertTrue(level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true)
.getReferencesForStructure(structure.value()).contains(origin.pack()), "FULL chunk lost its native structure reference"));
var expected = rendered(piece); blocks += expected.blocks().size();
for (var entry : expected.blocks().entrySet()) {
var at = entry.getKey(); var wanted = entry.getValue(); helper.assertTrue(piece.getBoundingBox().isInside(at), "Renderer escaped saved bounds");
if (reload && at.equals(gold)) continue;
helper.assertTrue(level.getBlockState(at).is(wanted.getBlock()), "Clipping or decoration changed heritage at " + at + ": expected " + wanted + " got " + level.getBlockState(at));
helper.assertTrue(!wanted.is(Blocks.SPAWNER), "Heritage must not create spawners");
}
connections = connections.add(NativeConnections20.verify(helper, expected.blocks(), reload ? Set.of(gold) : Set.of()));
if (piece.isSite()) {
supports += verifySiteBearing(helper, generator, plan, piece.site());
if (!reload && gold == null) {
gold = expected.blocks().entrySet().stream().filter(e -> isOuterWall(piece.site(), e.getKey()) && e.getValue().isCollisionShapeFullBlock(level, e.getKey()))
.map(Map.Entry::getKey).sorted(Comparator.<BlockPos>comparingInt(p -> p.getY()).thenComparingInt(p -> p.getX()).thenComparingInt(p -> p.getZ())).findFirst().orElse(null);
}
} else for (var cell : piece.segment().cells()) { supports++; assertRock(helper, new BlockPos(cell.x(), cell.minY() - 1, cell.z())); }
for (var chest : expected.chests()) {
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Deferred chest is missing");
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
helper.assertTrue(entity.getLootTable() != null && entity.getLootTable().equals(chest.table()) && entity.getLootTableSeed() == chest.seed(), "Deferred loot table/seed was lost or consumed");
if (piece.isSite()) helper.assertTrue(chest.table().equals(lootKey(piece.site().kind())), "Site uses another kind's tool table");
loot.add(Map.of("position", coords(chest.position()), "table", chest.table().identifier().toString(), "seed", chest.seed())); chests++;
}
}
helper.assertTrue(actualPayloads.equals(expectedPayloads) && start.getPieces().size() == expectedPayloads.size(), "Native start lost or duplicated planned pieces");
summaries.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", nativeHash, "pieces", start.getPieces().size(), "unopened_chests", loot));
}
report.put("connected_blocks", connections);
helper.assertTrue(connections.panes() > 0 && connections.stairs() > 0 && connections.nativeOracles() > 0,
"Reference20 needs real panes/stairs validated by vanilla neighbor rules");
if (level.getSeed() == 0 && generator.initialDiameter() == 724)
helper.assertTrue(connections.crossChunkPaneLinks() > 0, "Reference20 must demonstrate a real pane join across a chunk boundary");
verifyRuins();
report.put("starts", summaries); report.put("native_pieces", pieces); report.put("native_blocks_checked", blocks);
report.put("full_chunks_checked", chunks.size()); report.put("rock_supports_checked", supports); report.put("unopened_deferred_chests", chests);
report.put("stage", "verify_site_routes"); var sites = new ArrayList<Map<String, Object>>(); for (var site : plan.sites()) sites.add(verifyAccess(helper, site)); report.put("native_sites", sites);
report.put("stage", "verify_read_only_landings");
report.put("read_only_landmarks", verifyReadOnlyLandings(helper, generator, plan));
report.put("stage", "verify_island_routes"); report.put("native_links", verifyLinks(helper, plan));
int greatestSpan = plan.links().stream().mapToInt(Heritage21WorldGameTests::routeSpan).max().orElse(0);
report.put("greatest_single_route_span", greatestSpan);
var reachedIds = new HashSet<String>(); plan.links().forEach(l -> { reachedIds.add(l.fromId()); reachedIds.add(l.toId()); });
report.put("connected_destination_ids", reachedIds.stream().sorted().toList());
report.put("isolated_destination_ids", plan.sites().stream().map(HeritagePlan19.Site::id).filter(id -> !reachedIds.contains(id)).toList());
report.put("connected_city_arrivals", plan.landmarks().stream().filter(l -> l.kind() == HeritagePlan19.LandmarkKind.CITY && reachedIds.contains(l.id())).map(HeritagePlan19.Landmark::id).toList());
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
helper.assertTrue(located != null && located.getSecond().is(HeritageStructures19.KEY) && plan.sites().stream().anyMatch(s -> HeritagePiece21.locator(s).equals(located.getFirst())), "Locate must lead to a real exterior site entry");
report.put("locate", coords(located.getFirst()));
}
void verifyRuins() {
var context = StructurePieceSerializationContext.fromLevel(level);
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(RuinsStructures17.KEY);
var summaries = new ArrayList<Map<String, Object>>(); int count = 0, blockCount = 0, lootCount = 0;
var connections = NativeConnections20.Stats.empty();
for (var start : ruinsStarts) {
var origin = start.getChunkPos(); var saved = start.createTag(context, origin); var digest = hash(saved.toString());
var loaded = StructureStart.loadStaticStart(context, saved, level.getSeed());
helper.assertTrue(loaded != null && loaded.createTag(context, origin).equals(saved), "Forgotten20 native start NBT changed");
var expectedRooms = generator.ruinsPlan17().networks().stream().filter(n -> n.startChunkX() == origin.x() && n.startChunkZ() == origin.z()).flatMap(n -> n.rooms().stream()).toList();
var actualRooms = start.getPieces().stream().map(RuinsPiece20.class::cast).map(RuinsPiece20::room).toList();
helper.assertTrue(expectedRooms.size() == actualRooms.size() && new HashSet<>(expectedRooms).equals(new HashSet<>(actualRooms)), "Forgotten20 native rooms differ from plan");
if (reload) {
JsonObject prior = null;
for (var value : previous.getAsJsonArray("ruins_starts")) { var item = value.getAsJsonObject(); var at = item.getAsJsonArray("start_chunk"); if (at.get(0).getAsInt() == origin.x() && at.get(1).getAsInt() == origin.z()) prior = item; }
helper.assertTrue(prior != null && prior.get("native_start_sha256").getAsString().equals(digest), "Forgotten20 native start changed on a second JVM load");
}
for (var raw : start.getPieces()) {
var piece = (RuinsPiece20) raw; var room = piece.room(); count++;
var tag = piece.createTag(context); var roundtrip = new RuinsPiece20(context, tag);
helper.assertTrue(roundtrip.room().equals(room) && roundtrip.createTag(context).equals(tag), "Forgotten20 piece lost saved parameters");
piece.getBoundingBox().intersectingChunks().forEach(at -> helper.assertTrue(level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true).getReferencesForStructure(registry.value()).contains(origin.pack()), "Forgotten20 chunk lost native references"));
var expected = rendered(room); blockCount += expected.blocks().size();
for (var entry : expected.blocks().entrySet()) {
helper.assertTrue(piece.getBoundingBox().isInside(entry.getKey()), "Forgotten20 renderer escaped saved bounds");
helper.assertTrue(level.getBlockState(entry.getKey()).is(entry.getValue().getBlock()), "Forgotten20 native geometry changed at " + entry.getKey());
}
connections = connections.add(NativeConnections20.verify(helper, expected.blocks(), Set.of()));
for (var support : room.supports()) assertRock(helper, new BlockPos(support.x(), support.minY() - 1, support.z()));
for (var chest : expected.chests()) {
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Forgotten20 missing deferred chest");
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
helper.assertTrue(chest.table().equals(entity.getLootTable()) && chest.seed() == entity.getLootTableSeed(), "Forgotten20 consumed/changed saved loot"); lootCount++;
}
}
summaries.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", digest, "pieces", start.getPieces().size()));
}
report.put("ruins_starts", summaries); report.put("ruins_native_pieces", count); report.put("ruins_native_blocks_checked", blockCount);
report.put("ruins_unopened_deferred_chests", lootCount); report.put("ruins_connections", connections);
report.put("ruins_access", generator.ruinsPlan17().networks().stream().map(n -> verifyAccess(helper, n)).toList());
}
}
private record Chest(BlockPos position, ResourceKey<LootTable> table, long seed) {}
private record Rendered(Map<BlockPos, BlockState> blocks, List<Chest> chests) {}
private static Rendered rendered(HeritagePiece21 piece) {
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
int x0 = piece.isSite() ? piece.site().minX() : piece.segment().minX();
int y0 = piece.isSite() ? piece.site().floorY() : piece.segment().minY();
int z0 = piece.isSite() ? piece.site().minZ() : piece.segment().minZ();
var writer = new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(new BlockPos(x0 + x, y0 + y, z0 + z), state); }
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
var at = new BlockPos(x0 + x, y0 + y, z0 + z); blocks.put(at, Blocks.CHEST.defaultBlockState()); chests.add(new Chest(at, table, RandomSource.create(seed).nextLong()));
}
};
if (piece.isSite()) HeritageRenderer21.render(piece.site(), writer); else HeritageRenderer21.renderSegment(piece.segment(), writer);
return new Rendered(blocks, chests);
}
private static Rendered rendered(RuinsPlan17.Room room) {
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
RuinsRenderer20.render(room, new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(at(room, x, y, z), state); }
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
var position = at(room, x, y, z); blocks.put(position, Blocks.CHEST.defaultBlockState()); chests.add(new Chest(position, table, RandomSource.create(seed).nextLong()));
}
});
return new Rendered(blocks, chests);
}
private static Map<String, Object> verifyAccess(GameTestHelper helper, RuinsPlan17.Network network) {
var level = helper.getLevel(); var access = network.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS).findFirst().orElseThrow();
var hatches = new HashMap<BlockPos, BlockState>();
rendered(access).blocks().forEach((at, state) -> { if (state.is(Blocks.DARK_OAK_TRAPDOOR)) hatches.put(at, level.getBlockState(at)); });
helper.assertTrue(hatches.size() == 1, "Each network must have one usable wooden surface hatch");
hatches.forEach((at, state) -> {
helper.assertTrue(state.is(Blocks.DARK_OAK_TRAPDOOR) && state.canSurvive(level, at), "Surface hatch is missing its native support");
level.setBlock(at, state.setValue(BlockStateProperties.OPEN, true), 2);
});
try {
Predicate<BlockPos> inside = pos -> network.rooms().stream().anyMatch(r -> r.contains(pos.getX(), pos.getY(), pos.getZ()));
var start = RuinsPiece20.locator(access); var reached = walk(helper, start, inside);
int targets = 0;
for (var room : network.rooms()) for (var target : RuinsRenderer20.targets(room)) {
var point = at(room, target.x(), target.y(), target.z());
helper.assertTrue(reached.contains(point), "Native network " + network.id() + " cannot reach " + room.kind() + "/" + target.name() + " at " + point);
targets++;
}
for (var hatch : hatches.keySet()) helper.assertTrue(reached.contains(hatch), "Surface approach cannot enter its ladder shaft");
return Map.of("id", network.id(), "family", network.family().name(), "rooms", network.rooms().size(),
"entrance", coords(start), "walkable_targets", targets, "standing_cells", reached.size());
} finally { hatches.forEach((at, state) -> level.setBlock(at, state, 2)); }
}
private static BlockPos at(RuinsPlan17.Room room, int x, int y, int z) { return new BlockPos(room.minX() + x, room.minY() + y, room.minZ() + z); }
private static boolean isOuterWall(HeritagePlan19.Site site, BlockPos pos) {
return pos.getY() >= site.floorY() + 2 && pos.getY() <= site.floorY() + 3
&& (pos.getX() == site.minX() + 2 || pos.getX() == site.maxX() - 2 || pos.getZ() == site.minZ() + 2 || pos.getZ() == site.maxZ() - 2)
&& Math.abs(pos.getX() - site.entry().x()) + Math.abs(pos.getZ() - site.entry().z()) > 6;
}
private static Map<String, Object> verifyAccess(GameTestHelper helper, HeritagePlan19.Site site) {
var level = helper.getLevel(); var expected = rendered(new HeritagePiece21(site)); var doors = new HashMap<BlockPos, BlockState>();
expected.blocks().forEach((at, state) -> {
if ((state.getBlock() instanceof DoorBlock || state.getBlock() instanceof TrapDoorBlock) && state.hasProperty(BlockStateProperties.OPEN)) doors.put(at, level.getBlockState(at));
});
doors.forEach((at, state) -> level.setBlock(at, state.setValue(BlockStateProperties.OPEN, true), 2));
try {
var entry = HeritagePiece21.locator(site);
Predicate<BlockPos> inside = p -> p.equals(entry) || p.getX() >= site.minX() && p.getX() <= site.maxX()
&& p.getZ() >= site.minZ() && p.getZ() <= site.maxZ() && p.getY() >= site.floorY() - 4 && p.getY() < site.maxY();
var reached = walk(helper, entry, inside); int targets = 0;
for (var target : HeritageRenderer21.targets(site)) {
var at = new BlockPos(site.minX() + target.x(), site.floorY() + target.y(), site.minZ() + target.z());
helper.assertTrue(reached.contains(at), "Site " + site.id() + " cannot reach " + target.name() + " at " + at); targets++;
}
return Map.of("id", site.id(), "kind", site.kind().name(), "entry", coords(entry), "standing_cells", reached.size(), "walkable_targets", targets);
} finally { doors.forEach((at, state) -> level.setBlock(at, state, 2)); }
}
private static List<Map<String, Object>> verifyLinks(GameTestHelper helper, HeritagePlan19 plan) {
helper.assertTrue(!plan.links().isEmpty(), "Reference island needs a real route between accepted destinations");
var entries = new HashMap<String, HeritagePlan19.Entry>(); plan.sites().forEach(s -> entries.put(s.id(), s.entry())); plan.landmarks().forEach(s -> entries.put(s.id(), s.entry()));
var cells = new HashMap<Long, HeritagePlan19.Cell>(); plan.segments().forEach(s -> s.cells().forEach(c -> cells.put(column(c.x(), c.z()), c)));
var result = new ArrayList<Map<String, Object>>();
for (var link : plan.links()) {
var from = entries.get(link.fromId()); var to = entries.get(link.toId());
helper.assertTrue(from != null && to != null, "Route points to an absent destination");
var first = link.path().getFirst(); var last = link.path().getLast();
helper.assertTrue(first.x() == from.x() && first.z() == from.z() && first.floorY() + 1 == from.y()
&& last.x() == to.x() && last.z() == to.z() && last.floorY() + 1 == to.y(), "Route does not meet its real entrances");
int present = 0, missing = 0;
for (int i = 0; i < link.path().size(); i++) {
var point = link.path().get(i); var at = new BlockPos(point.x(), point.floorY() + 1, point.z());
helper.assertTrue(standable(helper.getLevel(), at), "Ballast route " + link.id() + " is blocked at " + at);
if (i > 0) { var prior = link.path().get(i - 1); if (point.floorY() > prior.floorY()) helper.assertTrue(clearBody(helper.getLevel(), new BlockPos(prior.x(), prior.floorY() + 2, prior.z())), "Route slope cannot be climbed by a player"); }
var cell = cells.get(column(point.x(), point.z()));
if (cell != null && cell.rail() != HeritagePlan19.Rail.NONE) {
var actual = helper.getLevel().getBlockState(at);
if (cell.railPresent()) {
helper.assertTrue(actual.is(Blocks.RAIL), "Saved existing rail disappeared");
helper.assertTrue(actual.getValue(BlockStateProperties.RAIL_SHAPE).name().equals(cell.rail().name()), "Native rail update changed the intended slope or curve at " + at); present++;
} else { helper.assertTrue(!actual.is(Blocks.RAIL), "An intentionally missing rail regenerated"); missing++; }
}
}
int spanX = link.path().stream().mapToInt(HeritagePlan19.Point::x).max().orElseThrow() - link.path().stream().mapToInt(HeritagePlan19.Point::x).min().orElseThrow();
int spanZ = link.path().stream().mapToInt(HeritagePlan19.Point::z).max().orElseThrow() - link.path().stream().mapToInt(HeritagePlan19.Point::z).min().orElseThrow();
result.add(Map.of("id", link.id(), "from", link.fromId(), "to", link.toId(), "kind", link.kind().name(), "length", link.path().size(),
"span_x", spanX, "span_z", spanZ, "existing_rails", present, "deliberately_missing_rails", missing, "path", pathCoords(link)));
}
return result;
}
private static int routeSpan(HeritagePlan19.Link link) {
int spanX = link.path().stream().mapToInt(HeritagePlan19.Point::x).max().orElseThrow()
- link.path().stream().mapToInt(HeritagePlan19.Point::x).min().orElseThrow();
int spanZ = link.path().stream().mapToInt(HeritagePlan19.Point::z).max().orElseThrow()
- link.path().stream().mapToInt(HeritagePlan19.Point::z).min().orElseThrow();
return Math.max(spanX, spanZ);
}
private static List<List<Integer>> pathCoords(HeritagePlan19.Link link) {
return link.path().stream().map(point -> List.of(point.x(), point.floorY(), point.z())).toList();
}
private static int verifySiteBearing(GameTestHelper helper, SanctuaryChunkGenerator generator, HeritagePlan19 plan, HeritagePlan19.Site site) {
int checked = 0;
// The planner certifies a nine-point bearing grid, either directly below its slab or below a saved short footing.
for (int dx : new int[] {1, 8, 15}) for (int dz : new int[] {1, 8, 15}) {
int x = site.minX() + dx, z = site.minZ() + dz;
var support = site.supports().stream().filter(s -> s.x() == x && s.z() == z).findFirst();
int bottom = support.map(HeritagePlan19.Support::minY).orElse(site.minY());
for (int below = 1; below <= 2; below++) {
var at = new BlockPos(x, bottom - below, z); assertRock(helper, at);
helper.assertTrue(plan.protects(at.getX(), at.getY(), at.getZ()) && generator.protectsCityDecoration(at),
"Site's certified bearing layer is not published in the decoration mask: " + site.id() + " at " + at);
helper.assertTrue(!plan.contains(at.getX(), at.getY(), at.getZ()), "Original bearing rock became a writable heritage volume: " + at);
checked++;
}
}
return checked;
}
private static List<Map<String, Object>> verifyReadOnlyLandings(GameTestHelper helper, SanctuaryChunkGenerator generator, HeritagePlan19 plan) {
var used = new HashSet<String>(); plan.links().forEach(link -> { used.add(link.fromId()); used.add(link.toId()); });
var result = new ArrayList<Map<String, Object>>();
for (var landmark : plan.landmarks()) if (used.contains(landmark.id())) {
var e = landmark.entry(); int checked = 0;
for (int side = -1; side <= 1; side++) {
int x = e.x() + ((e.facing() & 1) == 0 ? side : 0), z = e.z() + ((e.facing() & 1) == 1 ? side : 0);
for (int y = e.y() - 2; y <= e.y() + 2; y++) {
var at = new BlockPos(x, y, z);
helper.assertTrue(plan.protects(x, y, z) && generator.protectsCityDecoration(at), "Read-only arrival lost its decoration mask: " + landmark.id() + " at " + at);
helper.assertTrue(!plan.contains(x, y, z), "Heritage gained write ownership of a historical arrival: " + landmark.id() + " at " + at);
if (y == e.y() - 1) assertRock(helper, at);
if (y >= e.y()) helper.assertTrue(helper.getLevel().getBlockState(at).isAir(), "Decoration obstructed a certified historical arrival: " + at);
checked++;
}
}
result.add(Map.of("id", landmark.id(), "entry", List.of(e.x(), e.y(), e.z()), "read_only_cells", checked));
}
return result;
}
private static void assertRock(GameTestHelper helper, BlockPos at) {
helper.assertTrue(helper.getLevel().getBlockState(at).isSolid() && helper.getLevel().getFluidState(at).isEmpty(), "Lost original rock bearing at " + at);
}
private static void verifyLootTables(GameTestHelper helper, Map<String, Object> report) {
var result = new ArrayList<Map<String, Object>>(); var level = helper.getLevel();
var params = new LootParams.Builder(level).withParameter(LootContextParams.ORIGIN, new Vec3(0, 160, 0)).create(LootContextParamSets.CHEST);
for (var kind : HeritagePlan19.Kind.values()) {
var table = level.getServer().reloadableRegistries().getLootTable(lootKey(kind));
helper.assertTrue(table != LootTable.EMPTY, "Heritage tool table did not load: " + kind);
for (long seed : new long[] {0, 42, -1}) helper.assertTrue(table.getRandomItems(params, seed).stream().anyMatch(stack -> stack.is(tool(kind))), "Tool table is empty or lacks its expected primary tool: " + kind);
result.add(Map.of("kind", kind.name(), "table", lootKey(kind).identifier().toString(), "sampled_seeds", List.of(0, 42, -1)));
}
report.put("tool_tables_sampled_separately", result);
}
private static ResourceKey<LootTable> lootKey(HeritagePlan19.Kind kind) { return ResourceKey.create(Registries.LOOT_TABLE, SanctuaryMod.id("chests/heritage19/" + kind.name().toLowerCase(Locale.ROOT))); }
private static Item tool(HeritagePlan19.Kind kind) {
return switch (kind) {
case OBSERVATORY -> Items.SPYGLASS;
case POST_OFFICE, EXPEDITION_OFFICE, RADIO_RELAY -> Items.COMPASS;
case SUBSTATION, IMPOSSIBLE_SERVICE -> Items.CLOCK;
case FREIGHT_STATION -> Items.MINECART;
case COMMUNAL_DEPOT -> Items.IRON_AXE;
case RAIL_WORKSHOP -> Items.IRON_PICKAXE;
case PUMP_STATION -> Items.BUCKET;
case GREENHOUSE -> Items.IRON_HOE;
case VOID_DOCK -> Items.LEAD;
case PHOTO_CLUB -> Items.BOOK;
case CONSTRUCTION_REFUGE -> Items.IRON_SHOVEL;
};
}
private static Set<BlockPos> walk(GameTestHelper helper, BlockPos start, Predicate<BlockPos> inside) {
var level = helper.getLevel(); helper.assertTrue(inside.test(start) && standable(level, start), "Blocked or unsupported exterior entrance: " + start);
var seen = new HashSet<BlockPos>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
while (!queue.isEmpty()) {
var pos = queue.removeFirst();
for (var direction : HORIZONTAL) for (int dy = -1; dy <= 1; dy++) {
var next = pos.relative(direction).offset(0, dy, 0);
if (!inside.test(next) || dy > 0 && !clearBody(level, pos.above())) continue;
if (standable(level, next) && seen.add(next)) queue.add(next);
}
for (int dy : new int[] {-1, 1}) {
var next = pos.offset(0, dy, 0); if (!inside.test(next)) continue;
if ((level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(next).is(Blocks.LADDER) || level.getBlockState(pos.below()).is(Blocks.LADDER))
&& standable(level, next) && seen.add(next)) queue.add(next);
}
helper.assertTrue(seen.size() <= 50_000, "Native site traversal escaped its bounds");
}
return seen;
}
private static boolean standable(ServerLevel level, BlockPos pos) {
if (!clearBody(level, pos)) return false;
if (level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(pos.below()).is(Blocks.LADDER)) return true;
var below = pos.below(); return level.getBlockState(below).getCollisionShape(level, below).toAabbs().stream()
.anyMatch(b -> b.maxY >= .5 && b.minX <= .5 && b.maxX >= .5 && b.minZ <= .5 && b.maxZ >= .5);
}
private static boolean clearBody(ServerLevel level, BlockPos pos) {
var body = new AABB(pos.getX() + .2, pos.getY(), pos.getZ() + .2, pos.getX() + .8, pos.getY() + 1.8, pos.getZ() + .8);
for (var at : new BlockPos[] {pos, pos.above()}) {
if (!level.getFluidState(at).isEmpty()) return false;
for (var shape : level.getBlockState(at).getCollisionShape(level, at).toAabbs()) if (shape.move(at.getX(), at.getY(), at.getZ()).intersects(body)) return false;
}
return true;
}
private static long column(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
private static List<Integer> coords(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
private static BlockPos readPos(com.google.gson.JsonArray value) { return new BlockPos(value.get(0).getAsInt(), value.get(1).getAsInt(), value.get(2).getAsInt()); }
private static Path world(ServerLevel level) { return level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize(); }
private static Path proof(ServerLevel level) { return world(level).resolve("heritage21-create-proof.json"); }
private static String planHash(HeritagePlan19 plan) { return hash(plan.sites().toString() + plan.landmarks() + plan.links() + plan.segments()); }
private static void write(ServerLevel level, Map<String, Object> report, boolean reload) throws IOException {
var path = Path.of("diagnostics", "heritage21-" + (reload ? "reload" : "create") + ".json"); Files.createDirectories(path.getParent());
var value = new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n"; Files.writeString(path, value); if (!reload) Files.writeString(proof(level), value);
}
private static String hash(String value) {
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8))); }
catch (NoSuchAlgorithmException impossible) { throw new AssertionError(impossible); }
}
}
@@ -0,0 +1,93 @@
package fr.koka.sanctuary.gametest;
import java.util.Map;
import java.util.Set;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.block.FenceBlock;
import net.minecraft.world.level.block.IronBarsBlock;
import net.minecraft.world.level.block.StairBlock;
import net.minecraft.world.level.block.WallBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
/** Independent oracle: actual native neighbor-update rules, with datapack tags loaded by the test server. */
public final class NativeConnections20 {
private NativeConnections20() {}
public record Stats(int panes, int fences, int walls, int stairs, int nativeOracles, int externalNeighbors,
int crossChunkPaneLinks, int settledExternalStates, int unsettledExternalStates) {
public Stats add(Stats other) { return new Stats(panes + other.panes, fences + other.fences, walls + other.walls,
stairs + other.stairs, nativeOracles + other.nativeOracles, externalNeighbors + other.externalNeighbors,
crossChunkPaneLinks + other.crossChunkPaneLinks, settledExternalStates + other.settledExternalStates,
unsettledExternalStates + other.unsettledExternalStates); }
public static Stats empty() { return new Stats(0, 0, 0, 0, 0, 0, 0, 0, 0); }
}
public static Stats verify(GameTestHelper helper, Map<BlockPos, BlockState> geometry, Set<BlockPos> edits) {
var level = helper.getLevel(); int panes = 0, fences = 0, walls = 0, stairs = 0, oracles = 0, exterior = 0, cross = 0, settled = 0, unsettled = 0;
for (var entry : geometry.entrySet()) {
var pos = entry.getKey(); var wanted = entry.getValue(); if (edits.contains(pos)) continue;
boolean pane = wanted.getBlock() instanceof IronBarsBlock, fence = wanted.getBlock() instanceof FenceBlock;
boolean wall = wanted.getBlock() instanceof WallBlock, stair = wanted.getBlock() instanceof StairBlock;
if (!pane && !fence && !wall && !stair) continue;
if (pane) panes++; if (fence) fences++; if (wall) walls++; if (stair) stairs++;
var actual = level.getBlockState(pos);
helper.assertTrue(sameNonConnectionState(actual, wanted),
"Material, orientation, half or waterlogging changed at " + pos + ": expected " + wanted + " got " + actual);
boolean knownNeighborhood = true;
for (var side : Direction.values()) {
var at = pos.relative(side); var expectedNeighbor = geometry.get(at); var actualNeighbor = level.getBlockState(at);
// A changed neighbor's shape can propagate a legitimate exterior connection, e.g. stacked walls.
if (edits.contains(at) || (expectedNeighbor == null ? !actualNeighbor.isAir()
: !expectedNeighbor.equals(actualNeighbor))) knownNeighborhood = false;
}
var updated = actual;
for (var side : Direction.values()) {
var at = pos.relative(side);
updated = updated.updateShape(level, level, pos, side, at, level.getBlockState(at), RandomSource.create(20));
}
if (knownNeighborhood) {
helper.assertTrue(actual.equals(wanted), "Native detailed state differs inside known geometry at " + pos
+ ": expected " + wanted + " got " + actual);
helper.assertTrue(updated.equals(actual), "Vanilla neighbor rules disagree with generation20 connections at " + pos
+ ": saved " + actual + " native " + updated);
oracles++;
} else {
exterior++;
if (updated.equals(actual)) { settled++; oracles++; }
else {
// A fresh structure has not necessarily received an update from untouched surrounding rock.
// Permit only its exact planned state until that update; a partially/wrongly changed state fails.
helper.assertTrue(actual.equals(wanted) && sameNonConnectionState(updated, wanted),
"Exterior connection is neither the untouched plan nor a vanilla fixed point at " + pos
+ ": plan " + wanted + " saved " + actual + " native " + updated);
unsettled++;
}
}
if (pane) for (var side : new Direction[] {Direction.EAST, Direction.SOUTH}) {
var at = pos.relative(side); var neighbor = level.getBlockState(at);
if (neighbor.getBlock() instanceof IronBarsBlock && (pos.getX() >> 4 != at.getX() >> 4 || pos.getZ() >> 4 != at.getZ() >> 4)) {
var property = side == Direction.EAST ? BlockStateProperties.EAST : BlockStateProperties.SOUTH;
helper.assertTrue(actual.getValue(property), "Pane connection disappeared exactly at a native chunk boundary: " + pos);
cross++;
}
}
}
return new Stats(panes, fences, walls, stairs, oracles, exterior, cross, settled, unsettled);
}
/** Normalize only geometry-derived properties. Everything else, including fluids and facing, stays exact. */
private static boolean sameNonConnectionState(BlockState actual, BlockState wanted) {
if (!actual.is(wanted.getBlock())) return false;
if (wanted.getBlock() instanceof IronBarsBlock || wanted.getBlock() instanceof FenceBlock) {
for (var property : new net.minecraft.world.level.block.state.properties.BooleanProperty[] {
BlockStateProperties.NORTH, BlockStateProperties.EAST, BlockStateProperties.SOUTH, BlockStateProperties.WEST})
actual = actual.setValue(property, wanted.getValue(property));
} else if (wanted.getBlock() instanceof WallBlock) {
for (var property : WallBlock.PROPERTY_BY_DIRECTION.values()) actual = actual.setValue(property, wanted.getValue(property));
actual = actual.setValue(WallBlock.UP, wanted.getValue(WallBlock.UP));
} else if (wanted.getBlock() instanceof StairBlock) actual = actual.setValue(StairBlock.SHAPE, wanted.getValue(StairBlock.SHAPE));
return actual.equals(wanted);
}
}
@@ -210,8 +210,30 @@ public final class PopulationDiagnostics {
}
void prepare() throws IOException {
helper.assertTrue(regionsAtInitialSpawn > 0 && regionsAtInitialSpawn <= 4,
"Initial spawn must plan only nearby regions, not materialize the large island: " + regionsAtInitialSpawn);
boolean heritage = level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator sanctuary
&& (sanctuary.rootGeneration() == 19 || sanctuary.rootGeneration() == 20 || sanctuary.rootGeneration() == 21) && sanctuary.experimentalStructures();
// Generation19 explicitly prospects destinations across the root island before publishing them.
// A consulted hydrology region is a bounded immutable plan, not a loaded/generated chunk.
// Historical generators retain their original near-spawn contract.
int regionLimit = 4;
if (heritage) {
int extent = capacity.terrainLimit();
var first = capacity.regionAt(-extent, -extent); var last = capacity.regionAt(extent, extent);
regionLimit = 0;
for (int x = first.x(); x <= last.x(); x++) for (int z = first.z(); z <= last.z(); z++) {
var region = new IslandCapacity.Region(x, z);
double nearestX = Math.max(0, Math.max(region.minX(), -region.maxX()));
double nearestZ = Math.max(0, Math.max(region.minZ(), -region.maxZ()));
if (Math.hypot(nearestX, nearestZ) < extent) regionLimit++;
}
helper.assertTrue(regionLimit <= 9, "Heritage starter hydrology must fit within nine local regions");
var plan = ((SanctuaryChunkGenerator) level.getChunkSource().getGenerator()).heritagePlan19();
helper.assertTrue(plan.columns() <= fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.MAX_COLUMNS
&& plan.samples() <= fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.MAX_SAMPLES,
"Island-wide heritage prospecting exceeded its independent column/probe budget");
}
helper.assertTrue(regionsAtInitialSpawn > 0 && regionsAtInitialSpawn <= regionLimit,
"Initial hydrology exceeded its geographic planning scope (limit " + regionLimit + "): " + regionsAtInitialSpawn);
List<IslandCapacity.Region> targets = new ArrayList<>();
targets.add(new IslandCapacity.Region(0,0));
if (capacity.players() == 20) targets.add(new IslandCapacity.Region(1,0));
@@ -0,0 +1,453 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mojang.serialization.JsonOps;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import fr.koka.sanctuary.worldgen.transit.TransitPiece21;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21;
import fr.koka.sanctuary.worldgen.transit.TransitRenderer21;
import fr.koka.sanctuary.worldgen.transit.TransitStructures21;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderSet;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.StairBlock;
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.LevelResource;
import net.minecraft.world.level.storage.loot.LootTable;
import net.minecraft.world.phys.AABB;
/** Opt-in native proof confined to the new generation21 starter island in a disposable development world. */
public final class Transit21WorldGameTests {
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
@GameTest(maxTicks = 12000)
public void industrialTransitPersistsNatively(GameTestHelper helper) {
helper.assertTrue(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator,
"The industrial-transits proof requires a Sanctuary generator");
var generator = (SanctuaryChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
helper.assertTrue(generator.rootGeneration() == 21, "Use a fresh generation21 test world; do not convert old saves");
new Scenario(helper, generator, Boolean.getBoolean("sanctuary.test.expansionReload")).advance();
}
private static final class Scenario {
final GameTestHelper helper;
final ServerLevel level;
final SanctuaryChunkGenerator generator;
final TransitPlan21 plan;
final boolean reload;
final Map<String, Object> report = new LinkedHashMap<>();
final List<ChunkPos> chunks = new ArrayList<>();
final List<StructureStart> starts = new ArrayList<>();
final Map<BlockPos, BlockState> allGeometry = new HashMap<>();
JsonObject previous;
BlockPos gold;
int nextChunk;
boolean prepared;
Scenario(GameTestHelper helper, SanctuaryChunkGenerator generator, boolean reload) {
this.helper = helper; this.level = helper.getLevel(); this.generator = generator; this.reload = reload;
this.plan = generator.transitPlan21();
report.put("seed", level.getSeed()); report.put("size", generator.initialDiameter());
report.put("process_id", ProcessHandle.current().pid()); report.put("world_directory", world(level).toString());
report.put("experimental_structures", generator.experimentalStructures());
report.put("structures_enabled", level.structureManager().shouldGenerateStructures());
report.put("planning_diagnostic", plan.diagnostic());
report.put("scope", "Native starts, references, geometry, rock bearings, unopened loot and a player-sized stair walk between two surface gates. "
+ "No expansion is created and no personal save is opened. A single outer-wall gold edit in this disposable world "
+ "is saved and checked in a second JVM. Traversal remains inside the modules and certified portico approaches; underground one-block rises need real correctly oriented stairs. Only the certified natural portico approaches permit an ordinary one-block jump. No flight, ladder or external terrain shortcut is allowed. No fluid simulation is claimed.");
}
void advance() {
try {
if (!prepared) {
prepare(); prepared = true;
if (plan.isEmpty()) { finishDisabled(); return; }
}
if (nextChunk < chunks.size()) {
var pos = chunks.get(nextChunk++);
var loaded = level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true);
helper.assertTrue(loaded instanceof LevelChunk, "Every room footprint must reach native FULL: " + pos);
if (nextChunk % 10 == 0) SanctuaryMod.LOGGER.info("Industrial transit21: loaded {}/{} footprint chunks", nextChunk, chunks.size());
helper.runAtTickTime(helper.getTick() + 1, this::advance); return;
}
verifyPlaced();
report.put("passed", true);
if (!reload) {
helper.assertTrue(gold != null && level.getBlockState(gold).isCollisionShapeFullBlock(level, gold),
"Gameplay witness must replace a solid outer wall away from every doorway");
level.setBlock(gold, Blocks.GOLD_BLOCK.defaultBlockState(), 3);
report.put("gold_witness", coords(gold));
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush native structures, deferred loot and the gold edit");
report.put("world_flushed_before_shutdown", true);
} else {
helper.assertTrue(level.getBlockState(gold).is(Blocks.GOLD_BLOCK), "Reload replaced the existing wall edit");
report.put("gold_witness", coords(gold)); report.put("gold_witness_preserved", true);
}
write(level, report, reload); helper.succeed();
} catch (Exception failure) {
report.put("passed", false); report.put("failure", failure.toString());
try { write(level, report, reload); } catch (IOException ignored) { }
throw new IllegalStateException("Industrial transit21 native " + (reload ? "reload" : "creation") + " failed", failure);
}
}
void prepare() throws IOException {
var ops = RegistryOps.create(JsonOps.INSTANCE, level.registryAccess());
var encoded = ChunkGenerator.CODEC.encodeStart(ops, generator).getOrThrow();
var decoded = ChunkGenerator.CODEC.parse(ops, encoded).getOrThrow();
helper.assertTrue(decoded instanceof SanctuaryChunkGenerator copy && copy.rootGeneration() == 21
&& copy.experimentalStructures() == generator.experimentalStructures(), "The generation21 codec must preserve its experimental switch");
report.put("generator_codec", encoded); report.put("plan_sha256", planHash(plan)); report.put("planning_samples", plan.samples());
helper.assertTrue(plan.samples() <= TransitPlan21.MAX_SAMPLES, "Transit planning exceeded its budget");
if (reload) {
previous = JsonParser.parseString(Files.readString(proof(level))).getAsJsonObject();
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("world_flushed_before_shutdown").getAsBoolean(),
"Reload requires the successful, flushed creation proof");
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload must run in a separate JVM");
helper.assertTrue(previous.get("seed").getAsLong() == level.getSeed()
&& previous.get("size").getAsInt() == generator.initialDiameter()
&& previous.get("world_directory").getAsString().equals(world(level).toString()), "Reload used another world, seed or size");
helper.assertTrue(previous.get("generator_codec").equals(encoded) && previous.get("plan_sha256").getAsString().equals(planHash(plan)),
"Saved switch or planned generation21 room identities changed on reload");
if (previous.has("gold_witness")) gold = readPos(previous.getAsJsonArray("gold_witness"));
}
boolean enabled = generator.experimentalStructures() && level.structureManager().shouldGenerateStructures();
if (!enabled) {
helper.assertTrue(plan.isEmpty(), "Experimental=false or generateStructures=false must publish no industrial transit");
return;
}
helper.assertTrue(!plan.isEmpty(), "Enabled reference island has no experimental network");
helper.assertTrue(plan.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.ACCESS).count() == 2,
"A through-route needs two independent saved surface stairs");
int spanX = plan.modules().stream().mapToInt(TransitPlan21.Module::maxX).max().orElseThrow()
- plan.modules().stream().mapToInt(TransitPlan21.Module::minX).min().orElseThrow() + 1;
int spanZ = plan.modules().stream().mapToInt(TransitPlan21.Module::maxZ).max().orElseThrow()
- plan.modules().stream().mapToInt(TransitPlan21.Module::minZ).min().orElseThrow() + 1;
report.put("span_x", spanX); report.put("span_z", spanZ);
helper.assertTrue(Math.max(spanX, spanZ) >= generator.initialDiameter() * .4,
"The native transit reference must cross at least40% of the island's nominal diameter");
report.put("modules", plan.modules().size());
report.put("halls", plan.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.HALL).count());
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(TransitStructures21.KEY);
var origins = new HashSet<Long>(); var uniqueChunks = new HashSet<ChunkPos>();
for (var network : plan.modules()) {
long origin = new ChunkPos(network.startChunkX(), network.startChunkZ()).pack();
if (!origins.add(origin)) continue;
var chunk = level.getChunk(network.startChunkX(), network.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
var start = chunk.getStartForStructure(structure.value());
helper.assertTrue(start != null && start.isValid(), "Accepted experimental network has no native start: " + network.kind() + " at " + network.centerX() + "," + network.centerZ());
starts.add(start);
for (var piece : start.getPieces()) {
helper.assertTrue(piece instanceof TransitPiece21, "Industrial transit loaded a foreign or historical piece type");
piece.getBoundingBox().intersectingChunks().forEach(at -> {
helper.assertTrue(Math.abs(at.x() - network.startChunkX()) <= 7 && Math.abs(at.z() - network.startChunkZ()) <= 7,
"Transit native start exceeds its local reference range");
uniqueChunks.add(at);
});
}
}
helper.assertTrue(uniqueChunks.size() <= 256, "The bounded root-only proof unexpectedly covers more than256 chunks");
chunks.addAll(uniqueChunks.stream().sorted(java.util.Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList());
}
void finishDisabled() throws IOException {
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(TransitStructures21.KEY);
var chunk = level.getChunk(0, 0, ChunkStatus.STRUCTURE_STARTS, true);
var start = chunk.getStartForStructure(structure.value());
helper.assertTrue(start == null || !start.isValid(), "Disabled experimental generation emitted a native start");
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
helper.assertTrue(located == null, "Disabled experimental structures must not be located or prepared");
report.put("empty_plan_verified", true); report.put("passed", true);
if (!reload) {
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save the disabled experiment setting");
report.put("world_flushed_before_shutdown", true);
}
write(level, report, reload); helper.succeed();
}
void verifyPlaced() {
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(TransitStructures21.KEY);
var context = StructurePieceSerializationContext.fromLevel(level);
var sites = new ArrayList<Map<String, Object>>();
var modules = new ArrayList<Map<String, Object>>();
int blocks = 0, supports = 0, chests = 0, pieces = 0;
var bearingMasks = new HashMap<TransitPlan21.Module, Integer>();
for (var start : starts) {
var origin = start.getChunkPos(); var saved = start.createTag(context, origin);
var copy = StructureStart.loadStaticStart(context, saved, level.getSeed());
helper.assertTrue(copy != null && copy.isValid() && copy.createTag(context, origin).equals(saved), "The native start changed on an NBT roundtrip");
var expectedRooms = plan.modulesForChunk(origin.x(), origin.z());
var actualRooms = start.getPieces().stream().map(TransitPiece21.class::cast).map(TransitPiece21::module).toList();
helper.assertTrue(actualRooms.size() == expectedRooms.size() && new HashSet<>(actualRooms).equals(new HashSet<>(expectedRooms)),
"Native start did not persist every planned room exactly once");
String nativeHash = hash(saved.toString());
if (reload) {
JsonObject prior = null;
for (var value : previous.getAsJsonArray("starts")) {
var site = value.getAsJsonObject(); var at = site.getAsJsonArray("start_chunk");
if (at.get(0).getAsInt() == origin.x() && at.get(1).getAsInt() == origin.z()) prior = site;
}
helper.assertTrue(prior != null && prior.get("native_start_sha256").getAsString().equals(nativeHash),
"Reload altered native room parameters, supports or reference count");
}
var loot = new ArrayList<Map<String, Object>>();
for (var piece : start.getPieces()) {
var roomPiece = (TransitPiece21) piece; var room = roomPiece.module(); pieces++;
piece.getBoundingBox().intersectingChunks().forEach(at -> {
var full = level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true);
helper.assertTrue(full.getReferencesForStructure(structure.value()).contains(origin.pack()), "A saved FULL chunk lost the native structure reference");
});
var roomTag = roomPiece.createTag(context);
var roundtrip = new TransitPiece21(context, roomTag);
helper.assertTrue(roundtrip.module().equals(room) && roundtrip.createTag(context).equals(roomTag), "The native room failed its exact saved-field roundtrip");
var corrupt = roomTag.copy(); var payload = corrupt.getCompound("SanctuaryModule").orElseThrow(); payload.putInt("Schema", 99);
boolean rejected = false;
try { new TransitPiece21(context, corrupt); } catch (IllegalArgumentException expected) { rejected = true; }
helper.assertTrue(rejected, "An unknown generation21 piece schema must be refused");
var expected = rendered(room); blocks += expected.blocks().size();
expected.blocks().forEach((at, state) -> helper.assertTrue(allGeometry.putIfAbsent(at, state) == null,
"Two saved transit modules paint the same position: " + at));
modules.add(Map.of("kind", room.kind().name(), "min", List.of(room.minX(), room.minY(), room.minZ()),
"max", List.of(room.maxX(), room.maxY(), room.maxZ()), "floor_y", room.floorY(),
"surface_y", room.surfaceY(), "openings", room.openings(), "supports", room.supports().size(),
"start_chunk", List.of(room.startChunkX(), room.startChunkZ())));
for (var entry : expected.blocks().entrySet()) {
var at = entry.getKey(); var wanted = entry.getValue();
helper.assertTrue(TransitPiece21.bounds(room).isInside(at), "Renderer escaped its saved room/support bounds");
helper.assertTrue(plan.protects(at.getX(), at.getY(), at.getZ()) && generator.protectsCityDecoration(at),
"Transit construction/cleared air is missing its published decoration protection at " + at);
if (reload && at.equals(gold)) continue;
helper.assertTrue(level.getBlockState(at).is(wanted.getBlock()),
"Native clipping or later decoration changed " + room.kind() + " at " + at + ": expected " + wanted + " got " + level.getBlockState(at));
helper.assertTrue(!level.getBlockState(at).is(Blocks.SPAWNER), "Experimental architecture must not create a spawner");
}
supports += verifyBearings(helper, generator, room, bearingMasks);
for (var entry : expected.blocks().entrySet()) if (entry.getValue().is(Blocks.LANTERN)) {
var at = entry.getKey(); var state = level.getBlockState(at);
helper.assertTrue(state.canSurvive(level, at) && level.getBlockState(at.above()).isFaceSturdy(level, at.above(), Direction.DOWN),
"Transit lamp lost its complete ceiling bearing at " + at);
}
for (var chest : expected.chests()) {
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Deferred native chest block entity is missing");
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
helper.assertTrue(chest.table().equals(entity.getLootTable()) && entity.getLootTableSeed() == chest.seed(),
"Native chest loot must remain deferred with its exact table and seed");
loot.add(Map.of("position", coords(chest.position()), "table", chest.table().identifier().toString(), "seed", chest.seed())); chests++;
}
if (!reload && gold == null && room.kind() != TransitPlan21.Kind.ACCESS && room.kind() != TransitPlan21.Kind.JUNCTION)
gold = new BlockPos(room.minX(), room.floorY() + 1, room.minZ() + 1);
}
sites.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", nativeHash,
"pieces", start.getPieces().size(), "unopened_chests", loot));
}
report.put("module_coordinates", modules);
report.put("starts", sites);
int weakSpan = 0, longestSpan = 0;
for (var room : plan.modules()) {
weakSpan = bearingMasks.get(room) == 15 ? 0 : weakSpan + 1;
longestSpan = Math.max(longestSpan, weakSpan);
helper.assertTrue(weakSpan <= 2, "Native deck spans more than 34 blocks without distributed rock bearings");
}
report.put("longest_span_without_full_bearings", longestSpan * 17);
var ores = new java.util.TreeMap<String, Integer>();
allGeometry.values().forEach(state -> {
var id = net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString();
if (id.endsWith("_ore")) ores.merge(id, 1, Integer::sum);
});
report.put("finite_exposed_ores", ores);
var traversal = verifyAccess(helper, plan);
report.put("native_connections", NativeConnections20.verify(helper, allGeometry,
reload && gold != null ? Set.of(gold) : Set.of()));
report.put("module_coordinates", modules);
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
helper.assertTrue(located != null && located.getSecond().is(TransitStructures21.KEY), "Native locate cannot find a saved industrial transit");
helper.assertTrue(plan.modules().stream().filter(r -> r.kind() == TransitPlan21.Kind.ACCESS)
.anyMatch(r -> TransitPiece21.locator(r).equals(located.getFirst())), "Locate must lead to a surface entrance rather than an underground floor");
report.put("starts", sites); report.put("native_pieces", pieces); report.put("native_blocks_checked", blocks);
report.put("full_chunks_checked", chunks.size()); report.put("rock_supports_checked", supports);
report.put("unopened_deferred_chests", chests); report.put("native_routes", traversal); report.put("locate", coords(located.getFirst()));
}
}
private record Chest(BlockPos position, ResourceKey<LootTable> table, long seed) {}
private record Rendered(Map<BlockPos, BlockState> blocks, List<Chest> chests) {}
private static Rendered rendered(TransitPlan21.Module room) {
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
TransitRenderer21.render(room, new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(at(room, x, y, z), state); }
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
var position = at(room, x, y, z); blocks.put(position, Blocks.CHEST.defaultBlockState()); chests.add(new Chest(position, table, RandomSource.create(seed).nextLong()));
}
});
return new Rendered(blocks, chests);
}
private static int verifyBearings(GameTestHelper helper, SanctuaryChunkGenerator generator, TransitPlan21.Module room, Map<TransitPlan21.Module, Integer> masks) {
var level = helper.getLevel(); int checked = 0, quarters = 0;
for (int z = room.minZ();; z = Math.min(z + 4, room.maxZ())) {
for (int x = room.minX();; x = Math.min(x + 4, room.maxX())) {
int anchor = room.minY() - 1;
for (var support : room.supports()) if (support.x() == x && support.z() == z) anchor = support.minY() - 1;
var bearing = new BlockPos(x, anchor, z);
boolean supported = level.getBlockState(bearing).isSolid() && level.getFluidState(bearing).isEmpty()
&& level.getBlockState(bearing.below()).isSolid() && level.getFluidState(bearing.below()).isEmpty();
if (supported) {
quarters |= 1 << ((x < room.centerX() ? 0 : 1) + (z < room.centerZ() ? 0 : 2));
for (int dy = 0; dy >= -1; dy--) {
var at = new BlockPos(x, anchor + dy, z);
helper.assertTrue(level.getBlockState(at).isSolid() && level.getFluidState(at).isEmpty(),
"Transit bearing grid lost one of its two untouched rock layers at " + at);
helper.assertTrue(generator.protectsCityDecoration(at), "A certified transit rock dependency is unprotected at " + at);
checked++;
}
}
if (x == room.maxX()) break;
}
if (z == room.maxZ()) break;
}
helper.assertTrue(room.kind() == TransitPlan21.Kind.GALLERY || room.kind() == TransitPlan21.Kind.JUNCTION || quarters == 15, "The continuous deck lost its distributed rock bearings");
masks.put(room, quarters);
return checked;
}
private static Map<String, Object> verifyAccess(GameTestHelper helper, TransitPlan21 plan) {
var level = helper.getLevel();
var accesses = plan.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.ACCESS).toList();
helper.assertTrue(accesses.size() == 2, "Native transit requires exactly two surface gates");
Predicate<BlockPos> inside = pos -> plan.contains(pos.getX(), pos.getY(), pos.getZ())
|| accesses.stream().anyMatch(m -> pos.getX() >= m.centerX() - 2 && pos.getX() <= m.centerX() + 2
&& pos.getZ() >= m.minZ() - 3 && pos.getZ() < m.minZ()
&& pos.getY() >= m.surfaceY() && pos.getY() <= m.surfaceY() + 2);
var start = TransitPiece21.locator(accesses.getFirst());
Predicate<BlockPos> naturalApproach = pos -> accesses.stream().anyMatch(m ->
pos.getX() >= m.centerX() - 2 && pos.getX() <= m.centerX() + 2
&& pos.getZ() >= m.minZ() - 3 && pos.getZ() < m.minZ()
&& pos.getY() >= m.surfaceY() && pos.getY() <= m.surfaceY() + 2);
var reached = walk(helper, start, inside, naturalApproach); int targets = 0;
var arrivals = new ArrayList<Map<String, Object>>();
for (var module : plan.modules()) for (var target : TransitRenderer21.targets(module)) {
var point = at(module, target.x(), target.y(), target.z());
helper.assertTrue(reached.containsKey(Foot.at(point)), "Native transit cannot walk from its first portico to "
+ module.kind() + "/" + target.name() + " at " + point);
targets++;
}
for (var access : accesses) {
var gate = TransitPiece21.locator(access);
helper.assertTrue(reached.containsKey(Foot.at(gate)), "The second independent surface gate is not connected by real stairs");
int approaches = 0;
for (int x = access.centerX() - 2; x <= access.centerX() + 2; x++) for (int z = access.minZ() - 3; z < access.minZ(); z++) {
var high = new BlockPos(x, access.surfaceY() + 1, z);
var low = high.below(); var position = standable(level, high) ? high : low;
helper.assertTrue(standable(level, position) && reached.containsKey(Foot.at(position)),
"The certified natural approach no longer joins its portico at " + position);
helper.assertTrue(level.getBlockState(position.below()).isSolid()
&& level.getFluidState(position.below()).isEmpty(), "A surface arrival is suspended above air or water");
approaches++;
}
arrivals.add(Map.of("gate", coords(gate), "roadway", coords(at(access, 8, 2, 8)),
"rise", access.surfaceY() - access.floorY(), "steps_from_first_gate", reached.get(Foot.at(gate)),
"natural_approach_cells", approaches));
}
return Map.of("surface_accesses", arrivals, "walkable_targets", targets, "standing_cells", reached.size(),
"surface_to_surface_steps", reached.get(Foot.at(TransitPiece21.locator(accesses.getLast()))),
"traversal_policy", "0.6 by1.8 native collision volume, half-block sidewalk steps; underground rises require a real forward-facing stair; natural portico approaches alone permit a normal one-block jump; no vertical moves");
}
/** Half-block standing elevations retain real sidewalk slabs without pretending an integer cell can hover over them. */
private record Foot(int x, int halfY, int z) {
static Foot at(BlockPos pos) { return new Foot(pos.getX(), pos.getY() * 2, pos.getZ()); }
double y() { return halfY / 2.0; }
BlockPos block() { return new BlockPos(x, Math.floorDiv(halfY, 2), z); }
BlockPos bearing() { return new BlockPos(x, Math.floorDiv(halfY - 1, 2), z); }
Foot rise(int halves) { return new Foot(x, halfY + halves, z); }
Foot move(Direction direction, int halves) { return new Foot(x + direction.getStepX(), halfY + halves, z + direction.getStepZ()); }
}
private static Map<Foot, Integer> walk(GameTestHelper helper, BlockPos start, Predicate<BlockPos> inside, Predicate<BlockPos> naturalApproach) {
var level = helper.getLevel(); var first = Foot.at(start);
helper.assertTrue(inside.test(start) && standable(level, first), "Blocked native surface portico: " + start);
var seen = new HashMap<Foot, Integer>(); var queue = new ArrayDeque<Foot>(); seen.put(first, 0); queue.add(first);
while (!queue.isEmpty()) {
var pos = queue.removeFirst();
for (var direction : HORIZONTAL) for (int dy = -2; dy <= 2; dy++) {
var next = pos.move(direction, dy);
if (!inside.test(next.block()) || seen.containsKey(next)) continue;
boolean ordinaryThreshold = naturalApproach.test(pos.block()) || naturalApproach.test(next.block());
// Native step height is enough for a half slab; a full block needs the two treads of a stair.
if (dy > 0 && (!clearBody(level, pos.rise(dy))
|| dy == 2 && !ordinaryThreshold && !ascendingStair(level, next.bearing(), direction))) continue;
if (dy < 0 && (!clearBody(level, next.rise(-dy))
|| dy == -2 && !ordinaryThreshold && !ascendingStair(level, pos.bearing(), direction.getOpposite()))) continue;
if (standable(level, next)) { seen.put(next, seen.get(pos) + 1); queue.add(next); }
}
helper.assertTrue(seen.size() <= 120_000, "Native transit traversal exceeded its bounded room volumes");
}
return seen;
}
private static boolean ascendingStair(ServerLevel level, BlockPos pos, Direction direction) {
var state = level.getBlockState(pos);
return state.getBlock() instanceof StairBlock
&& state.getValue(BlockStateProperties.HORIZONTAL_FACING) == direction
&& state.getValue(BlockStateProperties.HALF) == net.minecraft.world.level.block.state.properties.Half.BOTTOM;
}
private static boolean standable(ServerLevel level, BlockPos pos) { return standable(level, Foot.at(pos)); }
private static boolean standable(ServerLevel level, Foot pos) {
if (!clearBody(level, pos)) return false;
var below = pos.bearing(); double top = pos.y() - below.getY();
return level.getBlockState(below).getCollisionShape(level, below).toAabbs().stream()
.anyMatch(b -> Math.abs(b.maxY - top) < .0001 && b.minX <= .5 && b.maxX >= .5 && b.minZ <= .5 && b.maxZ >= .5);
}
private static boolean clearBody(ServerLevel level, Foot pos) {
var body = new AABB(pos.x() + .2, pos.y(), pos.z() + .2, pos.x() + .8, pos.y() + 1.8, pos.z() + .8);
for (int y = (int) Math.floor(pos.y()); y <= (int) Math.floor(pos.y() + 1.8 - .0001); y++) {
var at = new BlockPos(pos.x(), y, pos.z());
if (!level.getFluidState(at).isEmpty()) return false;
for (var shape : level.getBlockState(at).getCollisionShape(level, at).toAabbs())
if (shape.move(at.getX(), at.getY(), at.getZ()).intersects(body)) return false;
}
return true;
}
private static BlockPos at(TransitPlan21.Module room, int x, int y, int z) { return new BlockPos(room.minX() + x, room.minY() + y, room.minZ() + z); }
private static List<Integer> coords(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
private static BlockPos readPos(com.google.gson.JsonArray value) { return new BlockPos(value.get(0).getAsInt(), value.get(1).getAsInt(), value.get(2).getAsInt()); }
private static Path world(ServerLevel level) { return level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize(); }
private static Path proof(ServerLevel level) { return world(level).resolve("transit21-create-proof.json"); }
private static String planHash(TransitPlan21 plan) { return hash(plan.modules().toString()); }
private static void write(ServerLevel level, Map<String, Object> report, boolean reload) throws IOException {
var path = Path.of("diagnostics", "transit21-" + (reload ? "reload" : "create") + ".json"); Files.createDirectories(path.getParent());
var value = new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n";
Files.writeString(path, value);
if (!reload) Files.writeString(proof(level), value);
}
private static String hash(String value) {
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8))); }
catch (NoSuchAlgorithmException impossible) { throw new AssertionError(impossible); }
}
}
@@ -45,8 +45,17 @@ final class UnifiedWorldSettingsDiagnostics {
var historical16 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v16"))).value().createWorldDimensions();
helper.assertTrue(historical16.overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 16
&& old.source().generation() == 16 && !old.experimentalStructures(), "Historical16 never gains experimental structures");
var historical17 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v17"))).value().createWorldDimensions();
helper.assertTrue(historical17.overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 17
&& old.source().generation() == 17 && old.experimentalStructures(), "Historical17 keeps its original experimental generator");
var historical19 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v19"))).value().createWorldDimensions();
helper.assertTrue(historical19.overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 19
&& old.source().generation() == 19 && old.experimentalStructures(), "Historical19 keeps its original renderer and world settings");
var generator = (SanctuaryChunkGenerator) standard.overworld();
helper.assertTrue(generator.rootGeneration() == 17 && generator.source().generation() == 17 && generator.experimentalStructures(), "The public preset uses generation17 with its new-world option");
var historical20 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v20")));
helper.assertTrue(historical20.value().createWorldDimensions().overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 20
&& old.source().generation() == 20 && old.experimentalStructures(), "Historical20 keeps its original architecture");
helper.assertTrue(generator.rootGeneration() == 21 && generator.source().generation() == 21 && generator.experimentalStructures(), "The public preset uses generation21 with its new-world option");
helper.assertTrue(generator.initialDiameter() == 724, "The public preset defaults to Medium / 724 blocks");
for (var tag : List.of(WorldPresetTags.NORMAL, WorldPresetTags.EXTENDED)) {
var visible = presets.getOrThrow(tag).stream().map(holder -> holder.getRegisteredName())
@@ -56,11 +65,11 @@ final class UnifiedWorldSettingsDiagnostics {
}
var ops = RegistryOps.create(JsonOps.INSTANCE, access);
var activeCodec = generator.rootGeneration() == 17 ? SanctuaryChunkGenerator.RUINS17_CODEC : generator.rootGeneration() == 16 ? SanctuaryChunkGenerator.CITY16_CODEC : generator.rootGeneration() == 15 ? SanctuaryChunkGenerator.CITY15_CODEC : generator.rootGeneration() == 14 ? SanctuaryChunkGenerator.CITY_CODEC : SanctuaryChunkGenerator.CODEC;
var activeCodec = generator.rootGeneration() == 21 ? SanctuaryChunkGenerator.NETWORK21_CODEC : generator.rootGeneration() == 20 ? SanctuaryChunkGenerator.ARCHITECTURE20_CODEC : generator.rootGeneration() == 19 ? SanctuaryChunkGenerator.HERITAGE19_CODEC : generator.rootGeneration() == 17 ? SanctuaryChunkGenerator.RUINS17_CODEC : generator.rootGeneration() == 16 ? SanctuaryChunkGenerator.CITY16_CODEC : generator.rootGeneration() == 15 ? SanctuaryChunkGenerator.CITY15_CODEC : generator.rootGeneration() == 14 ? SanctuaryChunkGenerator.CITY_CODEC : SanctuaryChunkGenerator.CODEC;
var serialized = activeCodec.codec().encodeStart(ops, generator).getOrThrow().getAsJsonObject();
serialized.remove("experimental_structures");
helper.assertTrue(activeCodec.codec().parse(ops, serialized).getOrThrow().experimentalStructures(),
"Generation17 omitted experimental option defaults to enabled");
"Generation21 omitted experimental option defaults to enabled");
helper.assertTrue(!((SanctuaryChunkGenerator) historical16.overworld()).withExperimentalStructures(true).experimentalStructures(),
"The new-world switch cannot activate experimental generation in a historical16 generator");
serialized.remove("initial_diameter");
@@ -21,7 +21,8 @@ public abstract class SanctuaryGameTestServerMixin {
// generation test's deadline before its IO/workers can complete. This is test-only pacing.
@Inject(method = "waitUntilNextTick", at = @At("RETURN"))
private void sanctuary$paceExpansionWorkers(CallbackInfo ci) {
if (Boolean.getBoolean("sanctuary.test.ruins17") || Boolean.getBoolean("sanctuary.test.expansion") || Boolean.getBoolean("sanctuary.test.lostCity")
if ((Boolean.getBoolean("sanctuary.test.heritage20") || Boolean.getBoolean("sanctuary.test.heritage21") || Boolean.getBoolean("sanctuary.test.lostCity21") || Boolean.getBoolean("sanctuary.test.transit21")) || Boolean.getBoolean("sanctuary.test.lostCity20")
|| Boolean.getBoolean("sanctuary.test.heritage19") || Boolean.getBoolean("sanctuary.test.ruins17") || Boolean.getBoolean("sanctuary.test.expansion") || Boolean.getBoolean("sanctuary.test.lostCity")
|| Boolean.getBoolean("sanctuary.test.lostCity16") || Boolean.getBoolean("sanctuary.test.lostCity15") || Boolean.getBoolean("sanctuary.test.voidReuse")) {
long until = System.nanoTime() + 20_000_000L;
long remaining;
@@ -46,9 +47,9 @@ public abstract class SanctuaryGameTestServerMixin {
"Lnet/minecraft/world/level/levelgen/presets/WorldPresets;FLAT_ALL_DIMENSIONS:Lnet/minecraft/resources/ResourceKey;"))
private static ResourceKey<WorldPreset> sanctuary$selectActualPreset() {
if (Boolean.getBoolean("sanctuary.test.unified")) {
int generation = Integer.getInteger("sanctuary.test.generation", 17);
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17) throw new IllegalArgumentException("Unsupported test generation");
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 17 ? "sanctuary" : "sanctuary_v" + generation));
int generation = Integer.getInteger("sanctuary.test.generation", 21);
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21) throw new IllegalArgumentException("Unsupported test generation");
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 21 ? "sanctuary" : "sanctuary_v" + generation));
}
if (Boolean.getBoolean("sanctuary.test.expansion")) return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("expansion_lab"));
int players = Integer.getInteger("sanctuary.test.players", 10);
@@ -48,6 +48,7 @@ public final class SanctuaryMod implements ModInitializer {
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("expansion_island"), fr.koka.sanctuary.expansion.ExpansionDensity.CODEC);
fr.koka.sanctuary.expansion.ExpansionRuntime.register();
fr.koka.sanctuary.expansion.ExpansionCommands.register();
fr.koka.sanctuary.worldgen.heritage.HeritageCommands19.register();
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("main_island"), MainIslandDensity.CODEC);
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v13"), SanctuaryChunkGenerator.CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v13"), SanctuaryBiomeSource.CODEC);
@@ -60,6 +61,14 @@ public final class SanctuaryMod implements ModInitializer {
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v17"), SanctuaryChunkGenerator.RUINS17_CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v17"), SanctuaryBiomeSource.RUINS17_CODEC);
fr.koka.sanctuary.worldgen.city.LostCityStructures.register();
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v19"), SanctuaryChunkGenerator.HERITAGE19_CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v19"), SanctuaryBiomeSource.HERITAGE19_CODEC);
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v20"), SanctuaryChunkGenerator.ARCHITECTURE20_CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v20"), SanctuaryBiomeSource.ARCHITECTURE20_CODEC);
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v21"), SanctuaryChunkGenerator.NETWORK21_CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v21"), SanctuaryBiomeSource.NETWORK21_CODEC);
fr.koka.sanctuary.worldgen.transit.TransitStructures21.register();
fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.register();
fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.register();
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("population_island"), PopulationIslandDensity.CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("population_island"), PopulationIslandBiomeSource.CODEC);
@@ -51,11 +51,11 @@ public final class SanctuaryWorldOptionsScreen extends Screen {
contents.addChild(sizeButton);
diameterHint = contents.addChild(new MultiLineTextWidget(diameterHint(), font)
.setMaxWidth(310).setCentered(true));
if (generator.rootGeneration() == 17) {
if (generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21) {
contents.addChild(CycleButton.builder((Boolean enabled) -> enabled ? CommonComponents.OPTION_ON : CommonComponents.OPTION_OFF, experimental)
.withValues(true, false).create(0, 0, 280, 20, Component.translatable("sanctuary.world_options.experimental"),
(button, value) -> experimental = value));
contents.addChild(new MultiLineTextWidget(Component.translatable("sanctuary.world_options.experimental_hint"), font)
contents.addChild(new MultiLineTextWidget(Component.translatable((generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21) ? "sanctuary.world_options.heritage_hint" : "sanctuary.world_options.experimental_hint"), font)
.setMaxWidth(310).setCentered(true));
}
layout.addToContents(contents);
@@ -122,6 +122,8 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
ChunkAccess chunk, StructureTemplateManager templates, ResourceKey<Level> dimension) {
fr.koka.sanctuary.worldgen.city.LostCityStructures.generate(this, registries, state, structures, chunk, templates, dimension);
fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.generate(this, registries, state, structures, chunk, templates, dimension);
fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.generate(this, registries, state, structures, chunk, templates, dimension);
fr.koka.sanctuary.worldgen.transit.TransitStructures21.generate(this, registries, state, structures, chunk, templates, dimension);
ExpansionStructures.generate(this, registries, state, structures, chunk, templates, dimension);
}
@Override public void spawnOriginalMobs(WorldGenRegion region) {
@@ -41,7 +41,7 @@ public record ExpansionIsland(String id, String parent, String direction, String
if (!((spatialGeneration(generation) ? DIAMETERS : LEGACY_DIAMETERS).contains(diameter) || (id.equals("sanctuary") && (diameter == 724 || (generation == 13 || spatialGeneration(generation)) && diameter == 1024)))) throw new IllegalArgumentException("Unsupported nominal diameter");
if (Math.abs((long) centerX) > 100_000 || Math.abs((long) centerZ) > 100_000 || (centerX & 15) != 0 || (centerZ & 15) != 0) throw new IllegalArgumentException("Invalid island centre");
if (!List.of("reserved", "ready").contains(status) || (generation != GENERATION && !spatialGeneration(generation) && !(generation == 13 && id.equals("sanctuary")))) throw new IllegalArgumentException("Unsupported island state or generation");
if (climate.equals("lost_city") && (!spatialGeneration(generation) || id.equals("sanctuary"))) throw new IllegalArgumentException("Lost City requires a generation 14, 15, 16 or 17 expansion");
if (climate.equals("lost_city") && (!spatialGeneration(generation) || id.equals("sanctuary"))) throw new IllegalArgumentException("Lost City requires a generation 14, 15, 16, 17, 19, 20 or 21 expansion");
if (reuseToken == null || !reuseToken.isEmpty() && (!spatialGeneration(generation) || id.equals("sanctuary") || !reuseToken.matches("[0-9a-f]{64}")))
throw new IllegalArgumentException("Invalid void reuse certificate token");
}
@@ -53,7 +53,7 @@ public record ExpansionIsland(String id, String parent, String direction, String
return new ExpansionIsland("sanctuary", "sanctuary", "north", "temperate", diameter, 0, 0, seed, "natural", "ready", generation);
}
public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16 || generation == 17; }
public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21; }
public boolean origin() { return id.equals("sanctuary"); }
public double referenceDiameter() { return origin() && diameter == 724 ? Math.sqrt(2) * 512 : 512; }
public double horizontalScale() { return origin() ? 1.0 : diameter / referenceDiameter(); }
@@ -25,7 +25,9 @@ public abstract class UnifiedDecorationProtectionMixin {
var destination = generator.islandForChunk(pos.getX() >> 4, pos.getZ() >> 4);
if (owner == null || destination == null || !owner.id().equals(destination.id())
|| !fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
&& !fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.isPlacing() && generator.protectsCityDecoration(pos))
&& !fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.isPlacing()
&& !fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.isPlacing()
&& !fr.koka.sanctuary.worldgen.transit.TransitStructures21.isPlacing() && generator.protectsCityDecoration(pos))
callback.setReturnValue(false);
}
@@ -40,7 +42,7 @@ public abstract class UnifiedDecorationProtectionMixin {
if (state.is(Blocks.SUGAR_CANE)) return;
// Generation16 reserves its architectural volume before decoration. Only its own
// native pieces may replace water there; published generations retain the old rule.
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17)
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21)
&& fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
&& generator.protectsCityDecoration(pos)) return;
if (generator.protectsDecoration(pos) && !region.getBlockState(pos).equals(state)) {
@@ -42,6 +42,18 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 17)));
public static final MapCodec<SanctuaryBiomeSource> HERITAGE19_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 19)));
public static final MapCodec<SanctuaryBiomeSource> ARCHITECTURE20_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 20)));
public static final MapCodec<SanctuaryBiomeSource> NETWORK21_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 21)));
private final List<Holder<Biome>> allBiomes;
private final PopulationIslandBiomeSource population;
private final boolean cities;
@@ -75,7 +87,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
cityPlans = java.util.Map.copyOf(plans);
}
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; }
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; }
@Override public BiomeResolver createResolver(Climate.Sampler sampler) {
BiomeResolver woodlands = population.createResolver(sampler);
@@ -45,9 +45,37 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 17, experimental)));
public static final MapCodec<SanctuaryChunkGenerator> HERITAGE19_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
SanctuaryBiomeSource.HERITAGE19_CODEC.fieldOf("biome_source").forGetter(SanctuaryChunkGenerator::source),
DIAMETER_CODEC.optionalFieldOf("initial_diameter", 724).forGetter(SanctuaryChunkGenerator::initialDiameter),
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 19, experimental)));
public static final MapCodec<SanctuaryChunkGenerator> ARCHITECTURE20_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
SanctuaryBiomeSource.ARCHITECTURE20_CODEC.fieldOf("biome_source").forGetter(SanctuaryChunkGenerator::source),
DIAMETER_CODEC.optionalFieldOf("initial_diameter", 724).forGetter(SanctuaryChunkGenerator::initialDiameter),
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 20, experimental)));
public static final MapCodec<SanctuaryChunkGenerator> NETWORK21_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
SanctuaryBiomeSource.NETWORK21_CODEC.fieldOf("biome_source").forGetter(SanctuaryChunkGenerator::source),
DIAMETER_CODEC.optionalFieldOf("initial_diameter", 724).forGetter(SanctuaryChunkGenerator::initialDiameter),
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 21, experimental)));
private final boolean experimentalStructures;
private volatile fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17 = fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.EMPTY;
private boolean ruinsPrepared;
private volatile fr.koka.sanctuary.worldgen.heritage.HeritagePlan19 heritagePlan19 = fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.EMPTY;
private boolean heritagePrepared;
private final java.util.Map<ExpansionIsland, fr.koka.sanctuary.worldgen.city.LostCityPlanner21.Result> cityPlacements21 = new java.util.concurrent.ConcurrentHashMap<>();
private volatile fr.koka.sanctuary.worldgen.transit.TransitPlan21 transitPlan21 = fr.koka.sanctuary.worldgen.transit.TransitPlan21.EMPTY;
private boolean transitPrepared;
public fr.koka.sanctuary.worldgen.transit.TransitPlan21 transitPlan21() { return transitPlan21; }
public fr.koka.sanctuary.worldgen.city.LostCityPlanner21.Result cityPlacement21(ExpansionIsland island) {
lostCityPlan(island);
return cityPlacements21.get(cityIdentity(island));
}
private volatile Context originContext;
private final int generation;
private net.minecraft.world.level.LevelHeightAccessor heightAccessor;
@@ -83,12 +111,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
}
private SanctuaryChunkGenerator(SanctuaryBiomeSource source, int initialDiameter, int generation, boolean experimental) {
super(source, initialDiameter, true);
this.experimentalStructures = generation == 17 && experimental;
this.experimentalStructures = (generation == 17 || generation == 19 || generation == 20 || generation == 21) && experimental;
this.generation = generation;
if (generation != source.generation()) throw new IllegalArgumentException("Sanctuary generator and biome source versions must agree");
}
@Override public SanctuaryBiomeSource source() { return (SanctuaryBiomeSource) super.source(); }
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; }
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; }
public SanctuaryChunkGenerator withInitialDiameter(int diameter) {
return new SanctuaryChunkGenerator(source().copy(), diameter, generation, experimentalStructures);
}
@@ -97,8 +125,11 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
return new SanctuaryChunkGenerator(source().copy(), initialDiameter(), generation, enabled);
}
public fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17() { return ruinsPlan17; }
public fr.koka.sanctuary.worldgen.heritage.HeritagePlan19 heritagePlan19() { return heritagePlan19; }
public boolean intersectsExperimentalStructures(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
return generation == 17 && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ);
return (generation == 17 || generation == 19 || generation == 20 || generation == 21) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 19 || generation == 20 || generation == 21) && heritagePlan19.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| generation == 21 && transitPlan21.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ);
}
public IslandCapacity capacity() {
return switch (initialDiameter()) {
@@ -134,11 +165,15 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
context(islands.getFirst());
prepareCities(islands);
prepareRuins(islands);
prepareHeritage(islands);
prepareTransit(islands);
}
@Override public void setIslands(List<ExpansionIsland> islands) {
// Complete and publish city geometry before workers can observe the new owner list.
prepareCities(islands);
prepareRuins(islands);
prepareHeritage(islands);
prepareTransit(islands);
super.setIslands(islands);
}
private void prepareCities(List<ExpansionIsland> islands) {
@@ -149,7 +184,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
source().setCityPlans(java.util.Map.of());
return;
}
if ((generation == 16 || generation == 17) && !cityStructuresEnabled) {
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21) && !cityStructuresEnabled) {
cityProtection16 = fr.koka.sanctuary.worldgen.city.LostCityProtection16.EMPTY;
activeCityPlans = java.util.Map.of();
source().setCityPlans(java.util.Map.of());
@@ -165,7 +200,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
}
cityProtection15 = protection.build();
}
if (generation == 16 || generation == 17) {
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21) {
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
for (var island : islands) {
protection.add(published.get(island.id()), undergroundPlan16(island));
@@ -180,7 +215,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
source().setCityPlans(published);
}
private void prepareRuins(List<ExpansionIsland> islands) {
if (generation != 17 || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return;
if ((generation != 17 && generation != 19 && generation != 20 && generation != 21) || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var terrain = context(root);
var sampled = new fr.koka.sanctuary.worldgen.ruins.RuinsTerrain17(root, terrain.generator(), terrain.random(),
@@ -192,6 +227,49 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
SanctuaryMod.LOGGER.info("Experimental ruins17 seed {}: {} networks, {} probes, {} noise columns in {} ms",
root.seed(), plan.networks().size(), plan.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000);
}
private void prepareHeritage(List<ExpansionIsland> islands) {
if ((generation != 19 && generation != 20 && generation != 21) || !experimentalStructures || !cityStructuresEnabled || heritagePrepared) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var terrain = context(root);
if (generation == 21) {
var sampled = new fr.koka.sanctuary.worldgen.heritage.HeritageTerrain21(root, terrain.generator(), terrain.random(),
heightAccessor, pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()));
long start = System.nanoTime();
sampled.discoverLandmarks(lostCityPlan(root), ruinsPlan17);
var plan = fr.koka.sanctuary.worldgen.heritage.HeritagePlanner21.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
heritagePlan19 = plan;
heritagePrepared = true;
SanctuaryMod.LOGGER.info("Heritage21 seed {}: {} sites, {} routes, {} segments, {} probes, {} native columns in {} ms; {}",
root.seed(), plan.sites().size(), plan.links().size(), plan.segments().size(), plan.samples(), sampled.columnsUsed(),
(System.nanoTime() - start) / 1_000_000, plan.diagnostics());
return;
}
var sampled = new fr.koka.sanctuary.worldgen.heritage.HeritageTerrain19(root, terrain.generator(), terrain.random(),
heightAccessor, pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()));
long start = System.nanoTime();
sampled.discoverLandmarks(lostCityPlan(root), ruinsPlan17);
var plan = fr.koka.sanctuary.worldgen.heritage.HeritagePlanner19.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
heritagePlan19 = plan;
heritagePrepared = true;
SanctuaryMod.LOGGER.info("Heritage19 seed {}: {} sites, {} routes, {} segments, {} probes, {} native columns in {} ms; {}",
root.seed(), plan.sites().size(), plan.links().size(), plan.segments().size(), plan.samples(), sampled.columnsUsed(),
(System.nanoTime() - start) / 1_000_000, plan.diagnostics());
}
private void prepareTransit(List<ExpansionIsland> islands) {
if (generation != 21 || !experimentalStructures || !cityStructuresEnabled || transitPrepared) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var terrain = context(root);
var sampled = new fr.koka.sanctuary.worldgen.transit.TransitTerrain21(root, terrain.generator(), terrain.random(),
heightAccessor, pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()) || heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ()));
long start = System.nanoTime();
transitPlan21 = fr.koka.sanctuary.worldgen.transit.TransitPlanner21.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
transitPrepared = true;
SanctuaryMod.LOGGER.info("Transit21 seed {}: {} modules, {} probes, {} columns in {} ms; {}", root.seed(),
transitPlan21.modules().size(), transitPlan21.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000, transitPlan21.diagnostic());
}
private static ExpansionIsland cityIdentity(ExpansionIsland island) {
// State and receipt do not change the terrain geometry used by a cached plan.
var ready = island.status().equals("ready") ? island : island.ready();
@@ -202,6 +280,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
var key = cityIdentity(island);
if (source().islands().stream().noneMatch(saved -> cityIdentity(saved).equals(key))) {
cityPlans.remove(key);
cityPlacements21.remove(key);
undergroundPlans.remove(key);
undergroundPlans16.remove(key);
citySupports.remove(key);
@@ -217,7 +296,40 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
return cityPlans.computeIfAbsent(cityIdentity(island), ignored -> {
long start = System.nanoTime();
var terrain = context(island);
if (generation == 16 || generation == 17) {
if (generation == 21) {
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain21(island, terrain.generator(), terrain.random(),
heightAccessor, pos -> island.origin() && protectsDecoration(pos));
var placement = fr.koka.sanctuary.worldgen.city.LostCityPlanner21.plan(island.seed(), island.centerX(), island.centerZ(),
island.diameter(), island.origin(), sampled);
var surface = placement.surface();
cityPlacements21.put(cityIdentity(island), placement);
citySupports16.put(cityIdentity(island), placement.supports());
if (placement.reservation() != null) cityReservations16.put(cityIdentity(island), placement.reservation());
var supports = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
for (var support : placement.supports()) supports.addViaductSupport(support);
if (placement.reservation() != null) supports.addReservation(placement.reservation());
var extraDistricts = surface.districts().stream()
.filter(d -> !placement.core().surface().districts().contains(d)).toList();
supports.add(new fr.koka.sanctuary.worldgen.city.LostCityPlan(extraDistricts),
new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0));
var supportMask = supports.build();
var underground = fr.koka.sanctuary.worldgen.city.LostCityUnderground16.plan(island.seed(), placement.core().surface(),
new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Terrain() {
@Override public boolean solid(int x, int y, int z) { return sampled.solid(x, y, z); }
@Override public boolean owns(int x, int z) { return sampled.owns(x, z); }
@Override public boolean exteriorAir(int x, int y, int z, int dx, int dz) { return sampled.exteriorAir(x, y, z, dx, dz); }
@Override public boolean protectedAt(int x, int y, int z) {
return supportMask.contains(x, y, z) || sampled.protectedAt(x, y, z);
}
});
undergroundPlans16.put(cityIdentity(island), underground);
SanctuaryMod.LOGGER.info("Lost City21 {} seed {}: {} city districts, {} underground networks, {} noise columns in {} ms",
island.id(), island.seed(), surface.districts().size(), underground.networks().size(), sampled.columnsUsed(),
(System.nanoTime() - start) / 1_000_000);
SanctuaryMod.LOGGER.info("City21 extensions: {}", placement.diagnostics());
return surface;
}
if (generation == 16 || generation == 17 || generation == 19 || generation == 20) {
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain16(island, terrain.generator(), terrain.random(),
heightAccessor, pos -> island.origin() && protectsDecoration(pos));
var placement = LostCityPlanner16.plan(island.seed(), island.centerX(), island.centerZ(),
@@ -288,8 +400,10 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
});
}
public boolean protectsCityDecoration(net.minecraft.core.BlockPos pos) {
if (generation == 16 || generation == 17) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| generation == 17 && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ());
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21) && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 19 || generation == 20 || generation == 21) && heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ())
|| generation == 21 && transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ());
if (generation == 15) return cityProtection15.contains(pos.getX(), pos.getY(), pos.getZ());
if (generation != 14) return false;
var island = islandAt(pos.getX(), pos.getZ());
@@ -311,20 +425,20 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
return citySupports.getOrDefault(cityIdentity(island), List.of());
}
public boolean intersectsCityConstruction16(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
return (generation == 16 || generation == 17) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
return (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
}
public fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan undergroundPlan16(ExpansionIsland island) {
if (generation != 16 && generation != 17) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
lostCityPlan(island);
return undergroundPlans16.getOrDefault(cityIdentity(island), new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0));
}
public List<LostCityPlanner16.Support> citySupports16(ExpansionIsland island) {
if (generation != 16 && generation != 17) return List.of();
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21) return List.of();
lostCityPlan(island);
return citySupports16.getOrDefault(cityIdentity(island), List.of());
}
public LostCityPlanner16.Fallback cityReservation16(ExpansionIsland island) {
if (generation != 16 && generation != 17) return null;
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21) return null;
lostCityPlan(island);
return cityReservations16.get(cityIdentity(island));
}
@@ -336,8 +450,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
net.minecraft.core.BlockPos origin, int radius, boolean skipReferenced) {
if (structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.city.LostCityStructures.KEY))
return fr.koka.sanctuary.worldgen.city.LostCityStructures.findNearest(level, structures, origin, radius, skipReferenced);
if (generation == 17 && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.KEY))
if ((generation == 17 || generation == 19 || generation == 20 || generation == 21) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.KEY))
return fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.findNearest(level, structures, origin, radius, skipReferenced);
if ((generation == 19 || generation == 20 || generation == 21) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.KEY))
return fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.findNearest(level, structures, origin, radius, skipReferenced);
if (generation == 21 && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.transit.TransitStructures21.KEY))
return fr.koka.sanctuary.worldgen.transit.TransitStructures21.findNearest(level, structures, origin, radius, skipReferenced);
return super.findNearestMapStructure(level, structures, origin, radius, skipReferenced);
}
@Override protected Context context(ExpansionIsland island) {
@@ -365,7 +483,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
@Override protected void afterTerrain(ExpansionIsland island, ChunkAccess chunk) {
// Root water/lava are already applied by the Population hook on the named delegate.
if (!island.origin()) super.afterTerrain(island, chunk);
if ((generation == 16 || generation == 17) && cityStructuresEnabled) {
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21) && cityStructuresEnabled) {
var reserved = cityReservations16.get(cityIdentity(island));
if (reserved != null) {
var mutable = new net.minecraft.core.BlockPos.MutableBlockPos();
@@ -0,0 +1,755 @@
package fr.koka.sanctuary.worldgen.city;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Kind;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Parcel;
import java.util.ArrayList;
import fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
import java.util.List;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BedPart;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.DoorHingeSide;
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
import net.minecraft.world.level.block.state.properties.SlabType;
import net.minecraft.world.level.storage.loot.BuiltInLootTables;
import net.minecraft.world.level.storage.loot.LootTable;
/** Complete generation-20 buildings. Published renderers deliberately never call this renderer.
* A north-facing plan is rotated once; all positions remain relative to the saved parcel. */
public final class LostCityInteriors20 {
private static final BlockState AIR = Blocks.AIR.defaultBlockState();
private static final BlockState STONE = Blocks.STONE.defaultBlockState();
private static final BlockState SMOOTH = Blocks.SMOOTH_STONE.defaultBlockState();
private static final BlockState WOOD = Blocks.OAK_PLANKS.defaultBlockState();
private static final BlockState TILE = Blocks.POLISHED_ANDESITE.defaultBlockState();
private static final int STOREY = 5;
private LostCityInteriors20() {}
/** Reachable standing positions, in the same rotated local coordinates as BlockWriter. */
public record Room(String name, int x, int y, int z) {}
/** Local, already rotated enclosure including the floor, full ceiling, four walls and door. */
public record Bathroom(String name, int minX, int minY, int minZ, int maxX, int maxY, int maxZ,
int doorX, int doorY, int doorZ) {}
private record Area(int left, int front, int right, int back) {}
/** Adjacent storeys always change plan; the saved parcel seed chooses which plan comes first. */
public static int layout(Parcel parcel, int floor) {
validate(parcel);
if (floor < 0 || floor >= parcel.floors()) throw new IllegalArgumentException("Invalid interior floor");
return (int) ((parcel.seed() ^ (parcel.seed() >>> 32)) & 1L) ^ (floor & 1);
}
private static Area rear(Parcel p, int floor, boolean east) {
boolean wide = layout(p, floor) == 1;
return east ? new Area(wide ? 10 : 11, wide ? 10 : 11, 14, 14)
: new Area(2, wide ? 10 : 11, wide ? 6 : 5, 14);
}
private static int frontWall(Parcel p, int floor) { return layout(p, floor) == 0 ? 7 : 8; }
private static String frontUse(Parcel p, int floor) {
int variant = layout(p, floor);
return switch (p.kind()) {
case APARTMENT -> variant == 0 ? "kitchen_dining" : "studio_bedroom";
case HOTEL -> floor == 0 ? "reception" : "guest_suite";
case LIBRARY -> variant == 0 ? "book_stacks" : "periodicals_reading";
case RESTAURANT -> variant == 0 ? "dining_room" : "kitchen";
case BARRACKS -> variant == 0 ? "dormitory" : "duty_office";
case HALL -> floor == 0 ? "ticket_office" : "administration";
default -> throw new IllegalArgumentException("Not an interior kind");
};
}
private static String rearUse(Parcel p, int floor, boolean east) {
int variant = layout(p, floor);
return switch (p.kind()) {
case APARTMENT -> east ? "bathroom" : variant == 0 ? "bedroom" : "kitchen_dining";
case HOTEL -> floor == 0 ? east ? "laundry" : "lounge"
: variant == 0 ? east ? "guest_room" : "bedroom" : east ? "bathroom" : "guest_lounge";
case LIBRARY -> variant == 0 ? east ? "archive" : "reading_room" : east ? "bathroom" : "catalogue_office";
case RESTAURANT -> variant == 0 ? east ? "kitchen" : "washrooms" : east ? "pantry" : "dining_room";
case BARRACKS -> east ? "equipment_store" : variant == 0 ? "washrooms" : "officer_bedroom";
case HALL -> variant == 0 ? east ? "bathroom" : "meeting_room" : east ? "meeting_room" : "archive";
default -> throw new IllegalArgumentException("Not an interior kind");
};
}
public static void render(Parcel parcel, LostCityRenderer.BlockWriter output) {
validate(parcel);
var connected = ConnectedBlocks20.buffer(output);
new Building(parcel, connected).render();
connected.flush();
}
/** These describe actual rooms and services, not decorative labels placed in the world. */
public static List<Room> rooms(Parcel parcel) {
validate(parcel);
List<Room> result = new ArrayList<>();
add(result, parcel, "street_entrance", 8, 1, 0);
add(result, parcel, "vestibule", 8, 1, 3);
for (int floor = 0; floor < parcel.floors(); floor++) {
int y = floor * STOREY + 1;
String prefix = "floor_" + floor + "/";
add(result, parcel, prefix + "corridor", 8, y, 8);
add(result, parcel, prefix + "stair_landing", 4, y, 8);
add(result, parcel, prefix + frontUse(parcel, floor), 11, y, 6);
if (parcel.kind() == Kind.HALL && floor < 2) {
if (floor == 0) {
add(result, parcel, prefix + "auditorium", 8, y, 11);
add(result, parcel, prefix + "stage", 9, y + 1, 13);
} else add(result, parcel, prefix + "gallery", 8, y, 10);
} else {
Area west = rear(parcel, floor, false), east = rear(parcel, floor, true);
add(result, parcel, prefix + rearUse(parcel, floor, false), west.right(), y, 11);
add(result, parcel, prefix + rearUse(parcel, floor, true), east.left(), y, 11);
}
}
for (var bath : bathroomPlans(parcel)) {
int targetZ = bath.southDoor() ? bath.z1() - 1 : bath.z0() + 1;
add(result, parcel, bath.name(), bath.doorX(), bath.floor() * STOREY + 1, targetZ);
}
add(result, parcel, "roof", 8, parcel.floors() * STOREY + 1, 8);
switch (parcel.kind()) {
case HOTEL -> add(result, parcel, "roof_laundry_pavilion", 10, parcel.floors() * STOREY + 1, 5);
case APARTMENT -> add(result, parcel, "roof_residents_terrace", 8, parcel.floors() * STOREY + 1, 10);
case LIBRARY -> add(result, parcel, "roof_reading_terrace", 11, parcel.floors() * STOREY + 1, 10);
case RESTAURANT -> add(result, parcel, "roof_dining_terrace", 11, parcel.floors() * STOREY + 1, 8);
case BARRACKS -> add(result, parcel, "roof_service_stair", 4, parcel.floors() * STOREY + 1, 8);
default -> { }
}
return List.copyOf(result);
}
private record BathPlan(String name, int floor, int x0, int z0, int x1, int z1, int doorX, boolean southDoor) {}
private static List<BathPlan> bathroomPlans(Parcel p) {
List<BathPlan> result = new ArrayList<>();
for (int floor = 0; floor < p.floors(); floor++) {
if (p.kind() == Kind.HOTEL && floor > 0)
result.add(new BathPlan("floor_" + floor + "/suite_bathroom", floor, 10, 1, 15, 4, 12, true));
if (p.kind() == Kind.HALL && floor < 2) continue;
for (boolean east : new boolean[]{false, true}) {
String use = rearUse(p, floor, east);
if (!use.equals("bathroom") && !use.equals("washrooms")) continue;
Area room = rear(p, floor, east);
result.add(new BathPlan("floor_" + floor + "/" + (east ? "east" : "west") + "_bathroom_interior",
floor, room.left() - 1, 12, room.right() + 1, 15, east ? 12 : 4, false));
}
}
return List.copyOf(result);
}
public static List<Bathroom> bathrooms(Parcel p) {
validate(p);
List<Bathroom> result = new ArrayList<>();
for (var b : bathroomPlans(p)) {
int x0 = rx(b.x0(), b.z0(), p.facing()), z0 = rz(b.x0(), b.z0(), p.facing());
int x1 = rx(b.x1(), b.z1(), p.facing()), z1 = rz(b.x1(), b.z1(), p.facing());
int doorZ = b.southDoor() ? b.z1() : b.z0();
result.add(new Bathroom(b.name(), Math.min(x0, x1), b.floor() * STOREY, Math.min(z0, z1),
Math.max(x0, x1), b.floor() * STOREY + 4, Math.max(z0, z1), rx(b.doorX(), doorZ, p.facing()),
b.floor() * STOREY + 1, rz(b.doorX(), doorZ, p.facing())));
}
return List.copyOf(result);
}
private static void validate(Parcel p) {
if (!p.kind().building() || p.width() != 17 || p.depth() != 17 || p.floors() < 2)
throw new IllegalArgumentException("Generation-20 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, Blocks.STONE_BRICKS.defaultBlockState());
box(0, 0, 0, 16, 0, 16, SMOOTH);
box(0, 1, 0, 16, roof + 4, 16, AIR);
shell(roof);
architecturalEnvelope(roof);
for (int floor = 0; floor < p.floors(); floor++) {
partitions(floor);
furnishings(floor);
}
stairs();
entrance();
roof(roof);
// Only one deferred loot table per building; empty furniture is not a resource vault.
int lootX = 6;
int lootZ = 6;
box(lootX, 2, lootZ, lootX, 3, lootZ, AIR);
ResourceKey<LootTable> table = switch (p.kind()) {
case LIBRARY, HALL -> BuiltInLootTables.VILLAGE_CARTOGRAPHER;
case RESTAURANT -> BuiltInLootTables.VILLAGE_BUTCHER;
case BARRACKS -> BuiltInLootTables.VILLAGE_FLETCHER;
default -> BuiltInLootTables.VILLAGE_PLAINS_HOUSE;
};
output.chest(rx(lootX, lootZ, p.facing()), 1, rz(lootX, lootZ, p.facing()), table,
p.seed() ^ 0x20c17a7eL ^ p.kind().ordinal());
}
void shell(int roof) {
for (int floor = 0; floor <= p.floors(); floor++) {
int base = floor * STOREY;
for (int x = 1; x <= 15; x++) for (int z = 1; z <= 15; z++)
set(x, base, z, floor == p.floors() ? SMOOTH : floorMaterial(floor, x, z));
if (floor == p.floors()) continue;
// A pale ceiling and contrasting structural floor are separate layers.
box(2, base + 4, 2, 14, base + 4, 14,
Blocks.SMOOTH_STONE_SLAB.defaultBlockState().setValue(BlockStateProperties.SLAB_TYPE, SlabType.TOP));
for (int y = 1; y <= 4; y++) for (int horizontal = 1; horizontal <= 15; horizontal++) {
facade(horizontal, base + y, 1, horizontal, y, true);
facade(horizontal, base + y, 15, horizontal, y, true);
facade(1, base + y, horizontal, horizontal, y, false);
facade(15, base + y, horizontal, horizontal, y, false);
}
// Floor bands remain intact even where windows have fallen out.
for (int n = 1; n <= 15; n++) {
set(n, base, 1, SMOOTH); set(n, base, 15, SMOOTH);
set(1, base, n, SMOOTH); set(15, base, n, SMOOTH);
}
if (p.kind() == Kind.HOTEL || p.kind() == Kind.LIBRARY) {
for (int x : new int[]{3, 13}) for (int y = 0; y <= 4; y++) set(x, base + y, 0, SMOOTH);
}
if (p.kind() == Kind.APARTMENT) for (int x = 11; x <= 14; x++)
set(x, base + 1, 0, Blocks.SMOOTH_STONE_SLAB);
if (p.kind() == Kind.RESTAURANT) box(10, base + 4, 0, 14, base + 4, 0,
Blocks.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(int floor, int x, int z) {
boolean border = x == 1 || z == 1 || x == 15 || z == 15;
if (border) return Blocks.TUFF_BRICKS.defaultBlockState();
return switch (p.kind()) {
case LIBRARY -> (x <= 5 || z >= 10 ? Blocks.OAK_PLANKS : Blocks.BIRCH_PLANKS).defaultBlockState();
case APARTMENT, HOTEL -> ((floor & 1) == 0 ? Blocks.OAK_PLANKS : Blocks.BIRCH_PLANKS).defaultBlockState();
case RESTAURANT -> ((x / 3 + z / 3) & 1) == 0 ? SMOOTH : Blocks.POLISHED_DIORITE.defaultBlockState();
case BARRACKS -> (x <= 5 ? Blocks.POLISHED_ANDESITE : Blocks.OAK_PLANKS).defaultBlockState();
default -> (x == 7 || x == 9 ? Blocks.POLISHED_GRANITE : Blocks.SMOOTH_STONE).defaultBlockState();
};
}
BlockState pier() {
return switch (p.kind()) {
case APARTMENT -> Blocks.STRIPPED_OAK_LOG.defaultBlockState();
case HOTEL -> Blocks.POLISHED_DIORITE.defaultBlockState();
case LIBRARY -> Blocks.STRIPPED_BIRCH_LOG.defaultBlockState();
case RESTAURANT -> Blocks.POLISHED_GRANITE.defaultBlockState();
case BARRACKS -> Blocks.TUFF_BRICKS.defaultBlockState();
default -> Blocks.CHISELED_TUFF_BRICKS.defaultBlockState();
};
}
void facade(int x, int y, int z, int horizontal, int withinFloor, boolean eastWest) {
boolean column = horizontal == 1 || horizontal == 6 || horizontal == 10 || horizontal == 15;
boolean window = !column && (withinFloor == 2 || withinFloor == 3
|| withinFloor == 1 && (p.kind() == Kind.LIBRARY || p.kind() == Kind.RESTAURANT || p.kind() == Kind.HALL));
if (column) set(x, y, z, pier());
else if (window) {
if (roll(x, y / 5, z, 17) != 0) set(x, y, z,
p.kind() == Kind.BARRACKS ? rail(eastWest) : pane(eastWest));
} else set(x, y, z, withinFloor == 4 ? Blocks.TUFF_BRICKS.defaultBlockState()
: p.kind() == Kind.APARTMENT || p.kind() == Kind.LIBRARY ? Blocks.BIRCH_PLANKS.defaultBlockState() : STONE);
}
/** Volume belongs to its use: projected bays, clipped corners and distinct roof profiles. */
void architecturalEnvelope(int roof) {
for (int floor = 0; floor < p.floors(); floor++) {
int y = floor * STOREY;
// Project the floor plates as a continuous, one-block cornice inside the parcel.
for (int n = 1; n <= 15; n++) {
set(n, y, 0, stairTrim(Direction.NORTH)); set(n, y, 16, stairTrim(Direction.SOUTH));
set(0, y, n, stairTrim(Direction.WEST)); set(16, y, n, stairTrim(Direction.EAST));
}
switch (p.kind()) {
case APARTMENT -> {
// Bay windows increase the living room rather than drawing flat stripes on it.
for (int z : new int[]{4, 5, 6, 11, 12, 13}) {
set(16, y, z, Blocks.BIRCH_PLANKS);
set(16, y + 1, z, Blocks.OAK_PLANKS);
set(16, y + 2, z, Blocks.GLASS_PANE);
set(16, y + 3, z, Blocks.GLASS_PANE);
set(16, y + 4, z, Blocks.BIRCH_SLAB.defaultBlockState().setValue(BlockStateProperties.SLAB_TYPE, SlabType.TOP));
}
for (int z : new int[]{3, 7, 10, 14}) box(16, y + 1, z, 16, y + 4, z, pier());
// Original wall remains the sill, with an open bay above it; privacy walls restore themselves later.
for (int z : new int[]{4, 5, 6, 11, 12, 13}) box(15, y + 2, z, 15, y + 3, z, AIR);
bevelFront(y);
}
case HOTEL -> {
for (int x : new int[]{2, 5, 11, 14}) box(x, y + 1, 0, x, y + 4, 0, pier());
for (int x : new int[]{3, 4, 12, 13}) set(x, y + 1, 0, Blocks.BIRCH_FENCE);
box(2, y + 4, 0, 5, y + 4, 0, Blocks.BIRCH_PLANKS.defaultBlockState());
box(11, y + 4, 0, 14, y + 4, 0, Blocks.BIRCH_PLANKS.defaultBlockState());
}
case LIBRARY -> {
// Tall fins frame broad glazing and project beyond the reading rooms.
for (int x : new int[]{2, 5, 11, 14}) box(x, y + 1, 0, x, y + 4, 0, Blocks.STRIPPED_BIRCH_LOG.defaultBlockState());
for (int x : new int[]{3, 4, 12, 13}) {
set(x, y + 4, 0, Blocks.OAK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
}
bevelFront(y);
}
case RESTAURANT -> {
// Separate fascia and timber valance shelter the shop windows.
for (int x = 10; x <= 14; x++) {
set(x, y + 3, 0, Blocks.BIRCH_SLAB);
set(x, y + 4, 0, ((x / 2) & 1) == 0 ? Blocks.OAK_PLANKS : Blocks.BIRCH_PLANKS);
}
for (int z : new int[]{3, 7, 11, 14}) set(16, y + 4, z, Blocks.POLISHED_GRANITE);
}
case BARRACKS -> {
for (int z : new int[]{2, 7, 12, 15}) box(0, y + 1, z, 0, y + 4, z, pier());
for (int x : new int[]{2, 5, 11, 14}) box(x, y + 1, 0, x, y + 4, 0, Blocks.TUFF_BRICKS.defaultBlockState());
for (int x : new int[]{3, 4, 12, 13}) set(x, y + 1, 0, Blocks.CHISELED_TUFF_BRICKS);
}
case HALL -> {
for (int x : new int[]{2, 5, 11, 14}) box(x, y + 1, 0, x, y + 4, 0, pier());
for (int x = 2; x <= 14; x++) set(x, y + 4, 0,
Blocks.POLISHED_DIORITE_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
}
default -> throw new IllegalStateException();
}
}
}
BlockState stairTrim(Direction outward) {
Block block = p.kind() == Kind.RESTAURANT ? Blocks.POLISHED_GRANITE_STAIRS : Blocks.TUFF_BRICK_STAIRS;
return facing(block, outward.getOpposite()).setValue(BlockStateProperties.HALF,
net.minecraft.world.level.block.state.properties.Half.TOP);
}
void bevelFront(int y) {
// Remove two rectangular corners and close them with actual stepped returns.
for (int x : new int[]{1, 15}) {
box(x, y, 1, x, y + 4, 1, AIR);
int adjacent = x == 1 ? 2 : 14;
box(adjacent, y + 1, 1, adjacent, y + 4, 1, pier());
box(x, y + 1, 2, x, y + 4, 2, pier());
}
}
void partitions(int floor) {
int y = floor * STOREY;
int front = frontWall(p, floor);
// The west stairwell, central corridor and vestibule are independent of furniture.
box(5, y + 1, 2, 5, y + 4, 9, SMOOTH);
door(5, y + 1, 8, Direction.WEST, false);
box(10, y + 1, 2, 10, y + 4, front, SMOOTH);
box(10, y + 1, front, 15, y + 4, front, SMOOTH);
door(10, y + 1, 6, Direction.EAST, false);
if (floor == 0) {
box(6, 1, 4, 9, 4, 4, SMOOTH);
door(8, 1, 4, Direction.SOUTH, false);
}
if (p.kind() == Kind.HALL && floor < 2) {
if (floor == 0) {
box(2, 1, 10, 14, 4, 10, SMOOTH);
door(7, 1, 10, Direction.SOUTH, true);
door(8, 1, 10, Direction.SOUTH, false);
}
} else {
Area west = rear(p, floor, false), east = rear(p, floor, true);
box(1, y + 1, west.front() - 1, west.right() + 1, y + 4, west.front() - 1, SMOOTH);
box(west.right() + 1, y + 1, west.front() - 1, west.right() + 1, y + 4, 15, SMOOTH);
box(east.left() - 1, y + 1, east.front() - 1, 15, y + 4, east.front() - 1, SMOOTH);
box(east.left() - 1, y + 1, east.front() - 1, east.left() - 1, y + 4, 15, SMOOTH);
door(west.right() + 1, y + 1, 11, Direction.WEST, false);
door(east.left() - 1, y + 1, 11, Direction.EAST, false);
}
// A hanging lantern touches a full ceiling block, never the recessed underside of a top slab.
set(8, y + 4, 8, SMOOTH);
set(8, y + 3, 8, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
// Weathering stays on facades and roofs: it must not replace a privacy partition.
}
void furnishings(int floor) {
int y = floor * STOREY;
int variant = layout(p, floor);
switch (p.kind()) {
case APARTMENT -> {
if (variant == 0) { kitchenFront(y); bedroom(floor, false); }
else { frontBedroom(floor); kitchenRear(floor, false); }
}
case HOTEL -> {
if (floor == 0) { reception(y); lounge(floor, false); laundry(floor); }
else {
anchoredBed(14, y + 1, frontWall(p, floor) - 2, Direction.SOUTH);
set(11, y + 1, 5, Blocks.BARREL); set(11, y + 2, 5, Blocks.FLOWER_POT);
if (variant == 0) { bedroom(floor, false); bedroom(floor, true); }
else lounge(floor, false);
}
}
case LIBRARY -> {
if (variant == 0) {
box(14, y + 1, 2, 14, y + 3, 6, Blocks.BOOKSHELF.defaultBlockState());
box(12, y + 1, 2, 12, y + 2, 3, Blocks.BOOKSHELF.defaultBlockState());
desk(12, y, 5, Direction.SOUTH);
readingRoom(floor, false); archive(floor, true);
} else {
readingFront(y); readingRoom(floor, false);
set(2, y + 1, 14, Blocks.CARTOGRAPHY_TABLE);
}
}
case RESTAURANT -> {
if (variant == 0) {
table(13, y, 3); table(13, y, 5);
set(11, y + 1, 2, Blocks.POTTED_DEAD_BUSH);
kitchenRear(floor, true);
} else {
kitchenFront(y); diningRear(floor, false); pantry(floor, true);
}
}
case BARRACKS -> {
if (variant == 0) {
anchoredBed(12, y + 1, 3, Direction.NORTH); anchoredBed(14, y + 1, 3, Direction.NORTH);
set(11, y + 1, 2, Blocks.BARREL); set(11, y + 2, 2, Blocks.BARREL);
seat(13, y + 1, 6, Direction.NORTH);
} else { reception(y); bedroom(floor, false); }
box(12, y + 1, 14, 14, y + 2, 14, Blocks.BARREL.defaultBlockState());
set(14, y + 1, 11, Blocks.FLETCHING_TABLE);
set(12, y + 1, 11, Blocks.GRINDSTONE);
}
case HALL -> {
if (floor == 0 || floor > 1 && variant == 0) reception(y); else readingFront(y);
if (floor == 0) auditorium();
else if (floor >= 2) {
if (variant == 0) readingRoom(floor, false);
else { archive(floor, false); readingRoom(floor, true); }
}
}
default -> throw new IllegalArgumentException("Not an interior kind");
}
for (var bath : bathroomPlans(p)) if (bath.floor() == floor) bathroom(bath);
}
void kitchenFront(int y) {
kitchenLine(y, 11, 2, Direction.SOUTH);
table(13, y, 5);
}
void kitchenRear(int floor, boolean east) {
int y = floor * STOREY;
Area room = rear(p, floor, east);
kitchenLine(y, room.left(), 14, Direction.NORTH);
if (east) {
set(14, y + 1, 11, SMOOTH); set(14, y + 2, 11, Blocks.FLOWER_POT);
set(12, y + 1, 11, Blocks.CRAFTING_TABLE);
} else table(room.left() + 2, y, 11);
}
void kitchenLine(int y, int left, int z, Direction face) {
set(left, y + 1, z, facing(Blocks.FURNACE, face));
set(left + 1, y + 1, z, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 2));
set(left + 2, y + 1, z, facing(Blocks.SMOKER, face));
set(left + 3, y + 1, z, Blocks.BARREL);
box(left, y + 3, z, left + 3, y + 3, z, SMOOTH);
set(left + 2, y + 2, z, Blocks.IRON_TRAPDOOR);
for (int x = left; x <= left + 3; x++) set(x, y + 3, z, Blocks.POLISHED_DIORITE);
for (int x = left; x <= left + 3; x++) set(x, y, z, (x & 1) == 0 ? SMOOTH : TILE);
}
void bedroom(int floor, boolean east) {
int y = floor * STOREY;
Area room = rear(p, floor, east);
anchoredBed(east ? room.right() - 1 : room.left() + 1, y + 1, 13, east ? Direction.EAST : Direction.WEST);
set(east ? room.right() : room.left(), y + 1, 11, Blocks.BARREL);
desk(east ? room.left() + 1 : room.right() - 1, y, 11, Direction.SOUTH);
set(east ? room.left() : room.right(), y + 1, 14, Blocks.BOOKSHELF);
}
void frontBedroom(int floor) {
int y = floor * STOREY;
anchoredBed(14, y + 1, 3, Direction.NORTH);
set(11, y + 1, 2, Blocks.BARREL); set(11, y + 2, 2, Blocks.FLOWER_POT);
desk(12, y, 5, Direction.SOUTH);
}
void bathroom(BathPlan bath) {
int y = bath.floor() * STOREY;
for (int x = bath.x0(); x <= bath.x1(); x++) for (int z = bath.z0(); z <= bath.z1(); z++) {
set(x, y, z, ((x + z) & 1) == 0 ? SMOOTH : TILE);
set(x, y + 4, z, SMOOTH);
boolean wall = x == bath.x0() || x == bath.x1() || z == bath.z0() || z == bath.z1();
for (int up = 1; up <= 3; up++) set(x, y + up, z, wall ? SMOOTH : AIR);
}
int doorZ = bath.southDoor() ? bath.z1() : bath.z0();
door(bath.doorX(), y + 1, doorZ, bath.southDoor() ? Direction.NORTH : Direction.SOUTH, false);
int fixtureZ = bath.southDoor() ? bath.z0() + 1 : bath.z1() - 1;
int toiletX = bath.southDoor() ? bath.x1() - 2 : bath.x0() + 1;
Direction view = bath.southDoor() ? Direction.SOUTH : Direction.EAST;
set(toiletX, y + 1, fixtureZ, seating(Blocks.POLISHED_ANDESITE_STAIRS, view));
set(toiletX, y + 2, fixtureZ, Blocks.IRON_TRAPDOOR);
set(bath.x1() - 1, y + 1, fixtureZ, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 2));
}
void reception(int y) {
box(12, y + 1, 5, 14, y + 1, 5, Blocks.BIRCH_PLANKS.defaultBlockState());
set(13, y + 2, 5, Blocks.FLOWER_POT);
set(14, y + 1, 2, Blocks.BOOKSHELF); set(14, y + 2, 2, Blocks.BARREL);
desk(12, y, 3, Direction.SOUTH);
set(11, y + 1, 2, Blocks.POTTED_DEAD_BUSH);
}
void laundry(int floor) {
int y = floor * STOREY;
for (int z = 11; z <= 12; z++) set(14, y + 1, z, Blocks.WATER_CAULDRON.defaultBlockState()
.setValue(BlockStateProperties.LEVEL_CAULDRON, z == 11 ? 1 : 2));
box(12, y + 1, 14, 14, y + 1, 14, Blocks.BARREL.defaultBlockState());
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) {
// Clipped parapets and a heavy coping keep each accessible roof legible from below.
for (int n = 2; n <= 14; n++) {
set(n, y + 1, 1, Blocks.TUFF_BRICKS); set(n, y + 1, 15, Blocks.TUFF_BRICKS);
set(1, y + 1, n, Blocks.TUFF_BRICKS); set(15, y + 1, n, Blocks.TUFF_BRICKS);
set(n, y + 2, 1, Blocks.TUFF_BRICK_SLAB); set(n, y + 2, 15, Blocks.TUFF_BRICK_SLAB);
set(1, y + 2, n, Blocks.TUFF_BRICK_SLAB); set(15, y + 2, n, Blocks.TUFF_BRICK_SLAB);
}
switch (p.kind()) {
case APARTMENT -> {
// Two small roof gardens and a pergola leave the stair landing and central crossing free.
roofPlanter(10, y, 4); roofPlanter(10, y, 12);
for (int x : new int[]{6, 13}) box(x, y + 1, 14, x, y + 3, 14, Blocks.STRIPPED_OAK_LOG.defaultBlockState());
box(6, y + 4, 14, 13, y + 4, 14, Blocks.BIRCH_SLAB.defaultBlockState());
desk(8, y, 12, Direction.NORTH);
}
case HOTEL -> {
roofRoom(y, 9, 3, 14, 7, Blocks.BIRCH_PLANKS);
door(9, y + 1, 5, Direction.EAST, false);
set(12, y + 1, 4, Blocks.BARREL); set(13, y + 1, 4, Blocks.LOOM);
box(11, y + 1, 12, 13, y + 1, 12, seating(Blocks.DARK_OAK_STAIRS, Direction.NORTH));
}
case LIBRARY -> {
// Clerestory above the stacks; its raised glass ridge is distinct from a flat roof.
for (int x = 10; x <= 13; x++) for (int z = 3; z <= 7; z++) {
set(x, y + 1, z, x == 10 || x == 13 || z == 3 || z == 7 ? Blocks.STRIPPED_BIRCH_LOG : Blocks.GLASS);
if (x == 11 || x == 12) set(x, y + 2, z, Blocks.GLASS);
}
// Cut both the roof plate and the recessed ceiling beneath the glass.
// The six-column light well stays enclosed by its complete timber frame;
// the glass cap and the surrounding roof walkway remain intact.
box(11, y - 1, 4, 12, y, 6, AIR);
desk(11, y, 12, Direction.NORTH); set(14, y + 1, 12, Blocks.LECTERN);
}
case RESTAURANT -> {
// A real dining terrace and rear kitchen extraction with full supporting columns.
table(11, y, 6); table(11, y, 11);
for (int z : new int[]{3, 14}) box(14, y + 1, z, 14, y + 3, z, Blocks.STRIPPED_OAK_LOG.defaultBlockState());
box(14, y + 4, 3, 14, y + 4, 14, Blocks.BIRCH_PLANKS.defaultBlockState());
box(6, y + 1, 14, 7, y + 3, 14, Blocks.TUFF_BRICKS.defaultBlockState());
set(6, y + 4, 14, Blocks.IRON_BARS); set(7, y + 4, 14, Blocks.IRON_BARS);
}
case BARRACKS -> {
// Enclosed stair head gives the barracks a service tower, without hiding its usable exit.
// The whole last flight sits inside the stair head: a front wall at z=6
// would cross the headroom over its penultimate step.
roofRoom(y, 1, 2, 5, 10, Blocks.TUFF_BRICKS);
door(5, y + 1, 8, Direction.EAST, false);
box(10, y + 1, 12, 13, y + 1, 14, Blocks.POLISHED_ANDESITE.defaultBlockState());
set(11, y + 2, 13, Blocks.IRON_TRAPDOOR);
}
case HALL -> {
for (int step = 0; step < 3; step++) {
int edge = 2 + step * 2;
box(edge, y + 1 + step, 1, 16 - edge, y + 1 + step, 2, Blocks.TUFF_BRICKS.defaultBlockState());
}
for (int x : new int[]{4, 12}) set(x, y + 4, 2, Blocks.CHISELED_TUFF_BRICKS);
desk(11, y, 12, Direction.NORTH);
}
default -> throw new IllegalStateException();
}
}
void roofRoom(int y, int x0, int z0, int x1, int z1, Block wall) {
for (int x = x0; x <= x1; x++) for (int z = z0; z <= z1; z++) {
boolean edge = x == x0 || x == x1 || z == z0 || z == z1;
for (int up = 1; up <= 3; up++) set(x, y + up, z, edge ? wall.defaultBlockState() : AIR);
set(x, y + 4, z, Blocks.TUFF_BRICK_SLAB.defaultBlockState().setValue(BlockStateProperties.SLAB_TYPE, SlabType.TOP));
}
}
void roofPlanter(int x, int y, int z) {
for (int dx = 0; dx <= 3; dx++) set(x + dx, y + 1, z, Blocks.TUFF_BRICKS);
set(x + 1, y + 1, z, Blocks.DIRT); set(x + 2, y + 1, z, Blocks.DIRT);
set(x + 1, y + 2, z, Blocks.OXEYE_DAISY); set(x + 2, y + 2, z, Blocks.CORNFLOWER);
}
void door(int x, int y, int z, Direction facing, boolean rightHinge) {
BlockState state = facing(Blocks.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,767 @@
package fr.koka.sanctuary.worldgen.city;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Kind;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Parcel;
import java.util.ArrayList;
import fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
import java.util.List;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BedPart;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.DoorHingeSide;
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
import net.minecraft.world.level.block.state.properties.SlabType;
import net.minecraft.world.level.storage.loot.BuiltInLootTables;
import net.minecraft.world.level.storage.loot.LootTable;
/** Complete generation-21 buildings. Published renderers deliberately never call this renderer.
* A north-facing plan is rotated once; all positions remain relative to the saved parcel. */
public final class LostCityInteriors21 {
private static final BlockState AIR = Blocks.AIR.defaultBlockState();
private static final BlockState STONE = Blocks.STONE.defaultBlockState();
private static final BlockState SMOOTH = Blocks.SMOOTH_STONE.defaultBlockState();
private static final BlockState WOOD = Blocks.OAK_PLANKS.defaultBlockState();
private static final BlockState TILE = Blocks.POLISHED_ANDESITE.defaultBlockState();
private static final int STOREY = 5;
private LostCityInteriors21() {}
/** Reachable standing positions, in the same rotated local coordinates as BlockWriter. */
public record Room(String name, int x, int y, int z) {}
/** Local, already rotated enclosure including the floor, full ceiling, four walls and door. */
public record Bathroom(String name, int minX, int minY, int minZ, int maxX, int maxY, int maxZ,
int doorX, int doorY, int doorZ) {}
private record Area(int left, int front, int right, int back) {}
/** Adjacent storeys always change plan; the saved parcel seed chooses which plan comes first. */
public static int layout(Parcel parcel, int floor) {
validate(parcel);
if (floor < 0 || floor >= parcel.floors()) throw new IllegalArgumentException("Invalid interior floor");
return (int) ((parcel.seed() ^ (parcel.seed() >>> 32)) & 1L) ^ (floor & 1);
}
private static Area rear(Parcel p, int floor, boolean east) {
boolean wide = layout(p, floor) == 1;
return east ? new Area(wide ? 10 : 11, wide ? 10 : 11, 14, 14)
: new Area(2, wide ? 10 : 11, wide ? 6 : 5, 14);
}
private static int frontWall(Parcel p, int floor) { return layout(p, floor) == 0 ? 7 : 8; }
private static String frontUse(Parcel p, int floor) {
int variant = layout(p, floor);
return switch (p.kind()) {
case APARTMENT -> variant == 0 ? "kitchen_dining" : "studio_bedroom";
case HOTEL -> floor == 0 ? "reception" : "guest_suite";
case LIBRARY -> variant == 0 ? "book_stacks" : "periodicals_reading";
case RESTAURANT -> variant == 0 ? "dining_room" : "kitchen";
case BARRACKS -> variant == 0 ? "dormitory" : "duty_office";
case HALL -> floor == 0 ? "ticket_office" : "administration";
default -> throw new IllegalArgumentException("Not an interior kind");
};
}
private static String rearUse(Parcel p, int floor, boolean east) {
int variant = layout(p, floor);
return switch (p.kind()) {
case APARTMENT -> east ? "bathroom" : variant == 0 ? "bedroom" : "kitchen_dining";
case HOTEL -> floor == 0 ? east ? "laundry" : "lounge"
: variant == 0 ? east ? "guest_room" : "bedroom" : east ? "bathroom" : "guest_lounge";
case LIBRARY -> variant == 0 ? east ? "archive" : "reading_room" : east ? "bathroom" : "catalogue_office";
case RESTAURANT -> variant == 0 ? east ? "kitchen" : "washrooms" : east ? "pantry" : "dining_room";
case BARRACKS -> east ? "equipment_store" : variant == 0 ? "washrooms" : "officer_bedroom";
case HALL -> variant == 0 ? east ? "bathroom" : "meeting_room" : east ? "meeting_room" : "archive";
default -> throw new IllegalArgumentException("Not an interior kind");
};
}
public static void render(Parcel parcel, LostCityRenderer.BlockWriter output) {
validate(parcel);
var connected = ConnectedBlocks20.buffer(output);
new Building(parcel, connected).render();
connected.flush();
}
/** These describe actual rooms and services, not decorative labels placed in the world. */
public static List<Room> rooms(Parcel parcel) {
validate(parcel);
List<Room> result = new ArrayList<>();
add(result, parcel, "street_entrance", 8, 1, 0);
add(result, parcel, "vestibule", 8, 1, 3);
for (int floor = 0; floor < parcel.floors(); floor++) {
int y = floor * STOREY + 1;
String prefix = "floor_" + floor + "/";
add(result, parcel, prefix + "corridor", 8, y, 8);
add(result, parcel, prefix + "stair_landing", 4, y, 8);
add(result, parcel, prefix + frontUse(parcel, floor), 11, y, 6);
if (parcel.kind() == Kind.HALL && floor < 2) {
if (floor == 0) {
add(result, parcel, prefix + "auditorium", 8, y, 11);
add(result, parcel, prefix + "stage", 9, y + 1, 13);
} else add(result, parcel, prefix + "gallery", 8, y, 10);
} else {
Area west = rear(parcel, floor, false), east = rear(parcel, floor, true);
add(result, parcel, prefix + rearUse(parcel, floor, false), west.right(), y, 11);
add(result, parcel, prefix + rearUse(parcel, floor, true), east.left(), y, 11);
}
}
for (var bath : bathroomPlans(parcel)) {
int targetZ = bath.southDoor() ? bath.z1() - 1 : bath.z0() + 1;
add(result, parcel, bath.name(), bath.doorX(), bath.floor() * STOREY + 1, targetZ);
}
add(result, parcel, "roof", 8, parcel.floors() * STOREY + 1, 8);
switch (parcel.kind()) {
case HOTEL -> add(result, parcel, "roof_laundry_pavilion", 10, parcel.floors() * STOREY + 1, 5);
case APARTMENT -> add(result, parcel, "roof_residents_terrace", 8, parcel.floors() * STOREY + 1, 10);
case LIBRARY -> add(result, parcel, "roof_reading_terrace", 11, parcel.floors() * STOREY + 1, 10);
case RESTAURANT -> add(result, parcel, "roof_dining_terrace", 11, parcel.floors() * STOREY + 1, 8);
case BARRACKS -> add(result, parcel, "roof_service_stair", 4, parcel.floors() * STOREY + 1, 8);
default -> { }
}
return List.copyOf(result);
}
private record BathPlan(String name, int floor, int x0, int z0, int x1, int z1, int doorX, boolean southDoor) {}
private static List<BathPlan> bathroomPlans(Parcel p) {
List<BathPlan> result = new ArrayList<>();
for (int floor = 0; floor < p.floors(); floor++) {
if (p.kind() == Kind.HOTEL && floor > 0)
result.add(new BathPlan("floor_" + floor + "/suite_bathroom", floor, 10, 1, 15, 4, 12, true));
if (p.kind() == Kind.HALL && floor < 2) continue;
for (boolean east : new boolean[]{false, true}) {
String use = rearUse(p, floor, east);
if (!use.equals("bathroom") && !use.equals("washrooms")) continue;
Area room = rear(p, floor, east);
result.add(new BathPlan("floor_" + floor + "/" + (east ? "east" : "west") + "_bathroom_interior",
floor, room.left() - 1, 12, room.right() + 1, 15, east ? 12 : 4, false));
}
}
return List.copyOf(result);
}
public static List<Bathroom> bathrooms(Parcel p) {
validate(p);
List<Bathroom> result = new ArrayList<>();
for (var b : bathroomPlans(p)) {
int x0 = rx(b.x0(), b.z0(), p.facing()), z0 = rz(b.x0(), b.z0(), p.facing());
int x1 = rx(b.x1(), b.z1(), p.facing()), z1 = rz(b.x1(), b.z1(), p.facing());
int doorZ = b.southDoor() ? b.z1() : b.z0();
result.add(new Bathroom(b.name(), Math.min(x0, x1), b.floor() * STOREY, Math.min(z0, z1),
Math.max(x0, x1), b.floor() * STOREY + 4, Math.max(z0, z1), rx(b.doorX(), doorZ, p.facing()),
b.floor() * STOREY + 1, rz(b.doorX(), doorZ, p.facing())));
}
return List.copyOf(result);
}
private static void validate(Parcel p) {
if (!p.kind().building() || p.width() != 17 || p.depth() != 17 || p.floors() < 2)
throw new IllegalArgumentException("Generation-21 interiors require a 17x17 building with 2..5 floors");
}
private static void add(List<Room> result, Parcel p, String name, int x, int y, int z) {
result.add(new Room(name, rx(x, z, p.facing()), y, rz(x, z, p.facing())));
}
private static int rx(int x, int z, int facing) {
return switch (facing) { case 0 -> x; case 1 -> 16 - z; case 2 -> 16 - x; default -> z; };
}
private static int rz(int x, int z, int facing) {
return switch (facing) { case 0 -> z; case 1 -> x; case 2 -> 16 - z; default -> 16 - x; };
}
private static final class Building {
final Parcel p;
final LostCityRenderer.BlockWriter output;
final Rotation rotation;
Building(Parcel p, LostCityRenderer.BlockWriter output) {
this.p = p;
this.output = output;
rotation = switch (p.facing()) {
case 0 -> Rotation.NONE; case 1 -> Rotation.CLOCKWISE_90;
case 2 -> Rotation.CLOCKWISE_180; default -> Rotation.COUNTERCLOCKWISE_90;
};
}
void set(int x, int y, int z, BlockState state) {
output.set(rx(x, z, p.facing()), y, rz(x, z, p.facing()), state.rotate(rotation));
}
void set(int x, int y, int z, Block block) { set(x, y, z, block.defaultBlockState()); }
void box(int x0, int y0, int z0, int x1, int y1, int z1, BlockState state) {
for (int x = x0; x <= x1; x++) for (int z = z0; z <= z1; z++)
for (int y = y0; y <= y1; y++) set(x, y, z, state);
}
BlockState facing(Block block, Direction direction) {
return block.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, direction);
}
BlockState brick(int x, int y, int z) { return LostCityPalette.masonry(p.seed(), x, y, z); }
BlockState weatheredBrick(int x, int y, int z) {
// Weathering follows a few masonry joints, never private room partitions.
int weather = roll(x, y, z, 12);
return (weather == 0 ? Blocks.MOSSY_STONE_BRICKS : weather < 3
? Blocks.CRACKED_STONE_BRICKS : Blocks.STONE_BRICKS).defaultBlockState();
}
int roll(int x, int y, int z, int bound) { return LostCityPalette.roll(p.seed(), x, y, z, bound); }
void render() {
int roof = p.floors() * STOREY;
box(0, -p.foundationDepth(), 0, 16, -1, 16, Blocks.STONE_BRICKS.defaultBlockState());
box(0, 0, 0, 16, 0, 16, SMOOTH);
box(0, 1, 0, 16, roof + 4, 16, AIR);
shell(roof);
architecturalEnvelope(roof);
for (int floor = 0; floor < p.floors(); floor++) {
partitions(floor);
furnishings(floor);
}
stairs();
entrance();
roof(roof);
// Only one deferred loot table per building; empty furniture is not a resource vault.
int lootX = 6;
int lootZ = 6;
box(lootX, 2, lootZ, lootX, 3, lootZ, AIR);
ResourceKey<LootTable> table = switch (p.kind()) {
case LIBRARY, HALL -> BuiltInLootTables.VILLAGE_CARTOGRAPHER;
case RESTAURANT -> BuiltInLootTables.VILLAGE_BUTCHER;
case BARRACKS -> BuiltInLootTables.VILLAGE_FLETCHER;
default -> BuiltInLootTables.VILLAGE_PLAINS_HOUSE;
};
output.chest(rx(lootX, lootZ, p.facing()), 1, rz(lootX, lootZ, p.facing()), table,
p.seed() ^ 0x21c17a7eL ^ p.kind().ordinal());
}
void shell(int roof) {
for (int floor = 0; floor <= p.floors(); floor++) {
int base = floor * STOREY;
for (int x = 1; x <= 15; x++) for (int z = 1; z <= 15; z++)
set(x, base, z, floor == p.floors() ? SMOOTH : floorMaterial(floor, x, z));
if (floor == p.floors()) continue;
// A pale ceiling and contrasting structural floor are separate layers.
box(2, base + 4, 2, 14, base + 4, 14,
Blocks.SMOOTH_STONE_SLAB.defaultBlockState().setValue(BlockStateProperties.SLAB_TYPE, SlabType.TOP));
for (int y = 1; y <= 4; y++) for (int horizontal = 1; horizontal <= 15; horizontal++) {
facade(horizontal, base + y, 1, horizontal, y, true);
facade(horizontal, base + y, 15, horizontal, y, true);
facade(1, base + y, horizontal, horizontal, y, false);
facade(15, base + y, horizontal, horizontal, y, false);
}
// Floor bands remain intact even where windows have fallen out.
for (int n = 1; n <= 15; n++) {
set(n, base, 1, SMOOTH); set(n, base, 15, SMOOTH);
set(1, base, n, SMOOTH); set(15, base, n, SMOOTH);
}
if (p.kind() == Kind.HOTEL || p.kind() == Kind.LIBRARY) {
for (int x : new int[]{3, 13}) for (int y = 0; y <= 4; y++) set(x, base + y, 0, SMOOTH);
}
if (p.kind() == Kind.APARTMENT) for (int x = 11; x <= 14; x++)
set(x, base + 1, 0, Blocks.SMOOTH_STONE_SLAB);
if (p.kind() == Kind.RESTAURANT) box(10, base + 4, 0, 14, base + 4, 0,
Blocks.OAK_SLAB.defaultBlockState());
}
if (p.kind() == Kind.HALL) {
// The auditorium spans two storeys; its upper level is a guarded gallery.
box(3, 4, 11, 13, 5, 14, AIR);
for (int x = 3; x <= 13; x++) set(x, 6, 11, rail(true));
for (int z = 12; z <= 14; z++) { set(3, 6, z, rail(false)); set(13, 6, z, rail(false)); }
}
}
BlockState floorMaterial(int floor, int x, int z) {
if (x == 1 || z == 1 || x == 15 || z == 15) return SMOOTH;
return switch (p.kind()) {
case LIBRARY -> (x <= 5 || z >= 10 ? Blocks.OAK_PLANKS : Blocks.SMOOTH_STONE).defaultBlockState();
case APARTMENT, HOTEL -> WOOD;
case RESTAURANT -> ((x / 3 + z / 3) & 1) == 0 ? SMOOTH : TILE;
case BARRACKS -> x <= 5 ? TILE : WOOD;
default -> x == 7 || x == 9 ? TILE : SMOOTH;
};
}
BlockState pier() {
return switch (p.kind()) {
case APARTMENT, LIBRARY -> Blocks.STRIPPED_OAK_LOG.defaultBlockState();
case HOTEL, RESTAURANT -> SMOOTH;
case BARRACKS -> Blocks.STONE_BRICKS.defaultBlockState();
default -> Blocks.CHISELED_STONE_BRICKS.defaultBlockState();
};
}
void facade(int x, int y, int z, int horizontal, int withinFloor, boolean eastWest) {
boolean column = horizontal == 1 || horizontal == 6 || horizontal == 10 || horizontal == 15;
boolean window = !column && (withinFloor == 2 || withinFloor == 3
|| withinFloor == 1 && (p.kind() == Kind.LIBRARY || p.kind() == Kind.RESTAURANT || p.kind() == Kind.HALL));
if (column) set(x, y, z, pier());
else if (window) {
if (roll(x, y / 5, z, 17) != 0) set(x, y, z,
p.kind() == Kind.BARRACKS ? rail(eastWest) : pane(eastWest));
} else set(x, y, z, withinFloor == 4 && horizontal % 5 == 0
? weatheredBrick(x, y, z) : SMOOTH);
}
/** Volume belongs to its use: projected bays, clipped corners and distinct roof profiles. */
void architecturalEnvelope(int roof) {
for (int floor = 0; floor < p.floors(); floor++) {
int y = floor * STOREY;
// Project the floor plates as a continuous, one-block cornice inside the parcel.
for (int n = 1; n <= 15; n++) {
set(n, y, 0, stairTrim(Direction.NORTH)); set(n, y, 16, stairTrim(Direction.SOUTH));
set(0, y, n, stairTrim(Direction.WEST)); set(16, y, n, stairTrim(Direction.EAST));
}
switch (p.kind()) {
case APARTMENT -> {
// Bay windows increase the living room rather than drawing flat stripes on it.
for (int z : new int[]{4, 5, 6, 11, 12, 13}) {
set(16, y, z, Blocks.OAK_PLANKS);
set(16, y + 1, z, Blocks.OAK_PLANKS);
set(16, y + 2, z, Blocks.GLASS_PANE);
set(16, y + 3, z, Blocks.GLASS_PANE);
set(16, y + 4, z, Blocks.OAK_SLAB.defaultBlockState().setValue(BlockStateProperties.SLAB_TYPE, SlabType.TOP));
}
for (int z : new int[]{3, 7, 10, 14}) box(16, y + 1, z, 16, y + 4, z, pier());
// Original wall remains the sill, with an open bay above it; privacy walls restore themselves later.
for (int z : new int[]{4, 5, 6, 11, 12, 13}) box(15, y + 2, z, 15, y + 3, z, AIR);
bevelFront(y);
}
case HOTEL -> {
for (int x : new int[]{2, 5, 11, 14}) box(x, y + 1, 0, x, y + 4, 0, pier());
for (int x : new int[]{3, 4, 12, 13}) set(x, y + 1, 0, Blocks.OAK_FENCE);
box(2, y + 4, 0, 5, y + 4, 0, Blocks.OAK_PLANKS.defaultBlockState());
box(11, y + 4, 0, 14, y + 4, 0, Blocks.OAK_PLANKS.defaultBlockState());
}
case LIBRARY -> {
// Tall fins frame broad glazing and project beyond the reading rooms.
for (int x : new int[]{2, 5, 11, 14}) box(x, y + 1, 0, x, y + 4, 0, Blocks.STRIPPED_OAK_LOG.defaultBlockState());
for (int x : new int[]{3, 4, 12, 13}) {
set(x, y + 4, 0, Blocks.OAK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
}
bevelFront(y);
}
case RESTAURANT -> {
// Separate fascia and timber valance shelter the shop windows.
for (int x = 10; x <= 14; x++) {
set(x, y + 3, 0, Blocks.OAK_SLAB);
set(x, y + 4, 0, Blocks.OAK_PLANKS);
}
for (int z : new int[]{3, 7, 11, 14}) set(16, y + 4, z, Blocks.ANDESITE);
}
case BARRACKS -> {
for (int z : new int[]{2, 7, 12, 15}) box(0, y + 1, z, 0, y + 4, z, pier());
for (int x : new int[]{2, 5, 11, 14}) box(x, y + 1, 0, x, y + 4, 0, Blocks.STONE_BRICKS.defaultBlockState());
for (int x : new int[]{3, 4, 12, 13}) set(x, y + 1, 0, Blocks.CHISELED_STONE_BRICKS);
}
case HALL -> {
for (int x : new int[]{2, 5, 11, 14}) box(x, y + 1, 0, x, y + 4, 0, pier());
for (int x = 2; x <= 14; x++) set(x, y + 4, 0,
Blocks.ANDESITE_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
}
default -> throw new IllegalStateException();
}
}
}
BlockState stairTrim(Direction outward) {
Block block = Blocks.STONE_BRICK_STAIRS;
return facing(block, outward.getOpposite()).setValue(BlockStateProperties.HALF,
net.minecraft.world.level.block.state.properties.Half.TOP);
}
void bevelFront(int y) {
// Remove two rectangular corners and close them with actual stepped returns.
for (int x : new int[]{1, 15}) {
box(x, y, 1, x, y + 4, 1, AIR);
int adjacent = x == 1 ? 2 : 14;
box(adjacent, y + 1, 1, adjacent, y + 4, 1, pier());
box(x, y + 1, 2, x, y + 4, 2, pier());
}
}
void partitions(int floor) {
int y = floor * STOREY;
int front = frontWall(p, floor);
// The west stairwell, central corridor and vestibule are independent of furniture.
box(5, y + 1, 2, 5, y + 4, 9, SMOOTH);
door(5, y + 1, 8, Direction.WEST, false);
box(10, y + 1, 2, 10, y + 4, front, SMOOTH);
box(10, y + 1, front, 15, y + 4, front, SMOOTH);
door(10, y + 1, 6, Direction.EAST, false);
if (floor == 0) {
box(6, 1, 4, 9, 4, 4, SMOOTH);
door(8, 1, 4, Direction.SOUTH, false);
}
if (p.kind() == Kind.HALL && floor < 2) {
if (floor == 0) {
box(2, 1, 10, 14, 4, 10, SMOOTH);
door(7, 1, 10, Direction.SOUTH, true);
door(8, 1, 10, Direction.SOUTH, false);
}
} else {
Area west = rear(p, floor, false), east = rear(p, floor, true);
box(1, y + 1, west.front() - 1, west.right() + 1, y + 4, west.front() - 1, SMOOTH);
box(west.right() + 1, y + 1, west.front() - 1, west.right() + 1, y + 4, 15, SMOOTH);
box(east.left() - 1, y + 1, east.front() - 1, 15, y + 4, east.front() - 1, SMOOTH);
box(east.left() - 1, y + 1, east.front() - 1, east.left() - 1, y + 4, 15, SMOOTH);
door(west.right() + 1, y + 1, 11, Direction.WEST, false);
door(east.left() - 1, y + 1, 11, Direction.EAST, false);
}
// A hanging lantern touches a full ceiling block, never the recessed underside of a top slab.
set(8, y + 4, 8, SMOOTH);
set(8, y + 3, 8, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
// Weathering stays on facades and roofs: it must not replace a privacy partition.
}
void furnishings(int floor) {
int y = floor * STOREY;
int variant = layout(p, floor);
switch (p.kind()) {
case APARTMENT -> {
if (variant == 0) { kitchenFront(y); bedroom(floor, false); }
else { frontBedroom(floor); kitchenRear(floor, false); }
}
case HOTEL -> {
if (floor == 0) { reception(y); lounge(floor, false); laundry(floor); }
else {
anchoredBed(14, y + 1, frontWall(p, floor) - 2, Direction.SOUTH);
set(11, y + 1, 5, Blocks.BARREL); set(11, y + 2, 5, Blocks.FLOWER_POT);
if (variant == 0) { bedroom(floor, false); bedroom(floor, true); }
else lounge(floor, false);
}
}
case LIBRARY -> {
if (variant == 0) {
box(14, y + 1, 2, 14, y + 3, 6, Blocks.BOOKSHELF.defaultBlockState());
box(12, y + 1, 2, 12, y + 2, 3, Blocks.BOOKSHELF.defaultBlockState());
desk(12, y, 5, Direction.SOUTH);
readingRoom(floor, false); archive(floor, true);
} else {
readingFront(y); readingRoom(floor, false);
set(2, y + 1, 14, Blocks.CARTOGRAPHY_TABLE);
}
}
case RESTAURANT -> {
if (variant == 0) {
table(13, y, 3); table(13, y, 5);
set(11, y + 1, 2, Blocks.POTTED_DEAD_BUSH);
kitchenRear(floor, true);
} else {
kitchenFront(y); diningRear(floor, false); pantry(floor, true);
}
}
case BARRACKS -> {
if (variant == 0) {
anchoredBed(12, y + 1, 3, Direction.NORTH); anchoredBed(14, y + 1, 3, Direction.NORTH);
set(11, y + 1, 2, Blocks.BARREL); set(11, y + 2, 2, Blocks.BARREL);
seat(13, y + 1, 6, Direction.NORTH);
} else { reception(y); bedroom(floor, false); }
box(12, y + 1, 14, 14, y + 2, 14, Blocks.BARREL.defaultBlockState());
set(14, y + 1, 11, Blocks.FLETCHING_TABLE);
set(12, y + 1, 11, Blocks.GRINDSTONE.defaultBlockState()
.setValue(BlockStateProperties.ATTACH_FACE,
net.minecraft.world.level.block.state.properties.AttachFace.FLOOR));
}
case HALL -> {
if (floor == 0 || floor > 1 && variant == 0) reception(y); else readingFront(y);
if (floor == 0) auditorium();
else if (floor >= 2) {
if (variant == 0) readingRoom(floor, false);
else { archive(floor, false); readingRoom(floor, true); }
}
}
default -> throw new IllegalArgumentException("Not an interior kind");
}
for (var bath : bathroomPlans(p)) if (bath.floor() == floor) bathroom(bath);
}
void kitchenFront(int y) {
kitchenLine(y, 11, 2, Direction.SOUTH);
table(13, y, 5);
}
void kitchenRear(int floor, boolean east) {
int y = floor * STOREY;
Area room = rear(p, floor, east);
kitchenLine(y, room.left(), 14, Direction.NORTH);
if (east) {
set(14, y + 1, 11, SMOOTH); set(14, y + 2, 11, Blocks.FLOWER_POT);
set(12, y + 1, 11, Blocks.CRAFTING_TABLE);
} else table(room.left() + 2, y, 11);
}
void kitchenLine(int y, int left, int z, Direction face) {
set(left, y + 1, z, facing(Blocks.FURNACE, face));
set(left + 1, y + 1, z, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 2));
set(left + 2, y + 1, z, facing(Blocks.SMOKER, face));
set(left + 3, y + 1, z, Blocks.BARREL);
box(left, y + 3, z, left + 3, y + 3, z, SMOOTH);
set(left + 2, y + 2, z, Blocks.IRON_TRAPDOOR);
for (int x = left; x <= left + 3; x++) set(x, y + 3, z, Blocks.SMOOTH_STONE);
for (int x = left; x <= left + 3; x++) set(x, y, z, (x & 1) == 0 ? SMOOTH : TILE);
}
void bedroom(int floor, boolean east) {
int y = floor * STOREY;
Area room = rear(p, floor, east);
anchoredBed(east ? room.right() - 1 : room.left() + 1, y + 1, 13, east ? Direction.EAST : Direction.WEST);
set(east ? room.right() : room.left(), y + 1, 11, Blocks.BARREL);
desk(east ? room.left() + 1 : room.right() - 1, y, 11, Direction.SOUTH);
set(east ? room.left() : room.right(), y + 1, 14, Blocks.BOOKSHELF);
}
void frontBedroom(int floor) {
int y = floor * STOREY;
anchoredBed(14, y + 1, 3, Direction.NORTH);
set(11, y + 1, 2, Blocks.BARREL); set(11, y + 2, 2, Blocks.FLOWER_POT);
desk(12, y, 5, Direction.SOUTH);
}
void bathroom(BathPlan bath) {
int y = bath.floor() * STOREY;
for (int x = bath.x0(); x <= bath.x1(); x++) for (int z = bath.z0(); z <= bath.z1(); z++) {
set(x, y, z, ((x + z) & 1) == 0 ? SMOOTH : TILE);
set(x, y + 4, z, SMOOTH);
boolean wall = x == bath.x0() || x == bath.x1() || z == bath.z0() || z == bath.z1();
for (int up = 1; up <= 3; up++) set(x, y + up, z, wall ? SMOOTH : AIR);
}
int doorZ = bath.southDoor() ? bath.z1() : bath.z0();
door(bath.doorX(), y + 1, doorZ, bath.southDoor() ? Direction.NORTH : Direction.SOUTH, false);
int fixtureZ = bath.southDoor() ? bath.z0() + 1 : bath.z1() - 1;
int toiletX = bath.southDoor() ? bath.x1() - 2 : bath.x0() + 1;
Direction view = bath.southDoor() ? Direction.SOUTH : Direction.EAST;
set(toiletX, y + 1, fixtureZ, seating(Blocks.POLISHED_ANDESITE_STAIRS, view));
set(toiletX, y + 2, fixtureZ, Blocks.IRON_TRAPDOOR);
set(bath.x1() - 1, y + 1, fixtureZ, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 2));
}
void reception(int y) {
box(12, y + 1, 5, 14, y + 1, 5, Blocks.OAK_PLANKS.defaultBlockState());
set(13, y + 2, 5, Blocks.FLOWER_POT);
set(14, y + 1, 2, Blocks.BOOKSHELF); set(14, y + 2, 2, Blocks.BARREL);
desk(12, y, 3, Direction.SOUTH);
set(11, y + 1, 2, Blocks.POTTED_DEAD_BUSH);
}
void laundry(int floor) {
int y = floor * STOREY;
for (int z = 11; z <= 12; z++) set(14, y + 1, z, Blocks.WATER_CAULDRON.defaultBlockState()
.setValue(BlockStateProperties.LEVEL_CAULDRON, z == 11 ? 1 : 2));
box(12, y + 1, 14, 14, y + 1, 14, Blocks.BARREL.defaultBlockState());
// The rack stands on the storage counter; recessed rear glazing cannot carry it.
for (int x : new int[]{12, 14}) set(x, y + 2, 14, Blocks.OAK_PLANKS);
box(12, y + 3, 14, 14, y + 3, 14, Blocks.OAK_SLAB.defaultBlockState());
set(12, y + 1, 11, Blocks.LOOM);
}
void lounge(int floor, boolean east) {
int y = floor * STOREY;
Area room = rear(p, floor, east);
box(room.left(), y + 1, 14, room.left() + 2, y + 1, 14, seating(Blocks.OAK_STAIRS, Direction.NORTH));
desk(room.left() + 1, y, 11, Direction.SOUTH);
set(room.left(), y + 1, 12, Blocks.BOOKSHELF);
}
void readingRoom(int floor, boolean east) {
int y = floor * STOREY;
Area room = rear(p, floor, east);
desk(east ? room.left() + 1 : room.right() - 1, y, 11, Direction.SOUTH);
box(room.left(), y + 1, 14, room.right(), y + 2, 14, Blocks.BOOKSHELF.defaultBlockState());
set(east ? room.right() : room.left(), y + 1, 11, facing(Blocks.LECTERN, Direction.NORTH));
}
void readingFront(int y) {
desk(12, y, 3, Direction.SOUTH);
box(14, y + 1, 2, 14, y + 2, 5, Blocks.BOOKSHELF.defaultBlockState());
set(11, y + 1, 2, facing(Blocks.LECTERN, Direction.SOUTH));
}
void archive(int floor, boolean east) {
int y = floor * STOREY;
Area room = rear(p, floor, east);
box(room.left(), y + 1, 14, room.right(), y + 3, 14, Blocks.BOOKSHELF.defaultBlockState());
box(east ? room.right() : room.left(), y + 1, 11, east ? room.right() : room.left(), y + 2, 12, Blocks.BARREL.defaultBlockState());
set(room.left() + 1, y + 1, 12, facing(Blocks.LECTERN, Direction.NORTH));
}
void pantry(int floor, boolean east) {
int y = floor * STOREY;
Area room = rear(p, floor, east);
box(room.left(), y + 1, 14, room.right(), y + 2, 14, Blocks.BARREL.defaultBlockState());
set(room.right(), y + 1, 11, Blocks.CRAFTING_TABLE);
set(room.right(), y + 2, 11, Blocks.FLOWER_POT);
}
void diningRear(int floor, boolean east) {
Area room = rear(p, floor, east);
int y = floor * STOREY;
table(room.left() + 2, y, 12);
box(room.left(), y + 1, 14, room.right(), y + 1, 14, seating(Blocks.OAK_STAIRS, Direction.NORTH));
}
void auditorium() {
for (int x : new int[]{3, 4, 5, 11, 12, 13}) seat(x, 1, 11, Direction.SOUTH);
box(3, 1, 13, 13, 1, 14, WOOD);
set(8, 1, 12, facing(Blocks.OAK_STAIRS, Direction.SOUTH));
set(8, 2, 14, facing(Blocks.LECTERN, Direction.NORTH));
for (int x : new int[]{3, 13}) {
box(x, 2, 14, x, 3, 14, WOOD);
set(x, 4, 14, Blocks.STONE_BRICK_SLAB);
}
}
void stairs() {
for (int floor = 0; floor < p.floors(); floor++) {
int base = floor * STOREY;
for (int step = 1; step <= STOREY; step++) {
int z = step + 2;
box(2, base + step, z, 3, base + step + 3, z, AIR);
for (int x = 2; x <= 3; x++) {
// A one-block inclined spine joins successive treads physically, without
// filling the headroom above the preceding storey's matching flight.
set(x, base + step - 1, z, Blocks.STONE_BRICKS);
set(x, base + step, z, facing(Blocks.STONE_BRICK_STAIRS, Direction.SOUTH));
}
}
box(2, base + STOREY, 8, 4, base + STOREY, 9, TILE);
box(2, base + STOREY + 1, 8, 4, base + STOREY + 3, 9, AIR);
}
// This full-height side aisle joins every lower stair approach to its landing door.
for (int floor = 0; floor <= p.floors(); floor++) {
int y = floor * STOREY;
box(4, y + 1, 2, 4, y + 3, 9, AIR);
}
}
void entrance() {
for (int x : new int[]{6, 9}) box(x, 1, 0, x, 3, 0, SMOOTH);
box(6, 4, 0, 9, 4, 1, Blocks.STONE_BRICK_SLAB.defaultBlockState());
box(7, 0, 0, 8, 0, 3, TILE);
// Native SOUTH/RIGHT opens against the west jamb; SOUTH/LEFT against the east jamb.
door(7, 1, 1, Direction.SOUTH, true);
door(8, 1, 1, Direction.SOUTH, false);
set(4, 1, 0, Blocks.POTTED_DEAD_BUSH);
set(12, 1, 16, Blocks.COMPOSTER);
}
void roof(int y) {
// Clipped parapets and a heavy coping keep each accessible roof legible from below.
for (int n = 2; n <= 14; n++) {
set(n, y + 1, 1, Blocks.STONE_BRICKS); set(n, y + 1, 15, Blocks.STONE_BRICKS);
set(1, y + 1, n, Blocks.STONE_BRICKS); set(15, y + 1, n, Blocks.STONE_BRICKS);
set(n, y + 2, 1, Blocks.STONE_BRICK_SLAB); set(n, y + 2, 15, Blocks.STONE_BRICK_SLAB);
set(1, y + 2, n, Blocks.STONE_BRICK_SLAB); set(15, y + 2, n, Blocks.STONE_BRICK_SLAB);
}
switch (p.kind()) {
case APARTMENT -> {
// Two small roof gardens and a timber portico leave the stair landing and central crossing free.
roofPlanter(10, y, 4); roofPlanter(10, y, 12);
for (int x : new int[]{6, 13}) box(x, y + 1, 14, x, y + 3, 14, Blocks.STRIPPED_OAK_LOG.defaultBlockState());
box(6, y + 4, 14, 13, y + 4, 14, Blocks.OAK_SLAB.defaultBlockState());
desk(8, y, 12, Direction.NORTH);
}
case HOTEL -> {
roofRoom(y, 9, 3, 14, 7, Blocks.SMOOTH_STONE);
door(9, y + 1, 5, Direction.EAST, false);
set(12, y + 1, 4, Blocks.BARREL); set(13, y + 1, 4, Blocks.LOOM);
box(11, y + 1, 12, 13, y + 1, 12, seating(Blocks.OAK_STAIRS, Direction.NORTH));
}
case LIBRARY -> {
// One transparent cap above a hollow timber curb, open all the way to the stacks.
// No opaque former roof plate or second glass floor remains under this canopy.
for (int x = 10; x <= 13; x++) for (int z = 3; z <= 7; z++) {
boolean edge = x == 10 || x == 13 || z == 3 || z == 7;
set(x, y + 1, z, edge ? Blocks.STRIPPED_OAK_LOG.defaultBlockState() : AIR);
set(x, y + 2, z, edge ? Blocks.OAK_SLAB : Blocks.GLASS);
}
box(11, y - 1, 4, 12, y + 1, 6, AIR);
desk(11, y, 12, Direction.NORTH); set(14, y + 1, 12, Blocks.LECTERN);
}
case RESTAURANT -> {
// A real dining terrace and rear kitchen extraction with full supporting columns.
table(11, y, 6); table(11, y, 11);
for (int z : new int[]{3, 14}) box(14, y + 1, z, 14, y + 3, z, Blocks.STRIPPED_OAK_LOG.defaultBlockState());
box(14, y + 4, 3, 14, y + 4, 14, Blocks.OAK_PLANKS.defaultBlockState());
box(6, y + 1, 14, 7, y + 3, 14, Blocks.STONE_BRICKS.defaultBlockState());
set(6, y + 4, 14, Blocks.IRON_BARS); set(7, y + 4, 14, Blocks.IRON_BARS);
}
case BARRACKS -> {
// Enclosed stair head gives the barracks a service tower, without hiding its usable exit.
// The whole last flight sits inside the stair head: a front wall at z=6
// would cross the headroom over its penultimate step.
roofRoom(y, 1, 2, 5, 10, Blocks.SMOOTH_STONE);
door(5, y + 1, 8, Direction.EAST, false);
box(10, y + 1, 12, 13, y + 1, 14, Blocks.POLISHED_ANDESITE.defaultBlockState());
set(11, y + 2, 13, Blocks.IRON_TRAPDOOR);
}
case HALL -> {
for (int step = 0; step < 3; step++) {
int edge = 2 + step * 2;
box(edge, y + 1 + step, 1, 16 - edge, y + 1 + step, 2, Blocks.STONE_BRICKS.defaultBlockState());
}
for (int x : new int[]{6, 10}) set(x, y + 4, 2, Blocks.CHISELED_STONE_BRICKS);
desk(11, y, 12, Direction.NORTH);
}
default -> throw new IllegalStateException();
}
}
void roofRoom(int y, int x0, int z0, int x1, int z1, Block wall) {
for (int x = x0; x <= x1; x++) for (int z = z0; z <= z1; z++) {
boolean edge = x == x0 || x == x1 || z == z0 || z == z1;
for (int up = 1; up <= 3; up++) set(x, y + up, z, edge ? wall.defaultBlockState() : AIR);
// A bottom slab touches the full-height walls immediately below it.
// A top slab here would float half a block above the pavilion.
set(x, y + 4, z, Blocks.STONE_BRICK_SLAB.defaultBlockState());
}
}
void roofPlanter(int x, int y, int z) {
for (int dx = 0; dx <= 3; dx++) set(x + dx, y + 1, z, Blocks.STONE_BRICKS);
set(x + 1, y + 1, z, Blocks.DIRT); set(x + 2, y + 1, z, Blocks.DIRT);
set(x + 1, y + 2, z, Blocks.OXEYE_DAISY); set(x + 2, y + 2, z, Blocks.CORNFLOWER);
}
void door(int x, int y, int z, Direction facing, boolean rightHinge) {
BlockState state = facing(Blocks.OAK_DOOR, facing)
.setValue(BlockStateProperties.DOOR_HINGE, rightHinge ? DoorHingeSide.RIGHT : DoorHingeSide.LEFT)
.setValue(BlockStateProperties.OPEN, false)
.setValue(BlockStateProperties.POWERED, false);
set(x, y, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.LOWER));
set(x, y + 1, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.UPPER));
}
void anchoredBed(int x, int y, int z, Direction facing) {
BlockState state = facing(Blocks.BED.white(), facing);
set(x, y, z, state.setValue(BlockStateProperties.BED_PART, BedPart.FOOT));
set(x + facing.getStepX(), y, z + facing.getStepZ(), state.setValue(BlockStateProperties.BED_PART, BedPart.HEAD));
int wallX = x + facing.getStepX() * 2, wallZ = z + facing.getStepZ() * 2;
Direction alongWall = facing.getClockWise();
for (int width = -1; width <= 1; width++) for (int up = 0; up <= 3; up++)
set(wallX + alongWall.getStepX() * width, y + up, wallZ + alongWall.getStepZ() * width, SMOOTH);
}
void table(int x, int y, int z) {
set(x, y + 1, z, Blocks.OAK_FENCE); set(x, y + 2, z, Blocks.OAK_PRESSURE_PLATE);
seat(x - 1, y + 1, z, Direction.EAST);
seat(x + 1, y + 1, z, Direction.WEST);
}
void desk(int x, int y, int z, Direction approachSide) {
set(x, y + 1, z, Blocks.OAK_FENCE); set(x, y + 2, z, Blocks.OAK_PRESSURE_PLATE);
seat(x + approachSide.getStepX(), y + 1, z + approachSide.getStepZ(), approachSide.getOpposite());
}
void seat(int x, int y, int z, Direction lookingToward) {
set(x, y, z, seating(Blocks.OAK_STAIRS, lookingToward));
}
BlockState seating(Block stair, Direction lookingToward) {
// Stair FACING points uphill, into its full-height back, opposite a seated player's view.
return facing(stair, lookingToward.getOpposite());
}
BlockState rail(boolean eastWest) { return connected(Blocks.IRON_BARS, eastWest); }
BlockState pane(boolean eastWest) { return connected(Blocks.GLASS_PANE, eastWest); }
BlockState connected(Block block, boolean eastWest) {
return block.defaultBlockState().setValue(BlockStateProperties.EAST, eastWest)
.setValue(BlockStateProperties.WEST, eastWest).setValue(BlockStateProperties.NORTH, !eastWest)
.setValue(BlockStateProperties.SOUTH, !eastWest);
}
}
}
@@ -0,0 +1,92 @@
package fr.koka.sanctuary.worldgen.city;
import java.util.Arrays;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceKey;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.loot.LootTable;
/** Generation20 surface parcel. Its distinct native type preserves all published city renderers. */
public final class LostCityPiece20 extends StructurePiece {
private static final int SCHEMA = 1;
private final LostCityPlan.Parcel parcel;
public LostCityPiece20(LostCityPlan.Parcel parcel) {
super(LostCityStructures.PIECE20, 0, bounds(parcel));
this.parcel = parcel;
setOrientation(null);
}
public LostCityPiece20(StructurePieceSerializationContext context, CompoundTag tag) {
super(LostCityStructures.PIECE20, 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) {
LostCitySurface20.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,92 @@
package fr.koka.sanctuary.worldgen.city;
import java.util.Arrays;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceKey;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.loot.LootTable;
/** Generation21 surface parcel. Its distinct native type preserves all published city renderers. */
public final class LostCityPiece21 extends StructurePiece {
private static final int SCHEMA = 1;
private final LostCityPlan.Parcel parcel;
public LostCityPiece21(LostCityPlan.Parcel parcel) {
super(LostCityStructures.PIECE21, 0, bounds(parcel));
this.parcel = parcel;
setOrientation(null);
}
public LostCityPiece21(StructurePieceSerializationContext context, CompoundTag tag) {
super(LostCityStructures.PIECE21, tag);
if (tag.getInt("SanctuarySchema").orElseThrow() != SCHEMA)
throw new IllegalArgumentException("Unsupported Lost City piece schema");
parcel = new LostCityPlan.Parcel(LostCityPlan.Kind.valueOf(tag.getString("Kind").orElseThrow()),
tag.getInt("MinX").orElseThrow(), tag.getInt("MinZ").orElseThrow(),
tag.getInt("Width").orElseThrow(), tag.getInt("Depth").orElseThrow(),
tag.getInt("FloorY").orElseThrow(), tag.getInt("Floors").orElseThrow(),
tag.getInt("Facing").orElseThrow(), tag.getLong("ParcelSeed").orElseThrow(),
tag.getInt("FoundationDepth").orElseThrow(), tag.getIntArray("SurfaceHeights").orElseThrow());
if (!getBoundingBox().equals(bounds(parcel)))
throw new IllegalArgumentException("Lost City parcel disagrees with its saved native bounding box");
setOrientation(null);
}
public LostCityPlan.Parcel parcel() { return parcel; }
@Override public BlockPos getLocatorPosition() {
int x = parcel.width() / 2, z = parcel.depth() / 2;
return new BlockPos(parcel.minX() + x, parcel.surfaceY(x, z) + 1, parcel.minZ() + z);
}
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
tag.putInt("SanctuarySchema", SCHEMA);
tag.putString("Kind", parcel.kind().name());
tag.putInt("MinX", parcel.minX()); tag.putInt("MinZ", parcel.minZ());
tag.putInt("Width", parcel.width()); tag.putInt("Depth", parcel.depth());
tag.putInt("FloorY", parcel.floorY()); tag.putInt("Floors", parcel.floors());
tag.putInt("Facing", parcel.facing()); tag.putLong("ParcelSeed", parcel.seed());
tag.putInt("FoundationDepth", parcel.foundationDepth());
tag.putIntArray("SurfaceHeights", parcel.surfaceHeights());
}
@Override public void postProcess(WorldGenLevel level, StructureManager structures, ChunkGenerator generator,
RandomSource random, BoundingBox chunkClip, ChunkPos chunkPos, BlockPos pivot) {
LostCityStructures.placing(() -> render(parcel, new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState block) {
BlockPos pos = world(x, y, z);
if (getBoundingBox().isInside(pos) && chunkClip.isInside(pos))
placeBlock(level, block, pos.getX(), pos.getY(), pos.getZ(), chunkClip);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
BlockPos pos = world(x, y, z);
if (getBoundingBox().isInside(pos) && chunkClip.isInside(pos))
createChest(level, chunkClip, RandomSource.create(seed), pos, loot, null);
}
}));
}
private static void render(LostCityPlan.Parcel parcel, LostCityRenderer.BlockWriter writer) {
LostCitySurface21.render(parcel, writer);
}
private BlockPos world(int x, int y, int z) {
return new BlockPos(parcel.minX() + x, parcel.floorY() + y, parcel.minZ() + z);
}
private static BoundingBox bounds(LostCityPlan.Parcel parcel) {
int low = Math.min(parcel.floorY(), Arrays.stream(parcel.surfaceHeights()).min().orElse(parcel.floorY()))
- parcel.foundationDepth();
int high = Math.max(parcel.maxY(), Arrays.stream(parcel.surfaceHeights()).max().orElse(parcel.floorY()) + 5);
return new BoundingBox(parcel.minX(), low, parcel.minZ(), parcel.maxX(), high, parcel.maxZ());
}
}
@@ -0,0 +1,248 @@
package fr.koka.sanctuary.worldgen.city;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.*;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/** Preserve the certified compact16 core. At most two modest natural outlying blocks are admitted
* together with complete three-wide approaches; a failed route publishes neither its road nor its block. */
public final class LostCityPlanner21 {
public static final int MAX_EXTRA_COLUMNS = 4_000, MAX_EXTRA_CHECKS = 220_000;
public static final int MAX_COLUMNS = LostCityPlanner16.MAX_COLUMNS + MAX_EXTRA_COLUMNS;
public static final int MAX_VOLUME_CHECKS = LostCityPlanner16.MAX_VOLUME_CHECKS + MAX_EXTRA_CHECKS;
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
private static final int INVALID = Integer.MIN_VALUE;
private LostCityPlanner21() {}
public record Point(int x, int floorY, int z) {}
public record Connection(int districtIndex, List<Point> path) { public Connection { path = List.copyOf(path); } }
public record Result(LostCityPlanner16.Result core, LostCityPlan surface,
List<LostCityPlanner16.Support> supports, int columns, int volumeChecks,
List<Connection> connections, Map<String, Integer> diagnostics) {
public Result {
supports = List.copyOf(supports); connections = List.copyOf(connections); diagnostics = Map.copyOf(diagnostics);
if (columns > MAX_COLUMNS || volumeChecks > MAX_VOLUME_CHECKS || connections.size() > 2)
throw new IllegalArgumentException("Generation21 city exceeded its bounded extension");
}
public LostCityPlanner16.Fallback reservation() { return core.reservation(); }
}
public static Result plan(long seed, int cx, int cz, int diameter, boolean origin, LostCityPlanner16.Terrain terrain) {
var core = LostCityPlanner16.plan(seed, cx, cz, diameter, origin, terrain);
var districts = new ArrayList<>(core.surface().districts()); var supports = new ArrayList<>(core.supports());
var links = new ArrayList<Connection>(); var probe = new Probe(terrain);
if (origin && !districts.isEmpty()) {
var center = districts.getFirst(); int mx = (center.minX() + center.maxX()) / 2, mz = (center.minZ() + center.maxZ()) / 2;
int first = (int) Long.remainderUnsigned(mix(seed ^ 0x21c175L), 4);
try {
for (int attempt = 0; attempt < 8 && links.size() < 2; attempt++) {
int d = (first + attempt) & 3;
int radius = Math.max(center.maxX() - center.minX(), center.maxZ() - center.minZ()) / 2 + (attempt < 4 ? 42 : 58);
int x = mx + DX[d] * radius, z = mz + DZ[d] * radius;
probe.reject("cluster-attempt");
// The old one-lot natural solver already certifies foundations, ramps and short supported bridges.
var mini = LostCityPlanner16.plan(mix(seed ^ attempt * 3191L ^ 0x2100L), x, z, 96, false, probe);
if (mini.surface().isEmpty()) { probe.reject("no-natural-block"); continue; }
var district = mini.surface().districts().getFirst();
if (overlap(district, districts, 14)) { probe.reject("insufficient-natural-gap"); continue; }
var all = new ArrayList<>(districts); all.add(district);
var path = connect(seed ^ attempt, center, district, all, probe);
if (path == null) { probe.reject("no-complete-approach"); continue; }
var parcels = new ArrayList<>(district.parcels()); parcels.addAll(path.roads());
District joined;
try { joined = new District(district.startChunkX(), district.startChunkZ(), district.seed(), parcels); }
catch (IllegalArgumentException tooWide) { probe.reject("native-reference-radius"); continue; }
links.add(new Connection(districts.size(), path.path())); districts.add(joined); supports.addAll(mini.supports());
probe.reject("connected-cluster");
}
} catch (Exhausted exhausted) { probe.reject("extension-budget"); }
}
return new Result(core, new LostCityPlan(districts), supports, core.columns() + probe.columns.size(),
core.volumeChecks() + probe.checks, links, probe.reasons);
}
private record Entry(int x, int y, int z, int facing) {}
private record XZ(int x, int z) { XZ move(int d, int n) { return new XZ(x + DX[d] * n, z + DZ[d] * n); } }
private record Pair(Entry a, Entry b) {}
private record Road(int x, int z, int floor, int foundation) {}
private record Footprint(int x, int z, int width, int depth) {}
private record Route(List<Point> path, List<Parcel> roads) {}
private static Route connect(long seed, District core, District extra, List<District> all, Probe probe) {
var pairs = new ArrayList<Pair>();
for (var a : entries(core, all)) for (var b : entries(extra, all)) pairs.add(new Pair(a, b));
pairs.sort(Comparator.comparingInt(p -> Math.abs(p.a.x - p.b.x) + Math.abs(p.a.z - p.b.z)));
for (var pair : pairs.stream().limit(6).toList()) {
var a = pair.a; var b = pair.b; var outA = new XZ(a.x, a.z).move(a.facing, 3); var outB = new XZ(b.x, b.z).move(b.facing, 3);
for (int variant = 0; variant < 4; variant++) {
var points = new ArrayList<XZ>(); points.add(new XZ(a.x, a.z)); line(points, outA);
if (variant == 0) line(points, new XZ(outB.x, outA.z));
else if (variant == 1) line(points, new XZ(outA.x, outB.z));
else if (variant == 2) { int mid = (outA.x + outB.x) / 2; line(points, new XZ(mid, outA.z)); line(points, new XZ(mid, outB.z)); }
else { int mid = (outA.z + outB.z) / 2; line(points, new XZ(outA.x, mid)); line(points, new XZ(outB.x, mid)); }
line(points, outB); line(points, new XZ(b.x, b.z));
if (points.size() > 192 || new java.util.HashSet<>(points).size() != points.size()) continue;
var route = profile(seed, points, a, b, all, probe); if (route != null) return route;
}
}
return null;
}
private static List<Entry> entries(District district, List<District> all) {
var result = new ArrayList<Entry>();
for (var p : district.parcels()) if (p.kind() == Kind.STREET && p.width() >= 3 && p.depth() >= 3) {
for (int d = 0; d < 4; d++) {
int lx = d == 1 ? p.width() - 1 : d == 3 ? 0 : p.width() / 2;
int lz = d == 2 ? p.depth() - 1 : d == 0 ? 0 : p.depth() / 2;
int x = p.minX() + lx, z = p.minZ() + lz, floor = p.surfaceY(lx, lz); boolean valid = true;
for (int side = -1; side <= 1; side++) {
int px = x + DZ[d] * side, pz = z + DX[d] * side;
if (existingFloor(px, pz, all) != floor || existingFloor(px + DX[d], pz + DZ[d], all) != INVALID) { valid = false; break; }
}
if (valid) result.add(new Entry(x, floor, z, d));
}
}
return result;
}
private static Route profile(long seed, List<XZ> path, Entry a, Entry b, List<District> all, Probe probe) {
int n = path.size(); int[] low = new int[n], high = new int[n], floor = new int[n];
// Partition the complete footprint before choosing elevations. Every partition boundary
// will be a level landing, so a renderer never needs a neighboring parcel to form a stair.
var footprintCells = new LinkedHashMap<Long, XZ>();
for (int i = 0; i < n; i++) for (var cell : strip(path, i))
if (existingFloor(cell.x, cell.z, all) == INVALID) footprintCells.putIfAbsent(key(cell.x, cell.z), cell);
var footprints = footprints(footprintCells);
var owner = new HashMap<Long, Integer>();
for (int i = 0; i < footprints.size(); i++) {
var f = footprints.get(i);
for (int z = f.z; z < f.z + f.depth; z++) for (int x = f.x; x < f.x + f.width; x++) owner.put(key(x, z), i);
}
int[] grade = new int[n];
for (int i = 1; i < n; i++) {
grade[i] = 1;
for (var aCell : strip(path, i - 1)) for (var bCell : strip(path, i)) {
int distance = Math.abs(aCell.x - bCell.x) + Math.abs(aCell.z - bCell.z);
if (distance == 0 || distance == 1 && !java.util.Objects.equals(
owner.get(key(aCell.x, aCell.z)), owner.get(key(bCell.x, bCell.z)))) grade[i] = 0;
}
}
for (int i = 0; i < n; i++) {
low[i] = 176; high[i] = 350;
for (var p : strip(path, i)) {
int old = existingFloor(p.x, p.z, all);
if (old == -1) return null;
if (old != INVALID) { low[i] = Math.max(low[i], old); high[i] = Math.min(high[i], old); }
else {
var g = probe.ground(p.x, p.z);
if (!g.dry() || g.protectedTerrain() || g.solidDepth() < 3) return null;
low[i] = Math.max(low[i], g.top() - 2); high[i] = Math.min(high[i], g.top() + 3);
}
}
}
low[0] = high[0] = a.y; low[n - 1] = high[n - 1] = b.y;
for (int i = 1; i < n; i++) { low[i] = Math.max(low[i], low[i - 1] - grade[i]); high[i] = Math.min(high[i], high[i - 1] + grade[i]); }
for (int i = n - 2; i >= 0; i--) { low[i] = Math.max(low[i], low[i + 1] - grade[i + 1]); high[i] = Math.min(high[i], high[i + 1] + grade[i + 1]); }
var cells = new LinkedHashMap<Long, Road>(); var result = new ArrayList<Point>();
for (int i = 0; i < n; i++) {
if (low[i] > high[i]) return null;
int min = i == 0 ? low[i] : Math.max(low[i], floor[i - 1] - grade[i]), max = i == 0 ? high[i] : Math.min(high[i], floor[i - 1] + grade[i]);
if (min > max) return null;
var p = path.get(i); floor[i] = Math.clamp(probe.ground(p.x, p.z).top(), min, max); result.add(new Point(p.x, floor[i], p.z));
for (var c : strip(path, i)) {
int old = existingFloor(c.x, c.z, all); if (old != INVALID) { if (old != floor[i]) return null; continue; }
var g = probe.ground(c.x, c.z); int foundation = Math.max(1, floor[i] - g.top() + 1), bottom = floor[i] - foundation;
if (!probe.solid(c.x, bottom, c.z) || !probe.solid(c.x, bottom - 1, c.z)) return null;
for (int y = bottom - 1; y <= floor[i] + 8; y++) if (probe.protectedAt(c.x, y, c.z)) return null;
var previous = cells.putIfAbsent(key(c.x, c.z), new Road(c.x, c.z, floor[i], foundation));
if (previous != null && previous.floor != floor[i]) return null;
}
}
// Bends may put nonconsecutive centerline strips next to each other. Their shared
// partition edge obeys the same level-landing contract, including existing town roads.
for (var cell : cells.values()) for (int d = 0; d < 4; d++) {
int nx = cell.x + DX[d], nz = cell.z + DZ[d];
var neighbor = cells.get(key(nx, nz));
if (neighbor != null && !owner.get(key(cell.x, cell.z)).equals(owner.get(key(nx, nz)))
&& neighbor.floor != cell.floor) return null;
int old = existingFloor(nx, nz, all);
if (old >= 0 && old != cell.floor) return null;
}
var parcels = new ArrayList<Parcel>();
for (var f : footprints) {
int foundation = 1;
for (int z = f.z; z < f.z + f.depth; z++) for (int x = f.x; x < f.x + f.width; x++)
foundation = Math.max(foundation, cells.get(key(x, z)).foundation);
int[] heights = new int[f.width * f.depth];
for (int z = f.z; z < f.z + f.depth; z++) for (int x = f.x; x < f.x + f.width; x++) {
var cell = cells.get(key(x, z)); int bottom = cell.floor - foundation;
// A rectangular parcel uses one foundation depth. Certify its deeper columns
// explicitly rather than extending another column's rock certificate sideways.
if (!probe.solid(x, bottom, z) || !probe.solid(x, bottom - 1, z)) return null;
for (int y = bottom - 1; y < cell.floor - cell.foundation - 1; y++)
if (probe.protectedAt(x, y, z)) return null;
heights[(z - f.z) * f.width + x - f.x] = cell.floor;
}
parcels.add(new Parcel(Kind.STREET, f.x, f.z, f.width, f.depth, heights[0], 0, 0,
mix(seed ^ key(f.x, f.z)), foundation, heights));
}
return new Route(List.copyOf(result), List.copyOf(parcels));
}
/** Disjoint, completely occupied rectangles, each contained in a single native chunk. */
private static List<Footprint> footprints(Map<Long, XZ> cells) {
var remaining = new java.util.HashSet<>(cells.keySet()); var result = new ArrayList<Footprint>();
for (var at : cells.values().stream().sorted(Comparator.comparingInt(XZ::z).thenComparingInt(XZ::x)).toList()) {
if (!remaining.contains(key(at.x, at.z))) continue;
int width = 1, depth = 1;
while ((at.x + width >> 4) == (at.x >> 4) && remaining.contains(key(at.x + width, at.z))) width++;
while ((at.z + depth >> 4) == (at.z >> 4)) {
boolean complete = true;
for (int x = at.x; x < at.x + width; x++) complete &= remaining.contains(key(x, at.z + depth));
if (!complete) break;
depth++;
}
for (int z = at.z; z < at.z + depth; z++) for (int x = at.x; x < at.x + width; x++) remaining.remove(key(x, z));
result.add(new Footprint(at.x, at.z, width, depth));
}
return List.copyOf(result);
}
private static List<XZ> strip(List<XZ> points, int i) {
var p = points.get(i); var result = new java.util.LinkedHashSet<XZ>(); result.add(p);
for (int next : new int[]{i - 1, i + 1}) if (next >= 0 && next < points.size()) {
var q = points.get(next); int dx = Integer.signum(q.x - p.x), dz = Integer.signum(q.z - p.z);
result.add(new XZ(p.x + dz, p.z + dx)); result.add(new XZ(p.x - dz, p.z - dx));
}
return List.copyOf(result);
}
private static void line(List<XZ> points, XZ goal) {
var p = points.getLast(); while (p.x != goal.x) { p = new XZ(p.x + Integer.signum(goal.x - p.x), p.z); points.add(p); }
while (p.z != goal.z) { p = new XZ(p.x, p.z + Integer.signum(goal.z - p.z)); points.add(p); }
}
private static int existingFloor(int x, int z, List<District> all) {
for (var d : all) for (var p : d.parcels()) if (x >= p.minX() && x <= p.maxX() && z >= p.minZ() && z <= p.maxZ())
return p.kind() == Kind.STREET ? p.surfaceY(x - p.minX(), z - p.minZ()) : -1;
return INVALID;
}
private static boolean overlap(District candidate, List<District> all, int gap) {
for (var d : all) if (candidate.minX() <= d.maxX() + gap && candidate.maxX() >= d.minX() - gap
&& candidate.minZ() <= d.maxZ() + gap && candidate.maxZ() >= d.minZ() - gap) return true;
return false;
}
private static final class Exhausted extends RuntimeException {}
private static final class Probe implements LostCityPlanner16.Terrain {
final LostCityPlanner16.Terrain source; final Map<Long, LostCityPlanner.Ground> columns = new HashMap<>();
final Map<String, Integer> reasons = new LinkedHashMap<>(); int checks;
Probe(LostCityPlanner16.Terrain source) { this.source = source; }
public boolean owns(int x, int z) { return source.owns(x, z); }
public LostCityPlanner.Ground ground(int x, int z) {
if (!owns(x, z)) return new LostCityPlanner.Ground(0, 0, false, true);
long key = key(x, z); var old = columns.get(key); if (old != null) return old;
if (columns.size() >= MAX_EXTRA_COLUMNS) throw new Exhausted();
var value = source.ground(x, z); columns.put(key, value); return value;
}
public boolean solid(int x, int y, int z) { count(); ground(x, z); return owns(x, z) && source.solid(x, y, z); }
public boolean protectedAt(int x, int y, int z) { count(); ground(x, z); return !owns(x, z) || source.protectedAt(x, y, z); }
void count() { if (checks >= MAX_EXTRA_CHECKS) throw new Exhausted(); checks++; }
void reject(String reason) { reasons.merge(reason, 1, Integer::sum); }
}
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
private static long mix(long v) { v = (v ^ v >>> 30) * 0xbf58476d1ce4e5b9L; v = (v ^ v >>> 27) * 0x94d049bb133111ebL; return v ^ v >>> 31; }
}
@@ -29,17 +29,18 @@ public final class LostCityStructure extends Structure {
throw new IllegalStateException("Lost City district exceeds native structure reference range");
}
return Optional.of(new GenerationStub(LostCityStructures.locator(district), pieces -> {
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17) && generator.cityReservation16(island) != null)
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21) && generator.cityReservation16(island) != null
&& (generator.rootGeneration() != 21 || generator.cityPlacement21(island).core().surface().districts().contains(district)))
pieces.addPiece(new LostCityFoundationPiece16(generator.cityReservation16(island), district.seed()));
if (generator.rootGeneration() == 15) for (var support : generator.citySupports(island)) {
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
pieces.addPiece(new LostCitySupportPiece15(support));
}
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17)) for (var support : generator.citySupports16(island)) {
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21)) for (var support : generator.citySupports16(island)) {
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
pieces.addPiece(new LostCitySupportPiece16(support));
}
district.parcels().forEach(parcel -> pieces.addPiece((generator.rootGeneration() == 16 || generator.rootGeneration() == 17) ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
district.parcels().forEach(parcel -> pieces.addPiece(generator.rootGeneration() == 21 ? new LostCityPiece21(parcel) : generator.rootGeneration() == 20 ? new LostCityPiece20(parcel) : (generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21) ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
? new LostCityPiece15(parcel) : new LostCityPiece(parcel)));
if (generator.rootGeneration() == 15) for (var network : generator.undergroundPlan(island).networks()) {
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
@@ -49,13 +50,13 @@ public final class LostCityStructure extends Structure {
network.modules().stream().filter(m -> m.kind() == LostCityUnderground15.Kind.ACCESS)
.forEach(m -> pieces.addPiece(new LostCityUndergroundPiece15(m)));
}
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17)) for (var network : generator.undergroundPlan16(island).networks()) {
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21)) 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)));
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
network.modules().stream().filter(m -> m.kind() == LostCityUnderground16.Kind.ACCESS)
.forEach(m -> pieces.addPiece(new LostCityUndergroundPiece16(m)));
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
}
}));
}
@@ -40,8 +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 PIECE21 = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
SanctuaryMod.id("lost_city_parcel_v21"), LostCityPiece21::new);
public static final StructurePieceType PIECE20 = Registry.register(
BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_parcel_v20"), LostCityPiece20::new);
public static final StructurePieceType PIECE16 = Registry.register(
BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_parcel_v16"), LostCityPiece16::new);
public static final StructurePieceType UNDERGROUND20 = Registry.register(
BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_underground_v20"), LostCityUndergroundPiece20::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(
@@ -0,0 +1,248 @@
package fr.koka.sanctuary.worldgen.city;
import fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.Half;
import net.minecraft.world.level.block.state.properties.SlabType;
import net.minecraft.world.level.storage.loot.LootTable;
/** New surface architecture on the saved generation16 footprints. Published renderers remain immutable. */
public final class LostCitySurface20 {
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
private LostCitySurface20() {}
public static void render(LostCityPlan.Parcel parcel, LostCityRenderer.BlockWriter output) {
if (parcel.kind().building()) { LostCityInteriors20.render(parcel, output); return; }
var connected = ConnectedBlocks20.buffer(output);
if (parcel.kind() == LostCityPlan.Kind.STREET || parcel.kind() == LostCityPlan.Kind.BRIDGE) {
road(parcel, connected);
} else {
new Exterior(parcel, connected).render();
}
connected.flush();
}
private static void road(LostCityPlan.Parcel p, LostCityRenderer.BlockWriter output) {
boolean alongX = p.width() > p.depth(), bridge = p.kind() == LostCityPlan.Kind.BRIDGE;
for (int z = 0; z < p.depth(); z++) for (int x = 0; x < p.width(); x++) {
int y = p.surfaceY(x, z) - p.floorY();
int across = alongX ? z : x, along = alongX ? x : z, breadth = alongX ? p.depth() : p.width();
boolean edge = across == 0 || across == breadth - 1;
// A legible curb and patterned shoulders, not per-block random pavement.
Block paving = edge ? Blocks.POLISHED_TUFF : across == 1 || across == breadth - 2
? along % 4 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS
: Blocks.POLISHED_ANDESITE;
if (!bridge) for (int down = 1; down <= p.foundationDepth(); down++)
output.set(x, y - down, z, Blocks.STONE_BRICKS.defaultBlockState());
output.set(x, y, z, paving.defaultBlockState());
for (int up = 1; up <= 8; up++) output.set(x, y + up, z, Blocks.AIR.defaultBlockState());
if (bridge && edge) {
// Posts grow from the existing deck only; the certified natural bearings are untouched.
output.set(x, y + 1, z, (along % 5 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.IRON_BARS).defaultBlockState());
if (along % 5 == 0) output.set(x, y + 2, z, Blocks.TUFF_BRICK_SLAB.defaultBlockState());
continue;
}
for (var direction : HORIZONTAL) {
int nx = x + direction.getStepX(), nz = z + direction.getStepZ();
if (nx >= 0 && nx < p.width() && nz >= 0 && nz < p.depth()
&& p.surfaceY(nx, nz) == p.surfaceY(x, z) + 1) {
output.set(x, y + 1, z, Blocks.TUFF_BRICK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, direction));
break;
}
}
}
}
private static final class Exterior implements LostCityRenderer.BlockWriter {
final LostCityPlan.Parcel p;
final LostCityRenderer.BlockWriter output;
final Rotation turn;
Exterior(LostCityPlan.Parcel p, LostCityRenderer.BlockWriter output) {
this.p = p; this.output = output;
turn = switch (p.facing()) { case 1 -> Rotation.CLOCKWISE_90; case 2 -> Rotation.CLOCKWISE_180;
case 3 -> Rotation.COUNTERCLOCKWISE_90; default -> Rotation.NONE; };
}
int x(int x, int z) { return switch (p.facing()) { case 1 -> p.width() - 1 - z; case 2 -> p.width() - 1 - x; case 3 -> z; default -> x; }; }
int z(int x, int z) { return switch (p.facing()) { case 1 -> x; case 2 -> p.depth() - 1 - z; case 3 -> p.depth() - 1 - x; default -> z; }; }
@Override public void set(int x, int y, int z, BlockState state) { output.set(x(x, z), y, z(x, z), state.rotate(turn)); }
void set(int x, int y, int z, Block block) { set(x, y, z, block.defaultBlockState()); }
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
output.chest(x(x, z), y, z(x, z), table, seed ^ 0x20c17a7eL);
}
void box(int x0, int y0, int z0, int x1, int y1, int z1, Block block) {
for (int x = x0; x <= x1; x++) for (int z = z0; z <= z1; z++) for (int y = y0; y <= y1; y++) set(x, y, z, block);
}
void render() {
if (p.width() != 17 || p.depth() != 17) throw new IllegalArgumentException("Generation20 amenities require the certified 17x17 footprint");
// Keep the proven pool containment, excavation steps and garage ramps. All new ornament is
// composed before connection resolution, so clipping cannot split a rail or window design.
var north = new LostCityPlan.Parcel(p.kind(), p.minX(), p.minZ(), 17, 17, p.floorY(), p.floors(), 0,
p.seed(), p.foundationDepth(), p.surfaceHeights());
LostCitySurface16.render(north, new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) {
if (state.is(Blocks.STONE) && y < 0) state = Blocks.STONE_BRICKS.defaultBlockState();
if (state.is(Blocks.DARK_OAK_PLANKS)) state = Blocks.OAK_PLANKS.defaultBlockState();
if (state.is(Blocks.DARK_OAK_FENCE)) state = Blocks.OAK_FENCE.defaultBlockState();
if (state.is(Blocks.DARK_OAK_STAIRS)) state = Blocks.OAK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, state.getValue(BlockStateProperties.HORIZONTAL_FACING));
Exterior.this.set(x, y, z, state);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { Exterior.this.chest(x, y, z, table, seed); }
});
// The north threshold is a level promenade, shared by every admitted parcel.
for (int x = 0; x <= 16; x++) set(x, 0, 0, x % 4 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS);
switch (p.kind()) {
case PARK -> park();
case PARK_GARDEN -> garden();
case PARK_PLAYGROUND -> playground();
case PARK_MEMORIAL -> memorial();
case PLAZA -> plaza();
case MARKET -> market();
case PARKING -> garage();
case POOL -> { if (p.foundationDepth() >= 8) pool(); else park(); }
case EXCAVATION -> { if (p.foundationDepth() >= 8) excavation(); else park(); }
default -> throw new IllegalArgumentException("Not an exterior amenity");
}
}
void paths() {
for (int n = 1; n <= 16; n++) for (int across = 7; across <= 9; across++) {
set(across, 0, n, across == 8 ? Blocks.POLISHED_TUFF : Blocks.TUFF_BRICKS);
set(n, 0, across, across == 8 ? Blocks.POLISHED_TUFF : Blocks.TUFF_BRICKS);
}
}
void bench(int x, int y, int z, int length, Direction view) {
for (int dx = 0; dx < length; dx++) set(x + dx, y, z, Blocks.OAK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, view.getOpposite()));
set(x - 1, y, z, Blocks.TUFF_BRICK_SLAB); set(x + length, y, z, Blocks.TUFF_BRICK_SLAB);
}
void lamp(int x, int z) {
set(x, 1, z, Blocks.CHISELED_TUFF_BRICKS); box(x, 2, z, x, 3, z, Blocks.BIRCH_FENCE);
set(x, 4, z, Blocks.BIRCH_PLANKS); set(x, 5, z, Blocks.LANTERN);
}
void park() {
paths(); bench(10, 1, 3, 3, Direction.SOUTH); bench(10, 1, 11, 3, Direction.NORTH);
lamp(5, 11); lamp(11, 5);
// A birch screen beside the existing oak trees changes the park's canopy and rhythm.
box(3, 1, 12, 3, 4, 12, Blocks.BIRCH_LOG);
for (int x = 2; x <= 4; x++) for (int z = 11; z <= 13; z++)
set(x, 5, z, Blocks.BIRCH_LEAVES.defaultBlockState().setValue(BlockStateProperties.PERSISTENT, true));
for (int z = 2; z <= 5; z++) set(6, 0, z, Blocks.POLISHED_GRANITE);
}
void garden() {
paths();
// Four parterres have low carved kerbs, soil beds and a shared central promenade.
for (int x0 : new int[]{2, 10}) for (int z0 : new int[]{2, 10}) {
for (int n = 0; n < 5; n++) {
set(x0 + n, 0, z0, Blocks.POLISHED_DIORITE); set(x0 + n, 0, z0 + 4, Blocks.POLISHED_DIORITE);
set(x0, 0, z0 + n, Blocks.POLISHED_DIORITE); set(x0 + 4, 0, z0 + n, Blocks.POLISHED_DIORITE);
}
set(x0, 1, z0, Blocks.CHISELED_TUFF); set(x0 + 4, 1, z0 + 4, Blocks.CHISELED_TUFF);
}
bench(7, 1, 15, 3, Direction.NORTH);
// Pergola along the west planting bed, not across a circulation route.
for (int z : new int[]{2, 6}) box(1, 1, z, 1, 3, z, Blocks.STRIPPED_OAK_LOG);
box(1, 4, 2, 1, 4, 6, Blocks.BIRCH_SLAB); lamp(15, 8);
}
void playground() {
paths();
for (int x = 1; x <= 7; x++) { set(x, 0, 1, Blocks.BIRCH_PLANKS); set(x, 0, 7, Blocks.BIRCH_PLANKS); }
for (int z = 1; z <= 7; z++) { set(1, 0, z, Blocks.BIRCH_PLANKS); set(7, 0, z, Blocks.BIRCH_PLANKS); }
// Braced climbing platform: low guards leave the three front steps open.
for (int x : new int[]{11, 13}) set(x, 4, 13, Blocks.BIRCH_FENCE);
set(12, 4, 13, Blocks.BIRCH_FENCE);
set(11, 4, 11, Blocks.BIRCH_FENCE); set(13, 4, 11, Blocks.BIRCH_FENCE);
bench(3, 1, 13, 3, Direction.NORTH); lamp(15, 3);
}
void plaza() {
for (int x = 1; x <= 15; x++) for (int z = 1; z <= 15; z++) {
int d = Math.max(Math.abs(x - 8), Math.abs(z - 8));
set(x, 0, z, d == 3 || d == 6 ? Blocks.POLISHED_GRANITE : d == 4 || d == 7 ? Blocks.TUFF_BRICKS : Blocks.POLISHED_DIORITE);
}
// Dry civic fountain: bevelled bowl, carved pedestal, projecting cap.
for (int x = 6; x <= 10; x++) for (int z = 6; z <= 10; z++) {
if (x != 6 && x != 10 && z != 6 && z != 10) continue;
boolean corner = (x == 6 || x == 10) && (z == 6 || z == 10);
set(x, 1, z, corner ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICK_SLAB);
}
set(8, 1, 8, Blocks.CHISELED_TUFF); set(8, 2, 8, Blocks.CHISELED_TUFF_BRICKS);
set(8, 3, 8, Blocks.POLISHED_TUFF_SLAB); lamp(2, 12); lamp(14, 4);
}
void memorial() {
// The memorial keeps its open central approach and gains stepped stone shoulders.
for (int x = 1; x <= 15; x++) for (int z = 1; z <= 15; z++)
set(x, 0, z, x == 5 || x == 11 || z == 5 || z == 11 ? Blocks.TUFF_BRICKS : Blocks.POLISHED_ANDESITE);
for (int x = 7; x <= 9; x++) {
set(x, 1, 7, Blocks.TUFF_BRICK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
set(x, 1, 9, Blocks.TUFF_BRICK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
}
set(8, 2, 8, Blocks.CHISELED_TUFF); set(8, 3, 8, Blocks.CHISELED_TUFF_BRICKS);
set(8, 4, 8, Blocks.CHISELED_STONE_BRICKS); set(8, 5, 8, Blocks.POLISHED_DIORITE_SLAB);
lamp(3, 8); lamp(13, 8);
}
void market() {
for (int x = 1; x <= 15; x++) for (int z = 1; z <= 15; z++)
set(x, 0, z, x >= 7 && x <= 9 || z >= 7 && z <= 9 ? Blocks.TUFF_BRICKS : Blocks.POLISHED_GRANITE);
for (int x0 : new int[]{2, 11}) for (int z0 : new int[]{2, 11}) {
for (int x = x0; x <= x0 + 3; x++) for (int z = z0; z <= z0 + 2; z++)
set(x, 4, z, (x - x0 & 1) == 0 ? Blocks.BIRCH_SLAB : Blocks.OAK_SLAB);
for (int x : new int[]{x0, x0 + 3}) {
set(x, 1, z0, Blocks.CHISELED_TUFF_BRICKS);
box(x, 2, z0, x, 3, z0, Blocks.STRIPPED_OAK_LOG);
}
set(x0 + 1, 5, z0 + 1, Blocks.BIRCH_SLAB);
}
lamp(1, 8); lamp(15, 8);
}
void pool() {
for (int x = 3; x <= 13; x++) for (int z = 4; z <= 13; z++) {
if (x == 3 || x == 13 || z == 4 || z == 13) set(x, 0, z, Blocks.POLISHED_DIORITE);
}
// Changing/showers canopy stands beside the deck; it never enters the sealed basin.
for (int x : new int[]{10, 15}) box(x, 1, 1, x, 3, 1, Blocks.STRIPPED_BIRCH_LOG);
box(10, 4, 1, 15, 4, 3, Blocks.BIRCH_SLAB);
for (int x = 11; x <= 14; x++) set(x, 1, 1, Blocks.WATER_CAULDRON.defaultBlockState()
.setValue(BlockStateProperties.LEVEL_CAULDRON, 2));
set(15, 4, 2, Blocks.BIRCH_PLANKS); set(15, 5, 2, Blocks.LANTERN);
for (int z : new int[]{5, 9}) set(15, 1, z, Blocks.OAK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST));
box(2, 0, 14, 14, 0, 15, Blocks.TUFF_BRICKS);
}
void excavation() {
// Reinforced temporary footings and timber trestles stay out of the central descent.
for (int x : new int[]{4, 12}) {
box(x, -4, 11, x, -1, 11, Blocks.CHISELED_STONE_BRICKS);
set(x, 0, 11, Blocks.OAK_PLANKS);
}
box(3, 3, 13, 13, 3, 13, Blocks.STRIPPED_OAK_LOG);
for (int x : new int[]{3, 13}) box(x, -4, 13, x, 2, 13, Blocks.STONE_BRICKS);
set(3, 4, 13, Blocks.LANTERN);
set(1, 1, 11, Blocks.GRINDSTONE); set(1, 1, 10, Blocks.CRAFTING_TABLE);
}
void garage() {
int base = p.foundationDepth() >= 8 ? -5 : 0;
for (int x : new int[]{3, 13}) for (int z : new int[]{3, 13}) {
box(x, base + 1, z, x, base + 4, z, Blocks.POLISHED_ANDESITE);
set(x, base + 1, z, Blocks.CHISELED_TUFF_BRICKS);
}
for (int z = 6; z <= 15; z++) for (int x = 1; x <= 15; x++)
if (x <= 5 || x >= 11) set(x, base, z, z % 4 == 0 ? Blocks.POLISHED_DIORITE : Blocks.POLISHED_ANDESITE);
// A ribbed structural fascia and ventilation blades make this an open modern garage.
for (int x = 0; x <= 16; x++) {
set(x, base + 5, 0, x % 4 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS);
if (x < 6 || x > 10) set(x, base + 3, 0, Blocks.IRON_TRAPDOOR.defaultBlockState()
.setValue(BlockStateProperties.OPEN, true).setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
}
for (int x : new int[]{5, 11}) {
set(x, base + 4, 9, Blocks.SMOOTH_STONE);
set(x, base + 3, 9, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
}
}
}
}
@@ -0,0 +1,302 @@
package fr.koka.sanctuary.worldgen.city;
import fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.Half;
import net.minecraft.world.level.block.state.properties.SlabType;
import net.minecraft.world.level.storage.loot.LootTable;
/** New surface architecture on the saved generation16 footprints. Published renderers remain immutable. */
public final class LostCitySurface21 {
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
private LostCitySurface21() {}
public static void render(LostCityPlan.Parcel parcel, LostCityRenderer.BlockWriter output) {
if (parcel.kind().building()) { LostCityInteriors21.render(parcel, output); return; }
var connected = ConnectedBlocks20.buffer(output);
if (parcel.kind() == LostCityPlan.Kind.STREET || parcel.kind() == LostCityPlan.Kind.BRIDGE) {
road(parcel, connected);
} else {
new Exterior(parcel, connected).render();
}
connected.flush();
}
private static void road(LostCityPlan.Parcel p, LostCityRenderer.BlockWriter output) {
boolean alongX = p.width() > p.depth(), bridge = p.kind() == LostCityPlan.Kind.BRIDGE;
for (int z = 0; z < p.depth(); z++) for (int x = 0; x < p.width(); x++) {
int y = p.surfaceY(x, z) - p.floorY();
int across = alongX ? z : x, along = alongX ? x : z, breadth = alongX ? p.depth() : p.width();
boolean edge = across == 0 || across == breadth - 1;
// A legible curb and patterned shoulders, not per-block random pavement.
Block paving = edge ? Blocks.POLISHED_TUFF : across == 1 || across == breadth - 2
? along % 4 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS
: Blocks.POLISHED_ANDESITE;
if (!bridge) for (int down = 1; down <= p.foundationDepth(); down++)
output.set(x, y - down, z, Blocks.STONE_BRICKS.defaultBlockState());
output.set(x, y, z, paving.defaultBlockState());
for (int up = 1; up <= 8; up++) output.set(x, y + up, z, Blocks.AIR.defaultBlockState());
if (bridge && edge) {
// Posts grow from the existing deck only; the certified natural bearings are untouched.
output.set(x, y + 1, z, (along % 5 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.IRON_BARS).defaultBlockState());
if (along % 5 == 0) output.set(x, y + 2, z, Blocks.TUFF_BRICK_SLAB.defaultBlockState());
continue;
}
for (var direction : HORIZONTAL) {
int nx = x + direction.getStepX(), nz = z + direction.getStepZ();
if (nx >= 0 && nx < p.width() && nz >= 0 && nz < p.depth()
&& p.surfaceY(nx, nz) == p.surfaceY(x, z) + 1) {
output.set(x, y + 1, z, Blocks.TUFF_BRICK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, direction));
break;
}
}
}
}
private static final class Exterior implements LostCityRenderer.BlockWriter {
final LostCityPlan.Parcel p;
final LostCityRenderer.BlockWriter output;
final Rotation turn;
Exterior(LostCityPlan.Parcel p, LostCityRenderer.BlockWriter output) {
this.p = p; this.output = output;
turn = switch (p.facing()) { case 1 -> Rotation.CLOCKWISE_90; case 2 -> Rotation.CLOCKWISE_180;
case 3 -> Rotation.COUNTERCLOCKWISE_90; default -> Rotation.NONE; };
}
int x(int x, int z) { return switch (p.facing()) { case 1 -> p.width() - 1 - z; case 2 -> p.width() - 1 - x; case 3 -> z; default -> x; }; }
int z(int x, int z) { return switch (p.facing()) { case 1 -> x; case 2 -> p.depth() - 1 - z; case 3 -> p.depth() - 1 - x; default -> z; }; }
@Override public void set(int x, int y, int z, BlockState state) {
output.set(x(x, z), y, z(x, z), palette(y, state).rotate(turn));
}
BlockState palette(int y, BlockState state) {
Block block = null;
if (state.is(Blocks.STONE) && y < 0) block = Blocks.STONE_BRICKS;
else if (state.is(Blocks.DARK_OAK_PLANKS)) block = Blocks.OAK_PLANKS;
else if (state.is(Blocks.DARK_OAK_FENCE)) block = Blocks.OAK_FENCE;
else if (state.is(Blocks.DARK_OAK_STAIRS)) block = Blocks.OAK_STAIRS;
else if (state.is(Blocks.DARK_OAK_SLAB)) block = Blocks.OAK_SLAB;
else if (state.is(Blocks.CALCITE)) block = Blocks.SMOOTH_STONE;
else if (y > 0) {
if (state.is(Blocks.TUFF_BRICKS)) block = Blocks.STONE_BRICKS;
else if (state.is(Blocks.CHISELED_TUFF_BRICKS) || state.is(Blocks.CHISELED_TUFF)) block = Blocks.CHISELED_STONE_BRICKS;
else if (state.is(Blocks.TUFF_BRICK_SLAB)) block = Blocks.STONE_BRICK_SLAB;
else if (state.is(Blocks.POLISHED_TUFF_SLAB)) block = Blocks.SMOOTH_STONE_SLAB;
else if (state.is(Blocks.TUFF_BRICK_STAIRS)) block = Blocks.STONE_BRICK_STAIRS;
else if (state.is(Blocks.POLISHED_TUFF)) block = Blocks.POLISHED_ANDESITE;
}
if (block == null) return state;
BlockState result = block.defaultBlockState();
for (var property : state.getProperties()) if (result.hasProperty(property)) result = copy(result, state, property);
return result;
}
<T extends Comparable<T>> BlockState copy(BlockState into, BlockState from,
net.minecraft.world.level.block.state.properties.Property<T> property) {
return into.setValue(property, from.getValue(property));
}
void set(int x, int y, int z, Block block) { set(x, y, z, block.defaultBlockState()); }
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
output.chest(x(x, z), y, z(x, z), table, seed ^ 0x21c17a7eL);
}
void box(int x0, int y0, int z0, int x1, int y1, int z1, Block block) {
for (int x = x0; x <= x1; x++) for (int z = z0; z <= z1; z++) for (int y = y0; y <= y1; y++) set(x, y, z, block);
}
void render() {
if (p.width() != 17 || p.depth() != 17) throw new IllegalArgumentException("Generation21 amenities require the certified 17x17 footprint");
// Keep the proven pool containment, excavation steps and garage ramps. All new ornament is
// composed before connection resolution, so clipping cannot split a rail or window design.
var north = new LostCityPlan.Parcel(p.kind(), p.minX(), p.minZ(), 17, 17, p.floorY(), p.floors(), 0,
p.seed(), p.foundationDepth(), p.surfaceHeights());
LostCitySurface16.render(north, new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) {
Exterior.this.set(x, y, z, state);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { Exterior.this.chest(x, y, z, table, seed); }
});
// The north threshold is a level promenade, shared by every admitted parcel.
for (int x = 0; x <= 16; x++) set(x, 0, 0, x % 4 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS);
switch (p.kind()) {
case PARK -> park();
case PARK_GARDEN -> garden();
case PARK_PLAYGROUND -> playground();
case PARK_MEMORIAL -> memorial();
case PLAZA -> plaza();
case MARKET -> market();
case PARKING -> garage();
case POOL -> { if (p.foundationDepth() >= 8) pool(); else park(); }
case EXCAVATION -> { if (p.foundationDepth() >= 8) excavation(); else park(); }
default -> throw new IllegalArgumentException("Not an exterior amenity");
}
}
void paths() {
for (int n = 1; n <= 16; n++) for (int across = 7; across <= 9; across++) {
set(across, 0, n, across == 8 ? Blocks.POLISHED_TUFF : Blocks.TUFF_BRICKS);
set(n, 0, across, across == 8 ? Blocks.POLISHED_TUFF : Blocks.TUFF_BRICKS);
}
}
void bench(int x, int y, int z, int length, Direction view) {
for (int dx = 0; dx < length; dx++) set(x + dx, y, z, Blocks.OAK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, view.getOpposite()));
set(x - 1, y, z, Blocks.TUFF_BRICK_SLAB); set(x + length, y, z, Blocks.TUFF_BRICK_SLAB);
}
void lamp(int x, int z) {
set(x, 1, z, Blocks.CHISELED_TUFF_BRICKS); box(x, 2, z, x, 3, z, Blocks.OAK_FENCE);
set(x, 4, z, Blocks.OAK_PLANKS); set(x, 5, z, Blocks.LANTERN);
}
void park() {
paths(); bench(10, 1, 3, 3, Direction.SOUTH); bench(10, 1, 11, 3, Direction.NORTH);
lamp(5, 11); lamp(11, 5);
// A small oak screen joins the existing trees without introducing another wood palette.
box(3, 1, 12, 3, 4, 12, Blocks.OAK_LOG);
for (int x = 2; x <= 4; x++) for (int z = 11; z <= 13; z++)
set(x, 5, z, Blocks.OAK_LEAVES.defaultBlockState().setValue(BlockStateProperties.PERSISTENT, true));
for (int z = 2; z <= 5; z++) set(6, 0, z, Blocks.POLISHED_ANDESITE);
}
void garden() {
paths();
// Four parterres have low carved kerbs, soil beds and a shared central promenade.
for (int x0 : new int[]{2, 10}) for (int z0 : new int[]{2, 10}) {
for (int n = 0; n < 5; n++) {
set(x0 + n, 0, z0, Blocks.SMOOTH_STONE); set(x0 + n, 0, z0 + 4, Blocks.SMOOTH_STONE);
set(x0, 0, z0 + n, Blocks.SMOOTH_STONE); set(x0 + 4, 0, z0 + n, Blocks.SMOOTH_STONE);
}
set(x0, 1, z0, Blocks.CHISELED_TUFF); set(x0 + 4, 1, z0 + 4, Blocks.CHISELED_TUFF);
}
bench(7, 1, 15, 3, Direction.NORTH);
// Pergola along the west planting bed, not across a circulation route.
for (int z : new int[]{2, 6}) box(1, 1, z, 1, 3, z, Blocks.STRIPPED_OAK_LOG);
box(1, 4, 2, 1, 4, 6, Blocks.OAK_SLAB); lamp(15, 8);
}
void playground() {
paths();
for (int x = 1; x <= 7; x++) { set(x, 0, 1, Blocks.OAK_PLANKS); set(x, 0, 7, Blocks.OAK_PLANKS); }
for (int z = 1; z <= 7; z++) { set(1, 0, z, Blocks.OAK_PLANKS); set(7, 0, z, Blocks.OAK_PLANKS); }
// The hanging seat's upper half meets the chain above; a bottom slab would leave a gap.
set(4, 2, 4, Blocks.OAK_SLAB.defaultBlockState().setValue(BlockStateProperties.SLAB_TYPE, SlabType.TOP));
// The last ascending step used to touch its neighbors only diagonally, over empty air.
set(12, 1, 10, Blocks.OAK_PLANKS);
// Braced climbing platform: low guards leave the three front steps open.
for (int x : new int[]{11, 13}) set(x, 4, 13, Blocks.OAK_FENCE);
set(12, 4, 13, Blocks.OAK_FENCE);
set(11, 4, 11, Blocks.OAK_FENCE); set(13, 4, 11, Blocks.OAK_FENCE);
bench(3, 1, 13, 3, Direction.NORTH); lamp(15, 3);
}
void plaza() {
for (int x = 1; x <= 15; x++) for (int z = 1; z <= 15; z++) {
int d = Math.max(Math.abs(x - 8), Math.abs(z - 8));
set(x, 0, z, d == 3 || d == 6 ? Blocks.POLISHED_ANDESITE : d == 4 || d == 7 ? Blocks.TUFF_BRICKS : Blocks.SMOOTH_STONE);
}
// Dry civic fountain: bevelled bowl, carved pedestal, projecting cap.
for (int x = 6; x <= 10; x++) for (int z = 6; z <= 10; z++) {
if (x != 6 && x != 10 && z != 6 && z != 10) continue;
boolean corner = (x == 6 || x == 10) && (z == 6 || z == 10);
set(x, 1, z, corner ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICK_SLAB);
}
set(8, 1, 8, Blocks.CHISELED_TUFF); set(8, 2, 8, Blocks.CHISELED_TUFF_BRICKS);
set(8, 3, 8, Blocks.POLISHED_TUFF_SLAB); lamp(2, 12); lamp(14, 4);
}
void memorial() {
// The memorial keeps its open central approach and gains stepped stone shoulders.
for (int x = 1; x <= 15; x++) for (int z = 1; z <= 15; z++)
set(x, 0, z, x == 5 || x == 11 || z == 5 || z == 11 ? Blocks.TUFF_BRICKS : Blocks.POLISHED_ANDESITE);
for (int x = 7; x <= 9; x++) {
set(x, 1, 7, Blocks.TUFF_BRICK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
set(x, 1, 9, Blocks.TUFF_BRICK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
}
set(8, 2, 8, Blocks.CHISELED_TUFF); set(8, 3, 8, Blocks.CHISELED_TUFF_BRICKS);
set(8, 4, 8, Blocks.CHISELED_STONE_BRICKS); set(8, 5, 8, Blocks.SMOOTH_STONE_SLAB);
lamp(3, 8); lamp(13, 8);
}
void market() {
for (int x = 1; x <= 15; x++) for (int z = 1; z <= 15; z++)
set(x, 0, z, x >= 7 && x <= 9 || z >= 7 && z <= 9 ? Blocks.TUFF_BRICKS : Blocks.POLISHED_ANDESITE);
for (int x0 : new int[]{2, 11}) for (int z0 : new int[]{2, 11}) {
for (int x = x0; x <= x0 + 3; x++) for (int z = z0; z <= z0 + 2; z++)
set(x, 4, z, Blocks.OAK_SLAB);
for (int x : new int[]{x0, x0 + 3}) {
set(x, 1, z0, Blocks.CHISELED_TUFF_BRICKS);
box(x, 2, z0, x, 3, z0, Blocks.STRIPPED_OAK_LOG);
}
// Only one roof layer: the former raised bottom slab was separated by half a block.
// Rear posts and a full-width beam make the canopy's load path visible from the stall.
for (int x : new int[]{x0, x0 + 3}) box(x, 2, z0 + 2, x, 3, z0 + 2, Blocks.STRIPPED_OAK_LOG);
box(x0, 3, z0, x0 + 3, 3, z0, Blocks.OAK_PLANKS);
box(x0, 3, z0 + 2, x0 + 3, 3, z0 + 2, Blocks.OAK_PLANKS);
}
// The shared chest replaces this stall's counter: a pot cannot sit on its recessed lid.
set(13, 2, 13, Blocks.AIR); set(12, 2, 13, Blocks.FLOWER_POT);
lamp(1, 8); lamp(15, 8);
}
void pool() {
// Each submerged tread bears on a masonry riser down to the basin floor;
// water between diagonal stairs is not a structural connection.
for (int step = 1; step <= 3; step++) for (int x = 7; x <= 9; x++)
for (int y = -3; y < -step; y++) set(x, y, step + 4, Blocks.SMOOTH_STONE);
for (int x = 3; x <= 13; x++) for (int z = 4; z <= 13; z++) {
if (x == 3 || x == 13 || z == 4 || z == 13) set(x, 0, z, Blocks.SMOOTH_STONE);
}
// Changing/showers canopy stands beside the deck; it never enters the sealed basin.
for (int x : new int[]{10, 15}) box(x, 1, 1, x, 3, 1, Blocks.STRIPPED_OAK_LOG);
for (int x : new int[]{10, 15}) box(x, 1, 3, x, 3, 3, Blocks.STRIPPED_OAK_LOG);
box(10, 3, 1, 15, 3, 1, Blocks.OAK_PLANKS);
box(10, 3, 3, 15, 3, 3, Blocks.OAK_PLANKS);
box(10, 4, 1, 15, 4, 3, Blocks.OAK_SLAB);
for (int x = 11; x <= 14; x++) set(x, 1, 1, Blocks.WATER_CAULDRON.defaultBlockState()
.setValue(BlockStateProperties.LEVEL_CAULDRON, 2));
set(15, 4, 2, Blocks.OAK_PLANKS); set(15, 5, 2, Blocks.LANTERN);
for (int z : new int[]{5, 9}) set(15, 1, z, Blocks.OAK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST));
box(2, 0, 14, 14, 0, 15, Blocks.TUFF_BRICKS);
}
void excavation() {
// Each descending stone step rests on a small riser down to the certified pit floor.
for (int step = 1; step <= 5; step++) for (int x = 7; x <= 9; x++)
for (int y = -4; y < -step; y++) set(x, y, step + 2, Blocks.STONE_BRICKS);
// Reinforced temporary footings and timber trestles stay out of the central descent.
for (int x : new int[]{4, 12}) {
box(x, -4, 11, x, -1, 11, Blocks.CHISELED_STONE_BRICKS);
set(x, 0, 11, Blocks.OAK_PLANKS);
}
box(3, 3, 13, 13, 3, 13, Blocks.STRIPPED_OAK_LOG);
for (int x : new int[]{3, 13}) box(x, -4, 13, x, 2, 13, Blocks.STONE_BRICKS);
// Replace the single beam with an open construction shelter, one canopy on grounded posts.
// All four trestles start on the excavated floor, not on the missing former ground plane.
for (int x : new int[]{3, 13}) box(x, -4, 10, x, 2, 10, Blocks.STONE_BRICKS);
box(3, 3, 10, 13, 3, 10, Blocks.STRIPPED_OAK_LOG);
for (int x : new int[]{3, 8, 13}) box(x, 3, 10, x, 3, 13, Blocks.OAK_PLANKS);
box(3, 4, 10, 13, 4, 13, Blocks.OAK_SLAB);
set(3, 4, 13, Blocks.OAK_PLANKS);
set(3, 5, 13, Blocks.LANTERN);
set(1, 1, 11, Blocks.GRINDSTONE); set(1, 1, 10, Blocks.CRAFTING_TABLE);
}
void garage() {
int base = p.foundationDepth() >= 8 ? -5 : 0;
// The ramp's five-wide masonry wedge joins every tread to the garage floor.
if (base < 0) for (int z = 0; z <= 4; z++) for (int x = 6; x <= 10; x++)
for (int y = base + 1; y < -z; y++) set(x, y, z, Blocks.STONE_BRICKS);
for (int x : new int[]{3, 13}) for (int z : new int[]{3, 13}) {
box(x, base + 1, z, x, base + 4, z, Blocks.POLISHED_ANDESITE);
set(x, base + 1, z, Blocks.CHISELED_TUFF_BRICKS);
}
for (int z = 6; z <= 15; z++) for (int x = 1; x <= 15; x++)
if (x <= 5 || x >= 11) set(x, base, z, z % 4 == 0 ? Blocks.SMOOTH_STONE : Blocks.POLISHED_ANDESITE);
// A ribbed structural fascia and ventilation blades make this an open modern garage.
for (int x = 0; x <= 16; x++) {
// The underground ramp cuts through this former ceiling strip: never paint it back.
if (base == 0 || x < 6 || x > 10)
set(x, base + 5, 0, x % 4 == 0 ? Blocks.CHISELED_STONE_BRICKS : Blocks.SMOOTH_STONE);
if (x < 6 || x > 10) set(x, base + 3, 0, Blocks.IRON_TRAPDOOR.defaultBlockState()
.setValue(BlockStateProperties.OPEN, true).setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
}
for (int x : new int[]{5, 11}) {
set(x, base + 4, 9, Blocks.SMOOTH_STONE);
set(x, base + 3, 9, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
}
}
}
}
@@ -0,0 +1,85 @@
package fr.koka.sanctuary.worldgen.city;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import fr.koka.sanctuary.expansion.ExpansionTerrain;
import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Predicate;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.RandomState;
/** Generation21 bounded noise-column cache shared by surface and underground planning, never loaded chunks. */
public final class LostCityTerrain21 implements LostCityUnderground16.Terrain, LostCityPlanner16.Terrain {
public static final int MAX_COLUMNS = 24_096;
private final ExpansionIsland island;
private final NoiseBasedChunkGenerator generator;
private final RandomState random;
private final LevelHeightAccessor height;
private final Predicate<BlockPos> protectedVolume;
private final Map<Long, NoiseColumn> columns = new HashMap<>();
private final BlockPos fluidCandidate;
public LostCityTerrain21(ExpansionIsland island, NoiseBasedChunkGenerator generator, RandomState random,
LevelHeightAccessor height, Predicate<BlockPos> protectedVolume) {
this.island = island; this.generator = generator; this.random = random;
this.height = height; this.protectedVolume = protectedVolume;
fluidCandidate = ExpansionTerrain.pocketCandidate(island);
}
private NoiseColumn column(int x, int z) {
if (!owns(x, z)) return null;
long key = ((long) x << 32) ^ (z & 0xffffffffL);
var cached = columns.get(key);
if (cached != null) return cached;
if (columns.size() >= MAX_COLUMNS) return null;
var result = generator.getBaseColumn(x, z, height, random);
columns.put(key, result);
return result;
}
public LostCityPlanner.Ground ground(int x, int z) {
var column = column(x, z);
if (column == null) return new LostCityPlanner.Ground(0, 0, false, true);
int top = 383;
while (top > 0 && column.getBlock(top).isAir()) top--;
int bottom = top;
while (bottom > 0 && top - bottom < 24 && column.getBlock(bottom).isSolid()
&& column.getBlock(bottom).getFluidState().isEmpty()) bottom--;
boolean dry = column.getBlock(top).isSolid() && column.getBlock(top).getFluidState().isEmpty();
boolean water = island.origin() && top >= 168 && dry && top - bottom >= 6
&& PopulationHydrologyRuntime.protects(generator, random, new BlockPos(x, top, z), 1, 30, 12);
return new LostCityPlanner.Ground(top, top - bottom, dry, water);
}
@Override public boolean solid(int x, int y, int z) {
if (y < 0 || y >= 384) return false;
var column = column(x, z);
return column != null && column.getBlock(y).isSolid() && column.getBlock(y).getFluidState().isEmpty();
}
@Override public boolean protectedAt(int x, int y, int z) {
if (y < 0 || y >= 384) return true;
var column = column(x, z);
if (column == null || !column.getBlock(y).getFluidState().isEmpty()) return true;
if (!island.origin() && island.relief().equals("volcano")
&& Math.abs(x - fluidCandidate.getX()) <= 2 && Math.abs(z - fluidCandidate.getZ()) <= 2) return true;
return protectedVolume.test(new BlockPos(x, y, z));
}
@Override public boolean exteriorAir(int x, int y, int z, int dx, int dz) {
if (Math.abs(dx) + Math.abs(dz) != 1 || y < 1 || y >= 380 || !owns(x, z)) return false;
for (int distance = 0; distance <= 32; distance++) {
int px = x + dx * distance, pz = z + dz * distance;
if (!owns(px, pz)) return false; // Never infer exterior air in another present or future owner.
var sample = column(px, pz);
if (sample == null) return false;
for (int dy = -3; dy < 3; dy++) if (!sample.getBlock(y + dy).isAir()
|| protectedVolume.test(new BlockPos(px, y + dy, pz))) return false;
}
return true;
}
@Override public boolean owns(int x, int z) { return island.contains(x, z); }
public int columnsUsed() { return columns.size(); }
}
@@ -0,0 +1,88 @@
package fr.koka.sanctuary.worldgen.city;
import fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.Half;
import net.minecraft.world.level.storage.loot.LootTable;
/** Architectural finish for the generation20 city undercroft. The certified generation16 geometry,
* stairs, wet tanks, outfalls and support positions are reused without changing their planning. */
public final class LostCityUnderground20 {
private LostCityUnderground20() {}
public static void render(LostCityUnderground16.Module module, LostCityRenderer.BlockWriter output) {
var buffer = ConnectedBlocks20.buffer(output);
LostCityUnderground16.render(module, new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) {
buffer.set(x, y, z, material(module, x, y, z, state));
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
buffer.chest(x, y, z, loot, seed);
}
});
if (module.kind() != LostCityUnderground16.Kind.ACCESS && module.kind() != LostCityUnderground16.Kind.GARAGE) {
int y = module.height() - 2;
Block molding = module.kind() == LostCityUnderground16.Kind.POOLROOM ? Blocks.POLISHED_DIORITE_STAIRS
: module.kind() == LostCityUnderground16.Kind.EXPANSION_HALL ? Blocks.OAK_STAIRS : Blocks.TUFF_BRICK_STAIRS;
// Coved edge beneath the high ceiling, with a complete external shell behind it.
// Doorway planes and floor circulation are left in their certified positions.
for (int x = 2; x < module.width() - 2; x++) {
buffer.set(x, y, 1, cove(molding, Direction.NORTH));
buffer.set(x, y, module.depth() - 2, cove(molding, Direction.SOUTH));
}
for (int z = 2; z < module.depth() - 2; z++) {
buffer.set(1, y, z, cove(molding, Direction.WEST));
buffer.set(module.width() - 2, y, z, cove(molding, Direction.EAST));
}
if (module.kind() == LostCityUnderground16.Kind.POOLROOM) {
for (int x : new int[] {8, 20, 34, 46}) {
// Buttress capitals continue the existing pool colonnade, then span as
// tall repeated ceiling ribs. The 50x25 pool and swimming volume remain open.
for (int z = 1; z < module.depth() - 1; z++)
buffer.set(x, y, z, (z == 1 || z == module.depth() - 2
? Blocks.CHISELED_TUFF_BRICKS : Blocks.POLISHED_DIORITE).defaultBlockState());
for (int z : new int[] {1, module.depth() - 2}) {
buffer.set(x - 1, y - 1, z, cove(Blocks.POLISHED_DIORITE_STAIRS, Direction.EAST));
buffer.set(x + 1, y - 1, z, cove(Blocks.POLISHED_DIORITE_STAIRS, Direction.WEST));
}
}
}
}
buffer.flush();
}
private static BlockState cove(Block block, Direction facing) {
return block.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing)
.setValue(BlockStateProperties.HALF, Half.TOP);
}
private static BlockState material(LostCityUnderground16.Module m, int x, int y, int z, BlockState state) {
if (state.is(Blocks.STONE) && (y <= 0 || m.kind() == LostCityUnderground16.Kind.GARAGE))
return Blocks.STONE_BRICKS.defaultBlockState();
boolean wall = x == 0 || z == 0 || x == m.width() - 1 || z == m.depth() - 1;
boolean masonry = state.is(Blocks.STONE_BRICKS) || state.is(Blocks.CRACKED_STONE_BRICKS)
|| state.is(Blocks.MOSSY_STONE_BRICKS);
if (wall && masonry && y > 2 && y < m.height() - 1) {
int along = x == 0 || x == m.width() - 1 ? z : x;
return switch (m.kind()) {
case POOLROOM -> (y == 7 ? Blocks.POLISHED_DIORITE : y == 8 && along % 6 == 0
? Blocks.CHISELED_TUFF_BRICKS : Blocks.CALCITE).defaultBlockState();
case EXPANSION_HALL -> (along % 5 == 0 ? Blocks.STRIPPED_OAK_WOOD
: y < 5 ? Blocks.TUFF_BRICKS : Blocks.SMOOTH_STONE).defaultBlockState();
case SEWER, SEWER_OUTLET -> (along % 6 == 0 ? Blocks.CHISELED_TUFF_BRICKS
: Blocks.TUFF_BRICKS).defaultBlockState();
case PASSAGE -> (y == 3 ? Blocks.POLISHED_GRANITE : Blocks.TUFF).defaultBlockState();
default -> state;
};
}
if (m.kind() == LostCityUnderground16.Kind.GARAGE && state.is(Blocks.SMOOTH_STONE)
&& y >= 1 && (y - 1) % 16 == 0 && (x <= 1 || z <= 1 || x >= m.width() - 2 || z >= m.depth() - 2))
return ((x + z) % 8 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS).defaultBlockState();
if (m.kind() == LostCityUnderground16.Kind.EXPANSION_HALL && state.is(Blocks.DARK_OAK_STAIRS))
return Blocks.OAK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING,
state.getValue(BlockStateProperties.HORIZONTAL_FACING));
return state;
}
}
@@ -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 LostCityUndergroundPiece20 extends StructurePiece {
private final LostCityUnderground16.Module module;
public LostCityUndergroundPiece20(LostCityUnderground16.Module module) {
super(LostCityStructures.UNDERGROUND20, 0, bounds(module));
this.module = module;
setOrientation(null);
}
public LostCityUndergroundPiece20(StructurePieceSerializationContext context, CompoundTag tag) {
super(LostCityStructures.UNDERGROUND20, 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(() -> LostCityUnderground20.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());
}
}
@@ -0,0 +1,170 @@
package fr.koka.sanctuary.worldgen.detail;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceKey;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.FenceBlock;
import net.minecraft.world.level.block.FenceGateBlock;
import net.minecraft.world.level.block.IronBarsBlock;
import net.minecraft.world.level.block.StairBlock;
import net.minecraft.world.level.block.WallBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.BooleanProperty;
import net.minecraft.world.level.block.state.properties.StairsShape;
import net.minecraft.world.level.block.state.properties.WallSide;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.level.storage.loot.LootTable;
import net.minecraft.world.phys.shapes.BooleanOp;
import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.VoxelShape;
/** Generation20 only: resolve the complete, already-rotated geometry before a native writer clips chunks.
* Unwritten neighbors are air. This never reads loaded terrain, changes saved older renderers or creates loot early. */
public final class ConnectedBlocks20 {
public static final int MAX_BLOCKS = 262_144;
private static final Direction[] SIDES = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
private static final VoxelShape POST_TEST = Block.column(2.0, 0.0, 16.0);
private static final Map<Direction, VoxelShape> WALL_TEST = Shapes.rotateHorizontal(Block.boxZ(2.0, 16.0, 0.0, 9.0));
private ConnectedBlocks20() {}
public static Buffer buffer(LostCityRenderer.BlockWriter destination) { return new Buffer(destination); }
public static final class Buffer implements LostCityRenderer.BlockWriter {
private final LostCityRenderer.BlockWriter destination;
private final Map<BlockPos, BlockState> blocks = new LinkedHashMap<>();
private final Map<BlockPos, Chest> chests = new LinkedHashMap<>();
private boolean flushed;
private Buffer(LostCityRenderer.BlockWriter destination) { this.destination = Objects.requireNonNull(destination); }
@Override public void set(int x, int y, int z, BlockState state) {
checkOpen(); var at = new BlockPos(x, y, z); admit(at);
blocks.put(at, Objects.requireNonNull(state)); chests.remove(at);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
checkOpen(); var at = new BlockPos(x, y, z); admit(at);
blocks.put(at, Blocks.CHEST.defaultBlockState()); chests.put(at, new Chest(Objects.requireNonNull(table), seed));
}
/** Final placement wins. Every connection sees all blocks even when the sink accepts only one chunk. */
public void flush() {
checkOpen(); flushed = true;
if (blocks.isEmpty()) return;
var view = new Geometry(blocks);
// Stair corners depend only on neighbors' facing and half, not on their previous corner shapes.
blocks.replaceAll((at, state) -> StairBlock.isStairs(state)
? state.setValue(StairBlock.SHAPE, stairsShape(state, view, at)) : state);
// A pane/fence above a wall changes the underside used for LOW/TALL. Resolve it first.
// Their horizontal wall connection uses the wall tag, independent of wall arm heights.
blocks.replaceAll((at, state) -> connected(state, view, at));
// Walls depend on the final underside and post of the wall above, so visit from top to bottom.
blocks.keySet().stream().filter(at -> blocks.get(at).getBlock() instanceof WallBlock)
.sorted(Comparator.<BlockPos>comparingInt(at -> at.getY()).reversed()).toList()
.forEach(at -> blocks.put(at, wall(blocks.get(at), view, at)));
try {
for (var entry : blocks.entrySet()) {
var at = entry.getKey(); var chest = chests.get(at);
if (chest == null) destination.set(at.getX(), at.getY(), at.getZ(), entry.getValue());
else destination.chest(at.getX(), at.getY(), at.getZ(), chest.table(), chest.seed());
}
} finally { blocks.clear(); chests.clear(); }
}
private void admit(BlockPos at) {
if (!blocks.containsKey(at) && blocks.size() >= MAX_BLOCKS)
throw new IllegalArgumentException("Generation20 connection buffer exceeded its per-piece block budget");
}
private void checkOpen() { if (flushed) throw new IllegalStateException("A generation20 geometry buffer can be flushed only once"); }
}
private record Chest(ResourceKey<LootTable> table, long seed) {}
private static BlockState connected(BlockState state, BlockGetter view, BlockPos at) {
if (!(state.getBlock() instanceof IronBarsBlock) && !(state.getBlock() instanceof FenceBlock)) return state;
for (var direction : SIDES) {
var neighborPos = at.relative(direction); var neighbor = view.getBlockState(neighborPos);
boolean sturdy = neighbor.isFaceSturdy(view, neighborPos, direction.getOpposite());
boolean connects = state.getBlock() instanceof IronBarsBlock pane ? pane.attachsTo(neighbor, sturdy)
: ((FenceBlock) state.getBlock()).connectsTo(neighbor, sturdy, direction.getOpposite());
state = state.setValue(property(direction), connects);
}
return state;
}
/** WallBlock's private MC26.3-pre-2 LOW/TALL/UP calculation, using the same native shapes. */
private static BlockState wall(BlockState state, BlockGetter view, BlockPos at) {
var abovePos = at.above(); var above = view.getBlockState(abovePos);
var support = above.getCollisionShape(view, abovePos).getFaceShape(Direction.DOWN);
for (var direction : SIDES) {
var neighborPos = at.relative(direction); var neighbor = view.getBlockState(neighborPos);
boolean sturdy = neighbor.isFaceSturdy(view, neighborPos, direction.getOpposite());
// Pane's predicate shares walls' solid/exception, pane and wall-tag rules; walls also accept gates.
boolean joins = ((IronBarsBlock) Blocks.IRON_BARS).attachsTo(neighbor, sturdy)
|| neighbor.getBlock() instanceof FenceGateBlock
&& FenceGateBlock.connectsToDirection(neighbor, direction.getOpposite());
state = state.setValue(WallBlock.PROPERTY_BY_DIRECTION.get(direction), !joins ? WallSide.NONE
: covered(support, WALL_TEST.get(direction)) ? WallSide.TALL : WallSide.LOW);
}
return state.setValue(WallBlock.UP, raisePost(state, above, support));
}
private static boolean raisePost(BlockState state, BlockState above, VoxelShape support) {
if (above.getBlock() instanceof WallBlock && above.getValue(WallBlock.UP)) return true;
var n = state.getValue(WallBlock.NORTH); var s = state.getValue(WallBlock.SOUTH);
var w = state.getValue(WallBlock.WEST); var e = state.getValue(WallBlock.EAST);
boolean noN = n == WallSide.NONE, noS = s == WallSide.NONE;
boolean noW = w == WallSide.NONE, noE = e == WallSide.NONE;
if (noN && noS && noW && noE || noN != noS || noW != noE) return true;
if (n == WallSide.TALL && s == WallSide.TALL || e == WallSide.TALL && w == WallSide.TALL) return false;
return above.is(BlockTags.WALL_POST_OVERRIDE) || covered(support, POST_TEST);
}
private static boolean covered(VoxelShape support, VoxelShape test) {
return !Shapes.joinIsNotEmpty(test, support, BooleanOp.ONLY_FIRST);
}
private static BooleanProperty property(Direction direction) {
return switch (direction) {
case NORTH -> BlockStateProperties.NORTH; case EAST -> BlockStateProperties.EAST;
case SOUTH -> BlockStateProperties.SOUTH; case WEST -> BlockStateProperties.WEST;
default -> throw new IllegalArgumentException("Connections are horizontal");
};
}
/** Matches MC26.3-pre-2 StairBlock's private shape selection, inspected in its native bytecode.
* Panes and fences call the native connection predicates directly, including tags and exceptions. */
private static StairsShape stairsShape(BlockState state, BlockGetter view, BlockPos at) {
var facing = state.getValue(StairBlock.FACING);
var front = view.getBlockState(at.relative(facing));
if (StairBlock.isStairs(front) && state.getValue(StairBlock.HALF) == front.getValue(StairBlock.HALF)) {
var other = front.getValue(StairBlock.FACING);
if (other.getAxis() != facing.getAxis() && canTakeShape(state, view, at, other.getOpposite()))
return other == facing.getCounterClockWise() ? StairsShape.OUTER_LEFT : StairsShape.OUTER_RIGHT;
}
var back = view.getBlockState(at.relative(facing.getOpposite()));
if (StairBlock.isStairs(back) && state.getValue(StairBlock.HALF) == back.getValue(StairBlock.HALF)) {
var other = back.getValue(StairBlock.FACING);
if (other.getAxis() != facing.getAxis() && canTakeShape(state, view, at, other))
return other == facing.getCounterClockWise() ? StairsShape.INNER_LEFT : StairsShape.INNER_RIGHT;
}
return StairsShape.STRAIGHT;
}
private static boolean canTakeShape(BlockState state, BlockGetter view, BlockPos at, Direction direction) {
var neighbor = view.getBlockState(at.relative(direction));
return !StairBlock.isStairs(neighbor) || neighbor.getValue(StairBlock.FACING) != state.getValue(StairBlock.FACING)
|| neighbor.getValue(StairBlock.HALF) != state.getValue(StairBlock.HALF);
}
private static final class Geometry implements BlockGetter {
private final Map<BlockPos, BlockState> blocks;
private final int low, high;
Geometry(Map<BlockPos, BlockState> blocks) {
this.blocks = blocks;
low = blocks.keySet().stream().mapToInt(at -> at.getY()).min().orElse(0);
high = blocks.keySet().stream().mapToInt(at -> at.getY()).max().orElse(0);
}
@Override public BlockState getBlockState(BlockPos at) { return blocks.getOrDefault(at, Blocks.AIR.defaultBlockState()); }
@Override public FluidState getFluidState(BlockPos at) { return getBlockState(at).getFluidState(); }
@Override public BlockEntity getBlockEntity(BlockPos at) { return null; }
@Override public int getHeight() { return Math.addExact(Math.subtractExact(high, low), 1); }
@Override public int getMinY() { return low; }
}
}
@@ -0,0 +1,39 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.util.Locale;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component;
import net.minecraft.world.level.Level;
/** Read-only operator catalogue. Inspecting a plan neither loads chunks nor activates an expansion. */
public final class HeritageCommands19 {
private HeritageCommands19() {}
public static void register() {
CommandRegistrationCallback.EVENT.register((dispatcher, registries, environment) ->
dispatcher.register(Commands.literal("sanctuary")
.requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS))
.then(Commands.literal("structures").executes(context -> {
var source = context.getSource();
var level = source.getLevel();
if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21) || !generator.experimentalStructures()) {
source.sendFailure(Component.translatable("sanctuary.heritage.unavailable"));
return 0;
}
var plan = generator.heritagePlan19();
source.sendSuccess(() -> Component.translatable("sanctuary.heritage.header", plan.sites().size(), plan.links().size()), false);
for (var site : plan.sites()) source.sendSuccess(() -> Component.translatable("sanctuary.heritage.entry",
Component.translatable("sanctuary.heritage.kind." + site.kind().name().toLowerCase(Locale.ROOT)),
site.entry().x(), site.entry().y(), site.entry().z()), false);
if (generator.rootGeneration() == 21) for (var module : generator.transitPlan21().modules()) {
if (module.kind() != fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind.ACCESS) continue;
var at = fr.koka.sanctuary.worldgen.transit.TransitPiece21.locator(module);
source.sendSuccess(() -> Component.translatable("sanctuary.transit.entry", at.getX(), at.getY(), at.getZ()), false);
}
return plan.sites().size();
}))));
}
}
@@ -0,0 +1,185 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import java.util.ArrayList;
import java.util.Set;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.ResourceKey;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.loot.LootTable;
/** Self-contained schema19 geometry. Reload never needs the planner or a distant route destination. */
public final class HeritagePiece19 extends StructurePiece {
private static final Set<String> SITE_KEYS = Set.of("Schema", "Type", "Id", "Kind", "MinX", "FloorY", "MinZ",
"Width", "Depth", "Height", "Facing", "Seed", "FoundationDepth", "Supports", "Entry");
private static final Set<String> ROUTE_KEYS = Set.of("Schema", "Type", "Id", "FromId", "ToId", "Kind",
"StartChunkX", "StartChunkZ", "Cells", "Seed");
private static final Set<String> SUPPORT_KEYS = Set.of("X", "MinY", "Z", "MaxY");
private static final Set<String> ENTRY_KEYS = Set.of("X", "Y", "Z", "Facing");
private static final Set<String> CELL_KEYS = Set.of("X", "FloorY", "Z", "FoundationDepth", "Rail", "RailPresent");
private final HeritagePlan19.Site site;
private final HeritagePlan19.Segment segment;
public HeritagePiece19(HeritagePlan19.Site site) {
super(HeritageStructures19.PIECE, 0, bounds(site));
this.site = site; this.segment = null; setOrientation(null);
}
public HeritagePiece19(HeritagePlan19.Segment segment) {
super(HeritageStructures19.PIECE, 0, bounds(segment));
this.site = null; this.segment = segment; setOrientation(null);
}
public HeritagePiece19(StructurePieceSerializationContext context, CompoundTag tag) {
super(HeritageStructures19.PIECE, tag);
var payload = tag.getCompound("SanctuaryHeritage").orElseThrow(() -> new IllegalArgumentException("Missing saved heritage geometry"));
String type = string(payload, "Type");
if (type.equals("SITE")) { site = decodeSite(payload); segment = null; }
else if (type.equals("ROUTE")) { site = null; segment = decodeSegment(payload); }
else throw new IllegalArgumentException("Unknown heritage piece type");
if (!getBoundingBox().equals(isSite() ? bounds(site) : bounds(segment)))
throw new IllegalArgumentException("Heritage geometry disagrees with its saved native bounds");
setOrientation(null);
}
public boolean isSite() { return site != null; }
public HeritagePlan19.Site site() { if (site == null) throw new IllegalStateException("This heritage piece is a route"); return site; }
public HeritagePlan19.Segment segment() { if (segment == null) throw new IllegalStateException("This heritage piece is a site"); return segment; }
@Override public BlockPos getLocatorPosition() {
if (isSite()) return locator(site);
var cell = segment.cells().getFirst(); return new BlockPos(cell.x(), cell.floorY() + 1, cell.z());
}
public static BlockPos locator(HeritagePlan19.Site site) { return new BlockPos(site.entry().x(), site.entry().y(), site.entry().z()); }
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
tag.put("SanctuaryHeritage", isSite() ? encodeSite(site) : encodeSegment(segment));
}
public static CompoundTag encodeSite(HeritagePlan19.Site site) {
var tag = header("SITE", site.id(), site.kind().name(), site.seed());
tag.putInt("MinX", site.minX()); tag.putInt("FloorY", site.floorY()); tag.putInt("MinZ", site.minZ());
tag.putInt("Width", site.width()); tag.putInt("Depth", site.depth()); tag.putInt("Height", site.height());
tag.putInt("Facing", site.facing()); tag.putInt("FoundationDepth", site.foundationDepth());
var entry = new CompoundTag(); entry.putInt("X", site.entry().x()); entry.putInt("Y", site.entry().y());
entry.putInt("Z", site.entry().z()); entry.putInt("Facing", site.entry().facing()); tag.put("Entry", entry);
var supports = new ListTag();
for (var support : site.supports()) {
var value = new CompoundTag(); value.putInt("X", support.x()); value.putInt("MinY", support.minY());
value.putInt("Z", support.z()); value.putInt("MaxY", support.maxY()); supports.add(value);
}
tag.put("Supports", supports); return tag;
}
public static CompoundTag encodeSegment(HeritagePlan19.Segment segment) {
var tag = header("ROUTE", segment.id(), segment.kind().name(), segment.seed());
tag.putString("FromId", segment.fromId()); tag.putString("ToId", segment.toId());
tag.putInt("StartChunkX", segment.startChunkX()); tag.putInt("StartChunkZ", segment.startChunkZ());
var cells = new ListTag();
for (var cell : segment.cells()) {
var value = new CompoundTag(); value.putInt("X", cell.x()); value.putInt("FloorY", cell.floorY());
value.putInt("Z", cell.z()); value.putInt("FoundationDepth", cell.foundationDepth());
value.putString("Rail", cell.rail().name()); value.putBoolean("RailPresent", cell.railPresent()); cells.add(value);
}
tag.put("Cells", cells); return tag;
}
public static HeritagePlan19.Site decodeSite(CompoundTag tag) {
schema(tag, "SITE", SITE_KEYS);
var entry = compound(tag, "Entry", ENTRY_KEYS);
var saved = list(tag, "Supports", 9); var supports = new ArrayList<HeritagePlan19.Support>();
for (var value : saved) {
var support = element(value, SUPPORT_KEYS);
supports.add(new HeritagePlan19.Support(coordinate(support, "X"), integer(support, "MinY"),
coordinate(support, "Z"), integer(support, "MaxY")));
}
return new HeritagePlan19.Site(id(tag, "Id"), HeritagePlan19.Kind.valueOf(string(tag, "Kind")),
coordinate(tag, "MinX"), integer(tag, "FloorY"), coordinate(tag, "MinZ"), integer(tag, "Width"),
integer(tag, "Depth"), integer(tag, "Height"), integer(tag, "Facing"), seed(tag), integer(tag, "FoundationDepth"),
supports, new HeritagePlan19.Entry(coordinate(entry, "X"), integer(entry, "Y"), coordinate(entry, "Z"), integer(entry, "Facing")));
}
public static HeritagePlan19.Segment decodeSegment(CompoundTag tag) {
schema(tag, "ROUTE", ROUTE_KEYS);
var saved = list(tag, "Cells", 256); var cells = new ArrayList<HeritagePlan19.Cell>();
for (var value : saved) {
var cell = element(value, CELL_KEYS);
type(cell, "RailPresent", Tag.TAG_BYTE);
byte present = cell.getByte("RailPresent").orElseThrow();
if (present != 0 && present != 1) throw new IllegalArgumentException("Invalid saved rail presence");
cells.add(new HeritagePlan19.Cell(coordinate(cell, "X"), integer(cell, "FloorY"), coordinate(cell, "Z"),
integer(cell, "FoundationDepth"), HeritagePlan19.Rail.valueOf(string(cell, "Rail")), present == 1));
}
String from = id(tag, "FromId"), to = id(tag, "ToId");
var kind = HeritagePlan19.RouteKind.valueOf(string(tag, "Kind"));
if (from.equals(to) && kind != HeritagePlan19.RouteKind.PATH)
throw new IllegalArgumentException("Only a local pedestrian landing may join a destination to itself");
return new HeritagePlan19.Segment(id(tag, "Id"), from, to, kind,
integer(tag, "StartChunkX"), integer(tag, "StartChunkZ"), cells, seed(tag));
}
private static CompoundTag header(String type, String id, String kind, long seed) {
var value = new CompoundTag(); value.putInt("Schema", 19); value.putString("Type", type);
value.putString("Id", id); value.putString("Kind", kind); value.putLong("Seed", seed); return value;
}
private static void schema(CompoundTag tag, String expected, Set<String> keys) {
if (!tag.keySet().equals(keys) || integer(tag, "Schema") != 19 || !string(tag, "Type").equals(expected))
throw new IllegalArgumentException("Unsupported or incomplete saved heritage schema");
}
private static CompoundTag compound(CompoundTag tag, String key, Set<String> keys) {
type(tag, key, Tag.TAG_COMPOUND); return element(tag.get(key), keys);
}
private static CompoundTag element(Tag tag, Set<String> keys) {
if (!(tag instanceof CompoundTag value) || !value.keySet().equals(keys))
throw new IllegalArgumentException("Unsupported or incomplete saved heritage element");
return value;
}
private static ListTag list(CompoundTag tag, String key, int maximum) {
type(tag, key, Tag.TAG_LIST); var value = tag.getList(key).orElseThrow();
if (value.size() > maximum) throw new IllegalArgumentException("Oversized saved heritage list"); return value;
}
private static long seed(CompoundTag tag) { type(tag, "Seed", Tag.TAG_LONG); return tag.getLong("Seed").orElseThrow(); }
private static String id(CompoundTag tag, String key) {
var value = string(tag, key);
if (value.isBlank() || value.length() > 128) throw new IllegalArgumentException("Invalid saved heritage identifier"); return value;
}
private static String string(CompoundTag tag, String key) { type(tag, key, Tag.TAG_STRING); return tag.getString(key).orElseThrow(); }
private static int coordinate(CompoundTag tag, String key) {
int value = integer(tag, key);
if (value < -30_000_000 || value > 30_000_000) throw new IllegalArgumentException("Heritage coordinate outside world bounds"); return value;
}
private static int integer(CompoundTag tag, String key) { type(tag, key, Tag.TAG_INT); return tag.getInt(key).orElseThrow(); }
private static void type(CompoundTag tag, String key, byte id) {
if (tag.get(key) == null || tag.get(key).getId() != id) throw new IllegalArgumentException("Invalid saved heritage field: " + key);
}
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
final int x0 = isSite() ? site.minX() : segment.minX();
final int y0 = isSite() ? site.floorY() : segment.minY();
final int z0 = isSite() ? site.minZ() : segment.minZ();
var writer = new LostCityRenderer.BlockWriter() {
private BlockPos world(int x, int y, int z) { return new BlockPos(Math.addExact(x0, x), Math.addExact(y0, y), Math.addExact(z0, z)); }
@Override public void set(int x, int y, int z, BlockState block) {
var position = world(x, y, z);
if (getBoundingBox().isInside(position) && clip.isInside(position))
placeBlock(level, block, position.getX(), position.getY(), position.getZ(), clip);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
var position = world(x, y, z);
if (getBoundingBox().isInside(position) && clip.isInside(position))
createChest(level, clip, RandomSource.create(seed), position, loot, null);
}
};
HeritageStructures19.placing(() -> {
if (isSite()) HeritageRenderer19.render(site, writer);
else HeritageRenderer19.renderSegment(segment, writer);
});
}
public static BoundingBox bounds(HeritagePlan19.Site site) {
return new BoundingBox(site.minX(), site.boundsMinY(), site.minZ(), site.maxX(), site.boundsMaxY(), site.maxZ());
}
public static BoundingBox bounds(HeritagePlan19.Segment segment) {
return new BoundingBox(segment.minX(), segment.boundsMinY(), segment.minZ(), segment.maxX(), segment.boundsMaxY(), segment.maxZ());
}
}
@@ -0,0 +1,185 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import java.util.ArrayList;
import java.util.Set;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.ResourceKey;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.loot.LootTable;
/** Self-contained schema20 geometry. Reload never needs the planner or a distant route destination. */
public final class HeritagePiece20 extends StructurePiece {
private static final Set<String> SITE_KEYS = Set.of("Schema", "Type", "Id", "Kind", "MinX", "FloorY", "MinZ",
"Width", "Depth", "Height", "Facing", "Seed", "FoundationDepth", "Supports", "Entry");
private static final Set<String> ROUTE_KEYS = Set.of("Schema", "Type", "Id", "FromId", "ToId", "Kind",
"StartChunkX", "StartChunkZ", "Cells", "Seed");
private static final Set<String> SUPPORT_KEYS = Set.of("X", "MinY", "Z", "MaxY");
private static final Set<String> ENTRY_KEYS = Set.of("X", "Y", "Z", "Facing");
private static final Set<String> CELL_KEYS = Set.of("X", "FloorY", "Z", "FoundationDepth", "Rail", "RailPresent");
private final HeritagePlan19.Site site;
private final HeritagePlan19.Segment segment;
public HeritagePiece20(HeritagePlan19.Site site) {
super(HeritageStructures19.PIECE20, 0, bounds(site));
this.site = site; this.segment = null; setOrientation(null);
}
public HeritagePiece20(HeritagePlan19.Segment segment) {
super(HeritageStructures19.PIECE20, 0, bounds(segment));
this.site = null; this.segment = segment; setOrientation(null);
}
public HeritagePiece20(StructurePieceSerializationContext context, CompoundTag tag) {
super(HeritageStructures19.PIECE20, tag);
var payload = tag.getCompound("SanctuaryHeritage").orElseThrow(() -> new IllegalArgumentException("Missing saved heritage geometry"));
String type = string(payload, "Type");
if (type.equals("SITE")) { site = decodeSite(payload); segment = null; }
else if (type.equals("ROUTE")) { site = null; segment = decodeSegment(payload); }
else throw new IllegalArgumentException("Unknown heritage piece type");
if (!getBoundingBox().equals(isSite() ? bounds(site) : bounds(segment)))
throw new IllegalArgumentException("Heritage geometry disagrees with its saved native bounds");
setOrientation(null);
}
public boolean isSite() { return site != null; }
public HeritagePlan19.Site site() { if (site == null) throw new IllegalStateException("This heritage piece is a route"); return site; }
public HeritagePlan19.Segment segment() { if (segment == null) throw new IllegalStateException("This heritage piece is a site"); return segment; }
@Override public BlockPos getLocatorPosition() {
if (isSite()) return locator(site);
var cell = segment.cells().getFirst(); return new BlockPos(cell.x(), cell.floorY() + 1, cell.z());
}
public static BlockPos locator(HeritagePlan19.Site site) { return new BlockPos(site.entry().x(), site.entry().y(), site.entry().z()); }
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
tag.put("SanctuaryHeritage", isSite() ? encodeSite(site) : encodeSegment(segment));
}
public static CompoundTag encodeSite(HeritagePlan19.Site site) {
var tag = header("SITE", site.id(), site.kind().name(), site.seed());
tag.putInt("MinX", site.minX()); tag.putInt("FloorY", site.floorY()); tag.putInt("MinZ", site.minZ());
tag.putInt("Width", site.width()); tag.putInt("Depth", site.depth()); tag.putInt("Height", site.height());
tag.putInt("Facing", site.facing()); tag.putInt("FoundationDepth", site.foundationDepth());
var entry = new CompoundTag(); entry.putInt("X", site.entry().x()); entry.putInt("Y", site.entry().y());
entry.putInt("Z", site.entry().z()); entry.putInt("Facing", site.entry().facing()); tag.put("Entry", entry);
var supports = new ListTag();
for (var support : site.supports()) {
var value = new CompoundTag(); value.putInt("X", support.x()); value.putInt("MinY", support.minY());
value.putInt("Z", support.z()); value.putInt("MaxY", support.maxY()); supports.add(value);
}
tag.put("Supports", supports); return tag;
}
public static CompoundTag encodeSegment(HeritagePlan19.Segment segment) {
var tag = header("ROUTE", segment.id(), segment.kind().name(), segment.seed());
tag.putString("FromId", segment.fromId()); tag.putString("ToId", segment.toId());
tag.putInt("StartChunkX", segment.startChunkX()); tag.putInt("StartChunkZ", segment.startChunkZ());
var cells = new ListTag();
for (var cell : segment.cells()) {
var value = new CompoundTag(); value.putInt("X", cell.x()); value.putInt("FloorY", cell.floorY());
value.putInt("Z", cell.z()); value.putInt("FoundationDepth", cell.foundationDepth());
value.putString("Rail", cell.rail().name()); value.putBoolean("RailPresent", cell.railPresent()); cells.add(value);
}
tag.put("Cells", cells); return tag;
}
public static HeritagePlan19.Site decodeSite(CompoundTag tag) {
schema(tag, "SITE", SITE_KEYS);
var entry = compound(tag, "Entry", ENTRY_KEYS);
var saved = list(tag, "Supports", 9); var supports = new ArrayList<HeritagePlan19.Support>();
for (var value : saved) {
var support = element(value, SUPPORT_KEYS);
supports.add(new HeritagePlan19.Support(coordinate(support, "X"), integer(support, "MinY"),
coordinate(support, "Z"), integer(support, "MaxY")));
}
return new HeritagePlan19.Site(id(tag, "Id"), HeritagePlan19.Kind.valueOf(string(tag, "Kind")),
coordinate(tag, "MinX"), integer(tag, "FloorY"), coordinate(tag, "MinZ"), integer(tag, "Width"),
integer(tag, "Depth"), integer(tag, "Height"), integer(tag, "Facing"), seed(tag), integer(tag, "FoundationDepth"),
supports, new HeritagePlan19.Entry(coordinate(entry, "X"), integer(entry, "Y"), coordinate(entry, "Z"), integer(entry, "Facing")));
}
public static HeritagePlan19.Segment decodeSegment(CompoundTag tag) {
schema(tag, "ROUTE", ROUTE_KEYS);
var saved = list(tag, "Cells", 256); var cells = new ArrayList<HeritagePlan19.Cell>();
for (var value : saved) {
var cell = element(value, CELL_KEYS);
type(cell, "RailPresent", Tag.TAG_BYTE);
byte present = cell.getByte("RailPresent").orElseThrow();
if (present != 0 && present != 1) throw new IllegalArgumentException("Invalid saved rail presence");
cells.add(new HeritagePlan19.Cell(coordinate(cell, "X"), integer(cell, "FloorY"), coordinate(cell, "Z"),
integer(cell, "FoundationDepth"), HeritagePlan19.Rail.valueOf(string(cell, "Rail")), present == 1));
}
String from = id(tag, "FromId"), to = id(tag, "ToId");
var kind = HeritagePlan19.RouteKind.valueOf(string(tag, "Kind"));
if (from.equals(to) && kind != HeritagePlan19.RouteKind.PATH)
throw new IllegalArgumentException("Only a local pedestrian landing may join a destination to itself");
return new HeritagePlan19.Segment(id(tag, "Id"), from, to, kind,
integer(tag, "StartChunkX"), integer(tag, "StartChunkZ"), cells, seed(tag));
}
private static CompoundTag header(String type, String id, String kind, long seed) {
var value = new CompoundTag(); value.putInt("Schema", 20); value.putString("Type", type);
value.putString("Id", id); value.putString("Kind", kind); value.putLong("Seed", seed); return value;
}
private static void schema(CompoundTag tag, String expected, Set<String> keys) {
if (!tag.keySet().equals(keys) || integer(tag, "Schema") != 20 || !string(tag, "Type").equals(expected))
throw new IllegalArgumentException("Unsupported or incomplete saved heritage schema");
}
private static CompoundTag compound(CompoundTag tag, String key, Set<String> keys) {
type(tag, key, Tag.TAG_COMPOUND); return element(tag.get(key), keys);
}
private static CompoundTag element(Tag tag, Set<String> keys) {
if (!(tag instanceof CompoundTag value) || !value.keySet().equals(keys))
throw new IllegalArgumentException("Unsupported or incomplete saved heritage element");
return value;
}
private static ListTag list(CompoundTag tag, String key, int maximum) {
type(tag, key, Tag.TAG_LIST); var value = tag.getList(key).orElseThrow();
if (value.size() > maximum) throw new IllegalArgumentException("Oversized saved heritage list"); return value;
}
private static long seed(CompoundTag tag) { type(tag, "Seed", Tag.TAG_LONG); return tag.getLong("Seed").orElseThrow(); }
private static String id(CompoundTag tag, String key) {
var value = string(tag, key);
if (value.isBlank() || value.length() > 128) throw new IllegalArgumentException("Invalid saved heritage identifier"); return value;
}
private static String string(CompoundTag tag, String key) { type(tag, key, Tag.TAG_STRING); return tag.getString(key).orElseThrow(); }
private static int coordinate(CompoundTag tag, String key) {
int value = integer(tag, key);
if (value < -30_000_000 || value > 30_000_000) throw new IllegalArgumentException("Heritage coordinate outside world bounds"); return value;
}
private static int integer(CompoundTag tag, String key) { type(tag, key, Tag.TAG_INT); return tag.getInt(key).orElseThrow(); }
private static void type(CompoundTag tag, String key, byte id) {
if (tag.get(key) == null || tag.get(key).getId() != id) throw new IllegalArgumentException("Invalid saved heritage field: " + key);
}
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
final int x0 = isSite() ? site.minX() : segment.minX();
final int y0 = isSite() ? site.floorY() : segment.minY();
final int z0 = isSite() ? site.minZ() : segment.minZ();
var writer = new LostCityRenderer.BlockWriter() {
private BlockPos world(int x, int y, int z) { return new BlockPos(Math.addExact(x0, x), Math.addExact(y0, y), Math.addExact(z0, z)); }
@Override public void set(int x, int y, int z, BlockState block) {
var position = world(x, y, z);
if (getBoundingBox().isInside(position) && clip.isInside(position))
placeBlock(level, block, position.getX(), position.getY(), position.getZ(), clip);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
var position = world(x, y, z);
if (getBoundingBox().isInside(position) && clip.isInside(position))
createChest(level, clip, RandomSource.create(seed), position, loot, null);
}
};
HeritageStructures19.placing(() -> {
if (isSite()) HeritageRenderer20.render(site, writer);
else HeritageRenderer20.renderSegment(segment, writer);
});
}
public static BoundingBox bounds(HeritagePlan19.Site site) {
return new BoundingBox(site.minX(), site.boundsMinY(), site.minZ(), site.maxX(), site.boundsMaxY(), site.maxZ());
}
public static BoundingBox bounds(HeritagePlan19.Segment segment) {
return new BoundingBox(segment.minX(), segment.boundsMinY(), segment.minZ(), segment.maxX(), segment.boundsMaxY(), segment.maxZ());
}
}
@@ -0,0 +1,185 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import java.util.ArrayList;
import java.util.Set;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.ResourceKey;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.loot.LootTable;
/** Self-contained schema21 geometry. Reload never needs the planner or a distant route destination. */
public final class HeritagePiece21 extends StructurePiece {
private static final Set<String> SITE_KEYS = Set.of("Schema", "Type", "Id", "Kind", "MinX", "FloorY", "MinZ",
"Width", "Depth", "Height", "Facing", "Seed", "FoundationDepth", "Supports", "Entry");
private static final Set<String> ROUTE_KEYS = Set.of("Schema", "Type", "Id", "FromId", "ToId", "Kind",
"StartChunkX", "StartChunkZ", "Cells", "Seed");
private static final Set<String> SUPPORT_KEYS = Set.of("X", "MinY", "Z", "MaxY");
private static final Set<String> ENTRY_KEYS = Set.of("X", "Y", "Z", "Facing");
private static final Set<String> CELL_KEYS = Set.of("X", "FloorY", "Z", "FoundationDepth", "Rail", "RailPresent");
private final HeritagePlan19.Site site;
private final HeritagePlan19.Segment segment;
public HeritagePiece21(HeritagePlan19.Site site) {
super(HeritageStructures19.PIECE21, 0, bounds(site));
this.site = site; this.segment = null; setOrientation(null);
}
public HeritagePiece21(HeritagePlan19.Segment segment) {
super(HeritageStructures19.PIECE21, 0, bounds(segment));
this.site = null; this.segment = segment; setOrientation(null);
}
public HeritagePiece21(StructurePieceSerializationContext context, CompoundTag tag) {
super(HeritageStructures19.PIECE21, tag);
var payload = tag.getCompound("SanctuaryHeritage").orElseThrow(() -> new IllegalArgumentException("Missing saved heritage geometry"));
String type = string(payload, "Type");
if (type.equals("SITE")) { site = decodeSite(payload); segment = null; }
else if (type.equals("ROUTE")) { site = null; segment = decodeSegment(payload); }
else throw new IllegalArgumentException("Unknown heritage piece type");
if (!getBoundingBox().equals(isSite() ? bounds(site) : bounds(segment)))
throw new IllegalArgumentException("Heritage geometry disagrees with its saved native bounds");
setOrientation(null);
}
public boolean isSite() { return site != null; }
public HeritagePlan19.Site site() { if (site == null) throw new IllegalStateException("This heritage piece is a route"); return site; }
public HeritagePlan19.Segment segment() { if (segment == null) throw new IllegalStateException("This heritage piece is a site"); return segment; }
@Override public BlockPos getLocatorPosition() {
if (isSite()) return locator(site);
var cell = segment.cells().getFirst(); return new BlockPos(cell.x(), cell.floorY() + 1, cell.z());
}
public static BlockPos locator(HeritagePlan19.Site site) { return new BlockPos(site.entry().x(), site.entry().y(), site.entry().z()); }
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
tag.put("SanctuaryHeritage", isSite() ? encodeSite(site) : encodeSegment(segment));
}
public static CompoundTag encodeSite(HeritagePlan19.Site site) {
var tag = header("SITE", site.id(), site.kind().name(), site.seed());
tag.putInt("MinX", site.minX()); tag.putInt("FloorY", site.floorY()); tag.putInt("MinZ", site.minZ());
tag.putInt("Width", site.width()); tag.putInt("Depth", site.depth()); tag.putInt("Height", site.height());
tag.putInt("Facing", site.facing()); tag.putInt("FoundationDepth", site.foundationDepth());
var entry = new CompoundTag(); entry.putInt("X", site.entry().x()); entry.putInt("Y", site.entry().y());
entry.putInt("Z", site.entry().z()); entry.putInt("Facing", site.entry().facing()); tag.put("Entry", entry);
var supports = new ListTag();
for (var support : site.supports()) {
var value = new CompoundTag(); value.putInt("X", support.x()); value.putInt("MinY", support.minY());
value.putInt("Z", support.z()); value.putInt("MaxY", support.maxY()); supports.add(value);
}
tag.put("Supports", supports); return tag;
}
public static CompoundTag encodeSegment(HeritagePlan19.Segment segment) {
var tag = header("ROUTE", segment.id(), segment.kind().name(), segment.seed());
tag.putString("FromId", segment.fromId()); tag.putString("ToId", segment.toId());
tag.putInt("StartChunkX", segment.startChunkX()); tag.putInt("StartChunkZ", segment.startChunkZ());
var cells = new ListTag();
for (var cell : segment.cells()) {
var value = new CompoundTag(); value.putInt("X", cell.x()); value.putInt("FloorY", cell.floorY());
value.putInt("Z", cell.z()); value.putInt("FoundationDepth", cell.foundationDepth());
value.putString("Rail", cell.rail().name()); value.putBoolean("RailPresent", cell.railPresent()); cells.add(value);
}
tag.put("Cells", cells); return tag;
}
public static HeritagePlan19.Site decodeSite(CompoundTag tag) {
schema(tag, "SITE", SITE_KEYS);
var entry = compound(tag, "Entry", ENTRY_KEYS);
var saved = list(tag, "Supports", 9); var supports = new ArrayList<HeritagePlan19.Support>();
for (var value : saved) {
var support = element(value, SUPPORT_KEYS);
supports.add(new HeritagePlan19.Support(coordinate(support, "X"), integer(support, "MinY"),
coordinate(support, "Z"), integer(support, "MaxY")));
}
return new HeritagePlan19.Site(id(tag, "Id"), HeritagePlan19.Kind.valueOf(string(tag, "Kind")),
coordinate(tag, "MinX"), integer(tag, "FloorY"), coordinate(tag, "MinZ"), integer(tag, "Width"),
integer(tag, "Depth"), integer(tag, "Height"), integer(tag, "Facing"), seed(tag), integer(tag, "FoundationDepth"),
supports, new HeritagePlan19.Entry(coordinate(entry, "X"), integer(entry, "Y"), coordinate(entry, "Z"), integer(entry, "Facing")));
}
public static HeritagePlan19.Segment decodeSegment(CompoundTag tag) {
schema(tag, "ROUTE", ROUTE_KEYS);
var saved = list(tag, "Cells", 256); var cells = new ArrayList<HeritagePlan19.Cell>();
for (var value : saved) {
var cell = element(value, CELL_KEYS);
type(cell, "RailPresent", Tag.TAG_BYTE);
byte present = cell.getByte("RailPresent").orElseThrow();
if (present != 0 && present != 1) throw new IllegalArgumentException("Invalid saved rail presence");
cells.add(new HeritagePlan19.Cell(coordinate(cell, "X"), integer(cell, "FloorY"), coordinate(cell, "Z"),
integer(cell, "FoundationDepth"), HeritagePlan19.Rail.valueOf(string(cell, "Rail")), present == 1));
}
String from = id(tag, "FromId"), to = id(tag, "ToId");
var kind = HeritagePlan19.RouteKind.valueOf(string(tag, "Kind"));
if (from.equals(to) && kind != HeritagePlan19.RouteKind.PATH)
throw new IllegalArgumentException("Only a local pedestrian landing may join a destination to itself");
return new HeritagePlan19.Segment(id(tag, "Id"), from, to, kind,
integer(tag, "StartChunkX"), integer(tag, "StartChunkZ"), cells, seed(tag));
}
private static CompoundTag header(String type, String id, String kind, long seed) {
var value = new CompoundTag(); value.putInt("Schema", 21); value.putString("Type", type);
value.putString("Id", id); value.putString("Kind", kind); value.putLong("Seed", seed); return value;
}
private static void schema(CompoundTag tag, String expected, Set<String> keys) {
if (!tag.keySet().equals(keys) || integer(tag, "Schema") != 21 || !string(tag, "Type").equals(expected))
throw new IllegalArgumentException("Unsupported or incomplete saved heritage schema");
}
private static CompoundTag compound(CompoundTag tag, String key, Set<String> keys) {
type(tag, key, Tag.TAG_COMPOUND); return element(tag.get(key), keys);
}
private static CompoundTag element(Tag tag, Set<String> keys) {
if (!(tag instanceof CompoundTag value) || !value.keySet().equals(keys))
throw new IllegalArgumentException("Unsupported or incomplete saved heritage element");
return value;
}
private static ListTag list(CompoundTag tag, String key, int maximum) {
type(tag, key, Tag.TAG_LIST); var value = tag.getList(key).orElseThrow();
if (value.size() > maximum) throw new IllegalArgumentException("Oversized saved heritage list"); return value;
}
private static long seed(CompoundTag tag) { type(tag, "Seed", Tag.TAG_LONG); return tag.getLong("Seed").orElseThrow(); }
private static String id(CompoundTag tag, String key) {
var value = string(tag, key);
if (value.isBlank() || value.length() > 128) throw new IllegalArgumentException("Invalid saved heritage identifier"); return value;
}
private static String string(CompoundTag tag, String key) { type(tag, key, Tag.TAG_STRING); return tag.getString(key).orElseThrow(); }
private static int coordinate(CompoundTag tag, String key) {
int value = integer(tag, key);
if (value < -30_000_000 || value > 30_000_000) throw new IllegalArgumentException("Heritage coordinate outside world bounds"); return value;
}
private static int integer(CompoundTag tag, String key) { type(tag, key, Tag.TAG_INT); return tag.getInt(key).orElseThrow(); }
private static void type(CompoundTag tag, String key, byte id) {
if (tag.get(key) == null || tag.get(key).getId() != id) throw new IllegalArgumentException("Invalid saved heritage field: " + key);
}
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
final int x0 = isSite() ? site.minX() : segment.minX();
final int y0 = isSite() ? site.floorY() : segment.minY();
final int z0 = isSite() ? site.minZ() : segment.minZ();
var writer = new LostCityRenderer.BlockWriter() {
private BlockPos world(int x, int y, int z) { return new BlockPos(Math.addExact(x0, x), Math.addExact(y0, y), Math.addExact(z0, z)); }
@Override public void set(int x, int y, int z, BlockState block) {
var position = world(x, y, z);
if (getBoundingBox().isInside(position) && clip.isInside(position))
placeBlock(level, block, position.getX(), position.getY(), position.getZ(), clip);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
var position = world(x, y, z);
if (getBoundingBox().isInside(position) && clip.isInside(position))
createChest(level, clip, RandomSource.create(seed), position, loot, null);
}
};
HeritageStructures19.placing(() -> {
if (isSite()) HeritageRenderer21.render(site, writer);
else HeritageRenderer21.renderSegment(segment, writer);
});
}
public static BoundingBox bounds(HeritagePlan19.Site site) {
return new BoundingBox(site.minX(), site.boundsMinY(), site.minZ(), site.maxX(), site.boundsMaxY(), site.maxZ());
}
public static BoundingBox bounds(HeritagePlan19.Segment segment) {
return new BoundingBox(segment.minX(), segment.boundsMinY(), segment.minZ(), segment.maxX(), segment.boundsMaxY(), segment.maxZ());
}
}
@@ -0,0 +1,200 @@
package fr.koka.sanctuary.worldgen.heritage;
import java.util.HashSet;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** Immutable generation19 catalogue and real-destination routes. Pieces save geometry, not terrain queries. */
public final class HeritagePlan19 {
private final List<Site> sites;
private final List<Landmark> landmarks;
private final List<Link> links;
private final List<Segment> segments;
private final int samples, columns;
private final Map<Kind, String> diagnostics;
private final Map<Long, List<Range>> protectedColumns;
private final Map<Long, List<Box>> protectedChunks;
public static final int MAX_COLUMNS = 16_000, MAX_SAMPLES = 800_000;
public static final HeritagePlan19 EMPTY = new HeritagePlan19(List.of(), List.of(), List.of(), List.of(), 0, 0, Map.of());
public enum Kind { FREIGHT_STATION, POST_OFFICE, COMMUNAL_DEPOT, EXPEDITION_OFFICE, OBSERVATORY,
RAIL_WORKSHOP, PUMP_STATION, SUBSTATION, GREENHOUSE, VOID_DOCK, RADIO_RELAY, PHOTO_CLUB,
IMPOSSIBLE_SERVICE, CONSTRUCTION_REFUGE }
public enum RouteKind { FREIGHT, PATH }
public enum LandmarkKind { CITY, MINE, UTILITY }
/** NONE is a side column or path; a missing rail retains its intended shape with railPresent=false. */
public enum Rail { NONE, NORTH_SOUTH, EAST_WEST, ASCENDING_EAST, ASCENDING_WEST, ASCENDING_NORTH,
ASCENDING_SOUTH, SOUTH_EAST, SOUTH_WEST, NORTH_WEST, NORTH_EAST }
/** Coordinates of the player's feet, immediately outside the facade. Facing0=N,1=E,2=S,3=W. */
public record Entry(int x, int y, int z, int facing) {
public Entry { if (!coordinate(x) || !coordinate(z) || y < 2 || y > 380 || facing < 0 || facing > 3) throw new IllegalArgumentException("Invalid heritage entry"); }
}
public record Support(int x, int minY, int z, int maxY) {
public Support { if (!coordinate(x) || !coordinate(z) || minY < 2 || maxY < minY || maxY - minY >= 12 || maxY > 370) throw new IllegalArgumentException("Invalid heritage support"); }
}
public record Site(String id, Kind kind, int minX, int floorY, int minZ, int width, int depth, int height,
int facing, long seed, int foundationDepth, List<Support> supports, Entry entry) {
public Site {
supports = List.copyOf(supports);
if (!identifier(id) || !coordinate(minX) || !coordinate(minZ) || kind == null || width != 17 || depth != 17 || height < 7 || height > 24
|| floorY < 32 || floorY + height > 380 || facing < 0 || facing > 3
|| foundationDepth < 3 || foundationDepth > 8 || supports.size() > 9 || entry == null)
throw new IllegalArgumentException("Invalid heritage site");
int ex = facing == 1 ? minX + width : facing == 3 ? minX - 1 : minX + width / 2;
int ez = facing == 2 ? minZ + depth : facing == 0 ? minZ - 1 : minZ + depth / 2;
if (entry.x() != ex || entry.z() != ez || entry.y() != floorY + 1 || entry.facing() != facing)
throw new IllegalArgumentException("Heritage entry does not meet its rotated facade");
var positions = new HashSet<Long>();
for (var support : supports) if (support.x() < minX || support.x() >= minX + width
|| support.z() < minZ || support.z() >= minZ + depth || support.maxY() != floorY - foundationDepth - 1
|| !positions.add(key(support.x(), support.z()))) throw new IllegalArgumentException("Invalid site footing");
}
public int maxX() { return minX + width - 1; }
public int maxZ() { return minZ + depth - 1; }
public int minY() { return floorY - foundationDepth; }
public int maxY() { return floorY + height; }
public int boundsMinY() { return supports.stream().mapToInt(Support::minY).min().orElse(minY()); }
public int boundsMaxY() { return maxY(); }
public int centerX() { return minX + width / 2; }
public int centerZ() { return minZ + depth / 2; }
public int startChunkX() { return centerX() >> 4; }
public int startChunkZ() { return centerZ() >> 4; }
}
/** A pre-existing, read-only destination. Its coordinate is never granted a mask bypass. */
public record Landmark(String id, LandmarkKind kind, Entry entry) {
public Landmark { if (!identifier(id) || kind == null || entry == null) throw new IllegalArgumentException("Invalid heritage landmark"); }
}
/** Floor/block-support Y, not player feet. Endpoints may be read-only landmark coordinates. */
public record Point(int x, int floorY, int z) {
public Point { if (!coordinate(x) || !coordinate(z) || floorY < 2 || floorY > 370) throw new IllegalArgumentException("Invalid route point"); }
}
public record Link(String id, String fromId, String toId, RouteKind kind, List<Point> path) {
public Link {
path = List.copyOf(path);
if (!identifier(id) || !identifier(fromId) || !identifier(toId) || fromId.equals(toId)
|| kind == null || path.size() < 2 || path.size() > 1024) throw new IllegalArgumentException("Invalid heritage link");
for (int i = 1; i < path.size(); i++) {
var a = path.get(i - 1); var b = path.get(i);
if (Math.abs(a.x() - b.x()) + Math.abs(a.z() - b.z()) != 1 || Math.abs(a.floorY() - b.floorY()) > 1)
throw new IllegalArgumentException("Heritage route has a discontinuous centerline");
}
}
}
/** Each written ground column belongs to exactly one Segment. Clear local y1..3 before rail placement. */
public record Cell(int x, int floorY, int z, int foundationDepth, Rail rail, boolean railPresent) {
public Cell {
if (!coordinate(x) || !coordinate(z) || floorY < 14 || floorY > 350 || foundationDepth < 1 || foundationDepth > 12 || rail == null
|| rail == Rail.NONE && railPresent) throw new IllegalArgumentException("Invalid heritage route cell");
}
public int minY() { return floorY - foundationDepth; }
public int maxY() { return floorY + 3; }
}
/** Chunk-local route piece: stable ownership prevents neighboring links painting a shared column twice. */
public record Segment(String id, String fromId, String toId, RouteKind kind, int startChunkX, int startChunkZ,
List<Cell> cells, long seed) {
public Segment {
cells = List.copyOf(cells);
if (!identifier(id) || !identifier(fromId) || !identifier(toId) || fromId.equals(toId) && kind != RouteKind.PATH || kind == null || cells.isEmpty() || cells.size() > 256)
throw new IllegalArgumentException("Invalid heritage segment");
var positions = new HashSet<Long>();
for (var cell : cells) if ((cell.x() >> 4) != startChunkX || (cell.z() >> 4) != startChunkZ
|| !positions.add(key(cell.x(), cell.z()))) throw new IllegalArgumentException("Route cells escaped their owning chunk");
}
public int minX() { return cells.stream().mapToInt(Cell::x).min().orElseThrow(); }
public int maxX() { return cells.stream().mapToInt(Cell::x).max().orElseThrow(); }
public int minZ() { return cells.stream().mapToInt(Cell::z).min().orElseThrow(); }
public int maxZ() { return cells.stream().mapToInt(Cell::z).max().orElseThrow(); }
public int minY() { return cells.stream().mapToInt(Cell::minY).min().orElseThrow(); }
public int maxY() { return cells.stream().mapToInt(Cell::maxY).max().orElseThrow(); }
public int boundsMinY() { return minY(); }
public int boundsMaxY() { return maxY(); }
}
public HeritagePlan19(List<Site> sites, List<Landmark> landmarks, List<Link> links, List<Segment> segments,
int samples, int columns, Map<Kind, String> diagnostics) {
sites = List.copyOf(sites); landmarks = List.copyOf(landmarks); links = List.copyOf(links); segments = List.copyOf(segments);
diagnostics = Map.copyOf(diagnostics);
if (sites.size() > 14 || landmarks.size() > 16 || links.size() > 24 || segments.size() > 512
|| samples < 0 || samples > MAX_SAMPLES || columns < 0 || columns > MAX_COLUMNS)
throw new IllegalArgumentException("Invalid heritage plan budget");
var destinations = new HashSet<String>();
for (var site : sites) if (!destinations.add(site.id())) throw new IllegalArgumentException("Duplicate heritage destination");
for (var landmark : landmarks) if (!destinations.add(landmark.id())) throw new IllegalArgumentException("Duplicate heritage destination");
for (var link : links) if (!destinations.contains(link.fromId()) || !destinations.contains(link.toId()))
throw new IllegalArgumentException("A heritage route promises a nonexistent destination");
var occupied = new HashSet<Long>();
for (var segment : segments) {
if (!destinations.contains(segment.fromId()) || !destinations.contains(segment.toId()))
throw new IllegalArgumentException("A heritage segment promises a nonexistent destination");
for (var cell : segment.cells()) if (!occupied.add(key(cell.x(), cell.z())))
throw new IllegalArgumentException("Heritage segments write the same column");
}
this.sites = sites; this.landmarks = landmarks; this.links = links; this.segments = segments;
this.samples = samples; this.columns = columns; this.diagnostics = diagnostics;
var columnIndex = new HashMap<Long, List<Range>>(); var chunkIndex = new HashMap<Long, List<Box>>();
for (var site : sites) {
index(columnIndex, chunkIndex, new Box(site.minX(), site.minY() - 2, site.minZ(), site.maxX(), site.maxY(), site.maxZ()));
for (var support : site.supports()) index(columnIndex, chunkIndex,
new Box(support.x(), support.minY() - 2, support.z(), support.x(), support.maxY(), support.z()));
}
for (var segment : segments) for (var cell : segment.cells()) index(columnIndex, chunkIndex,
new Box(cell.x(), cell.minY() - 1, cell.z(), cell.x(), cell.maxY(), cell.z()));
var usedDestinations = new HashSet<String>();
for (var link : links) { usedDestinations.add(link.fromId()); usedDestinations.add(link.toId()); }
for (var landmark : landmarks) if (usedDestinations.contains(landmark.id())) {
Entry e = landmark.entry();
for (int side = -1; side <= 1; side++) {
int x = e.x() + ((e.facing() & 1) == 0 ? side : 0);
int z = e.z() + ((e.facing() & 1) == 1 ? side : 0);
// Read-only dependencies: these cells never enter contains() or any native writer.
index(columnIndex, chunkIndex, new Box(x, e.y() - 2, z, x, e.y() + 2, z));
}
}
columnIndex.replaceAll((key, values) -> List.copyOf(values)); chunkIndex.replaceAll((key, values) -> List.copyOf(values));
protectedColumns = Map.copyOf(columnIndex); protectedChunks = Map.copyOf(chunkIndex);
}
public List<Site> sites() { return sites; }
public List<Landmark> landmarks() { return landmarks; }
public List<Link> links() { return links; }
public List<Segment> segments() { return segments; }
public int samples() { return samples; }
public int columns() { return columns; }
public Map<Kind, String> diagnostics() { return diagnostics; }
public boolean isEmpty() { return sites.isEmpty(); }
public boolean contains(int x, int y, int z) {
for (var site : sites) {
if (x < site.minX() || x > site.maxX() || z < site.minZ() || z > site.maxZ()) continue;
if (y >= site.minY() && y <= site.maxY()) return true;
for (var support : site.supports()) if (x == support.x() && z == support.z() && y >= support.minY() && y <= support.maxY()) return true;
}
for (var segment : segments) if ((x >> 4) == segment.startChunkX() && (z >> 4) == segment.startChunkZ())
for (var cell : segment.cells()) if (cell.x() == x && cell.z() == z && y >= cell.minY() && y <= cell.maxY()) return true;
return false;
}
/** Protect the original rock below slabs as well as every written volume. */
public boolean protects(int x, int y, int z) {
for (var range : protectedColumns.getOrDefault(key(x, z), List.of())) if (y >= range.minY() && y <= range.maxY()) return true;
return false;
}
public boolean intersectsProtection(int x0, int y0, int z0, int x1, int y1, int z1) {
if (x0 > x1 || y0 > y1 || z0 > z1) throw new IllegalArgumentException("Reversed heritage query");
for (int cx = x0 >> 4; cx <= x1 >> 4; cx++) for (int cz = z0 >> 4; cz <= z1 >> 4; cz++)
for (var box : protectedChunks.getOrDefault(key(cx, cz), List.of()))
if (x0 <= box.maxX() && x1 >= box.minX() && z0 <= box.maxZ() && z1 >= box.minZ()
&& y0 <= box.maxY() && y1 >= box.minY()) return true;
return false;
}
private record Range(int minY, int maxY) {}
private record Box(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {}
private static void index(Map<Long, List<Range>> columns, Map<Long, List<Box>> chunks, Box box) {
for (int x = box.minX(); x <= box.maxX(); x++) for (int z = box.minZ(); z <= box.maxZ(); z++)
columns.computeIfAbsent(key(x, z), ignored -> new ArrayList<>()).add(new Range(box.minY(), box.maxY()));
for (int x = box.minX() >> 4; x <= box.maxX() >> 4; x++) for (int z = box.minZ() >> 4; z <= box.maxZ() >> 4; z++)
chunks.computeIfAbsent(key(x, z), ignored -> new ArrayList<>()).add(box);
}
private static boolean coordinate(int value) { return Math.abs((long) value) <= 29_999_000; }
private static boolean identifier(String value) { return value != null && !value.isBlank() && value.length() <= 128; }
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
}
@@ -0,0 +1,545 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.*;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/** Generation19 only. A finite catalogue is admitted before complete, repairable routes are published.
* Everything samples native columns; no chunk, player object or future continent is requested. */
public final class HeritagePlanner19 {
public static final int MAX_COLUMNS = HeritagePlan19.MAX_COLUMNS, MAX_SAMPLES = HeritagePlan19.MAX_SAMPLES;
public static final int MAX_SITE_CUT = 6, MAX_ROUTE_CUT = 3;
private static final int SITE_COLUMNS = 11_000, SITE_SAMPLES = 540_000;
private static final int INVALID = Integer.MIN_VALUE;
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
private HeritagePlanner19() {}
public interface Terrain {
boolean owns(int x, int z);
int surfaceY(int x, int z);
boolean solid(int x, int y, int z);
boolean protectedAt(int x, int y, int z);
default List<Landmark> landmarks() { return List.of(); }
}
public static HeritagePlan19 plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
if (!List.of(512, 724, 1024).contains(diameter)) throw new IllegalArgumentException("Heritage19 requires a starter size");
var probe = new Probe(terrain, cx, cz, diameter);
List<Landmark> landmarks = List.copyOf(terrain.landmarks()).stream().limit(16).toList();
var sites = new ArrayList<Site>(); var cells = new LinkedHashMap<Long, OwnedCell>(); var diagnostics = new EnumMap<Kind, String>(Kind.class);
for (var kind : Kind.values()) {
int remaining = Kind.values().length - kind.ordinal() - 1;
probe.columnLimit = SITE_COLUMNS - remaining * (SITE_COLUMNS / 14);
probe.sampleLimit = SITE_SAMPLES - remaining * (SITE_SAMPLES / 14);
SiteDraft admitted = null; int attempts = 0;
var reasons = new java.util.TreeMap<String, Integer>(); probe.reasons = reasons;
try {
for (var candidate : candidates(seed, kind, cx, cz, diameter, sites, landmarks, probe)) {
attempts++;
admitted = site(seed, kind, candidate, sites, landmarks, cells, probe);
if (admitted != null) break;
}
} catch (BudgetExceeded exhausted) { reasons.merge(exhausted.columns ? "column-budget" : "probe-budget", 1, Integer::sum); }
if (admitted != null) {
sites.add(admitted.site());
Request landing = new Request(admitted.site().id() + "_landing", admitted.site().id(), admitted.site().id(), RouteKind.PATH);
for (var cell : admitted.landings()) cells.put(key(cell.x(), cell.z()), new OwnedCell(landing, cell));
}
diagnostics.put(kind, (admitted == null ? "not admitted" : "admitted") + "; candidates=" + attempts + "; " + reasons);
}
probe.columnLimit = MAX_COLUMNS; probe.sampleLimit = MAX_SAMPLES;
var links = new ArrayList<Link>();
var routeResults = new LinkedHashMap<String, String>();
var routeReasons = new java.util.TreeMap<String, Integer>(); probe.reasons = routeReasons;
Map<String, Endpoint> endpoints = new LinkedHashMap<>();
for (var site : sites) endpoints.put(site.id(), new Endpoint(site.id(), site.entry(), false));
for (var landmark : landmarks) if (!endpoints.containsKey(landmark.id()))
endpoints.put(landmark.id(), new Endpoint(landmark.id(), landmark.entry(), true));
List<Request> requests = requests(sites, landmarks).stream()
.filter(r -> endpoints.containsKey(r.from()) && endpoints.containsKey(r.to())).toList();
requests = new ArrayList<>(requests);
requests.sort(Comparator.comparingInt((Request r) -> r.kind() == RouteKind.FREIGHT ? 0
: span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry()) >= diameter / 4 ? 1 : 2)
.thenComparingInt(r -> span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry())));
int adaptiveLong = 0;
for (int requestIndex = 0; requestIndex < requests.size(); requestIndex++) {
var request = requests.get(requestIndex);
int remaining = requests.size() - requestIndex - 1;
// Failed alignments must not starve every later destination. Unused allowances carry forward.
boolean longAxis = span(endpoints.get(request.from()).entry(), endpoints.get(request.to()).entry()) >= diameter / 4;
int allowance = longAxis && adaptiveLong++ < 2 ? 4_500 : 1_400;
probe.columnLimit = Math.min(MAX_COLUMNS - remaining * 120, probe.columns.size() + allowance);
probe.sampleLimit = Math.min(MAX_SAMPLES - remaining * 4_000, probe.samples + (longAxis ? 150_000 : 75_000));
Endpoint from = endpoints.get(request.from()), to = endpoints.get(request.to());
if (from == null || to == null) continue;
int beforeRouteColumns = probe.columns.size();
try {
RouteDraft route = route(seed, request, from, to, sites, landmarks, cells, probe);
if (route == null) { probe.reject("no-complete-route"); routeResults.put(request.id(), "refused; columns=" + (probe.columns.size() - beforeRouteColumns)); continue; }
links.add(new Link(request.id(), request.from(), request.to(), request.kind(), route.path()));
routeResults.put(request.id(), "admitted; span=" + span(from.entry(), to.entry()) + "; length=" + route.path().size() + "; columns=" + (probe.columns.size() - beforeRouteColumns));
for (var cell : route.cells().values()) {
long key = key(cell.x(), cell.z());
OwnedCell old = cells.get(key);
if (old == null) cells.put(key, new OwnedCell(request, cell));
else if (cell.rail() != Rail.NONE && old.cell().rail() == Rail.NONE)
cells.put(key, new OwnedCell(old.request(), cell));
else if (cell.rail() != Rail.NONE && old.cell().rail() != Rail.NONE && cell.rail() != old.cell().rail()) {
// A missing switch is intentional; never emit a rail with contradictory connections.
var c = old.cell(); cells.put(key, new OwnedCell(old.request(),
new Cell(c.x(), c.floorY(), c.z(), c.foundationDepth(), c.rail(), false)));
}
}
} catch (BudgetExceeded exhausted) { probe.reject(exhausted.columns ? "route-column-budget" : "route-probe-budget"); routeResults.put(request.id(), "budget; columns=" + (probe.columns.size() - beforeRouteColumns)); }
}
var segments = segments(seed, cells);
if (Boolean.getBoolean("sanctuary.test.heritage19") || Boolean.getBoolean("sanctuary.heritage.plannerDiagnostics"))
System.getLogger(HeritagePlanner19.class.getName()).log(System.Logger.Level.INFO,
"Heritage19 seed=" + seed + " diameter=" + diameter + " sites=" + sites.stream().map(Site::kind).toList()
+ " links=" + links.size() + " segments=" + segments.size() + " columns=" + probe.columns.size()
+ " samples=" + probe.samples + " catalogue=" + diagnostics + " routes=" + routeReasons + " routeResults=" + routeResults);
return new HeritagePlan19(sites, landmarks, links, segments, probe.samples, probe.columns.size(), diagnostics);
}
private static List<Candidate> candidates(long seed, Kind kind, int cx, int cz, int diameter,
List<Site> sites, List<Landmark> landmarks, Probe probe) {
double angle = unit(mix(seed ^ 0x19ACEL)) * Math.PI * 2 + kind.ordinal() * 2.399963229728653;
double radius = diameter * (kind == Kind.VOID_DOCK ? .40 : kind == Kind.OBSERVATORY || kind == Kind.RADIO_RELAY ? .31 : .22);
int x = cx + (int) Math.round(Math.cos(angle) * radius), z = cz + (int) Math.round(Math.sin(angle) * radius);
Kind parent = switch (kind) {
case POST_OFFICE, COMMUNAL_DEPOT, RAIL_WORKSHOP -> Kind.FREIGHT_STATION;
case EXPEDITION_OFFICE, PHOTO_CLUB -> Kind.POST_OFFICE;
case SUBSTATION, CONSTRUCTION_REFUGE -> Kind.RAIL_WORKSHOP;
case GREENHOUSE, IMPOSSIBLE_SERVICE -> Kind.PUMP_STATION;
default -> null;
};
Site neighbor = parent == null ? null : find(sites, parent);
if (neighbor != null) {
double local = unit(mix(seed ^ kind.ordinal() * 179L)) * Math.PI * 2;
int distance = kind == Kind.RAIL_WORKSHOP ? 65 : 40;
x = neighbor.centerX() + (int) Math.round(Math.cos(local) * distance);
z = neighbor.centerZ() + (int) Math.round(Math.sin(local) * distance);
}
var candidates = new ArrayList<Candidate>();
for (int i = 0; i < 16; i++) {
double turn = angle + i * Math.PI / 3;
int offset = i == 0 ? 0 : i < 5 ? 24 : i < 8 ? 48 : i < 12 ? 72 : 96;
int px = x + (int) Math.round(Math.cos(turn) * offset), pz = z + (int) Math.round(Math.sin(turn) * offset);
if (Math.hypot((double) px - cx, (double) pz - cz) > diameter / 2.0 - 18) continue;
if (overlapsSites(px - 15, pz - 15, px + 15, pz + 15, sites)) continue;
int low = 384, high = -1; boolean dry = true;
for (int dz : new int[] {-8, 0, 8}) for (int dx : new int[] {-8, 0, 8}) {
int top = probe.surface(px + dx, pz + dz);
if (top < 48 || top > 330 || !probe.solid(px + dx, top, pz + dz)
|| probe.protectedAt(px + dx, top, pz + dz)) { dry = false; break; }
low = Math.min(low, top); high = Math.max(high, top);
}
if (!dry || high - low > 13) { probe.reject("coarse-terrain"); continue; }
int facing = direction(px, pz, neighbor == null ? cx : neighbor.centerX(), neighbor == null ? cz : neighbor.centerZ());
if (kind == Kind.VOID_DOCK) facing = (direction(px, pz, cx, cz) + 2) & 3;
int score = (i >= 8 ? 1_000 : 0) + high - low + (kind == Kind.OBSERVATORY || kind == Kind.RADIO_RELAY ? (300 - high) / 4 : 0);
candidates.add(new Candidate(px - 8, pz - 8, facing, score, mix(seed ^ kind.ordinal() * 463L ^ i)));
}
candidates.sort(Comparator.comparingInt(Candidate::score).thenComparingLong(Candidate::seed));
return candidates;
}
private static SiteDraft site(long seed, Kind kind, Candidate candidate, List<Site> previous,
List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
int x = candidate.x(), z = candidate.z(), low = 384, high = -1;
for (var landmark : landmarks) if (landmark.entry().x() >= x - 2 && landmark.entry().x() <= x + 18
&& landmark.entry().z() >= z - 2 && landmark.entry().z() <= z + 18) { probe.reject("existing-entry"); return null; }
var heights = new ArrayList<Integer>();
for (int dz = 0; dz < 17; dz++) for (int dx = 0; dx < 17; dx++) {
int top = probe.surface(x + dx, z + dz);
if (top < 48 || top > 330 || !probe.solid(x + dx, top, z + dz)) { probe.reject("dry-footprint"); return null; }
heights.add(top); low = Math.min(low, top); high = Math.max(high, top);
}
int minimum = Math.max(48, high - MAX_SITE_CUT), maximum = Math.min(330, low + 7);
if (minimum > maximum) { probe.reject("site-slope"); return null; }
heights.sort(Integer::compareTo);
int floor = Math.clamp(heights.get(heights.size() / 2), minimum, maximum);
int foundation = Math.max(3, floor - low + 1), height = height(kind);
int base = floor - foundation;
for (int dz = 0; dz < 17; dz++) for (int dx = 0; dx < 17; dx++)
for (int y = base; y <= floor + height; y++) if (probe.protectedAt(x + dx, y, z + dz)) {
probe.reject("site-protected-volume"); return null;
}
var supports = new ArrayList<Support>();
for (int dz : new int[] {1, 8, 15}) for (int dx : new int[] {1, 8, 15}) {
int bottom = base - 1, rock = bottom;
while (rock >= Math.max(2, base - 13) && (!probe.solid(x + dx, rock, z + dz)
|| !probe.solid(x + dx, rock - 1, z + dz))) rock--;
if (rock < Math.max(2, base - 13)) { probe.reject("site-no-bearing"); return null; }
for (int y = rock - 1; y <= bottom; y++) if (probe.protectedAt(x + dx, y, z + dz)) { probe.reject("support-protected"); return null; }
if (rock < bottom) supports.add(new Support(x + dx, rock + 1, z + dz, bottom));
}
int ex = candidate.facing() == 1 ? x + 17 : candidate.facing() == 3 ? x - 1 : x + 8;
int ez = candidate.facing() == 2 ? z + 17 : candidate.facing() == 0 ? z - 1 : z + 8;
Entry entry = new Entry(ex, floor + 1, ez, candidate.facing());
Site result = new Site(id(kind), kind, x, floor, z, 17, 17, height, candidate.facing(),
mix(seed ^ candidate.seed()), foundation, supports, entry);
var landings = landing(result.entry(), previous, probe);
if (landings == null) { probe.reject("entry-landing"); return null; }
for (var cell : landings) {
OwnedCell old = existing.get(key(cell.x(), cell.z()));
if (old != null && old.cell().floorY() != cell.floorY()) { probe.reject("landing-collision"); return null; }
}
return new SiteDraft(result, landings);
}
private static int height(Kind kind) {
return switch (kind) {
case FREIGHT_STATION, POST_OFFICE, RAIL_WORKSHOP, SUBSTATION, GREENHOUSE, PHOTO_CLUB -> 10;
case EXPEDITION_OFFICE, IMPOSSIBLE_SERVICE -> 12;
case COMMUNAL_DEPOT, VOID_DOCK, CONSTRUCTION_REFUGE -> 14;
case PUMP_STATION -> 18; case OBSERVATORY -> 20; case RADIO_RELAY -> 22;
};
}
private static List<Request> requests(List<Site> sites, List<Landmark> landmarks) {
var result = new ArrayList<Request>();
request(result, Kind.FREIGHT_STATION, Kind.COMMUNAL_DEPOT, RouteKind.FREIGHT);
request(result, Kind.FREIGHT_STATION, Kind.RAIL_WORKSHOP, RouteKind.FREIGHT);
request(result, Kind.COMMUNAL_DEPOT, Kind.PUMP_STATION, RouteKind.FREIGHT);
Site workshop = find(sites, Kind.RAIL_WORKSHOP);
for (var landmark : landmarks) if (landmark.kind() == LandmarkKind.MINE && workshop != null)
result.add(new Request("mine_freight", landmark.id(), workshop.id(), RouteKind.FREIGHT));
request(result, Kind.FREIGHT_STATION, Kind.POST_OFFICE, RouteKind.PATH);
request(result, Kind.POST_OFFICE, Kind.EXPEDITION_OFFICE, RouteKind.PATH);
request(result, Kind.EXPEDITION_OFFICE, Kind.OBSERVATORY, RouteKind.PATH);
request(result, Kind.COMMUNAL_DEPOT, Kind.VOID_DOCK, RouteKind.PATH);
request(result, Kind.RAIL_WORKSHOP, Kind.SUBSTATION, RouteKind.PATH);
request(result, Kind.SUBSTATION, Kind.PUMP_STATION, RouteKind.PATH);
request(result, Kind.PUMP_STATION, Kind.GREENHOUSE, RouteKind.PATH);
request(result, Kind.POST_OFFICE, Kind.PHOTO_CLUB, RouteKind.PATH);
request(result, Kind.PUMP_STATION, Kind.IMPOSSIBLE_SERVICE, RouteKind.PATH);
request(result, Kind.OBSERVATORY, Kind.RADIO_RELAY, RouteKind.PATH);
request(result, Kind.RAIL_WORKSHOP, Kind.CONSTRUCTION_REFUGE, RouteKind.PATH);
Site post = find(sites, Kind.POST_OFFICE);
for (var landmark : landmarks) if (landmark.kind() == LandmarkKind.CITY && post != null) {
result.add(new Request("city_post_" + result.size(), landmark.id(), post.id(), RouteKind.PATH)); break;
}
return result;
}
private static void request(List<Request> requests, Kind from, Kind to, RouteKind kind) {
requests.add(new Request(id(from) + "_" + id(to), id(from), id(to), kind));
}
/** Always persisted, including for an isolated destination: five steps return to natural terrain. */
private static List<Cell> landing(Entry entry, List<Site> sites, Probe probe) {
int facing = entry.facing(), floor = entry.y() - 1;
int endX = entry.x() + DX[facing] * 4, endZ = entry.z() + DZ[facing] * 4;
int target = probe.surface(endX, endZ);
if (target < 14 || target > 350 || Math.abs(target - floor) > 3) return null;
var result = new ArrayList<Cell>();
for (int step = 0; step < 5; step++) {
// Keep the first two rows flat for the facade and the first rail turn.
int y = floor + (int) Math.round((target - floor) * Math.max(0, step - 1) / 3.0);
for (int cross = -1; cross <= 1; cross++) {
int x = entry.x() + DX[facing] * step + DZ[facing] * cross;
int z = entry.z() + DZ[facing] * step - DX[facing] * cross;
if (overlapsSites(x, z, x, z, sites)) return null;
Cell cell = routeCell(x, y, z, Rail.NONE, false, probe);
if (cell == null) return null;
result.add(cell);
if (step == 4) {
int beyond = probe.surface(x + DX[facing], z + DZ[facing]);
if (Math.abs(beyond - y) > 1 || !probe.solid(x + DX[facing], beyond, z + DZ[facing])) return null;
}
}
}
return List.copyOf(result);
}
private static RouteDraft route(long seed, Request request, Endpoint from, Endpoint to, List<Site> sites,
List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
Entry a = from.entry(), b = to.entry();
XZ start = new XZ(a.x(), a.z()), end = new XZ(b.x(), b.z());
XZ outA = start.move(a.facing(), 6), outB = end.move(b.facing(), 6);
int middleX = (outA.x() + outB.x()) / 2, middleZ = (outA.z() + outB.z()) / 2;
var alternatives = List.of(
List.of(outA, new XZ(outB.x(), outA.z()), outB),
List.of(outA, new XZ(outA.x(), outB.z()), outB),
List.of(outA, new XZ(middleX, outA.z()), new XZ(middleX, outB.z()), outB),
List.of(outA, new XZ(outA.x(), middleZ), new XZ(outB.x(), middleZ), outB),
List.of(outA, new XZ(outA.x(), middleZ - 16), new XZ(outB.x(), middleZ - 16), outB),
List.of(outA, new XZ(middleX + 16, outA.z()), new XZ(middleX + 16, outB.z()), outB));
for (var waypoints : alternatives) {
var points = new ArrayList<XZ>(); points.add(start);
boolean okay = true;
for (var waypoint : waypoints) if (!line(points, waypoint)) { okay = false; break; }
if (!okay || !line(points, end) || points.size() > 1024 || new HashSet<>(points).size() != points.size()) continue;
RouteDraft result = profile(seed, request, from, to, points, sites, landmarks, existing, probe);
if (result != null) return result;
}
return adaptive(seed, request, from, to, start, end, outA, outB, sites, landmarks, existing, probe);
}
/** Adaptive fallback. The coarse graph only proposes an alignment; it grants no writing permission. */
private static RouteDraft adaptive(long seed, Request request, Endpoint from, Endpoint to, XZ start, XZ end,
XZ outA, XZ outB, List<Site> sites, List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
int stepSize = span(from.entry(), to.entry()) >= 160 ? 8 : 4;
int certificateReserve = Math.min(2_200, distance(outA, outB) * 3 + 80);
var excluded = new HashSet<XZ>();
for (int attempt = 0; attempt < 3; attempt++) {
var open = new java.util.PriorityQueue<SearchNode>(Comparator.comparingDouble(SearchNode::priority)
.thenComparingDouble(SearchNode::cost).thenComparingInt(n -> n.position().x()).thenComparingInt(n -> n.position().z()));
var best = new HashMap<XZ, Double>(); var parents = new HashMap<XZ, XZ>(); var closed = new HashSet<XZ>();
var terrainHeights = new HashMap<XZ, Integer>();
int begin = coarseRoute(outA, sites, terrainHeights, probe);
if (begin == INVALID) return null;
open.add(new SearchNode(outA, 0, distance(outA, outB))); best.put(outA, 0.0);
List<XZ> found = null;
int searchColumns = Math.max(probe.columns.size() + 150, probe.columnLimit - certificateReserve);
while (!open.isEmpty() && closed.size() < 500 && probe.columns.size() < searchColumns) {
var current = open.remove(); XZ p = current.position();
if (!closed.add(p) || current.cost() > best.getOrDefault(p, Double.POSITIVE_INFINITY)) continue;
if (distance(p, outB) <= stepSize * 2) {
var reversed = new ArrayList<XZ>(); XZ ancestor = p;
while (ancestor != null) { reversed.add(ancestor); ancestor = parents.get(ancestor); }
java.util.Collections.reverse(reversed);
for (int order = 0; order < 2 && found == null; order++) {
var candidate = new ArrayList<XZ>(); candidate.add(start); boolean valid = true;
for (XZ step : reversed) if (!line(candidate, step)) { valid = false; break; }
XZ turn = order == 0 ? new XZ(outB.x(), p.z()) : new XZ(p.x(), outB.z());
if (valid && line(candidate, turn) && line(candidate, outB) && line(candidate, end)
&& new HashSet<>(candidate).size() == candidate.size()) found = candidate;
}
if (found != null) break;
}
int top = terrainHeights.get(p); XZ parent = parents.get(p);
for (int d = 0; d < 4; d++) {
if (parent == null && d == ((from.entry().facing() + 2) & 3)) continue;
XZ next = p.move(d, stepSize);
if (closed.contains(next) || excluded.contains(next)
|| Math.min(distance(next, outA), distance(next, outB)) > distance(outA, outB) + 64) continue;
int height = coarseRoute(next, sites, terrainHeights, probe);
if (height == INVALID || Math.abs(height - top) > stepSize + 3) continue;
double turnCost = parent == null || parent.x() == next.x() || parent.z() == next.z() ? 0 : 1.5;
double cost = current.cost() + stepSize + Math.abs(height - top) * 1.1 + turnCost;
if (cost >= best.getOrDefault(next, Double.POSITIVE_INFINITY)) continue;
best.put(next, cost); parents.put(next, p);
open.add(new SearchNode(next, cost, cost + distance(next, outB) * 1.6));
}
}
if (found == null) { probe.reject("adaptive-no-alignment"); return null; }
RouteDraft certified = profile(seed, request, from, to, found, sites, landmarks, existing, probe);
if (certified != null) { probe.reject("adaptive-admitted"); return certified; }
if (probe.routeBlock == null) return null;
// A failed exact corridor removes only nearby coarse nodes on the next proposal.
XZ bad = probe.routeBlock;
int gx = outA.x() + (int) Math.round((bad.x() - outA.x()) / (double) stepSize) * stepSize;
int gz = outA.z() + (int) Math.round((bad.z() - outA.z()) / (double) stepSize) * stepSize;
excluded.add(new XZ(gx, gz));
for (int d = 0; d < 4; d++) excluded.add(new XZ(gx, gz).move(d, stepSize));
}
return null;
}
private record SearchNode(XZ position, double cost, double priority) {}
private static int distance(XZ a, XZ b) { return Math.abs(a.x() - b.x()) + Math.abs(a.z() - b.z()); }
private static int coarseRoute(XZ p, List<Site> sites, Map<XZ, Integer> cache, Probe probe) {
Integer old = cache.get(p); if (old != null) return old;
if (overlapsSites(p.x() - 2, p.z() - 2, p.x() + 2, p.z() + 2, sites)) { cache.put(p, INVALID); return INVALID; }
int top = probe.surface(p.x(), p.z());
if (top < 14 || top > 350 || !probe.solid(p.x(), top, p.z()) || probe.protectedAt(p.x(), top, p.z())) {
cache.put(p, INVALID); return INVALID;
}
for (int d = 0; d < 4; d++) {
XZ side = p.move(d, 1); int sideTop = probe.surface(side.x(), side.z());
if (sideTop < 14 || sideTop > 350 || Math.abs(sideTop - top) > 4 || probe.protectedAt(side.x(), sideTop, side.z())) {
cache.put(p, INVALID); return INVALID;
}
}
cache.put(p, top); return top;
}
/** Finite orthogonal alignments, each certified at full block resolution before publication. */
private static boolean line(List<XZ> points, XZ destination) {
XZ p = points.getLast();
if (p.x() != destination.x() && p.z() != destination.z()) return false;
int dx = Integer.signum(destination.x() - p.x()), dz = Integer.signum(destination.z() - p.z());
while (!p.equals(destination)) {
p = new XZ(p.x() + dx, p.z() + dz); points.add(p);
if (points.size() > 1024) return false;
}
return true;
}
private static RouteDraft profile(long seed, Request request, Endpoint from, Endpoint to, List<XZ> points,
List<Site> sites, List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
int size = points.size(); probe.routeBlock = null;
int[] lo = new int[size], hi = new int[size], desired = new int[size], group = new int[size];
boolean[] flat = new boolean[size - 1];
for (int i = 1; i < size - 1; i++) if (turn(points, i)) { flat[i - 1] = true; flat[i] = true; }
for (int i = 0; i < size; i++) {
XZ point = points.get(i); probe.routeBlock = point; lo[i] = 14; hi[i] = 350;
desired[i] = probe.surface(point.x(), point.z());
boolean readOnly = i == 0 && from.readOnly() || i == size - 1 && to.readOnly();
for (XZ column : strip(points, i)) {
if (overlapsSites(column.x(), column.z(), column.x(), column.z(), sites)) { probe.reject("route-building"); return null; }
int top = probe.surface(column.x(), column.z());
if (top < 14 || top > 350 || !probe.solid(column.x(), top, column.z())) { probe.reject("route-dry-ground"); return null; }
lo[i] = Math.max(lo[i], top - MAX_ROUTE_CUT); hi[i] = Math.min(hi[i], top + 4);
OwnedCell old = existing.get(key(column.x(), column.z()));
if (old != null) { lo[i] = Math.max(lo[i], old.cell().floorY()); hi[i] = Math.min(hi[i], old.cell().floorY()); }
if (readOnly && top != (i == 0 ? from.entry().y() : to.entry().y()) - 1) return null;
}
if (i == 0 || i == size - 1) {
int exact = (i == 0 ? from.entry().y() : to.entry().y()) - 1;
lo[i] = Math.max(lo[i], exact); hi[i] = Math.min(hi[i], exact);
}
if (lo[i] > hi[i]) { probe.reject("route-grade-interval"); return null; }
}
// Curves and both adjacent rails share one elevation; a Minecraft curved rail cannot slope.
int groups = 1;
for (int i = 1; i < size; i++) { if (!flat[i - 1]) groups++; group[i] = groups - 1; }
int[] gl = new int[groups], gh = new int[groups], gd = new int[groups], y = new int[groups];
java.util.Arrays.fill(gl, 14); java.util.Arrays.fill(gh, 350);
for (int i = 0; i < size; i++) { int g = group[i]; gl[g] = Math.max(gl[g], lo[i]); gh[g] = Math.min(gh[g], hi[i]); gd[g] = desired[i]; }
for (int g = 1; g < groups; g++) { gl[g] = Math.max(gl[g], gl[g - 1] - 1); gh[g] = Math.min(gh[g], gh[g - 1] + 1); }
for (int g = groups - 2; g >= 0; g--) { gl[g] = Math.max(gl[g], gl[g + 1] - 1); gh[g] = Math.min(gh[g], gh[g + 1] + 1); }
for (int g = 0; g < groups; g++) {
if (gl[g] > gh[g]) { probe.reject("route-grade"); return null; }
int lower = g == 0 ? gl[g] : Math.max(gl[g], y[g - 1] - 1), upper = g == 0 ? gh[g] : Math.min(gh[g], y[g - 1] + 1);
y[g] = Math.clamp(gd[g], lower, upper);
}
var path = new ArrayList<Point>(); var floors = new LinkedHashMap<Long, Integer>();
for (int i = 0; i < size; i++) {
XZ point = points.get(i); int floor = y[group[i]]; path.add(new Point(point.x(), floor, point.z()));
if (i == 0 && from.readOnly() || i == size - 1 && to.readOnly()) continue;
for (XZ column : strip(points, i)) {
long key = key(column.x(), column.z()); Integer old = floors.putIfAbsent(key, floor);
if (old != null && old != floor) { probe.reject("route-self-grade"); return null; }
// Historical endpoints and their lateral landing are read-only, even at another nearby turn.
for (var landmark : landmarks) if (landingColumn(landmark.entry(), column)) {
boolean actualEnd = from.readOnly() && landmark.id().equals(from.id())
|| to.readOnly() && landmark.id().equals(to.id());
if (!actualEnd || floor != landmark.entry().y() - 1) { probe.reject("route-historical-entry"); return null; }
floors.remove(key); break;
}
}
}
var cells = new LinkedHashMap<Long, Cell>();
for (var entry : floors.entrySet()) {
int x = (int) (entry.getKey() >> 32), z = (int) (long) entry.getKey();
probe.routeBlock = new XZ(x, z);
Cell cell = routeCell(x, entry.getValue(), z, Rail.NONE, false, probe);
if (cell == null) { probe.reject("route-column-certificate"); return null; }
cells.put(entry.getKey(), cell);
}
if (request.kind() == RouteKind.FREIGHT) for (int i = 0; i < path.size(); i++) {
Point point = path.get(i); long key = key(point.x(), point.z()); Cell cell = cells.get(key);
if (cell == null) continue;
Rail rail = rail(path, i);
// A one-block valley cannot carry two ascending slopes on the same rail. Keep a repairable rail gap.
boolean intact = rail != Rail.NONE && Math.floorMod(mix(seed ^ key ^ request.id().hashCode()), 19) != 0;
if (rail == Rail.NONE) rail = points.get(Math.max(0, i - 1)).x() == points.get(Math.min(size - 1, i + 1)).x() ? Rail.NORTH_SOUTH : Rail.EAST_WEST;
cells.put(key, new Cell(cell.x(), cell.floorY(), cell.z(), cell.foundationDepth(), rail, intact));
}
return new RouteDraft(List.copyOf(path), cells);
}
private static boolean landingColumn(Entry entry, XZ column) {
int dx = column.x() - entry.x(), dz = column.z() - entry.z();
return DX[entry.facing()] * dx + DZ[entry.facing()] * dz == 0 && Math.abs(dx) + Math.abs(dz) <= 1;
}
private static List<XZ> strip(List<XZ> points, int i) {
XZ p = points.get(i); var result = new ArrayList<XZ>();
if (turn(points, i)) {
for (int dx = -1; dx <= 1; dx++) for (int dz = -1; dz <= 1; dz++) result.add(new XZ(p.x() + dx, p.z() + dz));
} else {
XZ other = points.get(i == points.size() - 1 ? i - 1 : i + 1);
int dx = Integer.signum(other.x() - p.x()), dz = Integer.signum(other.z() - p.z());
for (int cross = -1; cross <= 1; cross++) result.add(new XZ(p.x() + dz * cross, p.z() - dx * cross));
}
return result;
}
private static boolean turn(List<XZ> points, int i) {
if (i == 0 || i == points.size() - 1) return false;
XZ a = points.get(i - 1), b = points.get(i + 1);
return a.x() != b.x() && a.z() != b.z();
}
private static Rail rail(List<Point> path, int i) {
Point p = path.get(i), a = path.get(Math.max(0, i - 1)), b = path.get(Math.min(path.size() - 1, i + 1));
if (a.floorY() > p.floorY() && b.floorY() > p.floorY()) return Rail.NONE;
Point high = a.floorY() > p.floorY() ? a : b.floorY() > p.floorY() ? b : null;
if (high != null) return switch (direction(p.x(), p.z(), high.x(), high.z())) {
case 0 -> Rail.ASCENDING_NORTH; case 1 -> Rail.ASCENDING_EAST; case 2 -> Rail.ASCENDING_SOUTH; default -> Rail.ASCENDING_WEST;
};
if (a.x() == b.x()) return Rail.NORTH_SOUTH;
if (a.z() == b.z()) return Rail.EAST_WEST;
boolean n = a.z() < p.z() || b.z() < p.z(), s = a.z() > p.z() || b.z() > p.z();
boolean e = a.x() > p.x() || b.x() > p.x();
return n ? e ? Rail.NORTH_EAST : Rail.NORTH_WEST : s && e ? Rail.SOUTH_EAST : Rail.SOUTH_WEST;
}
private static Cell routeCell(int x, int floor, int z, Rail rail, boolean present, Probe probe) {
int top = probe.surface(x, z);
if (floor < 14 || floor > 350 || top < floor - 4 || top > floor + MAX_ROUTE_CUT) return null;
int foundation = Math.max(1, floor - top + 1), base = floor - foundation;
if (!probe.solid(x, base - 1, z) || !probe.solid(x, base, z)) return null;
for (int y = base - 1; y <= floor + 3; y++) if (probe.protectedAt(x, y, z)) return null;
return new Cell(x, floor, z, foundation, rail, present);
}
private record XZ(int x, int z) { XZ move(int facing, int distance) { return new XZ(x + DX[facing] * distance, z + DZ[facing] * distance); } }
private record SegmentKey(Request request, int cx, int cz) {}
private static List<Segment> segments(long seed, Map<Long, OwnedCell> cells) {
var groups = new LinkedHashMap<SegmentKey, List<Cell>>();
for (var owned : cells.values()) {
Cell c = owned.cell(); var key = new SegmentKey(owned.request(), c.x() >> 4, c.z() >> 4);
groups.computeIfAbsent(key, ignored -> new ArrayList<>()).add(c);
}
var result = new ArrayList<Segment>();
for (var entry : groups.entrySet()) {
var k = entry.getKey(); var r = k.request();
// IDs are stable, bounded and independent of native generation order.
String id = "piece_" + r.id() + "_" + k.cx() + "_" + k.cz();
result.add(new Segment(id, r.from(), r.to(), r.kind(), k.cx(), k.cz(), entry.getValue(), mix(seed ^ key(k.cx(), k.cz()) ^ r.id().hashCode())));
}
return List.copyOf(result);
}
private static int span(Entry a, Entry b) { return Math.max(Math.abs(a.x() - b.x()), Math.abs(a.z() - b.z())); }
private static Site find(List<Site> sites, Kind kind) { return sites.stream().filter(site -> site.kind() == kind).findFirst().orElse(null); }
private static String id(Kind kind) { return kind.name().toLowerCase(Locale.ROOT); }
private static int direction(int x, int z, int tx, int tz) { return Math.abs(tx - x) >= Math.abs(tz - z) ? tx > x ? 1 : 3 : tz > z ? 2 : 0; }
private static boolean overlapsSites(int x0, int z0, int x1, int z1, List<Site> sites) {
for (var site : sites) if (x0 <= site.maxX() && x1 >= site.minX() && z0 <= site.maxZ() && z1 >= site.minZ()) return true;
return false;
}
private record SiteDraft(Site site, List<Cell> landings) {}
private record Candidate(int x, int z, int facing, int score, long seed) {}
private record Endpoint(String id, Entry entry, boolean readOnly) {}
private record Request(String id, String from, String to, RouteKind kind) {}
private record OwnedCell(Request request, Cell cell) {}
private record RouteDraft(List<Point> path, Map<Long, Cell> cells) {}
private static final class BudgetExceeded extends RuntimeException {
final boolean columns; BudgetExceeded(boolean columns) { this.columns = columns; }
}
private static final class Probe {
final Terrain terrain; final int cx, cz; final double radius;
final Map<Long, Integer> columns = new HashMap<>();
XZ routeBlock; Map<String, Integer> reasons = new HashMap<>(); int samples, columnLimit, sampleLimit;
Probe(Terrain terrain, int cx, int cz, int diameter) { this.terrain = java.util.Objects.requireNonNull(terrain); this.cx = cx; this.cz = cz; radius = diameter / 2.0 - 2; }
int surface(int x, int z) {
if (Math.hypot((double) x - cx, (double) z - cz) > radius || !terrain.owns(x, z)) return INVALID;
long key = key(x, z); var cached = columns.get(key); if (cached != null) return cached;
if (columns.size() >= columnLimit) throw new BudgetExceeded(true);
count(); int top = terrain.surfaceY(x, z); columns.put(key, top); return top;
}
boolean solid(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) == INVALID) return false; count(); return terrain.solid(x, y, z); }
boolean protectedAt(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) == INVALID) return true; count(); return terrain.protectedAt(x, y, z); }
void count() { if (samples >= sampleLimit) throw new BudgetExceeded(false); samples++; }
void reject(String reason) { reasons.merge(reason, 1, Integer::sum); }
}
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
private static double unit(long value) { return (value >>> 11) * 0x1.0p-53; }
private static long mix(long value) { value = (value ^ value >>> 30) * 0xbf58476d1ce4e5b9L; value = (value ^ value >>> 27) * 0x94d049bb133111ebL; return value ^ value >>> 31; }
}
@@ -0,0 +1,616 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.*;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/** Generation21 only. A finite catalogue is admitted before complete, repairable routes are published.
* Everything samples native columns; no chunk, player object or future continent is requested. */
public final class HeritagePlanner21 {
public static final int MAX_COLUMNS = HeritagePlan19.MAX_COLUMNS, MAX_SAMPLES = HeritagePlan19.MAX_SAMPLES;
// Local17x17 sites may cut8 blocks, with the same at-most8-block slab and certified short piers.
// Three-wide paths may cut4/fill5; grade stays one block, and water/old construction is never waived.
public static final int MAX_SITE_CUT = 8, MAX_ROUTE_CUT = 4, MAX_ROUTE_FILL = 5;
private static final int SITE_COLUMNS = 11_000, SITE_SAMPLES = 540_000;
private static final int INVALID = Integer.MIN_VALUE;
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
private HeritagePlanner21() {}
public interface Terrain extends HeritagePlanner19.Terrain {
boolean owns(int x, int z);
int surfaceY(int x, int z);
boolean solid(int x, int y, int z);
boolean protectedAt(int x, int y, int z);
default List<Landmark> landmarks() { return List.of(); }
}
public static HeritagePlan19 plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
if (!List.of(512, 724, 1024).contains(diameter)) throw new IllegalArgumentException("Heritage21 requires a starter size");
var probe = new Probe(terrain, cx, cz, diameter);
List<Landmark> landmarks = List.copyOf(terrain.landmarks()).stream().limit(16).toList();
var sites = new ArrayList<Site>(); var cells = new LinkedHashMap<Long, OwnedCell>(); var diagnostics = new EnumMap<Kind, String>(Kind.class);
for (var kind : Kind.values()) {
int remaining = Kind.values().length - kind.ordinal() - 1;
probe.columnLimit = SITE_COLUMNS - remaining * (SITE_COLUMNS / 14);
probe.sampleLimit = SITE_SAMPLES - remaining * (SITE_SAMPLES / 14);
SiteDraft admitted = null; int attempts = 0;
var reasons = new java.util.TreeMap<String, Integer>(); probe.reasons = reasons;
try {
for (var candidate : candidates(seed, kind, cx, cz, diameter, sites, landmarks, probe)) {
attempts++;
admitted = site(seed, kind, candidate, sites, landmarks, cells, probe);
if (admitted != null) break;
}
} catch (BudgetExceeded exhausted) { reasons.merge(exhausted.columns ? "column-budget" : "probe-budget", 1, Integer::sum); }
if (admitted != null) {
sites.add(admitted.site());
Request landing = new Request(admitted.site().id() + "_landing", admitted.site().id(), admitted.site().id(), RouteKind.PATH);
for (var cell : admitted.landings()) cells.put(key(cell.x(), cell.z()), new OwnedCell(landing, cell));
}
diagnostics.put(kind, (admitted == null ? "not admitted" : "admitted") + "; candidates=" + attempts + "; " + reasons);
}
probe.columnLimit = MAX_COLUMNS; probe.sampleLimit = MAX_SAMPLES;
var links = new ArrayList<Link>();
var routeResults = new LinkedHashMap<String, String>();
var routeReasons = new java.util.TreeMap<String, Integer>(); probe.reasons = routeReasons;
Map<String, Endpoint> endpoints = new LinkedHashMap<>();
for (var site : sites) endpoints.put(site.id(), new Endpoint(site.id(), site.entry(), false));
for (var landmark : landmarks) if (!endpoints.containsKey(landmark.id()))
endpoints.put(landmark.id(), new Endpoint(landmark.id(), landmark.entry(), true));
var requests = new ArrayList<>(requests(sites, landmarks).stream()
.filter(r -> endpoints.containsKey(r.from()) && endpoints.containsKey(r.to())).toList());
var components = new Components(endpoints.keySet());
// Multiple exterior entries into the certified connected city represent one already walkable destination.
String firstCity = null;
for (var landmark : landmarks) if (landmark.kind() == LandmarkKind.CITY) {
if (firstCity == null) firstCity = landmark.id(); else components.join(firstCity, landmark.id());
}
Request longFreight = requests.stream().filter(r -> r.kind() == RouteKind.FREIGHT
&& span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry()) >= diameter / 4)
.max(Comparator.comparingInt(r -> span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry()))).orElse(null);
var siteIds = new HashSet<String>(); sites.forEach(site -> siteIds.add(site.id()));
var addressed = new HashSet<String>();
var destinationAttempts = new HashMap<String, Integer>();
int attempted = 0;
while (!requests.isEmpty() && attempted < 36 && links.size() < 24) {
// Give each isolated site a first useful route before spending its columns on a cycle.
// Repeated city gates already belong to one walkable component; no redundant freight bypass is needed.
requests.removeIf(r -> components.same(r.from(), r.to()));
if (requests.isEmpty()) break;
requests.sort(Comparator.comparingInt((Request r) -> isolatedSites(r, siteIds, addressed) > 0
&& isolatedAttempts(r, siteIds, addressed, destinationAttempts) == 0 ? 0 : 1)
.thenComparingInt(r -> span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry()))
.thenComparingInt(r -> r.kind() == RouteKind.FREIGHT ? 0 : 1).thenComparing(Request::id));
var request = attempted == 0 && longFreight != null ? longFreight : requests.getFirst(); requests.remove(request);
attempted++;
int distance = Math.abs(endpoints.get(request.from()).entry().x() - endpoints.get(request.to()).entry().x())
+ Math.abs(endpoints.get(request.from()).entry().z() - endpoints.get(request.to()).entry().z());
boolean longAxis = span(endpoints.get(request.from()).entry(), endpoints.get(request.to()).entry()) >= diameter / 4;
int isolated = isolatedSites(request, siteIds, addressed);
var waiting = new HashSet<String>();
for (var next : requests) for (var id : List.of(next.from(), next.to()))
if (siteIds.contains(id) && !addressed.contains(id) && !id.equals(request.from()) && !id.equals(request.to())) waiting.add(id);
int reserve = Math.min(Math.min(waiting.size(), 8), 36 - attempted) * 100;
int allowance = longAxis && attempted == 1 ? 3_800 : isolated > 0 ? Math.clamp(distance * 5 + 350, 900, 2_400) : 900;
probe.columnLimit = Math.min(MAX_COLUMNS - reserve, probe.columns.size() + allowance);
probe.sampleLimit = Math.min(MAX_SAMPLES - reserve * 20, probe.samples + (longAxis ? 150_000 : 80_000));
for (var id : List.of(request.from(), request.to())) if (siteIds.contains(id) && !addressed.contains(id))
destinationAttempts.merge(id, 1, Integer::sum);
Endpoint from = endpoints.get(request.from()), to = endpoints.get(request.to());
int beforeRouteColumns = probe.columns.size();
var beforeReasons = new HashMap<>(routeReasons);
try {
RouteDraft route = route(seed, request, from, to, sites, landmarks, cells, probe);
if (route == null) { probe.reject("no-complete-route"); routeResults.put(request.id(), "refused; columns=" + (probe.columns.size() - beforeRouteColumns) + rejectionDelta(beforeReasons, routeReasons, probe.routeBlock)); continue; }
var groups = new HashSet<SegmentKey>();
cells.values().forEach(c -> groups.add(new SegmentKey(c.request(), c.cell().x() >> 4, c.cell().z() >> 4)));
route.cells().values().stream().filter(c -> !cells.containsKey(key(c.x(), c.z())))
.forEach(c -> groups.add(new SegmentKey(request, c.x() >> 4, c.z() >> 4)));
if (groups.size() > 512) { routeResults.put(request.id(), "refused; native-piece-budget"); continue; }
links.add(new Link(request.id(), request.from(), request.to(), request.kind(), route.path()));
components.join(request.from(), request.to());
addressed.add(request.from()); addressed.add(request.to());
routeResults.put(request.id(), "admitted; span=" + span(from.entry(), to.entry()) + "; length=" + route.path().size() + "; columns=" + (probe.columns.size() - beforeRouteColumns));
for (var cell : route.cells().values()) {
long key = key(cell.x(), cell.z());
OwnedCell old = cells.get(key);
if (old == null) cells.put(key, new OwnedCell(request, cell));
else if (cell.rail() != Rail.NONE && old.cell().rail() == Rail.NONE)
cells.put(key, new OwnedCell(old.request(), cell));
else if (cell.rail() != Rail.NONE && old.cell().rail() != Rail.NONE && cell.rail() != old.cell().rail()) {
// A missing switch is intentional; never emit a rail with contradictory connections.
var c = old.cell(); cells.put(key, new OwnedCell(old.request(),
new Cell(c.x(), c.floorY(), c.z(), c.foundationDepth(), c.rail(), false)));
}
}
} catch (BudgetExceeded exhausted) { probe.reject(exhausted.columns ? "route-column-budget" : "route-probe-budget"); routeResults.put(request.id(), "budget; columns=" + (probe.columns.size() - beforeRouteColumns) + rejectionDelta(beforeReasons, routeReasons, probe.routeBlock)); }
}
for (var site : sites) {
var incident = links.stream().filter(l -> l.fromId().equals(site.id()) || l.toId().equals(site.id())).map(Link::id).toList();
var attempts = new LinkedHashMap<String, String>();
routeResults.forEach((id, value) -> { if (id.contains(site.id())) attempts.put(id, value); });
diagnostics.put(site.kind(), diagnostics.get(site.kind()) + "; connected=" + !incident.isEmpty()
+ "; accepted-links=" + incident + "; route-attempts=" + attempts);
}
var segments = segments(seed, cells);
if (Boolean.getBoolean("sanctuary.test.heritage21") || Boolean.getBoolean("sanctuary.heritage.plannerDiagnostics"))
System.getLogger(HeritagePlanner21.class.getName()).log(System.Logger.Level.INFO,
"Heritage21 seed=" + seed + " diameter=" + diameter + " sites=" + sites.stream().map(Site::kind).toList()
+ " links=" + links.size() + " segments=" + segments.size() + " columns=" + probe.columns.size()
+ " samples=" + probe.samples + " catalogue=" + diagnostics + " routes=" + routeReasons + " routeResults=" + routeResults);
return new HeritagePlan19(sites, landmarks, links, segments, probe.samples, probe.columns.size(), diagnostics);
}
private static List<Candidate> candidates(long seed, Kind kind, int cx, int cz, int diameter,
List<Site> sites, List<Landmark> landmarks, Probe probe) {
double angle = unit(mix(seed ^ 0x19ACEL)) * Math.PI * 2 + kind.ordinal() * 2.399963229728653;
double radius = diameter * (kind == Kind.VOID_DOCK ? .40 : kind == Kind.OBSERVATORY || kind == Kind.RADIO_RELAY ? .31 : .22);
int x = cx + (int) Math.round(Math.cos(angle) * radius), z = cz + (int) Math.round(Math.sin(angle) * radius);
Kind parent = switch (kind) {
case POST_OFFICE, COMMUNAL_DEPOT, RAIL_WORKSHOP -> Kind.FREIGHT_STATION;
case EXPEDITION_OFFICE, PHOTO_CLUB -> Kind.POST_OFFICE;
case SUBSTATION, CONSTRUCTION_REFUGE -> Kind.RAIL_WORKSHOP;
case GREENHOUSE, IMPOSSIBLE_SERVICE -> Kind.PUMP_STATION;
default -> null;
};
Site neighbor = parent == null ? null : find(sites, parent);
if (neighbor != null) {
double local = unit(mix(seed ^ kind.ordinal() * 179L)) * Math.PI * 2;
int distance = kind == Kind.RAIL_WORKSHOP ? 65 : 40;
x = neighbor.centerX() + (int) Math.round(Math.cos(local) * distance);
z = neighbor.centerZ() + (int) Math.round(Math.sin(local) * distance);
}
var candidates = new ArrayList<Candidate>();
for (int i = 0; i < 16; i++) {
double turn = angle + i * Math.PI / 3;
int offset = i == 0 ? 0 : i < 5 ? 24 : i < 8 ? 48 : i < 12 ? 72 : 96;
int px = x + (int) Math.round(Math.cos(turn) * offset), pz = z + (int) Math.round(Math.sin(turn) * offset);
if (Math.hypot((double) px - cx, (double) pz - cz) > diameter / 2.0 - 18) continue;
if (overlapsSites(px - 15, pz - 15, px + 15, pz + 15, sites)) continue;
int low = 384, high = -1; boolean dry = true;
for (int dz : new int[] {-8, 0, 8}) for (int dx : new int[] {-8, 0, 8}) {
int top = probe.surface(px + dx, pz + dz);
if (top < 48 || top > 330 || !probe.solid(px + dx, top, pz + dz)
|| probe.protectedAt(px + dx, top, pz + dz)) { dry = false; break; }
low = Math.min(low, top); high = Math.max(high, top);
}
if (!dry || high - low > MAX_SITE_CUT + 7) { probe.reject("coarse-terrain"); continue; }
int facing = direction(px, pz, neighbor == null ? cx : neighbor.centerX(), neighbor == null ? cz : neighbor.centerZ());
if (kind == Kind.VOID_DOCK) facing = (direction(px, pz, cx, cz) + 2) & 3;
int score = (i >= 8 ? 1_000 : 0) + high - low + (kind == Kind.OBSERVATORY || kind == Kind.RADIO_RELAY ? (300 - high) / 4 : 0);
candidates.add(new Candidate(px - 8, pz - 8, facing, score, mix(seed ^ kind.ordinal() * 463L ^ i)));
}
candidates.sort(Comparator.comparingInt(Candidate::score).thenComparingLong(Candidate::seed));
return candidates;
}
private static SiteDraft site(long seed, Kind kind, Candidate candidate, List<Site> previous,
List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
int x = candidate.x(), z = candidate.z(), low = 384, high = -1;
for (var landmark : landmarks) if (landmark.entry().x() >= x - 2 && landmark.entry().x() <= x + 18
&& landmark.entry().z() >= z - 2 && landmark.entry().z() <= z + 18) { probe.reject("existing-entry"); return null; }
var heights = new ArrayList<Integer>();
for (int dz = 0; dz < 17; dz++) for (int dx = 0; dx < 17; dx++) {
int top = probe.surface(x + dx, z + dz);
if (top < 48 || top > 330 || !probe.solid(x + dx, top, z + dz)) { probe.reject("dry-footprint"); return null; }
heights.add(top); low = Math.min(low, top); high = Math.max(high, top);
}
int minimum = Math.max(48, high - MAX_SITE_CUT), maximum = Math.min(330, low + 7);
if (minimum > maximum) { probe.reject("site-slope"); return null; }
heights.sort(Integer::compareTo);
int floor = Math.clamp(heights.get(heights.size() / 2), minimum, maximum);
int foundation = Math.max(3, floor - low + 1), height = height(kind);
int base = floor - foundation;
for (int dz = 0; dz < 17; dz++) for (int dx = 0; dx < 17; dx++)
for (int y = base; y <= floor + height; y++) if (probe.protectedAt(x + dx, y, z + dz)) {
probe.reject("site-protected-volume"); return null;
}
var supports = new ArrayList<Support>();
for (int dz : new int[] {1, 8, 15}) for (int dx : new int[] {1, 8, 15}) {
int bottom = base - 1, rock = bottom;
while (rock >= Math.max(2, base - 13) && (!probe.solid(x + dx, rock, z + dz)
|| !probe.solid(x + dx, rock - 1, z + dz))) rock--;
if (rock < Math.max(2, base - 13)) { probe.reject("site-no-bearing"); return null; }
for (int y = rock - 1; y <= bottom; y++) if (probe.protectedAt(x + dx, y, z + dz)) { probe.reject("support-protected"); return null; }
if (rock < bottom) supports.add(new Support(x + dx, rock + 1, z + dz, bottom));
}
// The square envelope and bearing grid have already been certified. A difficult front slope
// should try another real facade before rejecting the entire building; no terrain is weakened.
for (int turn : new int[]{0, 1, 3, 2}) {
int facing = (candidate.facing() + turn) & 3;
int ex = facing == 1 ? x + 17 : facing == 3 ? x - 1 : x + 8;
int ez = facing == 2 ? z + 17 : facing == 0 ? z - 1 : z + 8;
Entry entry = new Entry(ex, floor + 1, ez, facing);
Site result = new Site(id(kind), kind, x, floor, z, 17, 17, height, facing,
mix(seed ^ candidate.seed()), foundation, supports, entry);
var landings = landing(result.entry(), previous, probe);
if (landings == null) { probe.reject("entry-landing"); continue; }
boolean collision = false;
for (var cell : landings) {
OwnedCell old = existing.get(key(cell.x(), cell.z()));
if (old != null && old.cell().floorY() != cell.floorY()) { collision = true; break; }
}
if (collision) { probe.reject("landing-collision"); continue; }
if (turn != 0) probe.reject("admitted-other-facade");
return new SiteDraft(result, landings);
}
return null;
}
private static int height(Kind kind) {
return switch (kind) {
case FREIGHT_STATION, POST_OFFICE, RAIL_WORKSHOP, SUBSTATION, GREENHOUSE, PHOTO_CLUB -> 10;
case EXPEDITION_OFFICE, IMPOSSIBLE_SERVICE -> 12;
case COMMUNAL_DEPOT, VOID_DOCK, CONSTRUCTION_REFUGE -> 14;
case PUMP_STATION -> 18; case OBSERVATORY -> 20; case RADIO_RELAY -> 22;
};
}
private static List<Request> requests(List<Site> sites, List<Landmark> landmarks) {
var result = new LinkedHashMap<String, Request>();
var endpoints = new ArrayList<Endpoint>();
sites.forEach(s -> endpoints.add(new Endpoint(s.id(), s.entry(), false)));
landmarks.forEach(l -> endpoints.add(new Endpoint(l.id(), l.entry(), true)));
var freight = new HashSet<String>();
for (var kind : new Kind[] {Kind.FREIGHT_STATION, Kind.COMMUNAL_DEPOT, Kind.RAIL_WORKSHOP, Kind.PUMP_STATION}) {
var site = find(sites, kind); if (site != null) freight.add(site.id());
}
landmarks.stream().filter(l -> l.kind() == LandmarkKind.MINE).forEach(l -> freight.add(l.id()));
for (var a : endpoints) {
// Four alternatives per entrance tolerate one protected cut without a quadratic terrain search.
var nearest = endpoints.stream().filter(b -> !a.id().equals(b.id()))
.sorted(Comparator.comparingInt((Endpoint b) -> span(a.entry(), b.entry())).thenComparing(Endpoint::id)).limit(4).toList();
for (var b : nearest) addRequest(result, a, b, freight.contains(a.id()) && freight.contains(b.id()) ? RouteKind.FREIGHT : RouteKind.PATH);
}
for (var pair : List.of(new Kind[]{Kind.FREIGHT_STATION, Kind.COMMUNAL_DEPOT},
new Kind[]{Kind.FREIGHT_STATION, Kind.RAIL_WORKSHOP}, new Kind[]{Kind.COMMUNAL_DEPOT, Kind.PUMP_STATION})) {
var a = find(sites, pair[0]); var b = find(sites, pair[1]);
if (a != null && b != null) addRequest(result, new Endpoint(a.id(), a.entry(), false), new Endpoint(b.id(), b.entry(), false), RouteKind.FREIGHT);
}
return List.copyOf(result.values());
}
private static void addRequest(Map<String, Request> result, Endpoint a, Endpoint b, RouteKind kind) {
if (a.id().compareTo(b.id()) > 0) { var swap = a; a = b; b = swap; }
String key = a.id() + "__" + b.id(); var old = result.get(key);
if (old == null || kind == RouteKind.FREIGHT) result.put(key, new Request("graph_" + key, a.id(), b.id(), kind));
}
private static String rejectionDelta(Map<String, Integer> before, Map<String, Integer> after, XZ obstacle) {
var changes = new java.util.TreeMap<String, Integer>();
after.forEach((key, count) -> { int difference = count - before.getOrDefault(key, 0); if (difference > 0) changes.put(key, difference); });
return "; reasons=" + changes + (obstacle == null ? "" : "; last-column=" + obstacle.x() + "," + obstacle.z());
}
private static int isolatedSites(Request request, Set<String> siteIds, Set<String> addressed) {
return (siteIds.contains(request.from()) && !addressed.contains(request.from()) ? 1 : 0)
+ (siteIds.contains(request.to()) && !addressed.contains(request.to()) ? 1 : 0);
}
private static int isolatedAttempts(Request request, Set<String> siteIds, Set<String> addressed, Map<String, Integer> attempts) {
int result = Integer.MAX_VALUE;
for (var id : List.of(request.from(), request.to())) if (siteIds.contains(id) && !addressed.contains(id))
result = Math.min(result, attempts.getOrDefault(id, 0));
return result == Integer.MAX_VALUE ? 0 : result;
}
private static final class Components {
final Map<String, String> parents = new HashMap<>();
Components(Set<String> ids) { for (var id : ids) parents.put(id, id); }
String root(String id) { String parent = parents.get(id); while (!parent.equals(id)) { id = parent; parent = parents.get(id); } return parent; }
boolean same(String a, String b) { return root(a).equals(root(b)); }
void join(String a, String b) { parents.put(root(b), root(a)); }
}
/** Always persisted, including for an isolated destination: five steps return to natural terrain. */
private static List<Cell> landing(Entry entry, List<Site> sites, Probe probe) {
int facing = entry.facing(), floor = entry.y() - 1;
int endX = entry.x() + DX[facing] * 4, endZ = entry.z() + DZ[facing] * 4;
int target = probe.surface(endX, endZ);
if (target < 14 || target > 350 || Math.abs(target - floor) > 3) return null;
var result = new ArrayList<Cell>();
for (int step = 0; step < 5; step++) {
// Keep the first two rows flat for the facade and the first rail turn.
int y = floor + (int) Math.round((target - floor) * Math.max(0, step - 1) / 3.0);
for (int cross = -1; cross <= 1; cross++) {
int x = entry.x() + DX[facing] * step + DZ[facing] * cross;
int z = entry.z() + DZ[facing] * step - DX[facing] * cross;
if (overlapsSites(x, z, x, z, sites)) return null;
Cell cell = routeCell(x, y, z, Rail.NONE, false, probe);
if (cell == null) return null;
result.add(cell);
if (step == 4) {
int beyond = probe.surface(x + DX[facing], z + DZ[facing]);
if (Math.abs(beyond - y) > 1 || !probe.solid(x + DX[facing], beyond, z + DZ[facing])) return null;
}
}
}
return List.copyOf(result);
}
private static RouteDraft route(long seed, Request request, Endpoint from, Endpoint to, List<Site> sites,
List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
Entry a = from.entry(), b = to.entry();
XZ start = new XZ(a.x(), a.z()), end = new XZ(b.x(), b.z());
XZ outA = start.move(a.facing(), 6), outB = end.move(b.facing(), 6);
int middleX = (outA.x() + outB.x()) / 2, middleZ = (outA.z() + outB.z()) / 2;
var alternatives = List.of(
List.of(outA, new XZ(outB.x(), outA.z()), outB),
List.of(outA, new XZ(outA.x(), outB.z()), outB),
List.of(outA, new XZ(middleX, outA.z()), new XZ(middleX, outB.z()), outB),
List.of(outA, new XZ(outA.x(), middleZ), new XZ(outB.x(), middleZ), outB),
List.of(outA, new XZ(outA.x(), middleZ - 16), new XZ(outB.x(), middleZ - 16), outB),
List.of(outA, new XZ(middleX + 16, outA.z()), new XZ(middleX + 16, outB.z()), outB));
for (var waypoints : alternatives) {
var points = new ArrayList<XZ>(); points.add(start);
boolean okay = true;
for (var waypoint : waypoints) if (!line(points, waypoint)) { okay = false; break; }
if (!okay || !line(points, end) || points.size() > 1024 || new HashSet<>(points).size() != points.size()) continue;
RouteDraft result = profile(seed, request, from, to, points, sites, landmarks, existing, probe);
if (result != null) return result;
}
return adaptive(seed, request, from, to, start, end, outA, outB, sites, landmarks, existing, probe);
}
/** Adaptive fallback. The coarse graph only proposes an alignment; it grants no writing permission. */
private static RouteDraft adaptive(long seed, Request request, Endpoint from, Endpoint to, XZ start, XZ end,
XZ outA, XZ outB, List<Site> sites, List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
int stepSize = span(from.entry(), to.entry()) >= 160 ? 8 : 4;
int certificateReserve = Math.min(2_200, distance(outA, outB) * 3 + 80);
var excluded = new HashSet<XZ>();
for (int attempt = 0; attempt < 3; attempt++) {
var open = new java.util.PriorityQueue<SearchNode>(Comparator.comparingDouble(SearchNode::priority)
.thenComparingDouble(SearchNode::cost).thenComparingInt(n -> n.position().x()).thenComparingInt(n -> n.position().z()));
var best = new HashMap<XZ, Double>(); var parents = new HashMap<XZ, XZ>(); var closed = new HashSet<XZ>();
var terrainHeights = new HashMap<XZ, Integer>();
int begin = coarseRoute(outA, sites, terrainHeights, probe);
if (begin == INVALID) return null;
open.add(new SearchNode(outA, 0, distance(outA, outB))); best.put(outA, 0.0);
List<XZ> found = null;
int searchColumns = Math.max(probe.columns.size() + 150, probe.columnLimit - certificateReserve);
while (!open.isEmpty() && closed.size() < 500 && probe.columns.size() < searchColumns) {
var current = open.remove(); XZ p = current.position();
if (!closed.add(p) || current.cost() > best.getOrDefault(p, Double.POSITIVE_INFINITY)) continue;
if (distance(p, outB) <= stepSize * 2) {
var reversed = new ArrayList<XZ>(); XZ ancestor = p;
while (ancestor != null) { reversed.add(ancestor); ancestor = parents.get(ancestor); }
java.util.Collections.reverse(reversed);
for (int order = 0; order < 2 && found == null; order++) {
var candidate = new ArrayList<XZ>(); candidate.add(start); boolean valid = true;
for (XZ step : reversed) if (!line(candidate, step)) { valid = false; break; }
XZ turn = order == 0 ? new XZ(outB.x(), p.z()) : new XZ(p.x(), outB.z());
if (valid && line(candidate, turn) && line(candidate, outB) && line(candidate, end)
&& new HashSet<>(candidate).size() == candidate.size()) found = candidate;
}
if (found != null) break;
}
int top = terrainHeights.get(p); XZ parent = parents.get(p);
for (int d = 0; d < 4; d++) {
if (parent == null && d == ((from.entry().facing() + 2) & 3)) continue;
XZ next = p.move(d, stepSize);
if (closed.contains(next) || excluded.contains(next)
|| Math.min(distance(next, outA), distance(next, outB)) > distance(outA, outB) + 64) continue;
int height = coarseRoute(next, sites, terrainHeights, probe);
if (height == INVALID || Math.abs(height - top) > stepSize + 3) continue;
double turnCost = parent == null || parent.x() == next.x() || parent.z() == next.z() ? 0 : 1.5;
double cost = current.cost() + stepSize + Math.abs(height - top) * 1.1 + turnCost;
if (cost >= best.getOrDefault(next, Double.POSITIVE_INFINITY)) continue;
best.put(next, cost); parents.put(next, p);
open.add(new SearchNode(next, cost, cost + distance(next, outB) * 1.6));
}
}
if (found == null) { probe.reject("adaptive-no-alignment"); return null; }
RouteDraft certified = profile(seed, request, from, to, found, sites, landmarks, existing, probe);
if (certified != null) { probe.reject("adaptive-admitted"); return certified; }
if (probe.routeBlock == null) return null;
// A failed exact corridor removes only nearby coarse nodes on the next proposal.
XZ bad = probe.routeBlock;
int gx = outA.x() + (int) Math.round((bad.x() - outA.x()) / (double) stepSize) * stepSize;
int gz = outA.z() + (int) Math.round((bad.z() - outA.z()) / (double) stepSize) * stepSize;
excluded.add(new XZ(gx, gz));
for (int d = 0; d < 4; d++) excluded.add(new XZ(gx, gz).move(d, stepSize));
}
return null;
}
private record SearchNode(XZ position, double cost, double priority) {}
private static int distance(XZ a, XZ b) { return Math.abs(a.x() - b.x()) + Math.abs(a.z() - b.z()); }
private static int coarseRoute(XZ p, List<Site> sites, Map<XZ, Integer> cache, Probe probe) {
Integer old = cache.get(p); if (old != null) return old;
if (overlapsSites(p.x() - 2, p.z() - 2, p.x() + 2, p.z() + 2, sites)) { cache.put(p, INVALID); return INVALID; }
int top = probe.surface(p.x(), p.z());
if (top < 14 || top > 350 || !probe.solid(p.x(), top, p.z()) || probe.protectedAt(p.x(), top, p.z())) {
cache.put(p, INVALID); return INVALID;
}
for (int d = 0; d < 4; d++) {
XZ side = p.move(d, 1); int sideTop = probe.surface(side.x(), side.z());
if (sideTop < 14 || sideTop > 350 || Math.abs(sideTop - top) > MAX_ROUTE_CUT + 1 || probe.protectedAt(side.x(), sideTop, side.z())) {
cache.put(p, INVALID); return INVALID;
}
}
cache.put(p, top); return top;
}
/** Finite orthogonal alignments, each certified at full block resolution before publication. */
private static boolean line(List<XZ> points, XZ destination) {
XZ p = points.getLast();
if (p.x() != destination.x() && p.z() != destination.z()) return false;
int dx = Integer.signum(destination.x() - p.x()), dz = Integer.signum(destination.z() - p.z());
while (!p.equals(destination)) {
p = new XZ(p.x() + dx, p.z() + dz); points.add(p);
if (points.size() > 1024) return false;
}
return true;
}
private static RouteDraft profile(long seed, Request request, Endpoint from, Endpoint to, List<XZ> points,
List<Site> sites, List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
int size = points.size(); probe.routeBlock = null;
int[] lo = new int[size], hi = new int[size], desired = new int[size], group = new int[size];
boolean[] flat = new boolean[size - 1];
for (int i = 1; i < size - 1; i++) if (turn(points, i)) { flat[i - 1] = true; flat[i] = true; }
for (int i = 0; i < size; i++) {
XZ point = points.get(i); probe.routeBlock = point; lo[i] = 14; hi[i] = 350;
desired[i] = probe.surface(point.x(), point.z());
boolean readOnly = i == 0 && from.readOnly() || i == size - 1 && to.readOnly();
for (XZ column : strip(points, i)) {
if (overlapsSites(column.x(), column.z(), column.x(), column.z(), sites)) { probe.reject("route-building"); return null; }
int top = probe.surface(column.x(), column.z());
if (top < 14 || top > 350 || !probe.solid(column.x(), top, column.z())) { probe.reject("route-dry-ground"); return null; }
lo[i] = Math.max(lo[i], top - MAX_ROUTE_CUT); hi[i] = Math.min(hi[i], top + MAX_ROUTE_FILL);
OwnedCell old = existing.get(key(column.x(), column.z()));
if (old != null) { lo[i] = Math.max(lo[i], old.cell().floorY()); hi[i] = Math.min(hi[i], old.cell().floorY()); }
if (readOnly && top != (i == 0 ? from.entry().y() : to.entry().y()) - 1) return null;
}
if (i == 0 || i == size - 1) {
int exact = (i == 0 ? from.entry().y() : to.entry().y()) - 1;
lo[i] = Math.max(lo[i], exact); hi[i] = Math.min(hi[i], exact);
}
if (lo[i] > hi[i]) { probe.reject("route-grade-interval"); return null; }
}
// Curves and both adjacent rails share one elevation; a Minecraft curved rail cannot slope.
int groups = 1;
for (int i = 1; i < size; i++) { if (!flat[i - 1]) groups++; group[i] = groups - 1; }
int[] gl = new int[groups], gh = new int[groups], gd = new int[groups], y = new int[groups];
java.util.Arrays.fill(gl, 14); java.util.Arrays.fill(gh, 350);
for (int i = 0; i < size; i++) { int g = group[i]; gl[g] = Math.max(gl[g], lo[i]); gh[g] = Math.min(gh[g], hi[i]); gd[g] = desired[i]; }
for (int g = 1; g < groups; g++) { gl[g] = Math.max(gl[g], gl[g - 1] - 1); gh[g] = Math.min(gh[g], gh[g - 1] + 1); }
for (int g = groups - 2; g >= 0; g--) { gl[g] = Math.max(gl[g], gl[g + 1] - 1); gh[g] = Math.min(gh[g], gh[g + 1] + 1); }
for (int g = 0; g < groups; g++) {
if (gl[g] > gh[g]) { probe.reject("route-grade"); return null; }
int lower = g == 0 ? gl[g] : Math.max(gl[g], y[g - 1] - 1), upper = g == 0 ? gh[g] : Math.min(gh[g], y[g - 1] + 1);
y[g] = Math.clamp(gd[g], lower, upper);
}
var path = new ArrayList<Point>(); var floors = new LinkedHashMap<Long, Integer>();
for (int i = 0; i < size; i++) {
XZ point = points.get(i); int floor = y[group[i]]; path.add(new Point(point.x(), floor, point.z()));
if (i == 0 && from.readOnly() || i == size - 1 && to.readOnly()) continue;
for (XZ column : strip(points, i)) {
long key = key(column.x(), column.z()); Integer old = floors.putIfAbsent(key, floor);
if (old != null && old != floor) { probe.reject("route-self-grade"); return null; }
// Historical endpoints and their lateral landing are read-only, even at another nearby turn.
for (var landmark : landmarks) if (landingColumn(landmark.entry(), column)) {
boolean actualEnd = from.readOnly() && landmark.id().equals(from.id())
|| to.readOnly() && landmark.id().equals(to.id());
if (!actualEnd || floor != landmark.entry().y() - 1) { probe.reject("route-historical-entry"); return null; }
floors.remove(key); break;
}
}
}
var cells = new LinkedHashMap<Long, Cell>();
for (var entry : floors.entrySet()) {
int x = (int) (entry.getKey() >> 32), z = (int) (long) entry.getKey();
probe.routeBlock = new XZ(x, z);
Cell cell = routeCell(x, entry.getValue(), z, Rail.NONE, false, probe);
if (cell == null) { probe.reject("route-column-certificate"); return null; }
cells.put(entry.getKey(), cell);
}
if (request.kind() == RouteKind.FREIGHT) for (int i = 0; i < path.size(); i++) {
Point point = path.get(i); long key = key(point.x(), point.z()); Cell cell = cells.get(key);
if (cell == null) continue;
Rail rail = rail(path, i);
// A one-block valley cannot carry two ascending slopes on the same rail. Keep a repairable rail gap.
boolean intact = rail != Rail.NONE && Math.floorMod(mix(seed ^ key ^ request.id().hashCode()), 19) != 0;
if (rail == Rail.NONE) rail = points.get(Math.max(0, i - 1)).x() == points.get(Math.min(size - 1, i + 1)).x() ? Rail.NORTH_SOUTH : Rail.EAST_WEST;
cells.put(key, new Cell(cell.x(), cell.floorY(), cell.z(), cell.foundationDepth(), rail, intact));
}
return new RouteDraft(List.copyOf(path), cells);
}
private static boolean landingColumn(Entry entry, XZ column) {
int dx = column.x() - entry.x(), dz = column.z() - entry.z();
return DX[entry.facing()] * dx + DZ[entry.facing()] * dz == 0 && Math.abs(dx) + Math.abs(dz) <= 1;
}
private static List<XZ> strip(List<XZ> points, int i) {
XZ p = points.get(i); var result = new ArrayList<XZ>();
if (turn(points, i)) {
for (int dx = -1; dx <= 1; dx++) for (int dz = -1; dz <= 1; dz++) result.add(new XZ(p.x() + dx, p.z() + dz));
} else {
XZ other = points.get(i == points.size() - 1 ? i - 1 : i + 1);
int dx = Integer.signum(other.x() - p.x()), dz = Integer.signum(other.z() - p.z());
for (int cross = -1; cross <= 1; cross++) result.add(new XZ(p.x() + dz * cross, p.z() - dx * cross));
}
return result;
}
private static boolean turn(List<XZ> points, int i) {
if (i == 0 || i == points.size() - 1) return false;
XZ a = points.get(i - 1), b = points.get(i + 1);
return a.x() != b.x() && a.z() != b.z();
}
private static Rail rail(List<Point> path, int i) {
Point p = path.get(i), a = path.get(Math.max(0, i - 1)), b = path.get(Math.min(path.size() - 1, i + 1));
if (a.floorY() > p.floorY() && b.floorY() > p.floorY()) return Rail.NONE;
Point high = a.floorY() > p.floorY() ? a : b.floorY() > p.floorY() ? b : null;
if (high != null) return switch (direction(p.x(), p.z(), high.x(), high.z())) {
case 0 -> Rail.ASCENDING_NORTH; case 1 -> Rail.ASCENDING_EAST; case 2 -> Rail.ASCENDING_SOUTH; default -> Rail.ASCENDING_WEST;
};
if (a.x() == b.x()) return Rail.NORTH_SOUTH;
if (a.z() == b.z()) return Rail.EAST_WEST;
boolean n = a.z() < p.z() || b.z() < p.z(), s = a.z() > p.z() || b.z() > p.z();
boolean e = a.x() > p.x() || b.x() > p.x();
return n ? e ? Rail.NORTH_EAST : Rail.NORTH_WEST : s && e ? Rail.SOUTH_EAST : Rail.SOUTH_WEST;
}
private static Cell routeCell(int x, int floor, int z, Rail rail, boolean present, Probe probe) {
int top = probe.surface(x, z);
if (floor < 14 || floor > 350 || top < floor - MAX_ROUTE_FILL || top > floor + MAX_ROUTE_CUT) return null;
int foundation = Math.max(1, floor - top + 1), base = floor - foundation;
if (!probe.solid(x, base - 1, z) || !probe.solid(x, base, z)) return null;
for (int y = base - 1; y <= floor + 3; y++) if (probe.protectedAt(x, y, z)) return null;
return new Cell(x, floor, z, foundation, rail, present);
}
private record XZ(int x, int z) { XZ move(int facing, int distance) { return new XZ(x + DX[facing] * distance, z + DZ[facing] * distance); } }
private record SegmentKey(Request request, int cx, int cz) {}
private static List<Segment> segments(long seed, Map<Long, OwnedCell> cells) {
var groups = new LinkedHashMap<SegmentKey, List<Cell>>();
for (var owned : cells.values()) {
Cell c = owned.cell(); var key = new SegmentKey(owned.request(), c.x() >> 4, c.z() >> 4);
groups.computeIfAbsent(key, ignored -> new ArrayList<>()).add(c);
}
var result = new ArrayList<Segment>();
for (var entry : groups.entrySet()) {
var k = entry.getKey(); var r = k.request();
// IDs are stable, bounded and independent of native generation order.
String id = "piece_" + r.id() + "_" + k.cx() + "_" + k.cz();
result.add(new Segment(id, r.from(), r.to(), r.kind(), k.cx(), k.cz(), entry.getValue(), mix(seed ^ key(k.cx(), k.cz()) ^ r.id().hashCode())));
}
return List.copyOf(result);
}
private static int span(Entry a, Entry b) { return Math.max(Math.abs(a.x() - b.x()), Math.abs(a.z() - b.z())); }
private static Site find(List<Site> sites, Kind kind) { return sites.stream().filter(site -> site.kind() == kind).findFirst().orElse(null); }
private static String id(Kind kind) { return kind.name().toLowerCase(Locale.ROOT); }
private static int direction(int x, int z, int tx, int tz) { return Math.abs(tx - x) >= Math.abs(tz - z) ? tx > x ? 1 : 3 : tz > z ? 2 : 0; }
private static boolean overlapsSites(int x0, int z0, int x1, int z1, List<Site> sites) {
for (var site : sites) if (x0 <= site.maxX() && x1 >= site.minX() && z0 <= site.maxZ() && z1 >= site.minZ()) return true;
return false;
}
private record SiteDraft(Site site, List<Cell> landings) {}
private record Candidate(int x, int z, int facing, int score, long seed) {}
private record Endpoint(String id, Entry entry, boolean readOnly) {}
private record Request(String id, String from, String to, RouteKind kind) {}
private record OwnedCell(Request request, Cell cell) {}
private record RouteDraft(List<Point> path, Map<Long, Cell> cells) {}
private static final class BudgetExceeded extends RuntimeException {
final boolean columns; BudgetExceeded(boolean columns) { this.columns = columns; }
}
private static final class Probe {
final Terrain terrain; final int cx, cz; final double radius;
final Map<Long, Integer> columns = new HashMap<>();
XZ routeBlock; Map<String, Integer> reasons = new HashMap<>(); int samples, columnLimit, sampleLimit;
Probe(Terrain terrain, int cx, int cz, int diameter) { this.terrain = java.util.Objects.requireNonNull(terrain); this.cx = cx; this.cz = cz; radius = diameter / 2.0 - 2; }
int surface(int x, int z) {
if (Math.hypot((double) x - cx, (double) z - cz) > radius || !terrain.owns(x, z)) return INVALID;
long key = key(x, z); var cached = columns.get(key); if (cached != null) return cached;
if (columns.size() >= columnLimit) throw new BudgetExceeded(true);
count(); int top = terrain.surfaceY(x, z); columns.put(key, top); return top;
}
boolean solid(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) == INVALID) return false; count(); return terrain.solid(x, y, z); }
boolean protectedAt(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) == INVALID) return true; count(); return terrain.protectedAt(x, y, z); }
void count() { if (samples >= sampleLimit) throw new BudgetExceeded(false); samples++; }
void reject(String reason) { reasons.merge(reason, 1, Integer::sum); }
}
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
private static double unit(long value) { return (value >>> 11) * 0x1.0p-53; }
private static long mix(long value) { value = (value ^ value >>> 30) * 0xbf58476d1ce4e5b9L; value = (value ^ value >>> 27) * 0x94d049bb133111ebL; return value ^ value >>> 31; }
}
@@ -0,0 +1,361 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.worldgen.city.LostCityPalette;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import net.minecraft.core.Direction;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.AttachFace;
import net.minecraft.world.level.block.state.properties.BedPart;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.DoorHingeSide;
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
import net.minecraft.world.level.block.state.properties.Half;
import net.minecraft.world.level.block.state.properties.RailShape;
/** Fourteen original, physical generation19 places. The saved site controls all geometry and loot.
* Site writes are local to minX/floorY/minZ; route writes are local to the segment's minimum bounds. */
public final class HeritageRenderer19 {
private HeritageRenderer19() {}
public record Target(String name, int x, int y, int z) {}
/** These are already rotated local standing positions, not world coordinates. */
public static List<Target> targets(HeritagePlan19.Site site) {
var b = new Builder(site, null); b.layout(); return List.copyOf(b.targets);
}
public static void render(HeritagePlan19.Site site, LostCityRenderer.BlockWriter writer) {
var b = new Builder(site, writer); b.ground(); b.layout();
// Footings have absolute saved coordinates and must not undergo a second facade rotation.
for (var support : site.supports()) for (int y = support.minY(); y <= support.maxY(); y++)
writer.set(support.x() - site.minX(), y - site.floorY(), support.z() - site.minZ(),
y == support.minY() ? Blocks.SMOOTH_STONE.defaultBlockState() : Blocks.STONE_BRICKS.defaultBlockState());
}
public static void renderSegment(HeritagePlan19.Segment segment, LostCityRenderer.BlockWriter writer) {
int x0 = segment.minX(), y0 = segment.minY(), z0 = segment.minZ();
for (var cell : segment.cells()) {
int x = cell.x() - x0, z = cell.z() - z0, floor = cell.floorY() - y0;
for (int y = cell.minY() - y0; y <= floor; y++) {
Block material = Blocks.STONE;
if (y == floor) material = segment.kind() == HeritagePlan19.RouteKind.FREIGHT
? cell.rail() != HeritagePlan19.Rail.NONE && Math.floorMod(cell.x() + cell.z(), 3) == 0
? Blocks.DARK_OAK_PLANKS : Blocks.GRAVEL
: LostCityPalette.roll(segment.seed(), cell.x(), y, cell.z(), 4) == 0 ? Blocks.ANDESITE : Blocks.STONE;
writer.set(x, y, z, material.defaultBlockState());
}
for (int y = floor + 1; y <= floor + 3; y++) writer.set(x, y, z, Blocks.AIR.defaultBlockState());
if (cell.railPresent()) writer.set(x, floor + 1, z, Blocks.RAIL.defaultBlockState()
.setValue(BlockStateProperties.RAIL_SHAPE, RailShape.valueOf(cell.rail().name())));
}
}
private static final class Builder {
final HeritagePlan19.Site s;
final LostCityRenderer.BlockWriter out;
final List<Target> targets = new ArrayList<>();
final Rotation rotation;
Builder(HeritagePlan19.Site s, LostCityRenderer.BlockWriter out) {
this.s = s; this.out = out;
rotation = switch (s.facing()) { case 1 -> Rotation.CLOCKWISE_90; case 2 -> Rotation.CLOCKWISE_180;
case 3 -> Rotation.COUNTERCLOCKWISE_90; default -> Rotation.NONE; };
}
int rx(int x, int z) { return switch (s.facing()) { case 1 -> 16 - z; case 2 -> 16 - x; case 3 -> z; default -> x; }; }
int rz(int x, int z) { return switch (s.facing()) { case 1 -> x; case 2 -> 16 - z; case 3 -> 16 - x; default -> z; }; }
void set(int x, int y, int z, BlockState state) {
if (x < 0 || x > 16 || z < 0 || z > 16 || y < -s.foundationDepth() || y > s.height())
throw new IllegalArgumentException("Heritage " + s.kind() + " renderer escaped saved site: " + x + "/" + y + "/" + z);
if (out != null) out.set(rx(x, z), y, rz(x, z), state.rotate(rotation));
}
void set(int x, int y, int z, Block b) { set(x, y, z, b.defaultBlockState()); }
void box(int x0, int y0, int z0, int x1, int y1, int z1, Block b) {
for (int y = y0; y <= y1; y++) for (int z = z0; z <= z1; z++) for (int x = x0; x <= x1; x++) set(x, y, z, b);
}
void target(String name, int x, int y, int z) { targets.add(new Target(name, rx(x, z), y, rz(x, z))); }
void ground() {
for (int y = -s.foundationDepth(); y <= s.height(); y++) for (int z = 0; z <= 16; z++) for (int x = 0; x <= 16; x++)
set(x, y, z, y < 0 ? Blocks.STONE.defaultBlockState() : y == 0
? LostCityPalette.pavement(s.seed(), x, z) : Blocks.AIR.defaultBlockState());
}
void layout() {
target("main_entry", 8, 1, 0);
switch (s.kind()) {
case FREIGHT_STATION -> station();
case POST_OFFICE -> post();
case COMMUNAL_DEPOT -> depot();
case EXPEDITION_OFFICE -> expedition();
case OBSERVATORY -> observatory();
case RAIL_WORKSHOP -> railway();
case PUMP_STATION -> pumping();
case SUBSTATION -> substation();
case GREENHOUSE -> greenhouse();
case VOID_DOCK -> dock();
case RADIO_RELAY -> radio();
case PHOTO_CLUB -> photo();
case IMPOSSIBLE_SERVICE -> impossible();
case CONSTRUCTION_REFUGE -> refuge();
}
}
void shell(int x0, int floor, int z0, int x1, int ceiling, int z1) {
for (int y = floor; y <= ceiling; y++) for (int z = z0; z <= z1; z++) for (int x = x0; x <= x1; x++) {
boolean wall = x == x0 || x == x1 || z == z0 || z == z1;
set(x, y, z, y == floor || y == ceiling ? Blocks.SMOOTH_STONE.defaultBlockState()
: wall ? LostCityPalette.masonry(s.seed(), x, y, z) : Blocks.AIR.defaultBlockState());
}
}
void gable(int x0, int z0, int x1, int z1, int y) {
int rise = Math.min(3, (x1 - x0) / 2);
for (int step = 0; step <= rise; step++)
box(x0 + step, y + step, z0, x1 - step, y + step, z1, step == rise ? Blocks.STONE_BRICK_SLAB : Blocks.STONE_BRICKS);
}
void door(int x, int y, int z, Direction facing) { door(x, y, z, facing, false); }
void door(int x, int y, int z, Direction facing, boolean right) {
BlockState state = Blocks.DARK_OAK_DOOR.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing)
.setValue(BlockStateProperties.DOOR_HINGE, right ? DoorHingeSide.RIGHT : DoorHingeSide.LEFT)
.setValue(BlockStateProperties.OPEN, false).setValue(BlockStateProperties.POWERED, false);
set(x, y, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.LOWER));
set(x, y + 1, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.UPPER));
}
void doubleDoor(int x, int y, int z) { door(x, y, z, Direction.SOUTH, true); door(x + 1, y, z, Direction.SOUTH, false); }
void northWindows(int x0, int x1, int z, int y) { box(x0, y, z, x1, y + 1, z, Blocks.GLASS_PANE); }
void hang(int x, int ceiling, int z) { set(x, ceiling - 1, z, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true)); }
void seat(int x, int y, int z, Direction view) {
set(x, y, z, Blocks.DARK_OAK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, view.getOpposite()));
}
void lever(int x, int y, int z, Direction facing) {
set(x, y, z, Blocks.LEVER.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR)
.setValue(BlockStateProperties.HORIZONTAL_FACING, facing));
}
void rail(int x, int z, RailShape shape) { set(x, 1, z, Blocks.RAIL.defaultBlockState().setValue(BlockStateProperties.RAIL_SHAPE, shape)); }
void loot(int x, int y, int z, int approachX, int approachZ) {
target("loot_approach", approachX, y, approachZ);
if (out != null) out.chest(rx(x, z), y, rz(x, z), ResourceKey.create(Registries.LOOT_TABLE,
SanctuaryMod.id("chests/heritage19/" + s.kind().name().toLowerCase(Locale.ROOT))), s.seed() ^ 0x19CA7A10L);
}
void stairs(int x, int z, int base, int rise) {
for (int step = 1; step <= rise; step++) for (int xx = x; xx <= x + 1; xx++) {
box(xx, base + step + 1, z + step - 1, xx, base + step + 3, z + step - 1, Blocks.AIR);
set(xx, base + step - 1, z + step - 1, Blocks.STONE_BRICKS);
set(xx, base + step, z + step - 1, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
}
}
void bed(int x, int y, int z, Direction facing) {
BlockState state = Blocks.BED.white().defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing);
set(x, y, z, state.setValue(BlockStateProperties.BED_PART, BedPart.FOOT));
set(x + facing.getStepX(), y, z + facing.getStepZ(), state.setValue(BlockStateProperties.BED_PART, BedPart.HEAD));
int wx = x + facing.getStepX() * 2, wz = z + facing.getStepZ() * 2;
Direction along = facing.getClockWise();
for (int side = -1; side <= 1; side++) for (int up = 0; up <= 2; up++)
set(wx + along.getStepX() * side, y + up, wz + along.getStepZ() * side, Blocks.SMOOTH_STONE);
}
void station() {
shell(4, 0, 3, 12, 6, 11); gable(4, 3, 12, 11, 6);
door(8, 1, 3, Direction.SOUTH); northWindows(5, 6, 3, 2); northWindows(10, 11, 3, 2);
box(5, 1, 5, 6, 1, 5, Blocks.SMOOTH_STONE); set(5, 1, 8, Blocks.BARREL); seat(10, 1, 5, Direction.WEST);
for (int z = 2; z <= 14; z++) rail(2, z, RailShape.NORTH_SOUTH);
set(2, 1, 15, Blocks.DARK_OAK_FENCE);
rail(8, 0, RailShape.NORTH_SOUTH); rail(8, 1, RailShape.NORTH_WEST);
for (int x = 3; x <= 7; x++) rail(x, 1, RailShape.EAST_WEST);
rail(2, 1, RailShape.SOUTH_EAST);
box(0, 5, 5, 3, 5, 11, Blocks.DARK_OAK_PLANKS);
for (int z : new int[] {5, 11}) box(1, 1, z, 1, 4, z, Blocks.STRIPPED_OAK_LOG);
shell(9, 0, 11, 15, 4, 15); door(10, 1, 11, Direction.SOUTH); door(12, 1, 15, Direction.NORTH);
set(10, 1, 13, Blocks.SMOOTH_STONE); lever(10, 2, 13, Direction.EAST);
northWindows(13, 14, 15, 2); hang(8, 6, 7); hang(12, 4, 12);
target("ticket_counter", 8, 1, 5); target("freight_platform", 3, 1, 9); target("signal_office", 12, 1, 12);
target("secondary_entry", 12, 1, 16); loot(14, 1, 14, 13, 14);
}
void post() {
shell(2, 0, 2, 14, 6, 14); door(8, 1, 2, Direction.SOUTH);
northWindows(3, 6, 2, 2); northWindows(10, 13, 2, 2);
box(3, 1, 7, 13, 5, 7, Blocks.SMOOTH_STONE); door(8, 1, 7, Direction.SOUTH);
box(4, 1, 6, 6, 1, 6, Blocks.DARK_OAK_PLANKS);
for (int z = 9; z <= 11; z++) { set(3, 1, z, Blocks.BARREL); set(3, 2, z, Blocks.BARREL); }
set(6, 1, 10, Blocks.CRAFTING_TABLE); set(8, 1, 12, Blocks.BARREL);
shell(10, 0, 10, 14, 6, 14); door(10, 1, 12, Direction.EAST); door(14, 1, 12, Direction.WEST);
set(11, 1, 11, Blocks.CARTOGRAPHY_TABLE); hang(8, 6, 4); hang(7, 6, 10); hang(12, 6, 12);
target("public_hall", 8, 1, 4); target("sorting_aisle", 7, 1, 10); target("factor_office", 12, 1, 12);
target("secondary_entry", 15, 1, 12); loot(13, 1, 13, 12, 13);
}
void depot() {
shell(1, 0, 2, 15, 9, 14); gable(1, 2, 15, 14, 9);
box(7, 1, 2, 9, 3, 2, Blocks.AIR); door(4, 1, 2, Direction.SOUTH);
for (int z : new int[] {5, 8}) {
box(4, 1, z, 5, 1, z + 1, Blocks.DARK_OAK_PLANKS); set(4, 2, z, Blocks.BARREL);
box(11, 1, z, 13, 1, z + 1, Blocks.DARK_OAK_PLANKS); set(13, 2, z + 1, Blocks.BARREL);
}
box(2, 5, 10, 14, 5, 13, Blocks.SMOOTH_STONE);
for (int x = 5; x <= 14; x++) set(x, 6, 10, Blocks.IRON_BARS);
shell(10, 5, 10, 14, 9, 14); door(10, 6, 12, Direction.EAST);
set(11, 6, 11, Blocks.LECTERN.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
stairs(2, 5, 0, 5); door(15, 1, 12, Direction.WEST); hang(8, 9, 6); hang(12, 9, 12);
for (int z = 0; z <= 8; z++) rail(8, z, RailShape.NORTH_SOUTH);
set(8, 1, 9, Blocks.DARK_OAK_FENCE);
target("unloading_bay", 8, 1, 5); target("inventory_mezzanine", 5, 6, 12);
target("secondary_entry", 16, 1, 12); loot(13, 6, 13, 12, 13);
}
void expedition() {
shell(2, 0, 2, 12, 6, 12); door(8, 1, 2, Direction.SOUTH);
northWindows(3, 5, 2, 2); northWindows(10, 11, 2, 2);
set(4, 1, 4, Blocks.CARTOGRAPHY_TABLE); set(5, 1, 4, Blocks.LECTERN.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH)); set(3, 1, 6, Blocks.BARREL);
shell(2, 0, 8, 6, 6, 12); door(6, 1, 10, Direction.WEST); set(3, 1, 9, Blocks.BOOKSHELF);
box(3, 0, 13, 14, 0, 15, Blocks.DARK_OAK_PLANKS);
box(3, 1, 15, 14, 1, 15, Blocks.DARK_OAK_FENCE); door(10, 1, 12, Direction.NORTH);
seat(6, 1, 13, Direction.SOUTH); hang(8, 6, 5); hang(4, 6, 10);
target("expedition_table", 5, 1, 5); target("balcony", 10, 1, 14);
target("secondary_entry", 10, 1, 13); loot(3, 1, 11, 4, 11);
}
boolean tower(int x, int z) { return x >= 3 && x <= 13 && z >= 3 && z <= 13 && Math.abs(x - 8) + Math.abs(z - 8) <= 8; }
void observatory() {
for (int y = 0; y <= 12; y++) for (int z = 3; z <= 13; z++) for (int x = 3; x <= 13; x++) if (tower(x, z)) {
boolean wall = !tower(x - 1, z) || !tower(x + 1, z) || !tower(x, z - 1) || !tower(x, z + 1);
set(x, y, z, y == 0 || y == 6 || y == 12 ? Blocks.SMOOTH_STONE.defaultBlockState()
: wall ? LostCityPalette.masonry(s.seed(), x, y, z) : Blocks.AIR.defaultBlockState());
if (y == 12 && wall) set(x, 13, z, Blocks.IRON_BARS);
}
door(8, 1, 3, Direction.SOUTH); door(8, 1, 13, Direction.NORTH);
northWindows(7, 9, 3, 8); box(13, 8, 7, 13, 9, 9, Blocks.GLASS_PANE);
shell(9, 0, 9, 12, 4, 12); door(9, 1, 10, Direction.EAST); set(10, 1, 11, Blocks.BARREL);
set(10, 7, 5, Blocks.CARTOGRAPHY_TABLE); set(11, 7, 8, Blocks.SMOOTH_STONE); lever(11, 8, 8, Direction.WEST);
stairs(4, 5, 0, 6); stairs(4, 5, 6, 6);
box(4, 12, 11, 6, 12, 11, Blocks.SMOOTH_STONE); box(4, 13, 11, 6, 15, 11, Blocks.AIR);
box(10, 13, 9, 10, 18, 9, Blocks.DARK_OAK_FENCE); box(9, 18, 9, 11, 18, 9, Blocks.IRON_BARS);
hang(8, 6, 6); hang(10, 4, 11);
target("instruments", 9, 7, 8); target("sky_platform", 8, 13, 8);
target("secondary_entry", 8, 1, 14); loot(11, 1, 11, 11, 10);
}
void railway() {
shell(1, 0, 2, 15, 6, 14); gable(1, 2, 15, 14, 6); doubleDoor(7, 1, 2);
for (int z = 3; z <= 13; z++) for (int x : new int[] {5, 11}) set(x, 1, z, Blocks.RAIL);
box(4, 1, 2, 6, 3, 2, Blocks.AIR); rail(8, 0, RailShape.NORTH_SOUTH); rail(8, 1, RailShape.NORTH_WEST);
rail(7, 1, RailShape.EAST_WEST); rail(6, 1, RailShape.EAST_WEST); rail(5, 1, RailShape.SOUTH_EAST); rail(5, 2, RailShape.NORTH_SOUTH);
box(7, -2, 5, 9, -2, 11, Blocks.SMOOTH_STONE); box(7, -1, 5, 9, 0, 11, Blocks.AIR);
for (int step = 0; step <= 2; step++) set(8, -step, 4 + step, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
for (int z = 6; z <= 11; z++) { set(6, 1, z, Blocks.IRON_BARS); set(10, 1, z, Blocks.IRON_BARS); }
set(2, 1, 4, Blocks.CRAFTING_TABLE); set(3, 1, 4, Blocks.FURNACE.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
set(2, 1, 6, Blocks.GRINDSTONE.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR));
shell(1, 0, 10, 5, 5, 14); door(5, 1, 12, Direction.WEST); door(8, 1, 14, Direction.NORTH);
set(5, 1, 9, Blocks.DARK_OAK_FENCE);
set(2, 1, 11, Blocks.ANVIL); hang(8, 6, 8); hang(3, 5, 12);
target("maintenance_pit", 8, -1, 9); target("workbench", 3, 1, 5);
target("secondary_entry", 8, 1, 15); loot(2, 1, 13, 3, 13);
}
void pumping() {
shell(2, 0, 3, 9, 6, 12); door(8, 1, 3, Direction.SOUTH);
set(3, 1, 5, Blocks.DISPENSER.defaultBlockState().setValue(BlockStateProperties.FACING, Direction.EAST));
set(4, 1, 5, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 3));
set(3, 2, 5, Blocks.POLISHED_ANDESITE); box(3, 3, 5, 3, 5, 8, Blocks.IRON_BARS);
for (int x : new int[] {10, 15}) for (int z : new int[] {5, 11}) box(x, 1, z, x, 7, z, Blocks.POLISHED_ANDESITE);
shell(10, 8, 5, 15, 13, 11); box(11, 9, 6, 14, 11, 10, Blocks.WATER);
box(10, 3, 6, 14, 3, 13, Blocks.SMOOTH_STONE); stairs(10, 3, 0, 3);
shell(2, 0, 10, 6, 5, 15); door(5, 1, 10, Direction.SOUTH); door(5, 1, 15, Direction.NORTH);
set(3, 1, 11, Blocks.BARREL); hang(7, 6, 7); hang(4, 5, 12);
target("pump_room", 6, 1, 6); target("reservoir_walkway", 13, 4, 12);
target("secondary_entry", 5, 1, 16); loot(3, 1, 14, 4, 14);
}
void substation() {
for (int z = 2; z <= 14; z++) { set(1, 1, z, Blocks.IRON_BARS); set(15, 1, z, Blocks.IRON_BARS); }
for (int x = 1; x <= 15; x++) if (x < 7 || x > 9) set(x, 1, 2, Blocks.IRON_BARS);
for (int z : new int[] {5, 8}) {
box(3, 1, z, 4, 2, z + 1, Blocks.POLISHED_ANDESITE); box(3, 3, z, 4, 3, z, Blocks.IRON_BARS);
}
shell(10, 0, 4, 15, 5, 10); door(10, 1, 6, Direction.EAST); northWindows(12, 14, 4, 2);
set(12, 1, 5, Blocks.SMOOTH_STONE); lever(12, 2, 5, Direction.SOUTH); set(14, 1, 5, Blocks.CRAFTING_TABLE);
shell(10, 0, 10, 15, 5, 15); door(12, 1, 10, Direction.SOUTH); door(15, 1, 13, Direction.WEST);
box(6, -2, 10, 7, -2, 13, Blocks.SMOOTH_STONE); box(6, -1, 10, 7, 0, 13, Blocks.AIR);
for (int step = 0; step <= 2; step++) set(6, -step, 9 + step, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
hang(13, 5, 7); hang(12, 5, 13); target("distribution_yard", 8, 1, 6); target("cable_trench", 6, -1, 12);
target("secondary_entry", 16, 1, 13); loot(14, 1, 14, 13, 14);
}
void greenhouse() {
shell(1, 0, 2, 11, 6, 14);
for (int y = 2; y <= 5; y++) for (int z = 3; z <= 13; z++) { set(1, y, z, Blocks.GLASS); set(11, y, z, Blocks.GLASS); }
northWindows(2, 5, 2, 2); northWindows(7, 10, 2, 2); door(6, 1, 2, Direction.SOUTH);
for (int x = 2; x <= 10; x++) for (int z = 3; z <= 13; z++) set(x, 6, z, x == 6 ? Blocks.SMOOTH_STONE : Blocks.GLASS);
for (int x : new int[] {2, 3, 4, 8, 9, 10}) for (int z = 4; z <= 10; z++) {
set(x, 0, z, Blocks.FARMLAND.defaultBlockState().setValue(BlockStateProperties.MOISTURE, 7));
set(x, 1, z, Blocks.WHEAT.defaultBlockState().setValue(BlockStateProperties.AGE_7,
2 + LostCityPalette.roll(s.seed(), x, 1, z, 6)));
}
set(3, 0, 6, Blocks.WATER); set(9, 0, 6, Blocks.WATER);
set(3, 1, 6, Blocks.AIR); set(9, 1, 6, Blocks.AIR);
shell(11, 0, 9, 15, 5, 15); door(11, 1, 12, Direction.EAST); door(13, 1, 15, Direction.NORTH);
set(12, 1, 10, Blocks.CRAFTING_TABLE); set(13, 1, 10, Blocks.FLOWER_POT); set(9, 1, 12, Blocks.COMPOSTER);
hang(6, 6, 5); hang(13, 5, 12); target("glasshouse_aisle", 6, 1, 7); target("potting_shed", 13, 1, 12);
target("secondary_entry", 13, 1, 16); loot(14, 1, 14, 13, 14);
}
void dock() {
box(0, 0, 0, 16, 0, 16, Blocks.DARK_OAK_PLANKS); box(0, 1, 16, 16, 1, 16, Blocks.DARK_OAK_FENCE);
box(0, 1, 1, 0, 1, 15, Blocks.DARK_OAK_FENCE); box(16, 1, 1, 16, 1, 15, Blocks.DARK_OAK_FENCE);
shell(2, 0, 3, 7, 5, 10); door(7, 1, 6, Direction.WEST); seat(3, 1, 5, Direction.EAST);
shell(2, 0, 10, 7, 5, 15); door(5, 1, 10, Direction.SOUTH); door(7, 1, 13, Direction.WEST);
box(12, 1, 5, 12, 11, 5, Blocks.STRIPPED_OAK_LOG); box(8, 11, 5, 14, 11, 5, Blocks.STRIPPED_OAK_LOG);
box(14, 7, 5, 14, 10, 5, Blocks.IRON_CHAIN); hang(10, 11, 5); hang(5, 5, 6); hang(5, 5, 12);
target("departure_deck", 8, 1, 14); target("waiting_room", 5, 1, 6); target("secondary_entry", 8, 1, 13);
loot(3, 1, 14, 4, 14);
}
void radio() {
shell(2, 0, 3, 10, 6, 12); door(8, 1, 3, Direction.SOUTH); northWindows(3, 5, 3, 2);
bed(4, 1, 5, Direction.NORTH); set(8, 1, 5, Blocks.CARTOGRAPHY_TABLE);
shell(2, 0, 8, 6, 6, 12); door(6, 1, 10, Direction.WEST); door(2, 1, 10, Direction.EAST);
box(11, 5, 6, 15, 5, 10, Blocks.SMOOTH_STONE);
for (int x : new int[] {11, 15}) for (int z : new int[] {6, 10}) box(x, 1, z, x, 4, z, Blocks.POLISHED_ANDESITE);
box(13, 1, 8, 13, 18, 8, Blocks.POLISHED_ANDESITE); box(11, 17, 8, 15, 17, 8, Blocks.IRON_BARS);
box(13, 19, 8, 13, 21, 8, Blocks.IRON_BARS); stairs(11, 1, 0, 5);
hang(7, 6, 6); hang(4, 6, 10);
target("caretaker_room", 7, 1, 6); target("antenna_platform", 12, 6, 9); target("secondary_entry", 1, 1, 10);
loot(3, 1, 11, 4, 11);
}
void photo() {
shell(2, 0, 2, 14, 6, 14); door(8, 1, 2, Direction.SOUTH); northWindows(3, 6, 2, 2); northWindows(10, 13, 2, 2);
box(3, 1, 8, 13, 5, 8, Blocks.SMOOTH_STONE); door(4, 1, 8, Direction.SOUTH);
box(8, 1, 9, 8, 5, 13, Blocks.SMOOTH_STONE); door(8, 1, 11, Direction.EAST);
box(10, 1, 5, 12, 1, 5, Blocks.DARK_OAK_PLANKS); set(3, 1, 4, Blocks.BOOKSHELF);
for (int z : new int[] {10, 12}) set(3, 1, z, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 3));
box(11, 4, 10, 11, 5, 12, Blocks.IRON_CHAIN); set(12, 1, 10, Blocks.CRAFTING_TABLE);
door(12, 1, 14, Direction.NORTH); hang(8, 6, 4); hang(5, 6, 11); hang(12, 6, 12);
target("club_room", 8, 1, 4); target("darkroom", 5, 1, 11); target("drying_room", 10, 1, 11);
target("secondary_entry", 12, 1, 15); loot(13, 1, 13, 12, 13);
}
void impossible() {
shell(3, 0, 2, 11, 4, 7); door(8, 1, 2, Direction.SOUTH);
for (int z = 3; z <= 5; z++) { set(4, 1, z, Blocks.BARREL); set(4, 2, z, Blocks.BARREL); }
seat(10, 1, 4, Direction.WEST);
shell(1, 0, 7, 15, 10, 15); door(8, 1, 7, Direction.SOUTH);
for (int x = 2; x <= 14; x++) for (int z = 8; z <= 14; z++) set(x, 0, z,
((x + z) & 1) == 0 ? Blocks.CALCITE : Blocks.POLISHED_ANDESITE);
for (int x : new int[] {3, 6, 10, 13}) box(x, 1, 11, x, 9, 11, Blocks.CALCITE);
shell(1, 0, 12, 5, 5, 15); door(5, 1, 13, Direction.WEST); door(3, 1, 15, Direction.NORTH);
set(2, 2, 12, Blocks.GLASS); set(2, 1, 13, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 2));
hang(8, 4, 4); hang(8, 10, 11); hang(3, 5, 13);
target("lockers", 8, 1, 4); target("oversized_tiled_hall", 8, 1, 11);
target("secondary_entry", 3, 1, 16); loot(2, 1, 14, 3, 14);
}
void refuge() {
box(3, 4, 5, 14, 4, 15, Blocks.DARK_OAK_PLANKS);
for (int x : new int[] {3, 14}) for (int z : new int[] {5, 15}) box(x, 1, z, x, 3, z, Blocks.STONE_BRICKS);
shell(5, 4, 6, 13, 10, 14); gable(5, 6, 13, 14, 10); door(8, 5, 6, Direction.SOUTH);
bed(6, 5, 8, Direction.NORTH); set(12, 5, 8, Blocks.CRAFTING_TABLE);
shell(6, 0, 9, 11, 4, 14); door(6, 1, 11, Direction.EAST);
// Carry the ladder through the floor so opening the hatch leaves a climbable lip.
for (int y = 1; y <= 4; y++) set(10, y, 11, Blocks.LADDER.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST));
set(10, 5, 11, Blocks.DARK_OAK_TRAPDOOR.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST)
.setValue(BlockStateProperties.HALF, Half.BOTTOM));
stairs(7, 1, 0, 4); box(2, 1, 3, 3, 11, 4, Blocks.STONE_BRICKS); box(2, 11, 3, 14, 11, 4, Blocks.STRIPPED_OAK_LOG);
hang(10, 10, 8); hang(8, 4, 12);
target("suspended_hut", 9, 5, 8); target("hidden_floor_hatch", 10, 5, 11); target("secondary_entry", 5, 1, 11);
loot(7, 1, 13, 8, 13);
}
}
}
@@ -0,0 +1,643 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.worldgen.city.LostCityPalette;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import net.minecraft.core.Direction;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.AttachFace;
import net.minecraft.world.level.block.state.properties.BedPart;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.DoorHingeSide;
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
import net.minecraft.world.level.block.state.properties.Half;
import net.minecraft.world.level.block.state.properties.RailShape;
/** Generation20 architectural rendition of the immutable generation19 site geometry. The saved site controls all geometry and loot.
* Site writes are local to minX/floorY/minZ; route writes are local to the segment's minimum bounds. */
public final class HeritageRenderer20 {
private HeritageRenderer20() {}
public record Target(String name, int x, int y, int z) {}
/** These are already rotated local standing positions, not world coordinates. */
public static List<Target> targets(HeritagePlan19.Site site) {
var b = new Builder(site, null); b.layout(); return List.copyOf(b.targets);
}
public static void render(HeritagePlan19.Site site, LostCityRenderer.BlockWriter writer) {
var buffer = ConnectedBlocks20.buffer(writer);
var b = new Builder(site, buffer); b.ground(); b.layout();
// Footings have absolute saved coordinates and must not undergo a second facade rotation.
for (var support : site.supports()) for (int y = support.minY(); y <= support.maxY(); y++)
buffer.set(support.x() - site.minX(), y - site.floorY(), support.z() - site.minZ(),
y == support.minY() ? Blocks.CHISELED_TUFF_BRICKS.defaultBlockState() : Blocks.STONE_BRICKS.defaultBlockState());
buffer.flush();
}
public static void renderSegment(HeritagePlan19.Segment segment, LostCityRenderer.BlockWriter writer) {
var buffer = ConnectedBlocks20.buffer(writer);
var occupied = new java.util.HashSet<Long>();
for (var cell : segment.cells()) occupied.add(columnKey(cell.x(), cell.z()));
int x0 = segment.minX(), y0 = segment.minY(), z0 = segment.minZ();
for (var cell : segment.cells()) {
int x = cell.x() - x0, z = cell.z() - z0, floor = cell.floorY() - y0;
for (int y = cell.minY() - y0; y <= floor; y++) {
Block material = Blocks.STONE_BRICKS;
if (y == floor) {
boolean edge = false;
for (Direction d : new Direction[] {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}) {
int nx = cell.x() + d.getStepX(), nz = cell.z() + d.getStepZ();
// Unknown neighboring chunks are continuations, not artificial crosswise curbs.
if ((nx >> 4) == segment.startChunkX() && (nz >> 4) == segment.startChunkZ()
&& !occupied.contains(columnKey(nx, nz))) edge = true;
}
if (cell.rail() != HeritagePlan19.Rail.NONE) material = Math.floorMod(cell.x() + cell.z(), 3) == 0 ? Blocks.OAK_PLANKS : Blocks.GRAVEL;
else if (edge) material = Math.floorMod(cell.x() + cell.z(), 8) == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS;
else material = LostCityPalette.roll(segment.seed(), cell.x(), 0, cell.z(), 5) == 0 ? Blocks.TUFF : Blocks.POLISHED_TUFF;
}
buffer.set(x, y, z, material.defaultBlockState());
}
for (int y = floor + 1; y <= floor + 3; y++) buffer.set(x, y, z, Blocks.AIR.defaultBlockState());
if (cell.railPresent()) buffer.set(x, floor + 1, z, Blocks.RAIL.defaultBlockState()
.setValue(BlockStateProperties.RAIL_SHAPE, RailShape.valueOf(cell.rail().name())));
}
buffer.flush();
}
private static long columnKey(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
private static final class Builder {
final HeritagePlan19.Site s;
final LostCityRenderer.BlockWriter out;
final List<Target> targets = new ArrayList<>();
final Rotation rotation;
Builder(HeritagePlan19.Site s, LostCityRenderer.BlockWriter out) {
this.s = s; this.out = out;
rotation = switch (s.facing()) { case 1 -> Rotation.CLOCKWISE_90; case 2 -> Rotation.CLOCKWISE_180;
case 3 -> Rotation.COUNTERCLOCKWISE_90; default -> Rotation.NONE; };
}
int rx(int x, int z) { return switch (s.facing()) { case 1 -> 16 - z; case 2 -> 16 - x; case 3 -> z; default -> x; }; }
int rz(int x, int z) { return switch (s.facing()) { case 1 -> x; case 2 -> 16 - z; case 3 -> 16 - x; default -> z; }; }
void set(int x, int y, int z, BlockState state) {
if (x < 0 || x > 16 || z < 0 || z > 16 || y < -s.foundationDepth() || y > s.height())
throw new IllegalArgumentException("Heritage " + s.kind() + " renderer escaped saved site: " + x + "/" + y + "/" + z);
if (out != null) out.set(rx(x, z), y, rz(x, z), state.rotate(rotation));
}
void set(int x, int y, int z, Block b) { set(x, y, z, b.defaultBlockState()); }
void box(int x0, int y0, int z0, int x1, int y1, int z1, Block b) {
for (int y = y0; y <= y1; y++) for (int z = z0; z <= z1; z++) for (int x = x0; x <= x1; x++) set(x, y, z, b);
}
void target(String name, int x, int y, int z) { targets.add(new Target(name, rx(x, z), y, rz(x, z))); }
void ground() {
for (int y = -s.foundationDepth(); y <= s.height(); y++) for (int z = 0; z <= 16; z++) for (int x = 0; x <= 16; x++)
set(x, y, z, y < 0 ? Blocks.STONE_BRICKS.defaultBlockState() : y == 0
? (x == 0 || z == 0 || x == 16 || z == 16 ? Blocks.TUFF_BRICKS
: x >= 7 && x <= 9 ? Blocks.POLISHED_TUFF
: LostCityPalette.roll(s.seed(), x, 0, z, 7) == 0 ? Blocks.TUFF : Blocks.ANDESITE).defaultBlockState()
: Blocks.AIR.defaultBlockState());
}
void layout() {
target("main_entry", 8, 1, 0);
switch (s.kind()) {
case FREIGHT_STATION -> station();
case POST_OFFICE -> post();
case COMMUNAL_DEPOT -> depot();
case EXPEDITION_OFFICE -> expedition();
case OBSERVATORY -> observatory();
case RAIL_WORKSHOP -> railway();
case PUMP_STATION -> pumping();
case SUBSTATION -> substation();
case GREENHOUSE -> greenhouse();
case VOID_DOCK -> dock();
case RADIO_RELAY -> radio();
case PHOTO_CLUB -> photo();
case IMPOSSIBLE_SERVICE -> impossible();
case CONSTRUCTION_REFUGE -> refuge();
}
architecture();
}
void shell(int x0, int floor, int z0, int x1, int ceiling, int z1) {
for (int y = floor; y <= ceiling; y++) for (int z = z0; z <= z1; z++) for (int x = x0; x <= x1; x++) {
boolean wall = x == x0 || x == x1 || z == z0 || z == z1;
set(x, y, z, y == floor ? floorMaterial().defaultBlockState() : y == ceiling ? Blocks.TUFF_BRICKS.defaultBlockState()
: wall ? wallMaterial(y - floor, ceiling - floor).defaultBlockState() : Blocks.AIR.defaultBlockState());
}
}
Block floorMaterial() {
return switch (s.kind()) {
case POST_OFFICE, EXPEDITION_OFFICE, RADIO_RELAY, CONSTRUCTION_REFUGE -> Blocks.OAK_PLANKS;
case PHOTO_CLUB, GREENHOUSE -> Blocks.BIRCH_PLANKS;
case IMPOSSIBLE_SERVICE -> Blocks.POLISHED_DIORITE;
default -> Blocks.POLISHED_ANDESITE;
};
}
Block wallMaterial(int y, int height) {
if (y == 1) return Blocks.STONE_BRICKS;
if (y == height - 1) return Blocks.CHISELED_TUFF_BRICKS;
return switch (s.kind()) {
case POST_OFFICE -> Blocks.POLISHED_GRANITE;
case FREIGHT_STATION, OBSERVATORY, PHOTO_CLUB -> Blocks.POLISHED_DIORITE;
case EXPEDITION_OFFICE, GREENHOUSE -> Blocks.BIRCH_PLANKS;
case RADIO_RELAY, VOID_DOCK, CONSTRUCTION_REFUGE -> Blocks.OAK_PLANKS;
case PUMP_STATION, SUBSTATION -> Blocks.ANDESITE;
case COMMUNAL_DEPOT, RAIL_WORKSHOP, IMPOSSIBLE_SERVICE -> Blocks.TUFF_BRICKS;
};
}
// These additions compose each use separately; all thresholds and maintenance passages stay in the saved footprint.
void architecture() {
switch (s.kind()) {
case FREIGHT_STATION -> stationArchitecture();
case POST_OFFICE -> postArchitecture();
case COMMUNAL_DEPOT -> depotArchitecture();
case EXPEDITION_OFFICE -> expeditionArchitecture();
case OBSERVATORY -> observatoryArchitecture();
case RAIL_WORKSHOP -> railwayArchitecture();
case PUMP_STATION -> pumpingArchitecture();
case SUBSTATION -> substationArchitecture();
case GREENHOUSE -> greenhouseArchitecture();
case VOID_DOCK -> dockArchitecture();
case RADIO_RELAY -> radioArchitecture();
case PHOTO_CLUB -> photoArchitecture();
case IMPOSSIBLE_SERVICE -> impossibleArchitecture();
case CONSTRUCTION_REFUGE -> refugeArchitecture();
}
}
void slab(int x, int y, int z, Block block) { set(x, y, z, block.defaultBlockState().setValue(BlockStateProperties.SLAB_TYPE, net.minecraft.world.level.block.state.properties.SlabType.BOTTOM)); }
void stair(int x, int y, int z, Block block, Direction direction, Half half) {
set(x, y, z, block.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, direction).setValue(BlockStateProperties.HALF, half));
}
/** Cut only the four exterior corner cells; the two adjacent wall faces keep the interior sealed. */
void bevel(int x0, int z0, int x1, int z1, int low, int high) {
for (int x : new int[] {x0, x1}) for (int z : new int[] {z0, z1}) {
box(x, low, z, x, high, z, Blocks.AIR);
set(x, low - 1, z, Blocks.CHISELED_STONE_BRICKS);
}
}
void rim(int x0, int z0, int x1, int z1, int y, Block material) {
for (int x = x0; x <= x1; x++) { slab(x, y, z0, material); slab(x, y, z1, material); }
for (int z = z0 + 1; z < z1; z++) { slab(x0, y, z, material); slab(x1, y, z, material); }
}
void screen(int x0, int z, int x1, int y) {
for (int x = x0; x <= x1; x++) set(x, y, z, Blocks.BIRCH_TRAPDOOR.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH).setValue(BlockStateProperties.OPEN, true));
}
void stationArchitecture() {
bevel(4, 3, 12, 11, 1, 6);
box(4, 7, 3, 12, 10, 11, Blocks.AIR);
rim(3, 2, 13, 12, 6, Blocks.TUFF_BRICK_SLAB);
for (int z = 5; z <= 9; z++) for (int x = 5; x <= 11; x++) {
if (x == 5 || x == 11 || z == 5 || z == 9) box(x, 7, z, x, 8, z, Blocks.GLASS_PANE);
slab(x, 9, z, Blocks.POLISHED_DIORITE_SLAB);
}
// Open the former ceiling beneath the sealed roof glazing. The middle beam
// still supports the existing hanging lantern and joins the two hall walls.
for (int z = 6; z <= 8; z++) for (int x = 6; x <= 10; x++) {
if (z != 7) set(x, 6, z, Blocks.AIR);
set(x, 9, z, Blocks.GLASS);
}
// The long cantilever reads as a train platform, distinct from the glazed passenger hall.
box(0, 5, 4, 3, 5, 12, Blocks.OAK_PLANKS);
for (int z : new int[] {4, 8, 12}) stair(0, 4, z, Blocks.TUFF_BRICK_STAIRS, Direction.EAST, Half.TOP);
for (int z : new int[] {5, 11}) box(1, 1, z, 1, 4, z, Blocks.OAK_WOOD);
for (int x : new int[] {4, 12}) box(x, 2, 4, x, 4, 4, Blocks.CHISELED_TUFF_BRICKS);
screen(5, 4, 6, 3); box(5, 0, 4, 11, 0, 10, Blocks.POLISHED_DIORITE);
box(7, 0, 3, 9, 0, 10, Blocks.BIRCH_PLANKS);
rim(9, 11, 15, 15, 4, Blocks.TUFF_BRICK_SLAB);
}
void postArchitecture() {
bevel(2, 2, 14, 14, 1, 6); rim(1, 1, 15, 15, 6, Blocks.POLISHED_GRANITE_SLAB);
box(4, 7, 4, 12, 7, 12, Blocks.TUFF_BRICKS);
rim(4, 4, 12, 12, 8, Blocks.TUFF_BRICK_SLAB);
for (int x : new int[] {2, 7, 9, 14}) {
box(x, 3, 1, x, 4, 1, Blocks.POLISHED_DIORITE);
stair(x, 5, 1, Blocks.TUFF_BRICK_STAIRS, Direction.SOUTH, Half.TOP);
}
box(5, 4, 1, 11, 4, 1, Blocks.CHISELED_TUFF_BRICKS);
box(4, 1, 6, 6, 1, 6, Blocks.BIRCH_PLANKS); screen(4, 6, 6, 3);
for (int z = 9; z <= 12; z++) { set(3, 3, z, Blocks.OAK_TRAPDOOR.defaultBlockState().setValue(BlockStateProperties.HALF, Half.TOP)); }
for (int x = 4; x <= 12; x++) set(x, 0, 5, x % 2 == 0 ? Blocks.POLISHED_GRANITE : Blocks.OAK_PLANKS);
box(10, 0, 8, 13, 0, 9, Blocks.DIORITE);
}
void depotArchitecture() {
bevel(1, 2, 15, 14, 1, 9); box(1, 10, 2, 15, 14, 14, Blocks.AIR);
// Three industrial north-light roof bays, not another pitched house roof.
for (int z : new int[] {3, 7, 11}) {
box(2, 10, z, 14, 11, z, Blocks.GLASS_PANE);
for (int x = 2; x <= 14; x++) {
slab(x, 12, z, Blocks.TUFF_BRICK_SLAB);
stair(x, 11, z + 1, Blocks.TUFF_BRICK_STAIRS, Direction.NORTH, Half.BOTTOM);
stair(x, 10, z + 2, Blocks.TUFF_BRICK_STAIRS, Direction.NORTH, Half.BOTTOM);
if (x > 2 && x < 14) {
// Both transparent faces close the north-light well, while the
// retained roof outside these bands keeps the warehouse sealed.
set(x, 9, z, Blocks.GLASS);
set(x, 12, z, Blocks.GLASS);
}
}
}
for (int z : new int[] {3, 7, 11}) for (int x : new int[] {1, 15}) box(x, 1, z, x, 9, z, Blocks.POLISHED_ANDESITE);
for (int x : new int[] {6, 10}) { box(x, 1, 1, x, 3, 1, Blocks.CHISELED_TUFF); stair(x, 4, 1, Blocks.TUFF_BRICK_STAIRS, Direction.SOUTH, Half.TOP); }
box(6, 4, 1, 10, 4, 1, Blocks.TUFF_BRICKS);
for (int z : new int[] {5, 8}) for (int x : new int[] {4, 12}) { set(x, 3, z, Blocks.OAK_TRAPDOOR.defaultBlockState().setValue(BlockStateProperties.HALF, Half.TOP)); }
for (int z = 3; z <= 9; z++) { set(6, 0, z, Blocks.TUFF_BRICKS); set(10, 0, z, Blocks.TUFF_BRICKS); }
box(2, 5, 11, 9, 5, 13, Blocks.BIRCH_PLANKS);
}
void expeditionArchitecture() {
bevel(2, 2, 12, 12, 1, 6); rim(1, 1, 13, 13, 6, Blocks.TUFF_BRICK_SLAB);
box(3, 7, 3, 6, 9, 7, Blocks.BIRCH_WOOD); rim(3, 3, 6, 7, 10, Blocks.TUFF_BRICK_SLAB);
box(4, 8, 3, 5, 8, 3, Blocks.GLASS_PANE);
// Sheltered map table, archive wing and broad shaded rear balcony.
box(7, 4, 1, 9, 4, 1, Blocks.CHISELED_TUFF_BRICKS);
for (int x : new int[] {2, 12}) box(x, 2, 3, x, 5, 3, Blocks.OAK_WOOD);
screen(3, 4, 5, 3); box(3, 3, 9, 5, 3, 9, Blocks.OAK_PLANKS);
for (int x = 3; x <= 14; x++) for (int z = 13; z <= 15; z++) set(x, 0, z, x % 3 == 0 ? Blocks.OAK_WOOD : Blocks.OAK_PLANKS);
for (int x = 3; x <= 14; x++) slab(x, 5, 14, Blocks.BIRCH_SLAB);
box(3, 1, 14, 3, 4, 14, Blocks.BIRCH_FENCE); box(14, 1, 14, 14, 4, 14, Blocks.BIRCH_FENCE);
}
void observatoryArchitecture() {
// Eight faceted buttresses below a broken observatory dome with an open central viewing slot.
for (int[] p : new int[][] {{3, 8}, {5, 4}, {11, 4}, {13, 8}, {11, 12}, {5, 12}}) {
box(p[0], 1, p[1], p[0], 11, p[1], Blocks.DIORITE);
set(p[0], 5, p[1], Blocks.CHISELED_TUFF_BRICKS);
stair(p[0], 12, p[1], Blocks.TUFF_BRICK_STAIRS, p[0] < 8 ? Direction.EAST : Direction.WEST, Half.BOTTOM);
}
for (int z = 4; z <= 12; z++) for (int x = 4; x <= 12; x++) {
int distance = Math.max(Math.abs(x - 8), Math.abs(z - 8));
if (distance == 4) set(x, 15, z, Blocks.GLASS_PANE);
if (distance == 3 && x != 8) slab(x, 17, z, Blocks.TUFF_BRICK_SLAB);
}
for (int x : new int[] {4, 12}) for (int z : new int[] {5, 11}) box(x, 13, z, x, 14, z, Blocks.CHISELED_TUFF_BRICKS);
// Four short ribs visibly carry the inset crown; the central viewing slot stays open.
for (int x : new int[] {4, 12}) for (int z : new int[] {5, 11}) {
box(x, 15, z, x, 16, z, Blocks.CHISELED_TUFF_BRICKS);
stair(x < 8 ? x + 1 : x - 1, 16, z, Blocks.TUFF_BRICK_STAIRS,
x < 8 ? Direction.EAST : Direction.WEST, Half.BOTTOM);
}
box(7, 0, 5, 9, 0, 11, Blocks.BIRCH_PLANKS);
box(7, 6, 5, 9, 6, 10, Blocks.POLISHED_DIORITE);
}
void railwayArchitecture() {
bevel(1, 2, 15, 14, 1, 6); box(1, 7, 2, 15, 10, 14, Blocks.AIR);
// Broad barrel-vault ribs sit above the two inspection tracks.
for (int z = 2; z <= 14; z++) for (int x = 1; x <= 15; x++) {
int rise = Math.min(3, Math.min(x - 1, 15 - x));
if (z % 4 == 2) set(x, 6 + rise, z, Blocks.TUFF_BRICKS);
else if (x >= 6 && x <= 10) set(x, 6 + rise, z, Blocks.GLASS);
else slab(x, 6 + rise, z, Blocks.TUFF_BRICK_SLAB);
}
for (int z : new int[] {3, 7, 11}) for (int x : new int[] {1, 15}) box(x, 2, z, x, 5, z, Blocks.GRANITE);
box(7, -2, 5, 9, -2, 11, Blocks.POLISHED_ANDESITE);
for (int z = 6; z <= 11; z++) { set(7, -1, z, Blocks.CHISELED_TUFF_BRICKS); set(9, -1, z, Blocks.CHISELED_TUFF_BRICKS); }
// Side ledges leave the one-block maintenance aisle and its stairs clear.
screen(2, 3, 3, 3); box(2, 3, 11, 4, 3, 11, Blocks.OAK_PLANKS);
}
void pumpingArchitecture() {
bevel(2, 3, 9, 12, 1, 6); rim(1, 2, 9, 13, 6, Blocks.TUFF_BRICK_SLAB);
for (int x : new int[] {10, 15}) for (int z : new int[] {5, 11}) {
box(x, 1, z, x, 7, z, Blocks.CHISELED_TUFF_BRICKS);
set(x, 8, z, Blocks.POLISHED_ANDESITE);
}
rim(9, 4, 16, 12, 13, Blocks.TUFF_BRICK_SLAB);
for (int x = 10; x <= 15; x++) for (int z = 5; z <= 11; z++) slab(x, 14, z, Blocks.POLISHED_ANDESITE_SLAB);
box(3, 7, 5, 3, 12, 5, Blocks.TUFF_BRICK_WALL);
box(3, 3, 6, 3, 5, 8, Blocks.IRON_BARS);
box(3, 0, 4, 8, 0, 9, Blocks.ANDESITE); box(5, 0, 4, 7, 0, 10, Blocks.POLISHED_DIORITE);
screen(3, 11, 4, 3);
}
void substationArchitecture() {
bevel(10, 4, 15, 10, 1, 5); rim(9, 3, 16, 15, 5, Blocks.TUFF_BRICK_SLAB);
for (int z : new int[] {2, 6, 10, 14}) for (int x : new int[] {1, 15}) {
set(x, 0, z, Blocks.STONE_BRICKS); set(x, 1, z, Blocks.CHISELED_TUFF_BRICKS);
}
box(2, 4, 5, 5, 4, 5, Blocks.IRON_BARS); box(2, 4, 8, 5, 4, 8, Blocks.IRON_BARS);
for (int x : new int[] {2, 5}) for (int z : new int[] {5, 8}) box(x, 1, z, x, 3, z, Blocks.TUFF_BRICK_WALL);
box(11, 6, 5, 14, 6, 9, Blocks.POLISHED_TUFF);
for (int z = 5; z <= 9; z += 2) screen(11, z, 14, 7);
for (int z = 3; z <= 8; z++) set(7, 0, z, Blocks.GRANITE);
box(6, -2, 10, 7, -2, 13, Blocks.CHISELED_TUFF_BRICKS);
}
void greenhouseArchitecture() {
// Tall birch ribs and a steep glass ridge give the glasshouse an agricultural silhouette.
for (int z = 2; z <= 14; z++) for (int x = 1; x <= 11; x++) {
int rise = Math.min(3, Math.min(x - 1, 11 - x));
if (z == 2 || z == 8 || z == 14 || x == 6) set(x, 6 + rise, z, Blocks.BIRCH_WOOD);
else set(x, 6 + rise, z, Blocks.GLASS);
}
for (int z : new int[] {3, 7, 11}) { box(1, 1, z, 1, 5, z, Blocks.BIRCH_WOOD); box(11, 1, z, 11, 5, z, Blocks.BIRCH_WOOD); }
rim(11, 9, 15, 15, 5, Blocks.TUFF_BRICK_SLAB);
box(12, 3, 10, 14, 3, 10, Blocks.OAK_PLANKS); screen(12, 10, 14, 4);
for (int z = 3; z <= 13; z++) set(6, 0, z, z % 3 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.POLISHED_TUFF);
}
void dockArchitecture() {
for (int x = 1; x <= 15; x++) for (int z = 0; z <= 15; z++) set(x, 0, z, x % 4 == 0 ? Blocks.OAK_WOOD : Blocks.OAK_PLANKS);
bevel(2, 3, 7, 10, 1, 5); rim(1, 2, 8, 15, 5, Blocks.BIRCH_SLAB);
for (int z : new int[] {2, 8, 14}) {
box(0, 1, z, 0, 2, z, Blocks.CHISELED_TUFF_BRICKS);
box(16, 1, z, 16, 2, z, Blocks.CHISELED_TUFF_BRICKS);
slab(0, 3, z, Blocks.TUFF_BRICK_SLAB); slab(16, 3, z, Blocks.TUFF_BRICK_SLAB);
}
// Brackets and a separated customs roof leave the departure platform open to the void.
for (int x = 2; x <= 7; x++) for (int z = 3; z <= 15; z++) if (z % 4 == 0) slab(x, 6, z, Blocks.BIRCH_SLAB);
stair(11, 9, 5, Blocks.OAK_STAIRS, Direction.EAST, Half.TOP);
stair(13, 9, 5, Blocks.OAK_STAIRS, Direction.WEST, Half.TOP);
screen(3, 4, 6, 3);
}
void radioArchitecture() {
bevel(2, 3, 10, 12, 1, 6); rim(1, 2, 10, 13, 6, Blocks.TUFF_BRICK_SLAB);
for (int x = 2; x <= 10; x++) for (int z = 3; z <= 12; z++) {
int roofY = 7 + Math.min(2, Math.min(x - 2, 10 - x));
slab(x, roofY, z, x <= 5 ? Blocks.BIRCH_SLAB : Blocks.TUFF_BRICK_SLAB);
}
box(9, 7, 4, 9, 11, 4, Blocks.CHISELED_TUFF_BRICKS);
for (int y : new int[] {10, 14, 18}) { box(12, y, 8, 14, y, 8, Blocks.IRON_BARS); box(13, y, 7, 13, y, 9, Blocks.IRON_BARS); }
box(3, 0, 8, 5, 0, 11, Blocks.BIRCH_PLANKS); screen(7, 4, 9, 3);
}
void photoArchitecture() {
bevel(2, 2, 14, 14, 1, 6); rim(1, 1, 15, 15, 6, Blocks.POLISHED_DIORITE_SLAB);
box(3, 7, 9, 13, 7, 13, Blocks.TUFF_BRICKS); rim(3, 9, 13, 13, 8, Blocks.TUFF_BRICK_SLAB);
for (int x : new int[] {2, 6, 10, 14}) stair(x, 4, 1, Blocks.TUFF_BRICK_STAIRS, Direction.SOUTH, Half.TOP);
box(3, 5, 1, 13, 5, 1, Blocks.CHISELED_TUFF_BRICKS);
for (int x = 4; x <= 12; x++) set(x, 0, 4, x % 2 == 0 ? Blocks.POLISHED_GRANITE : Blocks.BIRCH_PLANKS);
box(3, 0, 9, 7, 0, 13, Blocks.POLISHED_ANDESITE);
screen(10, 5, 12, 3); box(3, 3, 10, 3, 3, 12, Blocks.OAK_PLANKS);
}
void impossibleArchitecture() {
bevel(1, 7, 15, 15, 1, 10); rim(0, 6, 16, 16, 10, Blocks.TUFF_BRICK_SLAB);
box(4, 11, 9, 12, 11, 13, Blocks.TUFF_BRICKS);
for (int x : new int[] {4, 12}) for (int z : new int[] {9, 13}) set(x, 12, z, Blocks.CHISELED_TUFF);
// A cramped civil facade gives way to an over-scaled, deliberately ceremonial service hall.
for (int x : new int[] {3, 11}) box(x, 1, 1, x, 4, 1, Blocks.CHISELED_TUFF_BRICKS);
box(3, 5, 1, 11, 5, 1, Blocks.TUFF_BRICKS);
for (int x : new int[] {3, 6, 10, 13}) { set(x, 1, 11, Blocks.CHISELED_TUFF_BRICKS); set(x, 8, 11, Blocks.CHISELED_TUFF_BRICKS); }
screen(4, 3, 4, 3); box(12, 3, 14, 14, 3, 14, Blocks.BIRCH_PLANKS);
}
void refugeArchitecture() {
bevel(5, 6, 13, 14, 5, 10);
box(5, 11, 6, 13, 14, 14, Blocks.AIR);
for (int z = 6; z <= 14; z++) for (int x = 5; x <= 13; x++) {
int rise = Math.min(3, (x - 5) / 2);
slab(x, 11 + rise, z, (z & 1) == 0 ? Blocks.BIRCH_SLAB : Blocks.TUFF_BRICK_SLAB);
}
for (int x : new int[] {3, 14}) for (int z : new int[] {5, 15}) { box(x, 1, z, x, 3, z, Blocks.CHISELED_TUFF_BRICKS); set(x, 4, z, Blocks.OAK_WOOD); }
for (int z = 6; z <= 14; z += 4) box(4, 1, z, 4, 7, z, Blocks.IRON_BARS);
box(2, 1, 3, 3, 10, 4, Blocks.TUFF_BRICKS); box(2, 11, 3, 14, 11, 4, Blocks.OAK_WOOD);
// Upper lockers and a birch sleeping corner avoid the saved hatch and its complete ladder shaft.
screen(11, 7, 12, 7); box(7, 4, 7, 11, 4, 9, Blocks.BIRCH_PLANKS);
}
void gable(int x0, int z0, int x1, int z1, int y) {
int rise = Math.min(3, (x1 - x0) / 2);
for (int step = 0; step <= rise; step++)
box(x0 + step, y + step, z0, x1 - step, y + step, z1, step == rise ? Blocks.STONE_BRICK_SLAB : Blocks.STONE_BRICKS);
}
void door(int x, int y, int z, Direction facing) { door(x, y, z, facing, false); }
void door(int x, int y, int z, Direction facing, boolean right) {
BlockState state = Blocks.DARK_OAK_DOOR.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing)
.setValue(BlockStateProperties.DOOR_HINGE, right ? DoorHingeSide.RIGHT : DoorHingeSide.LEFT)
.setValue(BlockStateProperties.OPEN, false).setValue(BlockStateProperties.POWERED, false);
set(x, y, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.LOWER));
set(x, y + 1, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.UPPER));
}
void doubleDoor(int x, int y, int z) { door(x, y, z, Direction.SOUTH, true); door(x + 1, y, z, Direction.SOUTH, false); }
void northWindows(int x0, int x1, int z, int y) { box(x0, y, z, x1, y + 1, z, Blocks.GLASS_PANE); }
void hang(int x, int ceiling, int z) { set(x, ceiling - 1, z, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true)); }
void seat(int x, int y, int z, Direction view) {
set(x, y, z, Blocks.DARK_OAK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, view.getOpposite()));
}
void lever(int x, int y, int z, Direction facing) {
set(x, y, z, Blocks.LEVER.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR)
.setValue(BlockStateProperties.HORIZONTAL_FACING, facing));
}
void rail(int x, int z, RailShape shape) { set(x, 1, z, Blocks.RAIL.defaultBlockState().setValue(BlockStateProperties.RAIL_SHAPE, shape)); }
void loot(int x, int y, int z, int approachX, int approachZ) {
target("loot_approach", approachX, y, approachZ);
if (out != null) out.chest(rx(x, z), y, rz(x, z), ResourceKey.create(Registries.LOOT_TABLE,
SanctuaryMod.id("chests/heritage19/" + s.kind().name().toLowerCase(Locale.ROOT))), s.seed() ^ 0x19CA7A10L);
}
void stairs(int x, int z, int base, int rise) {
for (int step = 1; step <= rise; step++) for (int xx = x; xx <= x + 1; xx++) {
box(xx, base + step + 1, z + step - 1, xx, base + step + 3, z + step - 1, Blocks.AIR);
set(xx, base + step - 1, z + step - 1, Blocks.STONE_BRICKS);
set(xx, base + step, z + step - 1, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
}
}
void bed(int x, int y, int z, Direction facing) {
BlockState state = Blocks.BED.white().defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing);
set(x, y, z, state.setValue(BlockStateProperties.BED_PART, BedPart.FOOT));
set(x + facing.getStepX(), y, z + facing.getStepZ(), state.setValue(BlockStateProperties.BED_PART, BedPart.HEAD));
int wx = x + facing.getStepX() * 2, wz = z + facing.getStepZ() * 2;
Direction along = facing.getClockWise();
for (int side = -1; side <= 1; side++) for (int up = 0; up <= 2; up++)
set(wx + along.getStepX() * side, y + up, wz + along.getStepZ() * side, Blocks.SMOOTH_STONE);
}
void station() {
shell(4, 0, 3, 12, 6, 11); gable(4, 3, 12, 11, 6);
door(8, 1, 3, Direction.SOUTH); northWindows(5, 6, 3, 2); northWindows(10, 11, 3, 2);
box(5, 1, 5, 6, 1, 5, Blocks.SMOOTH_STONE); set(5, 1, 8, Blocks.BARREL); seat(10, 1, 5, Direction.WEST);
for (int z = 2; z <= 14; z++) rail(2, z, RailShape.NORTH_SOUTH);
set(2, 1, 15, Blocks.DARK_OAK_FENCE);
rail(8, 0, RailShape.NORTH_SOUTH); rail(8, 1, RailShape.NORTH_WEST);
for (int x = 3; x <= 7; x++) rail(x, 1, RailShape.EAST_WEST);
rail(2, 1, RailShape.SOUTH_EAST);
box(0, 5, 5, 3, 5, 11, Blocks.DARK_OAK_PLANKS);
for (int z : new int[] {5, 11}) box(1, 1, z, 1, 4, z, Blocks.STRIPPED_OAK_LOG);
shell(9, 0, 11, 15, 4, 15); door(10, 1, 11, Direction.SOUTH); door(12, 1, 15, Direction.NORTH);
set(10, 1, 13, Blocks.SMOOTH_STONE); lever(10, 2, 13, Direction.EAST);
northWindows(13, 14, 15, 2); hang(8, 6, 7); hang(12, 4, 12);
target("ticket_counter", 8, 1, 5); target("freight_platform", 3, 1, 9); target("signal_office", 12, 1, 12);
target("secondary_entry", 12, 1, 16); loot(14, 1, 14, 13, 14);
}
void post() {
shell(2, 0, 2, 14, 6, 14); door(8, 1, 2, Direction.SOUTH);
northWindows(3, 6, 2, 2); northWindows(10, 13, 2, 2);
box(3, 1, 7, 13, 5, 7, Blocks.SMOOTH_STONE); door(8, 1, 7, Direction.SOUTH);
box(4, 1, 6, 6, 1, 6, Blocks.DARK_OAK_PLANKS);
for (int z = 9; z <= 11; z++) { set(3, 1, z, Blocks.BARREL); set(3, 2, z, Blocks.BARREL); }
set(6, 1, 10, Blocks.CRAFTING_TABLE); set(8, 1, 12, Blocks.BARREL);
shell(10, 0, 10, 14, 6, 14); door(10, 1, 12, Direction.EAST); door(14, 1, 12, Direction.WEST);
set(11, 1, 11, Blocks.CARTOGRAPHY_TABLE); hang(8, 6, 4); hang(7, 6, 10); hang(12, 6, 12);
target("public_hall", 8, 1, 4); target("sorting_aisle", 7, 1, 10); target("factor_office", 12, 1, 12);
target("secondary_entry", 15, 1, 12); loot(13, 1, 13, 12, 13);
}
void depot() {
shell(1, 0, 2, 15, 9, 14); gable(1, 2, 15, 14, 9);
box(7, 1, 2, 9, 3, 2, Blocks.AIR); door(4, 1, 2, Direction.SOUTH);
for (int z : new int[] {5, 8}) {
box(4, 1, z, 5, 1, z + 1, Blocks.DARK_OAK_PLANKS); set(4, 2, z, Blocks.BARREL);
box(11, 1, z, 13, 1, z + 1, Blocks.DARK_OAK_PLANKS); set(13, 2, z + 1, Blocks.BARREL);
}
box(2, 5, 10, 14, 5, 13, Blocks.SMOOTH_STONE);
for (int x = 5; x <= 14; x++) set(x, 6, 10, Blocks.IRON_BARS);
shell(10, 5, 10, 14, 9, 14); door(10, 6, 12, Direction.EAST);
set(11, 6, 11, Blocks.LECTERN.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
stairs(2, 5, 0, 5); door(15, 1, 12, Direction.WEST); hang(8, 9, 6); hang(12, 9, 12);
for (int z = 0; z <= 8; z++) rail(8, z, RailShape.NORTH_SOUTH);
set(8, 1, 9, Blocks.DARK_OAK_FENCE);
target("unloading_bay", 8, 1, 5); target("inventory_mezzanine", 5, 6, 12);
target("secondary_entry", 16, 1, 12); loot(13, 6, 13, 12, 13);
}
void expedition() {
shell(2, 0, 2, 12, 6, 12); door(8, 1, 2, Direction.SOUTH);
northWindows(3, 5, 2, 2); northWindows(10, 11, 2, 2);
set(4, 1, 4, Blocks.CARTOGRAPHY_TABLE); set(5, 1, 4, Blocks.LECTERN.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH)); set(3, 1, 6, Blocks.BARREL);
shell(2, 0, 8, 6, 6, 12); door(6, 1, 10, Direction.WEST); set(3, 1, 9, Blocks.BOOKSHELF);
box(3, 0, 13, 14, 0, 15, Blocks.DARK_OAK_PLANKS);
box(3, 1, 15, 14, 1, 15, Blocks.DARK_OAK_FENCE); door(10, 1, 12, Direction.NORTH);
seat(6, 1, 13, Direction.SOUTH); hang(8, 6, 5); hang(4, 6, 10);
target("expedition_table", 5, 1, 5); target("balcony", 10, 1, 14);
target("secondary_entry", 10, 1, 13); loot(3, 1, 11, 4, 11);
}
boolean tower(int x, int z) { return x >= 3 && x <= 13 && z >= 3 && z <= 13 && Math.abs(x - 8) + Math.abs(z - 8) <= 8; }
void observatory() {
for (int y = 0; y <= 12; y++) for (int z = 3; z <= 13; z++) for (int x = 3; x <= 13; x++) if (tower(x, z)) {
boolean wall = !tower(x - 1, z) || !tower(x + 1, z) || !tower(x, z - 1) || !tower(x, z + 1);
set(x, y, z, y == 0 || y == 6 || y == 12 ? Blocks.SMOOTH_STONE.defaultBlockState()
: wall ? LostCityPalette.masonry(s.seed(), x, y, z) : Blocks.AIR.defaultBlockState());
if (y == 12 && wall) set(x, 13, z, Blocks.IRON_BARS);
}
door(8, 1, 3, Direction.SOUTH); door(8, 1, 13, Direction.NORTH);
northWindows(7, 9, 3, 8); box(13, 8, 7, 13, 9, 9, Blocks.GLASS_PANE);
shell(9, 0, 9, 12, 4, 12); door(9, 1, 10, Direction.EAST); set(10, 1, 11, Blocks.BARREL);
set(10, 7, 5, Blocks.CARTOGRAPHY_TABLE); set(11, 7, 8, Blocks.SMOOTH_STONE); lever(11, 8, 8, Direction.WEST);
stairs(4, 5, 0, 6); stairs(4, 5, 6, 6);
box(4, 12, 11, 6, 12, 11, Blocks.SMOOTH_STONE); box(4, 13, 11, 6, 15, 11, Blocks.AIR);
box(10, 13, 9, 10, 18, 9, Blocks.DARK_OAK_FENCE); box(9, 18, 9, 11, 18, 9, Blocks.IRON_BARS);
hang(8, 6, 6); hang(10, 4, 11);
target("instruments", 9, 7, 8); target("sky_platform", 8, 13, 8);
target("secondary_entry", 8, 1, 14); loot(11, 1, 11, 11, 10);
}
void railway() {
shell(1, 0, 2, 15, 6, 14); gable(1, 2, 15, 14, 6); doubleDoor(7, 1, 2);
for (int z = 3; z <= 13; z++) for (int x : new int[] {5, 11}) set(x, 1, z, Blocks.RAIL);
box(4, 1, 2, 6, 3, 2, Blocks.AIR); rail(8, 0, RailShape.NORTH_SOUTH); rail(8, 1, RailShape.NORTH_WEST);
rail(7, 1, RailShape.EAST_WEST); rail(6, 1, RailShape.EAST_WEST); rail(5, 1, RailShape.SOUTH_EAST); rail(5, 2, RailShape.NORTH_SOUTH);
box(7, -2, 5, 9, -2, 11, Blocks.SMOOTH_STONE); box(7, -1, 5, 9, 0, 11, Blocks.AIR);
for (int step = 0; step <= 2; step++) set(8, -step, 4 + step, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
for (int z = 6; z <= 11; z++) { set(6, 1, z, Blocks.IRON_BARS); set(10, 1, z, Blocks.IRON_BARS); }
set(2, 1, 4, Blocks.CRAFTING_TABLE); set(3, 1, 4, Blocks.FURNACE.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
set(2, 1, 6, Blocks.GRINDSTONE.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR));
shell(1, 0, 10, 5, 5, 14); door(5, 1, 12, Direction.WEST); door(8, 1, 14, Direction.NORTH);
set(5, 1, 9, Blocks.DARK_OAK_FENCE);
set(2, 1, 11, Blocks.ANVIL); hang(8, 6, 8); hang(3, 5, 12);
target("maintenance_pit", 8, -1, 9); target("workbench", 3, 1, 5);
target("secondary_entry", 8, 1, 15); loot(2, 1, 13, 3, 13);
}
void pumping() {
shell(2, 0, 3, 9, 6, 12); door(8, 1, 3, Direction.SOUTH);
set(3, 1, 5, Blocks.DISPENSER.defaultBlockState().setValue(BlockStateProperties.FACING, Direction.EAST));
set(4, 1, 5, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 3));
set(3, 2, 5, Blocks.POLISHED_ANDESITE); box(3, 3, 5, 3, 5, 8, Blocks.IRON_BARS);
for (int x : new int[] {10, 15}) for (int z : new int[] {5, 11}) box(x, 1, z, x, 7, z, Blocks.POLISHED_ANDESITE);
shell(10, 8, 5, 15, 13, 11); box(11, 9, 6, 14, 11, 10, Blocks.WATER);
box(10, 3, 6, 14, 3, 13, Blocks.SMOOTH_STONE); stairs(10, 3, 0, 3);
shell(2, 0, 10, 6, 5, 15); door(5, 1, 10, Direction.SOUTH); door(5, 1, 15, Direction.NORTH);
set(3, 1, 11, Blocks.BARREL); hang(7, 6, 7); hang(4, 5, 12);
target("pump_room", 6, 1, 6); target("reservoir_walkway", 13, 4, 12);
target("secondary_entry", 5, 1, 16); loot(3, 1, 14, 4, 14);
}
void substation() {
for (int z = 2; z <= 14; z++) { set(1, 1, z, Blocks.IRON_BARS); set(15, 1, z, Blocks.IRON_BARS); }
for (int x = 1; x <= 15; x++) if (x < 7 || x > 9) set(x, 1, 2, Blocks.IRON_BARS);
for (int z : new int[] {5, 8}) {
box(3, 1, z, 4, 2, z + 1, Blocks.POLISHED_ANDESITE); box(3, 3, z, 4, 3, z, Blocks.IRON_BARS);
}
shell(10, 0, 4, 15, 5, 10); door(10, 1, 6, Direction.EAST); northWindows(12, 14, 4, 2);
set(12, 1, 5, Blocks.SMOOTH_STONE); lever(12, 2, 5, Direction.SOUTH); set(14, 1, 5, Blocks.CRAFTING_TABLE);
shell(10, 0, 10, 15, 5, 15); door(12, 1, 10, Direction.SOUTH); door(15, 1, 13, Direction.WEST);
box(6, -2, 10, 7, -2, 13, Blocks.SMOOTH_STONE); box(6, -1, 10, 7, 0, 13, Blocks.AIR);
for (int step = 0; step <= 2; step++) set(6, -step, 9 + step, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
hang(13, 5, 7); hang(12, 5, 13); target("distribution_yard", 8, 1, 6); target("cable_trench", 6, -1, 12);
target("secondary_entry", 16, 1, 13); loot(14, 1, 14, 13, 14);
}
void greenhouse() {
shell(1, 0, 2, 11, 6, 14);
for (int y = 2; y <= 5; y++) for (int z = 3; z <= 13; z++) { set(1, y, z, Blocks.GLASS); set(11, y, z, Blocks.GLASS); }
northWindows(2, 5, 2, 2); northWindows(7, 10, 2, 2); door(6, 1, 2, Direction.SOUTH);
for (int x = 2; x <= 10; x++) for (int z = 3; z <= 13; z++) set(x, 6, z, x == 6 ? Blocks.SMOOTH_STONE : Blocks.GLASS);
for (int x : new int[] {2, 3, 4, 8, 9, 10}) for (int z = 4; z <= 10; z++) {
set(x, 0, z, Blocks.FARMLAND.defaultBlockState().setValue(BlockStateProperties.MOISTURE, 7));
set(x, 1, z, Blocks.WHEAT.defaultBlockState().setValue(BlockStateProperties.AGE_7,
2 + LostCityPalette.roll(s.seed(), x, 1, z, 6)));
}
set(3, 0, 6, Blocks.WATER); set(9, 0, 6, Blocks.WATER);
set(3, 1, 6, Blocks.AIR); set(9, 1, 6, Blocks.AIR);
shell(11, 0, 9, 15, 5, 15); door(11, 1, 12, Direction.EAST); door(13, 1, 15, Direction.NORTH);
set(12, 1, 10, Blocks.CRAFTING_TABLE); set(13, 1, 10, Blocks.FLOWER_POT); set(9, 1, 12, Blocks.COMPOSTER);
hang(6, 6, 5); hang(13, 5, 12); target("glasshouse_aisle", 6, 1, 7); target("potting_shed", 13, 1, 12);
target("secondary_entry", 13, 1, 16); loot(14, 1, 14, 13, 14);
}
void dock() {
box(0, 0, 0, 16, 0, 16, Blocks.DARK_OAK_PLANKS); box(0, 1, 16, 16, 1, 16, Blocks.DARK_OAK_FENCE);
box(0, 1, 1, 0, 1, 15, Blocks.DARK_OAK_FENCE); box(16, 1, 1, 16, 1, 15, Blocks.DARK_OAK_FENCE);
shell(2, 0, 3, 7, 5, 10); door(7, 1, 6, Direction.WEST); seat(3, 1, 5, Direction.EAST);
shell(2, 0, 10, 7, 5, 15); door(5, 1, 10, Direction.SOUTH); door(7, 1, 13, Direction.WEST);
box(12, 1, 5, 12, 11, 5, Blocks.STRIPPED_OAK_LOG); box(8, 11, 5, 14, 11, 5, Blocks.STRIPPED_OAK_LOG);
box(14, 7, 5, 14, 10, 5, Blocks.IRON_CHAIN); hang(10, 11, 5); hang(5, 5, 6); hang(5, 5, 12);
target("departure_deck", 8, 1, 14); target("waiting_room", 5, 1, 6); target("secondary_entry", 8, 1, 13);
loot(3, 1, 14, 4, 14);
}
void radio() {
shell(2, 0, 3, 10, 6, 12); door(8, 1, 3, Direction.SOUTH); northWindows(3, 5, 3, 2);
bed(4, 1, 5, Direction.NORTH); set(8, 1, 5, Blocks.CARTOGRAPHY_TABLE);
shell(2, 0, 8, 6, 6, 12); door(6, 1, 10, Direction.WEST); door(2, 1, 10, Direction.EAST);
box(11, 5, 6, 15, 5, 10, Blocks.SMOOTH_STONE);
for (int x : new int[] {11, 15}) for (int z : new int[] {6, 10}) box(x, 1, z, x, 4, z, Blocks.POLISHED_ANDESITE);
box(13, 1, 8, 13, 18, 8, Blocks.POLISHED_ANDESITE); box(11, 17, 8, 15, 17, 8, Blocks.IRON_BARS);
box(13, 19, 8, 13, 21, 8, Blocks.IRON_BARS); stairs(11, 1, 0, 5);
hang(7, 6, 6); hang(4, 6, 10);
target("caretaker_room", 7, 1, 6); target("antenna_platform", 12, 6, 9); target("secondary_entry", 1, 1, 10);
loot(3, 1, 11, 4, 11);
}
void photo() {
shell(2, 0, 2, 14, 6, 14); door(8, 1, 2, Direction.SOUTH); northWindows(3, 6, 2, 2); northWindows(10, 13, 2, 2);
box(3, 1, 8, 13, 5, 8, Blocks.SMOOTH_STONE); door(4, 1, 8, Direction.SOUTH);
box(8, 1, 9, 8, 5, 13, Blocks.SMOOTH_STONE); door(8, 1, 11, Direction.EAST);
box(10, 1, 5, 12, 1, 5, Blocks.DARK_OAK_PLANKS); set(3, 1, 4, Blocks.BOOKSHELF);
for (int z : new int[] {10, 12}) set(3, 1, z, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 3));
box(11, 4, 10, 11, 5, 12, Blocks.IRON_CHAIN); set(12, 1, 10, Blocks.CRAFTING_TABLE);
door(12, 1, 14, Direction.NORTH); hang(8, 6, 4); hang(5, 6, 11); hang(12, 6, 12);
target("club_room", 8, 1, 4); target("darkroom", 5, 1, 11); target("drying_room", 10, 1, 11);
target("secondary_entry", 12, 1, 15); loot(13, 1, 13, 12, 13);
}
void impossible() {
shell(3, 0, 2, 11, 4, 7); door(8, 1, 2, Direction.SOUTH);
for (int z = 3; z <= 5; z++) { set(4, 1, z, Blocks.BARREL); set(4, 2, z, Blocks.BARREL); }
seat(10, 1, 4, Direction.WEST);
shell(1, 0, 7, 15, 10, 15); door(8, 1, 7, Direction.SOUTH);
for (int x = 2; x <= 14; x++) for (int z = 8; z <= 14; z++) set(x, 0, z,
((x + z) & 1) == 0 ? Blocks.CALCITE : Blocks.POLISHED_ANDESITE);
for (int x : new int[] {3, 6, 10, 13}) box(x, 1, 11, x, 9, 11, Blocks.CALCITE);
shell(1, 0, 12, 5, 5, 15); door(5, 1, 13, Direction.WEST); door(3, 1, 15, Direction.NORTH);
set(2, 2, 12, Blocks.GLASS); set(2, 1, 13, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 2));
hang(8, 4, 4); hang(8, 10, 11); hang(3, 5, 13);
target("lockers", 8, 1, 4); target("oversized_tiled_hall", 8, 1, 11);
target("secondary_entry", 3, 1, 16); loot(2, 1, 14, 3, 14);
}
void refuge() {
box(3, 4, 5, 14, 4, 15, Blocks.DARK_OAK_PLANKS);
for (int x : new int[] {3, 14}) for (int z : new int[] {5, 15}) box(x, 1, z, x, 3, z, Blocks.STONE_BRICKS);
shell(5, 4, 6, 13, 10, 14); gable(5, 6, 13, 14, 10); door(8, 5, 6, Direction.SOUTH);
bed(6, 5, 8, Direction.NORTH); set(12, 5, 8, Blocks.CRAFTING_TABLE);
shell(6, 0, 9, 11, 4, 14); door(6, 1, 11, Direction.EAST);
// Carry the ladder through the floor so opening the hatch leaves a climbable lip.
for (int y = 1; y <= 4; y++) set(10, y, 11, Blocks.LADDER.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST));
set(10, 5, 11, Blocks.DARK_OAK_TRAPDOOR.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST)
.setValue(BlockStateProperties.HALF, Half.BOTTOM));
stairs(7, 1, 0, 4); box(2, 1, 3, 3, 11, 4, Blocks.STONE_BRICKS); box(2, 11, 3, 14, 11, 4, Blocks.STRIPPED_OAK_LOG);
hang(10, 10, 8); hang(8, 4, 12);
target("suspended_hut", 9, 5, 8); target("hidden_floor_hatch", 10, 5, 11); target("secondary_entry", 5, 1, 11);
loot(7, 1, 13, 8, 13);
}
}
}
@@ -0,0 +1,770 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.worldgen.city.LostCityPalette;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import net.minecraft.core.Direction;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.AttachFace;
import net.minecraft.world.level.block.state.properties.BedPart;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.DoorHingeSide;
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
import net.minecraft.world.level.block.state.properties.Half;
import net.minecraft.world.level.block.state.properties.RailShape;
/** Generation21 restrained architectural rendition of the immutable generation19 site geometry. The saved site controls all geometry and loot.
* Site writes are local to minX/floorY/minZ; route writes are local to the segment's minimum bounds. */
public final class HeritageRenderer21 {
private HeritageRenderer21() {}
public record Target(String name, int x, int y, int z) {}
/** These are already rotated local standing positions, not world coordinates. */
public static List<Target> targets(HeritagePlan19.Site site) {
var b = new Builder(site, null); b.layout(); return List.copyOf(b.targets);
}
public static void render(HeritagePlan19.Site site, LostCityRenderer.BlockWriter writer) {
var buffer = ConnectedBlocks20.buffer(writer);
var b = new Builder(site, buffer); b.ground(); b.layout();
// Footings have absolute saved coordinates and must not undergo a second facade rotation.
for (var support : site.supports()) for (int y = support.minY(); y <= support.maxY(); y++)
buffer.set(support.x() - site.minX(), y - site.floorY(), support.z() - site.minZ(),
y == support.minY() ? Blocks.CHISELED_TUFF_BRICKS.defaultBlockState() : Blocks.STONE_BRICKS.defaultBlockState());
buffer.flush();
}
public static void renderSegment(HeritagePlan19.Segment segment, LostCityRenderer.BlockWriter writer) {
var buffer = ConnectedBlocks20.buffer(writer);
var occupied = new java.util.HashSet<Long>();
for (var cell : segment.cells()) occupied.add(columnKey(cell.x(), cell.z()));
int x0 = segment.minX(), y0 = segment.minY(), z0 = segment.minZ();
for (var cell : segment.cells()) {
int x = cell.x() - x0, z = cell.z() - z0, floor = cell.floorY() - y0;
for (int y = cell.minY() - y0; y <= floor; y++) {
Block material = Blocks.STONE_BRICKS;
if (y == floor) {
boolean edge = false;
for (Direction d : new Direction[] {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}) {
int nx = cell.x() + d.getStepX(), nz = cell.z() + d.getStepZ();
// Unknown neighboring chunks are continuations, not artificial crosswise curbs.
if ((nx >> 4) == segment.startChunkX() && (nz >> 4) == segment.startChunkZ()
&& !occupied.contains(columnKey(nx, nz))) edge = true;
}
if (cell.rail() != HeritagePlan19.Rail.NONE) material = Math.floorMod(cell.x() + cell.z(), 3) == 0 ? Blocks.OAK_PLANKS : Blocks.GRAVEL;
else if (edge) material = Math.floorMod(cell.x() + cell.z(), 8) == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS;
else material = LostCityPalette.roll(segment.seed(), cell.x(), 0, cell.z(), 5) == 0 ? Blocks.TUFF : Blocks.POLISHED_TUFF;
}
buffer.set(x, y, z, material.defaultBlockState());
}
for (int y = floor + 1; y <= floor + 3; y++) buffer.set(x, y, z, Blocks.AIR.defaultBlockState());
if (cell.railPresent()) buffer.set(x, floor + 1, z, Blocks.RAIL.defaultBlockState()
.setValue(BlockStateProperties.RAIL_SHAPE, RailShape.valueOf(cell.rail().name())));
}
buffer.flush();
}
private static long columnKey(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
private static final class Builder {
final HeritagePlan19.Site s;
final LostCityRenderer.BlockWriter out;
final List<Target> targets = new ArrayList<>();
final Rotation rotation;
Builder(HeritagePlan19.Site s, LostCityRenderer.BlockWriter out) {
this.s = s; this.out = out;
rotation = switch (s.facing()) { case 1 -> Rotation.CLOCKWISE_90; case 2 -> Rotation.CLOCKWISE_180;
case 3 -> Rotation.COUNTERCLOCKWISE_90; default -> Rotation.NONE; };
}
int rx(int x, int z) { return switch (s.facing()) { case 1 -> 16 - z; case 2 -> 16 - x; case 3 -> z; default -> x; }; }
int rz(int x, int z) { return switch (s.facing()) { case 1 -> x; case 2 -> 16 - z; case 3 -> 16 - x; default -> z; }; }
void set(int x, int y, int z, BlockState state) {
if (x < 0 || x > 16 || z < 0 || z > 16 || y < -s.foundationDepth() || y > s.height())
throw new IllegalArgumentException("Heritage " + s.kind() + " renderer escaped saved site: " + x + "/" + y + "/" + z);
if (out != null) out.set(rx(x, z), y, rz(x, z), palette(state, x, y, z).rotate(rotation));
}
void set(int x, int y, int z, Block b) { set(x, y, z, b.defaultBlockState()); }
void box(int x0, int y0, int z0, int x1, int y1, int z1, Block b) {
for (int y = y0; y <= y1; y++) for (int z = z0; z <= z1; z++) for (int x = x0; x <= x1; x++) set(x, y, z, b);
}
void target(String name, int x, int y, int z) { targets.add(new Target(name, rx(x, z), y, rz(x, z))); }
void ground() {
for (int y = -s.foundationDepth(); y <= s.height(); y++) for (int z = 0; z <= 16; z++) for (int x = 0; x <= 16; x++)
set(x, y, z, y < 0 ? Blocks.STONE_BRICKS.defaultBlockState() : y == 0
? (x == 0 || z == 0 || x == 16 || z == 16 ? Blocks.TUFF_BRICKS
: x >= 7 && x <= 9 ? Blocks.POLISHED_TUFF
: LostCityPalette.roll(s.seed(), x, 0, z, 7) == 0 ? Blocks.TUFF : Blocks.ANDESITE).defaultBlockState()
: Blocks.AIR.defaultBlockState());
}
void layout() {
target("main_entry", 8, 1, 0);
switch (s.kind()) {
case FREIGHT_STATION -> station();
case POST_OFFICE -> post();
case COMMUNAL_DEPOT -> depot();
case EXPEDITION_OFFICE -> expedition();
case OBSERVATORY -> observatory();
case RAIL_WORKSHOP -> railway();
case PUMP_STATION -> pumping();
case SUBSTATION -> substation();
case GREENHOUSE -> greenhouse();
case VOID_DOCK -> dock();
case RADIO_RELAY -> radio();
case PHOTO_CLUB -> photo();
case IMPOSSIBLE_SERVICE -> impossible();
case CONSTRUCTION_REFUGE -> refuge();
}
architecture();
roofs21();
}
void shell(int x0, int floor, int z0, int x1, int ceiling, int z1) {
for (int y = floor; y <= ceiling; y++) for (int z = z0; z <= z1; z++) for (int x = x0; x <= x1; x++) {
boolean wall = x == x0 || x == x1 || z == z0 || z == z1;
set(x, y, z, y == floor ? floorMaterial().defaultBlockState() : y == ceiling ? Blocks.SMOOTH_STONE.defaultBlockState()
: wall ? wallMaterial(y - floor, ceiling - floor).defaultBlockState() : Blocks.AIR.defaultBlockState());
}
}
Block floorMaterial() {
return switch (s.kind()) {
case POST_OFFICE, EXPEDITION_OFFICE, RADIO_RELAY, CONSTRUCTION_REFUGE -> Blocks.OAK_PLANKS;
case PHOTO_CLUB, GREENHOUSE -> Blocks.BIRCH_PLANKS;
case IMPOSSIBLE_SERVICE -> Blocks.POLISHED_DIORITE;
default -> Blocks.POLISHED_ANDESITE;
};
}
Block wallMaterial(int y, int height) {
if (y == 1) return Blocks.STONE_BRICKS;
if (y == height - 1) return Blocks.ANDESITE;
return s.kind() == HeritagePlan19.Kind.GREENHOUSE ? Blocks.BIRCH_PLANKS : Blocks.SMOOTH_STONE;
}
BlockState palette(BlockState state, int x, int y, int z) {
Block original = state.getBlock(), replacement = original;
boolean birch = s.kind() == HeritagePlan19.Kind.GREENHOUSE;
if (original == Blocks.DARK_OAK_PLANKS || original == Blocks.OAK_PLANKS || original == Blocks.BIRCH_PLANKS)
replacement = birch ? Blocks.BIRCH_PLANKS : Blocks.OAK_PLANKS;
else if (original == Blocks.OAK_WOOD || original == Blocks.BIRCH_WOOD || original == Blocks.STRIPPED_OAK_WOOD)
replacement = birch ? Blocks.BIRCH_WOOD : Blocks.OAK_WOOD;
else if (original == Blocks.OAK_LOG || original == Blocks.BIRCH_LOG || original == Blocks.STRIPPED_OAK_LOG)
replacement = birch ? Blocks.STRIPPED_BIRCH_LOG : Blocks.STRIPPED_OAK_LOG;
else if (original == Blocks.DARK_OAK_SLAB || original == Blocks.OAK_SLAB || original == Blocks.BIRCH_SLAB)
replacement = birch ? Blocks.BIRCH_SLAB : Blocks.OAK_SLAB;
else if (original == Blocks.DARK_OAK_STAIRS || original == Blocks.OAK_STAIRS || original == Blocks.BIRCH_STAIRS)
replacement = birch ? Blocks.BIRCH_STAIRS : Blocks.OAK_STAIRS;
else if (original == Blocks.DARK_OAK_FENCE || original == Blocks.OAK_FENCE || original == Blocks.BIRCH_FENCE)
replacement = birch ? Blocks.BIRCH_FENCE : Blocks.OAK_FENCE;
else if (original == Blocks.DARK_OAK_DOOR || original == Blocks.OAK_DOOR || original == Blocks.BIRCH_DOOR)
replacement = birch ? Blocks.BIRCH_DOOR : Blocks.OAK_DOOR;
else if (original == Blocks.DARK_OAK_TRAPDOOR || original == Blocks.OAK_TRAPDOOR || original == Blocks.BIRCH_TRAPDOOR)
replacement = birch ? Blocks.BIRCH_TRAPDOOR : Blocks.OAK_TRAPDOOR;
else if (original == Blocks.GRANITE || original == Blocks.POLISHED_GRANITE || original == Blocks.DIORITE
|| original == Blocks.POLISHED_DIORITE || original == Blocks.POLISHED_ANDESITE)
replacement = original == Blocks.POLISHED_ANDESITE ? Blocks.ANDESITE : Blocks.SMOOTH_STONE;
else if (original == Blocks.POLISHED_GRANITE_SLAB || original == Blocks.POLISHED_DIORITE_SLAB
|| original == Blocks.POLISHED_ANDESITE_SLAB) replacement = Blocks.SMOOTH_STONE_SLAB;
if (y > 0) {
if (original == Blocks.TUFF || original == Blocks.POLISHED_TUFF) replacement = Blocks.SMOOTH_STONE;
else if (original == Blocks.TUFF_BRICKS) replacement = Blocks.STONE_BRICKS;
else if (original == Blocks.CHISELED_TUFF || original == Blocks.CHISELED_TUFF_BRICKS) replacement = Blocks.CHISELED_STONE_BRICKS;
else if (original == Blocks.TUFF_BRICK_SLAB) replacement = Blocks.SMOOTH_STONE_SLAB;
else if (original == Blocks.TUFF_BRICK_STAIRS) replacement = Blocks.STONE_BRICK_STAIRS;
else if (original == Blocks.TUFF_BRICK_WALL) replacement = Blocks.STONE_BRICK_WALL;
}
if ((original == Blocks.STONE_BRICKS || original == Blocks.CRACKED_STONE_BRICKS
|| original == Blocks.MOSSY_STONE_BRICKS) && y > 1 && y % 5 != 0)
replacement = Blocks.SMOOTH_STONE;
if (replacement == Blocks.ANDESITE && y > 0 && Math.floorMod(x + z, 4) == 0)
replacement = Blocks.STONE;
// Quiet, reproducible weathering follows construction joints, never random holes in access floors.
if (replacement == Blocks.STONE_BRICKS && y > 0) {
int weather = LostCityPalette.roll(s.seed(), x, y, z, 23);
if (weather == 0) replacement = Blocks.CRACKED_STONE_BRICKS;
else if (weather == 1 && y <= 3) replacement = Blocks.MOSSY_STONE_BRICKS;
}
if (replacement == original) return state;
var mapped = replacement.defaultBlockState();
for (var property : state.getProperties()) mapped = copyProperty(mapped, state, property);
return mapped;
}
private static <T extends Comparable<T>> BlockState copyProperty(BlockState destination, BlockState source,
net.minecraft.world.level.block.state.properties.Property<T> property) {
return destination.hasProperty(property) ? destination.setValue(property, source.getValue(property)) : destination;
}
// These additions compose each use separately; all thresholds and maintenance passages stay in the saved footprint.
void architecture() {
switch (s.kind()) {
case FREIGHT_STATION -> stationArchitecture();
case POST_OFFICE -> postArchitecture();
case COMMUNAL_DEPOT -> depotArchitecture();
case EXPEDITION_OFFICE -> expeditionArchitecture();
case OBSERVATORY -> observatoryArchitecture();
case RAIL_WORKSHOP -> railwayArchitecture();
case PUMP_STATION -> pumpingArchitecture();
case SUBSTATION -> substationArchitecture();
case GREENHOUSE -> greenhouseArchitecture();
case VOID_DOCK -> dockArchitecture();
case RADIO_RELAY -> radioArchitecture();
case PHOTO_CLUB -> photoArchitecture();
case IMPOSSIBLE_SERVICE -> impossibleArchitecture();
case CONSTRUCTION_REFUGE -> refugeArchitecture();
}
}
void slab(int x, int y, int z, Block block) { set(x, y, z, block.defaultBlockState().setValue(BlockStateProperties.SLAB_TYPE, net.minecraft.world.level.block.state.properties.SlabType.BOTTOM)); }
void stair(int x, int y, int z, Block block, Direction direction, Half half) {
set(x, y, z, block.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, direction).setValue(BlockStateProperties.HALF, half));
}
/** Cut only the four exterior corner cells; the two adjacent wall faces keep the interior sealed. */
void bevel(int x0, int z0, int x1, int z1, int low, int high) {
for (int x : new int[] {x0, x1}) for (int z : new int[] {z0, z1}) {
box(x, low, z, x, high, z, Blocks.AIR);
set(x, low - 1, z, Blocks.CHISELED_STONE_BRICKS);
}
}
void rim(int x0, int z0, int x1, int z1, int y, Block material) {
for (int x = x0; x <= x1; x++) { slab(x, y, z0, material); slab(x, y, z1, material); }
for (int z = z0 + 1; z < z1; z++) { slab(x0, y, z, material); slab(x1, y, z, material); }
}
void screen(int x0, int z, int x1, int y) {
for (int x = x0; x <= x1; x++) set(x, y, z, Blocks.BIRCH_TRAPDOOR.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH).setValue(BlockStateProperties.OPEN, true));
}
void stationArchitecture() {
bevel(4, 3, 12, 11, 1, 6);
box(4, 7, 3, 12, 10, 11, Blocks.AIR);
rim(3, 2, 13, 12, 6, Blocks.TUFF_BRICK_SLAB);
for (int z = 5; z <= 9; z++) for (int x = 5; x <= 11; x++) {
if (x == 5 || x == 11 || z == 5 || z == 9) box(x, 7, z, x, 8, z, Blocks.GLASS_PANE);
slab(x, 9, z, Blocks.POLISHED_DIORITE_SLAB);
}
// Open the former ceiling beneath the sealed roof glazing. The middle beam
// still supports the existing hanging lantern and joins the two hall walls.
for (int z = 6; z <= 8; z++) for (int x = 6; x <= 10; x++) {
if (z != 7) set(x, 6, z, Blocks.AIR);
set(x, 9, z, Blocks.GLASS);
}
// The long cantilever reads as a train platform, distinct from the glazed passenger hall.
box(0, 5, 4, 3, 5, 12, Blocks.OAK_PLANKS);
for (int z : new int[] {4, 8, 12}) stair(0, 4, z, Blocks.TUFF_BRICK_STAIRS, Direction.EAST, Half.TOP);
for (int z : new int[] {5, 11}) box(1, 1, z, 1, 4, z, Blocks.OAK_WOOD);
for (int x : new int[] {4, 12}) box(x, 2, 4, x, 4, 4, Blocks.CHISELED_TUFF_BRICKS);
screen(5, 4, 6, 3); box(5, 0, 4, 11, 0, 10, Blocks.POLISHED_DIORITE);
box(7, 0, 3, 9, 0, 10, Blocks.BIRCH_PLANKS);
rim(9, 11, 15, 15, 4, Blocks.TUFF_BRICK_SLAB);
}
void postArchitecture() {
bevel(2, 2, 14, 14, 1, 6); rim(1, 1, 15, 15, 6, Blocks.POLISHED_GRANITE_SLAB);
box(4, 7, 4, 12, 7, 12, Blocks.TUFF_BRICKS);
rim(4, 4, 12, 12, 8, Blocks.TUFF_BRICK_SLAB);
for (int x : new int[] {2, 7, 9, 14}) {
box(x, 3, 1, x, 4, 1, Blocks.POLISHED_DIORITE);
stair(x, 5, 1, Blocks.TUFF_BRICK_STAIRS, Direction.SOUTH, Half.TOP);
}
box(5, 4, 1, 11, 4, 1, Blocks.CHISELED_TUFF_BRICKS);
box(4, 1, 6, 6, 1, 6, Blocks.BIRCH_PLANKS); screen(4, 6, 6, 3);
for (int z = 9; z <= 12; z++) { set(3, 3, z, Blocks.OAK_TRAPDOOR.defaultBlockState().setValue(BlockStateProperties.HALF, Half.TOP)); }
for (int x = 4; x <= 12; x++) set(x, 0, 5, x % 2 == 0 ? Blocks.POLISHED_GRANITE : Blocks.OAK_PLANKS);
box(10, 0, 8, 13, 0, 9, Blocks.DIORITE);
}
void depotArchitecture() {
bevel(1, 2, 15, 14, 1, 9); box(1, 10, 2, 15, 14, 14, Blocks.AIR);
// Three industrial north-light roof bays, not another pitched house roof.
for (int z : new int[] {3, 7, 11}) {
box(2, 10, z, 14, 11, z, Blocks.GLASS_PANE);
for (int x = 2; x <= 14; x++) {
slab(x, 12, z, Blocks.TUFF_BRICK_SLAB);
stair(x, 11, z + 1, Blocks.TUFF_BRICK_STAIRS, Direction.NORTH, Half.BOTTOM);
stair(x, 10, z + 2, Blocks.TUFF_BRICK_STAIRS, Direction.NORTH, Half.BOTTOM);
if (x > 2 && x < 14) {
// Both transparent faces close the north-light well, while the
// retained roof outside these bands keeps the warehouse sealed.
set(x, 9, z, Blocks.GLASS);
set(x, 12, z, Blocks.GLASS);
}
}
}
for (int z : new int[] {3, 7, 11}) for (int x : new int[] {1, 15}) box(x, 1, z, x, 9, z, Blocks.POLISHED_ANDESITE);
for (int x : new int[] {6, 10}) { box(x, 1, 1, x, 3, 1, Blocks.CHISELED_TUFF); stair(x, 4, 1, Blocks.TUFF_BRICK_STAIRS, Direction.SOUTH, Half.TOP); }
box(6, 4, 1, 10, 4, 1, Blocks.TUFF_BRICKS);
for (int z : new int[] {5, 8}) {
// Thin wooden covers sit directly on the barrel stack and the lower packing bench.
set(4, 3, z, Blocks.OAK_TRAPDOOR.defaultBlockState().setValue(BlockStateProperties.HALF, Half.BOTTOM));
set(12, 2, z, Blocks.OAK_TRAPDOOR.defaultBlockState().setValue(BlockStateProperties.HALF, Half.BOTTOM));
}
for (int z = 3; z <= 9; z++) { set(6, 0, z, Blocks.TUFF_BRICKS); set(10, 0, z, Blocks.TUFF_BRICKS); }
box(2, 5, 11, 9, 5, 13, Blocks.BIRCH_PLANKS);
}
void expeditionArchitecture() {
bevel(2, 2, 12, 12, 1, 6); rim(1, 1, 13, 13, 6, Blocks.TUFF_BRICK_SLAB);
box(3, 7, 3, 6, 9, 7, Blocks.BIRCH_WOOD); rim(3, 3, 6, 7, 10, Blocks.TUFF_BRICK_SLAB);
box(4, 8, 3, 5, 8, 3, Blocks.GLASS_PANE);
// Sheltered map table, archive wing and broad shaded rear balcony.
box(7, 4, 1, 9, 4, 1, Blocks.CHISELED_TUFF_BRICKS);
for (int x : new int[] {2, 12}) box(x, 2, 3, x, 5, 3, Blocks.OAK_WOOD);
screen(3, 4, 5, 3); box(3, 3, 9, 5, 3, 9, Blocks.OAK_PLANKS);
for (int x = 3; x <= 14; x++) for (int z = 13; z <= 15; z++) set(x, 0, z, x % 3 == 0 ? Blocks.OAK_WOOD : Blocks.OAK_PLANKS);
for (int x = 3; x <= 14; x++) slab(x, 5, 14, Blocks.BIRCH_SLAB);
box(3, 1, 14, 3, 4, 14, Blocks.BIRCH_FENCE); box(14, 1, 14, 14, 4, 14, Blocks.BIRCH_FENCE);
}
void observatoryArchitecture() {
// Eight faceted buttresses below a broken observatory dome with an open central viewing slot.
for (int[] p : new int[][] {{3, 8}, {5, 4}, {11, 4}, {13, 8}, {11, 12}, {5, 12}}) {
box(p[0], 1, p[1], p[0], 11, p[1], Blocks.DIORITE);
set(p[0], 5, p[1], Blocks.CHISELED_TUFF_BRICKS);
stair(p[0], 12, p[1], Blocks.TUFF_BRICK_STAIRS, p[0] < 8 ? Direction.EAST : Direction.WEST, Half.BOTTOM);
}
for (int z = 4; z <= 12; z++) for (int x = 4; x <= 12; x++) {
int distance = Math.max(Math.abs(x - 8), Math.abs(z - 8));
if (distance == 4) set(x, 15, z, Blocks.GLASS_PANE);
if (distance == 3 && x != 8) slab(x, 17, z, Blocks.TUFF_BRICK_SLAB);
}
for (int x : new int[] {4, 12}) for (int z : new int[] {5, 11}) box(x, 13, z, x, 14, z, Blocks.CHISELED_TUFF_BRICKS);
// Four short ribs visibly carry the inset crown; the central viewing slot stays open.
for (int x : new int[] {4, 12}) for (int z : new int[] {5, 11}) {
box(x, 15, z, x, 16, z, Blocks.CHISELED_TUFF_BRICKS);
stair(x < 8 ? x + 1 : x - 1, 16, z, Blocks.TUFF_BRICK_STAIRS,
x < 8 ? Direction.EAST : Direction.WEST, Half.BOTTOM);
}
box(7, 0, 5, 9, 0, 11, Blocks.BIRCH_PLANKS);
box(7, 6, 5, 9, 6, 10, Blocks.POLISHED_DIORITE);
}
void railwayArchitecture() {
bevel(1, 2, 15, 14, 1, 6); box(1, 7, 2, 15, 10, 14, Blocks.AIR);
// Broad barrel-vault ribs sit above the two inspection tracks.
for (int z = 2; z <= 14; z++) for (int x = 1; x <= 15; x++) {
int rise = Math.min(3, Math.min(x - 1, 15 - x));
if (z % 4 == 2) set(x, 6 + rise, z, Blocks.TUFF_BRICKS);
else if (x >= 6 && x <= 10) set(x, 6 + rise, z, Blocks.GLASS);
else slab(x, 6 + rise, z, Blocks.TUFF_BRICK_SLAB);
}
for (int z : new int[] {3, 7, 11}) for (int x : new int[] {1, 15}) box(x, 2, z, x, 5, z, Blocks.GRANITE);
box(7, -2, 5, 9, -2, 11, Blocks.POLISHED_ANDESITE);
for (int z = 6; z <= 11; z++) { set(7, -1, z, Blocks.CHISELED_TUFF_BRICKS); set(9, -1, z, Blocks.CHISELED_TUFF_BRICKS); }
// Side ledges leave the one-block maintenance aisle and its stairs clear.
screen(2, 3, 3, 3); box(2, 3, 11, 4, 3, 11, Blocks.OAK_PLANKS);
}
void pumpingArchitecture() {
bevel(2, 3, 9, 12, 1, 6); rim(1, 2, 9, 13, 6, Blocks.TUFF_BRICK_SLAB);
for (int x : new int[] {10, 15}) for (int z : new int[] {5, 11}) {
box(x, 1, z, x, 7, z, Blocks.CHISELED_TUFF_BRICKS);
set(x, 8, z, Blocks.POLISHED_ANDESITE);
}
rim(9, 4, 16, 12, 13, Blocks.TUFF_BRICK_SLAB);
for (int x = 10; x <= 15; x++) for (int z = 5; z <= 11; z++) slab(x, 14, z, Blocks.POLISHED_ANDESITE_SLAB);
box(3, 7, 5, 3, 12, 5, Blocks.TUFF_BRICK_WALL);
box(3, 3, 6, 3, 5, 8, Blocks.IRON_BARS);
box(3, 0, 4, 8, 0, 9, Blocks.ANDESITE); box(5, 0, 4, 7, 0, 10, Blocks.POLISHED_DIORITE);
screen(3, 11, 4, 3);
}
void substationArchitecture() {
bevel(10, 4, 15, 10, 1, 5); rim(9, 3, 16, 15, 5, Blocks.TUFF_BRICK_SLAB);
for (int z : new int[] {2, 6, 10, 14}) for (int x : new int[] {1, 15}) {
set(x, 0, z, Blocks.STONE_BRICKS); set(x, 1, z, Blocks.CHISELED_TUFF_BRICKS);
}
box(2, 4, 5, 5, 4, 5, Blocks.IRON_BARS); box(2, 4, 8, 5, 4, 8, Blocks.IRON_BARS);
for (int x : new int[] {2, 5}) for (int z : new int[] {5, 8}) box(x, 1, z, x, 3, z, Blocks.TUFF_BRICK_WALL);
box(11, 6, 5, 14, 6, 9, Blocks.POLISHED_TUFF);
for (int z = 5; z <= 9; z += 2) screen(11, z, 14, 7);
for (int z = 3; z <= 8; z++) set(7, 0, z, Blocks.GRANITE);
box(6, -2, 10, 7, -2, 13, Blocks.CHISELED_TUFF_BRICKS);
}
void greenhouseArchitecture() {
// Tall birch ribs and a steep glass ridge give the glasshouse an agricultural silhouette.
for (int z = 2; z <= 14; z++) for (int x = 1; x <= 11; x++) {
int rise = Math.min(3, Math.min(x - 1, 11 - x));
if (z == 2 || z == 8 || z == 14 || x == 6) set(x, 6 + rise, z, Blocks.BIRCH_WOOD);
else set(x, 6 + rise, z, Blocks.GLASS);
}
for (int z : new int[] {3, 7, 11}) { box(1, 1, z, 1, 5, z, Blocks.BIRCH_WOOD); box(11, 1, z, 11, 5, z, Blocks.BIRCH_WOOD); }
rim(11, 9, 15, 15, 5, Blocks.TUFF_BRICK_SLAB);
box(12, 3, 10, 14, 3, 10, Blocks.OAK_PLANKS); screen(12, 10, 14, 4);
for (int z = 3; z <= 13; z++) set(6, 0, z, z % 3 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.POLISHED_TUFF);
}
void dockArchitecture() {
for (int x = 1; x <= 15; x++) for (int z = 0; z <= 15; z++) set(x, 0, z, x % 4 == 0 ? Blocks.OAK_WOOD : Blocks.OAK_PLANKS);
bevel(2, 3, 7, 10, 1, 5); rim(1, 2, 8, 15, 5, Blocks.BIRCH_SLAB);
for (int z : new int[] {2, 8, 14}) {
box(0, 1, z, 0, 2, z, Blocks.CHISELED_TUFF_BRICKS);
box(16, 1, z, 16, 2, z, Blocks.CHISELED_TUFF_BRICKS);
slab(0, 3, z, Blocks.TUFF_BRICK_SLAB); slab(16, 3, z, Blocks.TUFF_BRICK_SLAB);
}
// Brackets and a separated customs roof leave the departure platform open to the void.
for (int x = 2; x <= 7; x++) for (int z = 3; z <= 15; z++) if (z % 4 == 0) slab(x, 6, z, Blocks.BIRCH_SLAB);
stair(11, 9, 5, Blocks.OAK_STAIRS, Direction.EAST, Half.TOP);
stair(13, 9, 5, Blocks.OAK_STAIRS, Direction.WEST, Half.TOP);
screen(3, 4, 6, 3);
}
void radioArchitecture() {
bevel(2, 3, 10, 12, 1, 6); rim(1, 2, 10, 13, 6, Blocks.TUFF_BRICK_SLAB);
for (int x = 2; x <= 10; x++) for (int z = 3; z <= 12; z++) {
int roofY = 7 + Math.min(2, Math.min(x - 2, 10 - x));
slab(x, roofY, z, x <= 5 ? Blocks.BIRCH_SLAB : Blocks.TUFF_BRICK_SLAB);
}
box(9, 7, 4, 9, 11, 4, Blocks.CHISELED_TUFF_BRICKS);
for (int y : new int[] {10, 14, 18}) { box(12, y, 8, 14, y, 8, Blocks.IRON_BARS); box(13, y, 7, 13, y, 9, Blocks.IRON_BARS); }
box(3, 0, 8, 5, 0, 11, Blocks.BIRCH_PLANKS); screen(7, 4, 9, 3);
}
void photoArchitecture() {
bevel(2, 2, 14, 14, 1, 6); rim(1, 1, 15, 15, 6, Blocks.POLISHED_DIORITE_SLAB);
box(3, 7, 9, 13, 7, 13, Blocks.TUFF_BRICKS); rim(3, 9, 13, 13, 8, Blocks.TUFF_BRICK_SLAB);
for (int x : new int[] {3, 6, 10, 13}) stair(x, 4, 1, Blocks.TUFF_BRICK_STAIRS, Direction.SOUTH, Half.TOP);
box(3, 5, 1, 13, 5, 1, Blocks.CHISELED_TUFF_BRICKS);
for (int x = 4; x <= 12; x++) set(x, 0, 4, x % 2 == 0 ? Blocks.POLISHED_GRANITE : Blocks.BIRCH_PLANKS);
box(3, 0, 9, 7, 0, 13, Blocks.POLISHED_ANDESITE);
screen(10, 5, 12, 2); box(3, 3, 10, 3, 3, 12, Blocks.OAK_PLANKS);
}
void impossibleArchitecture() {
bevel(1, 7, 15, 15, 1, 10); rim(0, 6, 16, 16, 10, Blocks.TUFF_BRICK_SLAB);
box(4, 11, 9, 12, 11, 13, Blocks.TUFF_BRICKS);
for (int x : new int[] {4, 12}) for (int z : new int[] {9, 13}) set(x, 12, z, Blocks.CHISELED_TUFF);
// A cramped civil facade gives way to an over-scaled, deliberately ceremonial service hall.
for (int x : new int[] {3, 11}) box(x, 1, 1, x, 4, 1, Blocks.CHISELED_TUFF_BRICKS);
box(3, 5, 1, 11, 5, 1, Blocks.TUFF_BRICKS);
for (int x : new int[] {3, 6, 10, 13}) { set(x, 1, 11, Blocks.CHISELED_TUFF_BRICKS); set(x, 8, 11, Blocks.CHISELED_TUFF_BRICKS); }
screen(4, 3, 4, 3); box(12, 3, 14, 14, 3, 14, Blocks.BIRCH_PLANKS);
}
void refugeArchitecture() {
bevel(5, 6, 13, 14, 5, 10);
box(5, 11, 6, 13, 14, 14, Blocks.AIR);
for (int z = 6; z <= 14; z++) for (int x = 5; x <= 13; x++) {
int rise = Math.min(3, (x - 5) / 2);
slab(x, 11 + rise, z, (z & 1) == 0 ? Blocks.BIRCH_SLAB : Blocks.TUFF_BRICK_SLAB);
}
for (int x : new int[] {3, 14}) for (int z : new int[] {5, 15}) { box(x, 1, z, x, 3, z, Blocks.CHISELED_TUFF_BRICKS); set(x, 4, z, Blocks.OAK_WOOD); }
for (int z = 6; z <= 14; z += 4) box(4, 1, z, 4, 7, z, Blocks.IRON_BARS);
box(2, 1, 3, 3, 10, 4, Blocks.TUFF_BRICKS); box(2, 11, 3, 14, 11, 4, Blocks.OAK_WOOD);
// Upper lockers and a birch sleeping corner avoid the saved hatch and its complete ladder shaft.
screen(11, 7, 12, 7); box(7, 4, 7, 11, 4, 9, Blocks.BIRCH_PLANKS);
}
/** Replacement roofs: one weather skin, connected rafters and retained lamp-bearing beams.
* Clearing starts at the old roof, never at a floor, furnishing or standing threshold. */
void roofs21() {
switch (s.kind()) {
case RAIL_WORKSHOP -> {
vaultRoof(1, 2, 15, 14, 6, false, true);
box(1, 6, 8, 15, 6, 8, Blocks.OAK_WOOD); // existing workshop lamp
// Two missing panes expose an intact neighbouring rafter, not a floating roof fragment.
box(7, 9, 4, 8, 9, 4, Blocks.AIR);
}
case GREENHOUSE -> {
vaultRoof(1, 2, 11, 14, 6, true, true);
box(1, 6, 5, 11, 6, 5, Blocks.BIRCH_WOOD); // existing aisle lamp
}
case COMMUNAL_DEPOT -> sawtoothRoof();
case RADIO_RELAY -> {
vaultRoof(2, 3, 10, 12, 6, true, false);
box(2, 6, 6, 10, 6, 6, Blocks.OAK_WOOD);
box(2, 6, 10, 10, 6, 10, Blocks.OAK_WOOD);
box(9, 7, 4, 9, 11, 4, Blocks.STONE_BRICKS);
}
case CONSTRUCTION_REFUGE -> {
shedRoof(5, 6, 13, 14, 10);
box(5, 10, 8, 13, 10, 8, Blocks.OAK_WOOD); // lamp above the shared sleeping room
}
case POST_OFFICE -> box(5, 7, 5, 11, 7, 11, Blocks.AIR); // a parapet, not a second slab ceiling
case PHOTO_CLUB -> box(4, 7, 10, 12, 7, 12, Blocks.AIR);
default -> { } // Other roofs already have a single supported slab or a sealed working tank.
}
}
void vaultRoof(int x0, int z0, int x1, int z1, int eave, boolean timber, boolean glazed) {
box(x0, eave, z0, x1, s.height(), z1, Blocks.AIR);
int mid = (z0 + z1) / 2;
for (int z = z0; z <= z1; z++) for (int x = x0; x <= x1; x++) {
int top = vaultY(x, x0, x1, eave);
int adjoining = Math.min(vaultY(Math.max(x0, x - 1), x0, x1, eave),
vaultY(Math.min(x1, x + 1), x0, x1, eave));
boolean rib = z == z0 || z == mid || z == z1;
Block skin = rib ? Blocks.OAK_WOOD : glazed && x > x0 + 2 && x < x1 - 2
? Blocks.GLASS : timber ? Blocks.OAK_PLANKS : Blocks.SMOOTH_STONE;
// This short riser joins neighbouring height steps by a face, not just a diagonal corner.
box(x, Math.min(top, adjoining), z, x, top, z, skin);
if (z == z0 || z == z1) box(x, eave, z, x, top, z,
glazed && x > x0 + 1 && x < x1 - 1 ? Blocks.GLASS : Blocks.OAK_WOOD);
}
// Truss feet sit on the existing two long walls; no columns are introduced in an aisle.
box(x0, eave, mid, x1, eave, mid, Blocks.OAK_WOOD);
for (int x : new int[] {x0, x1}) box(x, eave, z0, x, eave, z1, Blocks.OAK_WOOD);
}
int vaultY(int x, int x0, int x1, int eave) {
return eave + Math.min(3, Math.min(x - x0, x1 - x));
}
void shedRoof(int x0, int z0, int x1, int z1, int eave) {
box(x0, eave, z0, x1, s.height(), z1, Blocks.AIR);
for (int z = z0; z <= z1; z++) for (int x = x0; x <= x1; x++) {
int top = eave + (x - x0) / 3;
int previous = eave + Math.max(0, x - 1 - x0) / 3;
box(x, previous, z, x, top, z,
z == z0 || z == z1 || z == (z0 + z1) / 2 ? Blocks.OAK_WOOD : Blocks.OAK_PLANKS);
if (z == z0 || z == z1 || x == x1) box(x, eave, z, x, top, z, Blocks.OAK_WOOD);
}
}
void sawtoothRoof() {
box(1, 9, 2, 15, s.height(), 14, Blocks.AIR);
for (int z = 2; z <= 14; z++) for (int x = 1; x <= 15; x++) {
int phase = Math.floorMod(z - 2, 4), top = z == 14 ? 9 : 12 - phase;
boolean frame = x == 1 || x == 15;
if (phase == 0 && z != 14) box(x, 9, z, x, top, z, frame ? Blocks.OAK_WOOD : Blocks.GLASS);
else box(x, Math.max(9, top - 1), z, x, top, z,
frame ? Blocks.OAK_WOOD : phase == 1 ? Blocks.GLASS : Blocks.SMOOTH_STONE);
if (frame) box(x, 9, z, x, top, z, Blocks.OAK_WOOD);
}
box(1, 9, 6, 15, 9, 6, Blocks.OAK_WOOD); // carries the original hall lamp
// Local, readable storm damage. The perimeter frame and remaining panes still share their bearings.
box(12, 10, 11, 13, 11, 11, Blocks.AIR);
}
void gable(int x0, int z0, int x1, int z1, int y) {
int rise = Math.min(3, (x1 - x0) / 2);
for (int step = 0; step <= rise; step++)
box(x0 + step, y + step, z0, x1 - step, y + step, z1, step == rise ? Blocks.STONE_BRICK_SLAB : Blocks.STONE_BRICKS);
}
void door(int x, int y, int z, Direction facing) { door(x, y, z, facing, false); }
void door(int x, int y, int z, Direction facing, boolean right) {
BlockState state = Blocks.DARK_OAK_DOOR.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing)
.setValue(BlockStateProperties.DOOR_HINGE, right ? DoorHingeSide.RIGHT : DoorHingeSide.LEFT)
.setValue(BlockStateProperties.OPEN, false).setValue(BlockStateProperties.POWERED, false);
set(x, y, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.LOWER));
set(x, y + 1, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.UPPER));
}
void doubleDoor(int x, int y, int z) { door(x, y, z, Direction.SOUTH, true); door(x + 1, y, z, Direction.SOUTH, false); }
void northWindows(int x0, int x1, int z, int y) { box(x0, y, z, x1, y + 1, z, Blocks.GLASS_PANE); }
void hang(int x, int ceiling, int z) { set(x, ceiling - 1, z, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true)); }
void seat(int x, int y, int z, Direction view) {
set(x, y, z, Blocks.DARK_OAK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, view.getOpposite()));
}
void lever(int x, int y, int z, Direction facing) {
set(x, y, z, Blocks.LEVER.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR)
.setValue(BlockStateProperties.HORIZONTAL_FACING, facing));
}
void rail(int x, int z, RailShape shape) { set(x, 1, z, Blocks.RAIL.defaultBlockState().setValue(BlockStateProperties.RAIL_SHAPE, shape)); }
void loot(int x, int y, int z, int approachX, int approachZ) {
target("loot_approach", approachX, y, approachZ);
if (out != null) out.chest(rx(x, z), y, rz(x, z), ResourceKey.create(Registries.LOOT_TABLE,
SanctuaryMod.id("chests/heritage19/" + s.kind().name().toLowerCase(Locale.ROOT))), s.seed() ^ 0x19CA7A10L);
}
void stairs(int x, int z, int base, int rise) {
for (int step = 1; step <= rise; step++) for (int xx = x; xx <= x + 1; xx++) {
box(xx, base + step + 1, z + step - 1, xx, base + step + 3, z + step - 1, Blocks.AIR);
set(xx, base + step - 1, z + step - 1, Blocks.STONE_BRICKS);
set(xx, base + step, z + step - 1, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
}
}
void bed(int x, int y, int z, Direction facing) {
BlockState state = Blocks.BED.white().defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing);
set(x, y, z, state.setValue(BlockStateProperties.BED_PART, BedPart.FOOT));
set(x + facing.getStepX(), y, z + facing.getStepZ(), state.setValue(BlockStateProperties.BED_PART, BedPart.HEAD));
int wx = x + facing.getStepX() * 2, wz = z + facing.getStepZ() * 2;
Direction along = facing.getClockWise();
for (int side = -1; side <= 1; side++) for (int up = 0; up <= 2; up++)
set(wx + along.getStepX() * side, y + up, wz + along.getStepZ() * side, Blocks.SMOOTH_STONE);
}
void station() {
shell(4, 0, 3, 12, 6, 11); gable(4, 3, 12, 11, 6);
door(8, 1, 3, Direction.SOUTH); northWindows(5, 6, 3, 2); northWindows(10, 11, 3, 2);
box(5, 1, 5, 6, 1, 5, Blocks.SMOOTH_STONE); set(5, 1, 8, Blocks.BARREL); seat(10, 1, 5, Direction.WEST);
for (int z = 2; z <= 14; z++) rail(2, z, RailShape.NORTH_SOUTH);
set(2, 1, 15, Blocks.DARK_OAK_FENCE);
rail(8, 0, RailShape.NORTH_SOUTH); rail(8, 1, RailShape.NORTH_WEST);
for (int x = 3; x <= 7; x++) rail(x, 1, RailShape.EAST_WEST);
rail(2, 1, RailShape.SOUTH_EAST);
box(0, 5, 5, 3, 5, 11, Blocks.DARK_OAK_PLANKS);
for (int z : new int[] {5, 11}) box(1, 1, z, 1, 4, z, Blocks.STRIPPED_OAK_LOG);
shell(9, 0, 11, 15, 4, 15); door(10, 1, 11, Direction.SOUTH); door(12, 1, 15, Direction.NORTH);
set(10, 1, 13, Blocks.SMOOTH_STONE); lever(10, 2, 13, Direction.EAST);
northWindows(13, 14, 15, 2); hang(8, 6, 7); hang(12, 4, 12);
target("ticket_counter", 8, 1, 5); target("freight_platform", 3, 1, 9); target("signal_office", 12, 1, 12);
target("secondary_entry", 12, 1, 16); loot(14, 1, 14, 13, 14);
}
void post() {
shell(2, 0, 2, 14, 6, 14); door(8, 1, 2, Direction.SOUTH);
northWindows(3, 6, 2, 2); northWindows(10, 13, 2, 2);
box(3, 1, 7, 13, 5, 7, Blocks.SMOOTH_STONE); door(8, 1, 7, Direction.SOUTH);
box(4, 1, 6, 6, 1, 6, Blocks.DARK_OAK_PLANKS);
for (int z = 9; z <= 11; z++) { set(3, 1, z, Blocks.BARREL); set(3, 2, z, Blocks.BARREL); }
set(6, 1, 10, Blocks.CRAFTING_TABLE); set(8, 1, 12, Blocks.BARREL);
shell(10, 0, 10, 14, 6, 14); door(10, 1, 12, Direction.EAST); door(14, 1, 12, Direction.WEST);
set(11, 1, 11, Blocks.CARTOGRAPHY_TABLE); hang(8, 6, 4); hang(7, 6, 10); hang(12, 6, 12);
target("public_hall", 8, 1, 4); target("sorting_aisle", 7, 1, 10); target("factor_office", 12, 1, 12);
target("secondary_entry", 15, 1, 12); loot(13, 1, 13, 12, 13);
}
void depot() {
shell(1, 0, 2, 15, 9, 14); gable(1, 2, 15, 14, 9);
box(7, 1, 2, 9, 3, 2, Blocks.AIR); door(4, 1, 2, Direction.SOUTH);
for (int z : new int[] {5, 8}) {
box(4, 1, z, 5, 1, z + 1, Blocks.DARK_OAK_PLANKS); set(4, 2, z, Blocks.BARREL);
box(11, 1, z, 13, 1, z + 1, Blocks.DARK_OAK_PLANKS); set(13, 2, z + 1, Blocks.BARREL);
}
box(2, 5, 10, 14, 5, 13, Blocks.SMOOTH_STONE);
for (int x = 5; x <= 14; x++) set(x, 6, 10, Blocks.IRON_BARS);
shell(10, 5, 10, 14, 9, 14); door(10, 6, 12, Direction.EAST);
set(11, 6, 11, Blocks.LECTERN.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
stairs(2, 5, 0, 5); door(15, 1, 12, Direction.WEST); hang(8, 9, 6); hang(12, 9, 12);
for (int z = 0; z <= 8; z++) rail(8, z, RailShape.NORTH_SOUTH);
set(8, 1, 9, Blocks.DARK_OAK_FENCE);
target("unloading_bay", 8, 1, 5); target("inventory_mezzanine", 5, 6, 12);
target("secondary_entry", 16, 1, 12); loot(13, 6, 13, 12, 13);
}
void expedition() {
shell(2, 0, 2, 12, 6, 12); door(8, 1, 2, Direction.SOUTH);
northWindows(3, 5, 2, 2); northWindows(10, 11, 2, 2);
set(4, 1, 4, Blocks.CARTOGRAPHY_TABLE); set(5, 1, 4, Blocks.LECTERN.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH)); set(3, 1, 6, Blocks.BARREL);
shell(2, 0, 8, 6, 6, 12); door(6, 1, 10, Direction.WEST); set(3, 1, 9, Blocks.BOOKSHELF);
box(3, 0, 13, 14, 0, 15, Blocks.DARK_OAK_PLANKS);
box(3, 1, 15, 14, 1, 15, Blocks.DARK_OAK_FENCE); door(10, 1, 12, Direction.NORTH);
seat(6, 1, 13, Direction.SOUTH); hang(8, 6, 5); hang(4, 6, 10);
target("expedition_table", 5, 1, 5); target("balcony", 10, 1, 14);
target("secondary_entry", 10, 1, 13); loot(3, 1, 11, 4, 11);
}
boolean tower(int x, int z) { return x >= 3 && x <= 13 && z >= 3 && z <= 13 && Math.abs(x - 8) + Math.abs(z - 8) <= 8; }
void observatory() {
for (int y = 0; y <= 12; y++) for (int z = 3; z <= 13; z++) for (int x = 3; x <= 13; x++) if (tower(x, z)) {
boolean wall = !tower(x - 1, z) || !tower(x + 1, z) || !tower(x, z - 1) || !tower(x, z + 1);
set(x, y, z, y == 0 || y == 6 || y == 12 ? Blocks.SMOOTH_STONE.defaultBlockState()
: wall ? LostCityPalette.masonry(s.seed(), x, y, z) : Blocks.AIR.defaultBlockState());
if (y == 12 && wall) set(x, 13, z, Blocks.IRON_BARS);
}
door(8, 1, 3, Direction.SOUTH); door(8, 1, 13, Direction.NORTH);
northWindows(7, 9, 3, 8); box(13, 8, 7, 13, 9, 9, Blocks.GLASS_PANE);
shell(9, 0, 9, 12, 4, 12); door(9, 1, 10, Direction.EAST); set(10, 1, 11, Blocks.BARREL);
set(10, 7, 5, Blocks.CARTOGRAPHY_TABLE); set(11, 7, 8, Blocks.SMOOTH_STONE); lever(11, 8, 8, Direction.WEST);
stairs(4, 5, 0, 6); stairs(4, 5, 6, 6);
box(4, 12, 11, 6, 12, 11, Blocks.SMOOTH_STONE); box(4, 13, 11, 6, 15, 11, Blocks.AIR);
box(10, 13, 9, 10, 18, 9, Blocks.DARK_OAK_FENCE); box(9, 18, 9, 11, 18, 9, Blocks.IRON_BARS);
hang(8, 6, 6); hang(10, 4, 11);
target("instruments", 9, 7, 8); target("sky_platform", 8, 13, 8);
target("secondary_entry", 8, 1, 14); loot(11, 1, 11, 11, 10);
}
void railway() {
shell(1, 0, 2, 15, 6, 14); gable(1, 2, 15, 14, 6); doubleDoor(7, 1, 2);
for (int z = 3; z <= 13; z++) for (int x : new int[] {5, 11}) set(x, 1, z, Blocks.RAIL);
box(4, 1, 2, 6, 3, 2, Blocks.AIR); rail(8, 0, RailShape.NORTH_SOUTH); rail(8, 1, RailShape.NORTH_WEST);
rail(7, 1, RailShape.EAST_WEST); rail(6, 1, RailShape.EAST_WEST); rail(5, 1, RailShape.SOUTH_EAST); rail(5, 2, RailShape.NORTH_SOUTH);
box(7, -2, 5, 9, -2, 11, Blocks.SMOOTH_STONE); box(7, -1, 5, 9, 0, 11, Blocks.AIR);
for (int step = 0; step <= 2; step++) set(8, -step, 4 + step, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
for (int z = 6; z <= 11; z++) { set(6, 1, z, Blocks.IRON_BARS); set(10, 1, z, Blocks.IRON_BARS); }
set(2, 1, 4, Blocks.CRAFTING_TABLE); set(3, 1, 4, Blocks.FURNACE.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
set(2, 1, 6, Blocks.GRINDSTONE.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR));
shell(1, 0, 10, 5, 5, 14); door(5, 1, 12, Direction.WEST); door(8, 1, 14, Direction.NORTH);
set(5, 1, 9, Blocks.DARK_OAK_FENCE);
set(2, 1, 11, Blocks.ANVIL); hang(8, 6, 8); hang(3, 5, 12);
target("maintenance_pit", 8, -1, 9); target("workbench", 3, 1, 5);
target("secondary_entry", 8, 1, 15); loot(2, 1, 13, 3, 13);
}
void pumping() {
shell(2, 0, 3, 9, 6, 12); door(8, 1, 3, Direction.SOUTH);
set(3, 1, 5, Blocks.DISPENSER.defaultBlockState().setValue(BlockStateProperties.FACING, Direction.EAST));
set(4, 1, 5, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 3));
set(3, 2, 5, Blocks.POLISHED_ANDESITE); box(3, 3, 5, 3, 5, 8, Blocks.IRON_BARS);
for (int x : new int[] {10, 15}) for (int z : new int[] {5, 11}) box(x, 1, z, x, 7, z, Blocks.POLISHED_ANDESITE);
shell(10, 8, 5, 15, 13, 11); box(11, 9, 6, 14, 11, 10, Blocks.WATER);
box(10, 3, 6, 14, 3, 13, Blocks.SMOOTH_STONE); stairs(10, 3, 0, 3);
shell(2, 0, 10, 6, 5, 15); door(5, 1, 10, Direction.SOUTH); door(5, 1, 15, Direction.NORTH);
set(3, 1, 11, Blocks.BARREL); hang(7, 6, 7); hang(4, 5, 12);
target("pump_room", 6, 1, 6); target("reservoir_walkway", 13, 4, 12);
target("secondary_entry", 5, 1, 16); loot(3, 1, 14, 4, 14);
}
void substation() {
for (int z = 2; z <= 14; z++) { set(1, 1, z, Blocks.IRON_BARS); set(15, 1, z, Blocks.IRON_BARS); }
for (int x = 1; x <= 15; x++) if (x < 7 || x > 9) set(x, 1, 2, Blocks.IRON_BARS);
for (int z : new int[] {5, 8}) {
box(3, 1, z, 4, 2, z + 1, Blocks.POLISHED_ANDESITE); box(3, 3, z, 4, 3, z, Blocks.IRON_BARS);
}
shell(10, 0, 4, 15, 5, 10); door(10, 1, 6, Direction.EAST); northWindows(12, 14, 4, 2);
set(12, 1, 5, Blocks.SMOOTH_STONE); lever(12, 2, 5, Direction.SOUTH); set(14, 1, 5, Blocks.CRAFTING_TABLE);
shell(10, 0, 10, 15, 5, 15); door(12, 1, 10, Direction.SOUTH); door(15, 1, 13, Direction.WEST);
box(6, -2, 10, 7, -2, 13, Blocks.SMOOTH_STONE); box(6, -1, 10, 7, 0, 13, Blocks.AIR);
for (int step = 0; step <= 2; step++) set(6, -step, 9 + step, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
hang(13, 5, 7); hang(12, 5, 13); target("distribution_yard", 8, 1, 6); target("cable_trench", 6, -1, 12);
target("secondary_entry", 16, 1, 13); loot(14, 1, 14, 13, 14);
}
void greenhouse() {
shell(1, 0, 2, 11, 6, 14);
for (int y = 2; y <= 5; y++) for (int z = 3; z <= 13; z++) { set(1, y, z, Blocks.GLASS); set(11, y, z, Blocks.GLASS); }
northWindows(2, 5, 2, 2); northWindows(7, 10, 2, 2); door(6, 1, 2, Direction.SOUTH);
for (int x = 2; x <= 10; x++) for (int z = 3; z <= 13; z++) set(x, 6, z, x == 6 ? Blocks.SMOOTH_STONE : Blocks.GLASS);
for (int x : new int[] {2, 3, 4, 8, 9, 10}) for (int z = 4; z <= 10; z++) {
set(x, 0, z, Blocks.FARMLAND.defaultBlockState().setValue(BlockStateProperties.MOISTURE, 7));
set(x, 1, z, Blocks.WHEAT.defaultBlockState().setValue(BlockStateProperties.AGE_7,
2 + LostCityPalette.roll(s.seed(), x, 1, z, 6)));
}
set(3, 0, 6, Blocks.WATER); set(9, 0, 6, Blocks.WATER);
set(3, 1, 6, Blocks.AIR); set(9, 1, 6, Blocks.AIR);
shell(11, 0, 9, 15, 5, 15); door(11, 1, 12, Direction.EAST); door(13, 1, 15, Direction.NORTH);
set(12, 1, 10, Blocks.CRAFTING_TABLE); set(13, 1, 10, Blocks.FLOWER_POT); set(9, 1, 12, Blocks.COMPOSTER);
hang(6, 6, 5); hang(13, 5, 12); target("glasshouse_aisle", 6, 1, 7); target("potting_shed", 13, 1, 12);
target("secondary_entry", 13, 1, 16); loot(14, 1, 14, 13, 14);
}
void dock() {
box(0, 0, 0, 16, 0, 16, Blocks.DARK_OAK_PLANKS); box(0, 1, 16, 16, 1, 16, Blocks.DARK_OAK_FENCE);
box(0, 1, 1, 0, 1, 15, Blocks.DARK_OAK_FENCE); box(16, 1, 1, 16, 1, 15, Blocks.DARK_OAK_FENCE);
shell(2, 0, 3, 7, 5, 10); door(7, 1, 6, Direction.WEST); seat(3, 1, 5, Direction.EAST);
shell(2, 0, 10, 7, 5, 15); door(5, 1, 10, Direction.SOUTH); door(7, 1, 13, Direction.WEST);
box(12, 1, 5, 12, 11, 5, Blocks.STRIPPED_OAK_LOG); box(8, 11, 5, 14, 11, 5, Blocks.STRIPPED_OAK_LOG);
box(14, 7, 5, 14, 10, 5, Blocks.IRON_CHAIN); hang(10, 11, 5); hang(5, 5, 6); hang(5, 5, 12);
target("departure_deck", 8, 1, 14); target("waiting_room", 5, 1, 6); target("secondary_entry", 8, 1, 13);
loot(3, 1, 14, 4, 14);
}
void radio() {
shell(2, 0, 3, 10, 6, 12); door(8, 1, 3, Direction.SOUTH); northWindows(3, 5, 3, 2);
bed(4, 1, 5, Direction.NORTH); set(8, 1, 5, Blocks.CARTOGRAPHY_TABLE);
shell(2, 0, 8, 6, 6, 12); door(6, 1, 10, Direction.WEST); door(2, 1, 10, Direction.EAST);
box(11, 5, 6, 15, 5, 10, Blocks.SMOOTH_STONE);
for (int x : new int[] {11, 15}) for (int z : new int[] {6, 10}) box(x, 1, z, x, 4, z, Blocks.POLISHED_ANDESITE);
box(13, 1, 8, 13, 18, 8, Blocks.POLISHED_ANDESITE); box(11, 17, 8, 15, 17, 8, Blocks.IRON_BARS);
box(13, 19, 8, 13, 21, 8, Blocks.IRON_BARS); stairs(11, 1, 0, 5);
hang(7, 6, 6); hang(4, 6, 10);
target("caretaker_room", 7, 1, 6); target("antenna_platform", 12, 6, 9); target("secondary_entry", 1, 1, 10);
loot(3, 1, 11, 4, 11);
}
void photo() {
shell(2, 0, 2, 14, 6, 14); door(8, 1, 2, Direction.SOUTH); northWindows(3, 6, 2, 2); northWindows(10, 13, 2, 2);
box(3, 1, 8, 13, 5, 8, Blocks.SMOOTH_STONE); door(4, 1, 8, Direction.SOUTH);
box(8, 1, 9, 8, 5, 13, Blocks.SMOOTH_STONE); door(8, 1, 11, Direction.EAST);
box(10, 1, 5, 12, 1, 5, Blocks.DARK_OAK_PLANKS); set(3, 1, 4, Blocks.BOOKSHELF);
for (int z : new int[] {10, 12}) set(3, 1, z, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 3));
box(11, 4, 10, 11, 5, 12, Blocks.IRON_CHAIN); set(12, 1, 10, Blocks.CRAFTING_TABLE);
door(12, 1, 14, Direction.NORTH); hang(8, 6, 4); hang(5, 6, 11); hang(12, 6, 12);
target("club_room", 8, 1, 4); target("darkroom", 5, 1, 11); target("drying_room", 10, 1, 11);
target("secondary_entry", 12, 1, 15); loot(13, 1, 13, 12, 13);
}
void impossible() {
shell(3, 0, 2, 11, 4, 7); door(8, 1, 2, Direction.SOUTH);
for (int z = 3; z <= 5; z++) { set(4, 1, z, Blocks.BARREL); set(4, 2, z, Blocks.BARREL); }
seat(10, 1, 4, Direction.WEST);
shell(1, 0, 7, 15, 10, 15); door(8, 1, 7, Direction.SOUTH);
for (int x = 2; x <= 14; x++) for (int z = 8; z <= 14; z++) set(x, 0, z,
((x + z) & 1) == 0 ? Blocks.CALCITE : Blocks.POLISHED_ANDESITE);
for (int x : new int[] {3, 6, 10, 13}) box(x, 1, 11, x, 9, 11, Blocks.CALCITE);
shell(1, 0, 12, 5, 5, 15); door(5, 1, 13, Direction.WEST); door(3, 1, 15, Direction.NORTH);
set(2, 2, 12, Blocks.GLASS); set(2, 1, 13, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 2));
hang(8, 4, 4); hang(8, 10, 11); hang(3, 5, 13);
target("lockers", 8, 1, 4); target("oversized_tiled_hall", 8, 1, 11);
target("secondary_entry", 3, 1, 16); loot(2, 1, 14, 3, 14);
}
void refuge() {
box(3, 4, 5, 14, 4, 15, Blocks.DARK_OAK_PLANKS);
for (int x : new int[] {3, 14}) for (int z : new int[] {5, 15}) box(x, 1, z, x, 3, z, Blocks.STONE_BRICKS);
shell(5, 4, 6, 13, 10, 14); gable(5, 6, 13, 14, 10); door(8, 5, 6, Direction.SOUTH);
bed(6, 5, 8, Direction.NORTH); set(12, 5, 8, Blocks.CRAFTING_TABLE);
shell(6, 0, 9, 11, 4, 14); door(6, 1, 11, Direction.EAST);
// Carry the ladder through the floor so opening the hatch leaves a climbable lip.
for (int y = 1; y <= 4; y++) set(10, y, 11, Blocks.LADDER.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST));
set(10, 5, 11, Blocks.DARK_OAK_TRAPDOOR.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST)
.setValue(BlockStateProperties.HALF, Half.BOTTOM));
stairs(7, 1, 0, 4); box(2, 1, 3, 3, 11, 4, Blocks.STONE_BRICKS); box(2, 11, 3, 14, 11, 4, Blocks.STRIPPED_OAK_LOG);
hang(10, 10, 8); hang(8, 4, 12);
target("suspended_hut", 9, 5, 8); target("hidden_floor_hatch", 10, 5, 11); target("secondary_entry", 5, 1, 11);
loot(7, 1, 13, 8, 13);
}
}
}
@@ -0,0 +1,42 @@
package fr.koka.sanctuary.worldgen.heritage;
import com.mojang.serialization.MapCodec;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.util.ArrayList;
import java.util.Optional;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureType;
/** Island-scale routes are split into small independent native starts before rendering. */
public final class HeritageStructure19 extends Structure {
public static final MapCodec<HeritageStructure19> CODEC = simpleCodec(HeritageStructure19::new);
public HeritageStructure19(StructureSettings settings) { super(settings); }
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21) || !generator.experimentalStructures()) return Optional.empty();
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
if (island == null || !island.origin() || (island.generation() != generator.rootGeneration())) return Optional.empty();
var plan = generator.heritagePlan19();
int x = context.chunkPos().x(), z = context.chunkPos().z();
var pieces = new ArrayList<net.minecraft.world.level.levelgen.structure.StructurePiece>();
// Local route segments render before buildings. No piece needs a distant link endpoint loaded.
for (var segment : plan.segments())
if (segment.startChunkX() == x && segment.startChunkZ() == z)
pieces.add(generator.rootGeneration() == 21 ? new HeritagePiece21(segment) : generator.rootGeneration() == 20 ? new HeritagePiece20(segment) : new HeritagePiece19(segment));
for (var site : plan.sites()) if (site.startChunkX() == x && site.startChunkZ() == z)
pieces.add(generator.rootGeneration() == 21 ? new HeritagePiece21(site) : generator.rootGeneration() == 20 ? new HeritagePiece20(site) : new HeritagePiece19(site));
if (pieces.isEmpty()) return Optional.empty();
for (var piece : pieces) {
var box = piece.getBoundingBox();
if (Math.abs((long) Math.floorDiv(box.minX(), 16) - x) > 7
|| Math.abs((long) Math.floorDiv(box.maxX(), 16) - x) > 7
|| Math.abs((long) Math.floorDiv(box.minZ(), 16) - z) > 7
|| Math.abs((long) Math.floorDiv(box.maxZ(), 16) - z) > 7)
throw new IllegalStateException("Heritage piece exceeds its local native reference contract");
}
var locator = pieces.stream().filter(piece -> piece instanceof HeritagePiece19 old && old.isSite() || piece instanceof HeritagePiece20 detail && detail.isSite() || piece instanceof HeritagePiece21 current && current.isSite()).findFirst().orElse(pieces.getFirst()).getLocatorPosition();
return Optional.of(new GenerationStub(locator, builder -> pieces.forEach(builder::addPiece)));
}
@Override public StructureType<?> type() { return HeritageStructures19.TYPE; }
}
@@ -0,0 +1,114 @@
package fr.koka.sanctuary.worldgen.heritage;
import com.mojang.datafixers.util.Pair;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.StructureType;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceType;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
/** Experimental generation19 uses independent native starts, references and saved room pieces. */
public final class HeritageStructures19 {
public static final ResourceKey<Structure> KEY = ResourceKey.create(Registries.STRUCTURE, SanctuaryMod.id("heritage_networks"));
public static final StructureType<HeritageStructure19> TYPE = Registry.register(BuiltInRegistries.STRUCTURE_TYPE,
SanctuaryMod.id("heritage_networks_v19"), () -> HeritageStructure19.CODEC);
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
SanctuaryMod.id("heritage_piece_v19"), HeritagePiece19::new);
public static final StructurePieceType PIECE21 = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
SanctuaryMod.id("heritage_piece_v21"), HeritagePiece21::new);
public static final StructurePieceType PIECE20 = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
SanctuaryMod.id("heritage_piece_v20"), HeritagePiece20::new);
private static final ThreadLocal<Integer> PLACEMENT_DEPTH = ThreadLocal.withInitial(() -> 0);
private HeritageStructures19() {}
public static void register() { /* Initialize serializers before the dynamic structure registry is read. */ }
public static boolean isPlacing() { return PLACEMENT_DEPTH.get() > 0; }
static void placing(Runnable renderer) {
int previous = PLACEMENT_DEPTH.get();
PLACEMENT_DEPTH.set(previous + 1);
try { renderer.run(); }
finally {
if (previous == 0) PLACEMENT_DEPTH.remove();
else PLACEMENT_DEPTH.set(previous);
}
}
public static void generate(ExpansionChunkGenerator generator, RegistryAccess registries,
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> level) {
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21)
|| !sanctuary.experimentalStructures()) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
var plan = sanctuary.heritagePlan19();
if (!hasStart(plan, chunk.getPos().x(), chunk.getPos().z())) return;
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
Structure structure = holder.value();
StructureStart previous = manager.getStartForStructure(structure, chunk);
if (previous != null && previous.isValid()) return;
var random = generator.randomState(island);
var sampler = random.createClimateSampler(SamplerContext.builder().enableCaches().build());
var start = structure.generate(holder, level, registries, generator, generator.getBiomeSource(), sampler,
random, templates, island.seed(), chunk.getPos(), previous == null ? 0 : previous.getReferences(),
chunk, structure.biomes()::contains);
if (start.isValid()) manager.setStartForStructure(structure, start, chunk);
}
/** Starts are loaded at STRUCTURE_STARTS; locating never creates an expansion or rebuilds an existing start. */
public static Pair<BlockPos, Holder<Structure>> findNearest(ServerLevel level, HolderSet<Structure> structures,
BlockPos origin, int radius, boolean skipReferenced) {
if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21) || !generator.experimentalStructures()
|| !level.structureManager().shouldGenerateStructures()) return null;
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
if (!structures.contains(holder)) return null;
var candidates = new ArrayList<HeritagePlan19.Site>();
int originChunkX = origin.getX() >> 4, originChunkZ = origin.getZ() >> 4;
for (var network : generator.heritagePlan19().sites()) {
var island = generator.islandForChunk(network.startChunkX(), network.startChunkZ());
if (island == null || !island.origin() || island.generation() != generator.rootGeneration() || !island.status().equals("ready")) continue;
if (Math.abs((long) network.startChunkX() - originChunkX) <= radius
&& Math.abs((long) network.startChunkZ() - originChunkZ) <= radius) candidates.add(network);
}
candidates.sort(Comparator.comparingDouble(n -> origin.distSqr(locator(n))));
var visited = new HashSet<Long>();
for (var network : candidates) {
long key = ((long) network.startChunkX() << 32) ^ (network.startChunkZ() & 0xffffffffL);
if (!visited.add(key)) continue;
var chunk = level.getChunk(network.startChunkX(), network.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
StructureStart start = level.structureManager().getStartForStructure(holder.value(), chunk);
if (start == null || !start.isValid() || skipReferenced && !start.canBeReferenced()) continue;
if (skipReferenced) level.structureManager().addReference(start);
var entrance = start.getPieces().stream().filter(p -> (p instanceof HeritagePiece19 site && site.isSite())
|| (p instanceof HeritagePiece20 detail && detail.isSite()) || (p instanceof HeritagePiece21 current && current.isSite())).findFirst().orElseThrow();
return Pair.of(entrance.getLocatorPosition(), holder);
}
return null;
}
static BlockPos locator(HeritagePlan19.Site site) { return HeritagePiece19.locator(site); }
static boolean hasStart(HeritagePlan19 plan, int x, int z) {
return plan.sites().stream().anyMatch(s -> s.startChunkX() == x && s.startChunkZ() == z)
|| plan.segments().stream().anyMatch(s -> s.startChunkX() == x && s.startChunkZ() == z);
}
}
@@ -0,0 +1,110 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import fr.koka.sanctuary.worldgen.city.LostCityPlan;
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.RandomState;
/** Read-only pre-decoration terrain. No loaded chunks or player-edited blocks enter a plan. */
public final class HeritageTerrain19 implements HeritagePlanner19.Terrain {
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
private final ExpansionIsland island;
private final NoiseBasedChunkGenerator generator;
private final RandomState random;
private final LevelHeightAccessor height;
private final Predicate<BlockPos> protection;
private final Map<Long, NoiseColumn> columns = new HashMap<>();
private List<HeritagePlan19.Landmark> landmarks = List.of();
public HeritageTerrain19(ExpansionIsland island, NoiseBasedChunkGenerator generator, RandomState random,
LevelHeightAccessor height, Predicate<BlockPos> protection) {
this.island = island; this.generator = generator; this.random = random;
this.height = height; this.protection = protection;
}
private NoiseColumn column(int x, int z) {
if (!owns(x, z)) return null;
long key = ((long) x << 32) ^ (z & 0xffffffffL);
var previous = columns.get(key);
if (previous != null) return previous;
if (columns.size() >= HeritagePlan19.MAX_COLUMNS) return null;
var result = generator.getBaseColumn(x, z, height, random);
columns.put(key, result);
return result;
}
@Override public boolean owns(int x, int z) { return island.contains(x, z); }
@Override public boolean solid(int x, int y, int z) {
if (y < 0 || y >= 384) return false;
var column = column(x, z);
return column != null && column.getBlock(y).isSolid() && column.getBlock(y).getFluidState().isEmpty();
}
@Override public boolean protectedAt(int x, int y, int z) {
if (y < 0 || y >= 384) return true;
var column = column(x, z);
return column == null || !column.getBlock(y).getFluidState().isEmpty()
|| protection.test(new BlockPos(x, y, z));
}
@Override public int surfaceY(int x, int z) {
var column = column(x, z);
if (column == null) return -1;
for (int y = 380; y >= 1; y--) {
var state = column.getBlock(y);
if (!state.isAir()) return state.isSolid() && state.getFluidState().isEmpty() ? y : -1;
}
return -1;
}
@Override public List<HeritagePlan19.Landmark> landmarks() { return landmarks; }
public int columnsUsed() { return columns.size(); }
/** Select actual dry ground abutting existing walkable platforms. Their protected blocks stay intact. */
public void discoverLandmarks(LostCityPlan city, RuinsPlan17 ruins) {
var result = new ArrayList<HeritagePlan19.Landmark>();
for (var network : ruins.networks()) {
var access = network.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS).findFirst().orElse(null);
if (access == null) continue;
int deck = access.maxY() - 2;
for (int direction = 0; direction < 4; direction++) {
var entry = dryEntry(access.centerX() + DX[direction] * 3, access.centerZ() + DZ[direction] * 3, deck, direction);
if (entry == null) continue;
var kind = network.family() == RuinsPlan17.Family.MINE ? HeritagePlan19.LandmarkKind.MINE : HeritagePlan19.LandmarkKind.UTILITY;
result.add(new HeritagePlan19.Landmark("old_" + network.id(), kind, entry));
break;
}
}
int cityEntries = 0;
outer: for (var district : city.districts()) for (var parcel : district.parcels()) {
if (parcel.kind() != LostCityPlan.Kind.STREET) continue;
boolean eastWest = parcel.width() > parcel.depth();
for (int direction : eastWest ? new int[]{1, 3} : new int[]{0, 2}) {
int x = direction == 1 ? parcel.width() - 1 : direction == 3 ? 0 : parcel.width() / 2;
int z = direction == 2 ? parcel.depth() - 1 : direction == 0 ? 0 : parcel.depth() / 2;
var entry = dryEntry(parcel.minX() + x + DX[direction], parcel.minZ() + z + DZ[direction], parcel.surfaceY(x, z), direction);
if (entry == null) continue;
result.add(new HeritagePlan19.Landmark("old_city_" + cityEntries++, HeritagePlan19.LandmarkKind.CITY, entry));
if (cityEntries == 2) break outer;
}
}
landmarks = List.copyOf(result);
}
private HeritagePlan19.Entry dryEntry(int x, int z, int deck, int direction) {
int y = surfaceY(x, z);
if (y < 32 || y > 348 || Math.abs(y - deck) > 1) return null;
// A three-wide arrival can abut a platform without cutting its wall or footings.
for (int side = -1; side <= 1; side++) {
int px = x + DZ[direction] * side, pz = z + DX[direction] * side;
var column = column(px, pz);
if (column == null || surfaceY(px, pz) != y || !solid(px, y, pz)) return null;
for (int by = y - 1; by <= y + 3; by++) if (protectedAt(px, by, pz)) return null;
for (int by = y + 1; by <= y + 3; by++) if (!column.getBlock(by).isAir()) return null;
}
return new HeritagePlan19.Entry(x, y + 1, z, direction);
}
}
@@ -0,0 +1,113 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import fr.koka.sanctuary.worldgen.city.LostCityPlan;
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.RandomState;
/** Read-only pre-decoration terrain. No loaded chunks or player-edited blocks enter a plan. */
public final class HeritageTerrain21 implements HeritagePlanner21.Terrain {
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
private final ExpansionIsland island;
private final NoiseBasedChunkGenerator generator;
private final RandomState random;
private final LevelHeightAccessor height;
private final Predicate<BlockPos> protection;
private final Map<Long, NoiseColumn> columns = new HashMap<>();
private List<HeritagePlan19.Landmark> landmarks = List.of();
public HeritageTerrain21(ExpansionIsland island, NoiseBasedChunkGenerator generator, RandomState random,
LevelHeightAccessor height, Predicate<BlockPos> protection) {
this.island = island; this.generator = generator; this.random = random;
this.height = height; this.protection = protection;
}
private NoiseColumn column(int x, int z) {
if (!owns(x, z)) return null;
long key = ((long) x << 32) ^ (z & 0xffffffffL);
var previous = columns.get(key);
if (previous != null) return previous;
if (columns.size() >= HeritagePlan19.MAX_COLUMNS) return null;
var result = generator.getBaseColumn(x, z, height, random);
columns.put(key, result);
return result;
}
@Override public boolean owns(int x, int z) { return island.contains(x, z); }
@Override public boolean solid(int x, int y, int z) {
if (y < 0 || y >= 384) return false;
var column = column(x, z);
return column != null && column.getBlock(y).isSolid() && column.getBlock(y).getFluidState().isEmpty();
}
@Override public boolean protectedAt(int x, int y, int z) {
if (y < 0 || y >= 384) return true;
var column = column(x, z);
return column == null || !column.getBlock(y).getFluidState().isEmpty()
|| protection.test(new BlockPos(x, y, z));
}
@Override public int surfaceY(int x, int z) {
var column = column(x, z);
if (column == null) return -1;
for (int y = 380; y >= 1; y--) {
var state = column.getBlock(y);
if (!state.isAir()) return state.isSolid() && state.getFluidState().isEmpty() ? y : -1;
}
return -1;
}
@Override public List<HeritagePlan19.Landmark> landmarks() { return landmarks; }
public int columnsUsed() { return columns.size(); }
/** Select actual dry ground abutting existing walkable platforms. Their protected blocks stay intact. */
public void discoverLandmarks(LostCityPlan city, RuinsPlan17 ruins) {
var result = new ArrayList<HeritagePlan19.Landmark>();
for (var network : ruins.networks()) {
var access = network.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS).findFirst().orElse(null);
if (access == null) continue;
int deck = access.maxY() - 2;
for (int direction = 0; direction < 4; direction++) {
var entry = dryEntry(access.centerX() + DX[direction] * 3, access.centerZ() + DZ[direction] * 3, deck, direction);
if (entry == null) continue;
var kind = network.family() == RuinsPlan17.Family.MINE ? HeritagePlan19.LandmarkKind.MINE : HeritagePlan19.LandmarkKind.UTILITY;
result.add(new HeritagePlan19.Landmark("old_" + network.id(), kind, entry));
break;
}
}
int cityEntries = 0;
for (var district : city.districts()) {
int districtEntries = 0;
districtLoop: for (var parcel : district.parcels()) {
if (parcel.kind() != LostCityPlan.Kind.STREET || parcel.width() < 3 || parcel.depth() < 3) continue;
for (int direction = 0; direction < 4; direction++) {
int x = direction == 1 ? parcel.width() - 1 : direction == 3 ? 0 : parcel.width() / 2;
int z = direction == 2 ? parcel.depth() - 1 : direction == 0 ? 0 : parcel.depth() / 2;
var entry = dryEntry(parcel.minX() + x + DX[direction], parcel.minZ() + z + DZ[direction], parcel.surfaceY(x, z), direction);
if (entry == null) continue;
result.add(new HeritagePlan19.Landmark("old_city_" + cityEntries++, HeritagePlan19.LandmarkKind.CITY, entry));
if (++districtEntries == 2 || result.size() >= 16) break districtLoop;
}
}
if (result.size() >= 16) break;
}
landmarks = List.copyOf(result);
}
private HeritagePlan19.Entry dryEntry(int x, int z, int deck, int direction) {
int y = surfaceY(x, z);
if (y < 32 || y > 348 || Math.abs(y - deck) > 1) return null;
// A three-wide arrival can abut a platform without cutting its wall or footings.
for (int side = -1; side <= 1; side++) {
int px = x + DZ[direction] * side, pz = z + DX[direction] * side;
var column = column(px, pz);
if (column == null || surfaceY(px, pz) != y || !solid(px, y, pz)) return null;
for (int by = y - 1; by <= y + 3; by++) if (protectedAt(px, by, pz)) return null;
for (int by = y + 1; by <= y + 3; by++) if (!column.getBlock(by).isAir()) return null;
}
return new HeritagePlan19.Entry(x, y + 1, z, direction);
}
}
@@ -0,0 +1,114 @@
package fr.koka.sanctuary.worldgen.ruins;
import java.util.ArrayList;
import java.util.Set;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.ResourceKey;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.loot.LootTable;
/** Saved generation20 room geometry is rendered without replanning or reading neighboring chunks. */
public final class RuinsPiece20 extends StructurePiece {
private static final Set<String> ROOM_KEYS = Set.of("Schema", "Kind", "MinX", "MinY", "MinZ", "Width",
"Height", "Depth", "Openings", "RoomSeed", "Supports");
private static final Set<String> SUPPORT_KEYS = Set.of("X", "MinY", "Z", "MaxY");
private final RuinsPlan17.Room room;
public RuinsPiece20(RuinsPlan17.Room room) {
super(RuinsStructures17.PIECE20, 0, bounds(room));
this.room = room;
setOrientation(null);
}
public RuinsPiece20(StructurePieceSerializationContext context, CompoundTag tag) {
super(RuinsStructures17.PIECE20, tag);
room = decodeRoom(tag.getCompound("SanctuaryRoom")
.orElseThrow(() -> new IllegalArgumentException("Missing saved forgotten room")));
if (!getBoundingBox().equals(bounds(room)))
throw new IllegalArgumentException("Forgotten room disagrees with its saved native bounding box");
setOrientation(null);
}
public RuinsPlan17.Room room() { return room; }
@Override public BlockPos getLocatorPosition() { return locator(room); }
public static BlockPos locator(RuinsPlan17.Room room) {
return new BlockPos(room.minX() + room.width() / 2,
room.kind() == RuinsPlan17.Kind.ACCESS ? room.minY() + room.height() - 2 : room.floorY() + 1,
room.minZ() + room.depth() / 2);
}
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
tag.put("SanctuaryRoom", encodeRoom(room));
}
public static CompoundTag encodeRoom(RuinsPlan17.Room room) {
var tag = new CompoundTag(); tag.putInt("Schema", 1); tag.putString("Kind", room.kind().name());
tag.putInt("MinX", room.minX()); tag.putInt("MinY", room.minY()); tag.putInt("MinZ", room.minZ());
tag.putInt("Width", room.width()); tag.putInt("Height", room.height()); tag.putInt("Depth", room.depth());
tag.putInt("Openings", room.openings()); tag.putLong("RoomSeed", room.seed());
var supports = new ListTag();
for (var support : room.supports()) {
var value = new CompoundTag(); value.putInt("X", support.x()); value.putInt("MinY", support.minY());
value.putInt("Z", support.z()); value.putInt("MaxY", support.maxY()); supports.add(value);
}
tag.put("Supports", supports);
return tag;
}
public static RuinsPlan17.Room decodeRoom(CompoundTag tag) {
if (!tag.keySet().equals(ROOM_KEYS)) throw new IllegalArgumentException("Incomplete or unknown forgotten room fields");
if (integer(tag, "Schema") != 1) throw new IllegalArgumentException("Unsupported forgotten room schema");
var kind = RuinsPlan17.Kind.valueOf(tag.getString("Kind").orElseThrow(() -> new IllegalArgumentException("Missing room kind")));
type(tag, "RoomSeed", Tag.TAG_LONG); type(tag, "Supports", Tag.TAG_LIST);
var saved = tag.getList("Supports").orElseThrow();
if (saved.size() > 64) throw new IllegalArgumentException("Too many saved forgotten room supports");
var supports = new ArrayList<RuinsPlan17.Support>();
for (var entry : saved) {
if (!(entry instanceof CompoundTag value) || !value.keySet().equals(SUPPORT_KEYS))
throw new IllegalArgumentException("Invalid forgotten room support fields");
supports.add(new RuinsPlan17.Support(integer(value, "X"), integer(value, "MinY"), integer(value, "Z"), integer(value, "MaxY")));
}
return new RuinsPlan17.Room(kind, integer(tag, "MinX"), integer(tag, "MinY"), integer(tag, "MinZ"),
integer(tag, "Width"), integer(tag, "Height"), integer(tag, "Depth"), integer(tag, "Openings"),
tag.getLong("RoomSeed").orElseThrow(), supports);
}
private static int integer(CompoundTag tag, String key) {
type(tag, key, Tag.TAG_INT);
return tag.getInt(key).orElseThrow();
}
private static void type(CompoundTag tag, String key, byte id) {
if (tag.get(key) == null || tag.get(key).getId() != id)
throw new IllegalArgumentException("Invalid saved forgotten room field: " + key);
}
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
RuinsStructures17.placing(() -> RuinsRenderer20.render(room, new LostCityRenderer.BlockWriter() {
private BlockPos world(int x, int y, int z) { return new BlockPos(room.minX() + x, room.minY() + y, room.minZ() + z); }
@Override public void set(int x, int y, int z, BlockState block) {
var position = world(x, y, z);
if (getBoundingBox().isInside(position) && clip.isInside(position))
placeBlock(level, block, position.getX(), position.getY(), position.getZ(), clip);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
var position = world(x, y, z);
if (getBoundingBox().isInside(position) && clip.isInside(position))
createChest(level, clip, RandomSource.create(seed), position, loot, null);
}
}));
}
public static BoundingBox bounds(RuinsPlan17.Room room) {
int minY = room.minY(), maxY = Math.addExact(room.minY(), room.height() - 1);
for (var support : room.supports()) {
minY = Math.min(minY, support.minY()); maxY = Math.max(maxY, support.maxY());
}
return new BoundingBox(room.minX(), minY, room.minZ(), Math.addExact(room.minX(), room.width() - 1),
maxY, Math.addExact(room.minZ(), room.depth() - 1));
}
}
@@ -0,0 +1,347 @@
package fr.koka.sanctuary.worldgen.ruins;
import fr.koka.sanctuary.worldgen.city.LostCityPalette;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.core.Direction;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.AttachFace;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.storage.loot.BuiltInLootTables;
/** Generation20 architectural details: original, physical rooms reconstructed solely from their saved inputs.
* Coordinates given to the writer are local to the saved room. There are no terrain reads, entities,
* active machines or free fluids. Openings are restored last so connected pieces share dry thresholds. */
public final class RuinsRenderer20 {
private static final Direction[] DIRECTIONS = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
private RuinsRenderer20() {}
/** Local standing positions for native path checks, including approaches to useful fixtures. */
public record Target(String name, int x, int y, int z) {}
public static List<Target> targets(RuinsPlan17.Room room) {
var targets = new ArrayList<Target>();
if (room.kind() == RuinsPlan17.Kind.ACCESS) {
targets.add(new Target("surface_hatch_approach", room.width() / 2, room.height() - 2, room.depth() / 2));
targets.add(new Target("shaft_bottom", room.width() / 2, 2, room.depth() / 2));
} else {
targets.add(new Target("central_crossing", room.width() / 2, 2, room.depth() / 2));
switch (room.kind()) {
case WORKSHOP -> {
targets.add(decorTarget(room, "workbench", 2, 2, 2));
targets.add(decorTarget(room, "unfinished_work_platform", room.width() - 3, 4, room.depth() - 3));
}
case STORE -> targets.add(decorTarget(room, "supply_chest", room.width() - 3, 2, room.depth() - 2));
case ARCHIVE -> targets.add(decorTarget(room, "reading_desk", 2, 2, 2));
case PUMP_HALL, CONTROL_ROOM -> targets.add(decorTarget(room, "controls", 2, 2, 2));
default -> { }
}
}
for (int direction = 0; direction < 4; direction++) if ((room.openings() & 1 << direction) != 0) {
int x = direction == 1 ? room.width() - 1 : direction == 3 ? 0 : room.width() / 2;
int z = direction == 2 ? room.depth() - 1 : direction == 0 ? 0 : room.depth() / 2;
targets.add(new Target("opening_" + DIRECTIONS[direction].getName(), x, 2, z));
}
return List.copyOf(targets);
}
public static void render(RuinsPlan17.Room room, LostCityRenderer.BlockWriter output) {
var buffer = fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20.buffer(output);
var b = new Builder(room, buffer);
b.shell();
switch (room.kind()) {
case ACCESS -> b.access();
case CORRIDOR -> b.corridor();
case MINE_GALLERY -> b.mine();
case WORKSHOP -> b.workshop();
case STORE -> b.store();
case TILED_HALL -> b.tiledHall();
case ARCHIVE -> b.archive();
case SERVICE_HALL -> b.service();
case PUMP_HALL -> b.pumps();
case CONTROL_ROOM -> b.controls();
}
if (room.kind() != RuinsPlan17.Kind.ACCESS) { b.architecture(); b.lights(); }
b.openings();
for (var support : room.supports()) for (int y = support.minY(); y <= support.maxY(); y++)
buffer.set(support.x() - room.minX(), y - room.minY(), support.z() - room.minZ(),
Blocks.STONE_BRICKS.defaultBlockState());
buffer.flush();
}
private static boolean turn(RuinsPlan17.Room room) { return (room.seed() & 1L) != 0; }
private static Target decorTarget(RuinsPlan17.Room room, String name, int x, int y, int z) {
return new Target(name, turn(room) ? room.width() - 1 - x : x, y, turn(room) ? room.depth() - 1 - z : z);
}
private static final class Builder {
final RuinsPlan17.Room room;
final LostCityRenderer.BlockWriter out;
final int w, h, d, cx, cz;
final boolean reverse;
Builder(RuinsPlan17.Room room, LostCityRenderer.BlockWriter out) {
this.room = room; this.out = out; w = room.width(); h = room.height(); d = room.depth();
cx = w / 2; cz = d / 2; reverse = turn(room);
}
void set(int x, int y, int z, BlockState state) { out.set(x, y, z, state); }
void set(int x, int y, int z, net.minecraft.world.level.block.Block block) { set(x, y, z, block.defaultBlockState()); }
void decor(int x, int y, int z, BlockState state) {
out.set(reverse ? w - 1 - x : x, y, reverse ? d - 1 - z : z,
reverse ? state.rotate(Rotation.CLOCKWISE_180) : state);
}
void decor(int x, int y, int z, net.minecraft.world.level.block.Block block) { decor(x, y, z, block.defaultBlockState()); }
boolean crossing(int x, int z) { return Math.abs(x - cx) <= 1 || Math.abs(z - cz) <= 1; }
void shell() {
for (int y = 0; y < h; y++) for (int z = 0; z < d; z++) for (int x = 0; x < w; x++) {
BlockState state;
boolean wall = x == 0 || x == w - 1 || z == 0 || z == d - 1;
if (y == 0) state = Blocks.STONE_BRICKS.defaultBlockState();
else if (y == 1) state = floor(x, z);
else if (wall || y == h - 1) state = wallMaterial(x, y, z);
else state = Blocks.AIR.defaultBlockState();
set(x, y, z, state);
}
// A continuous ceiling cornice and stone thresholds remain intact despite the weathering.
for (int x = 0; x < w; x++) for (int z = 0; z < d; z++) if (x == 0 || x == w - 1 || z == 0 || z == d - 1)
set(x, h - 2, z, trim());
}
net.minecraft.world.level.block.Block trim() {
return switch (room.kind()) {
case ARCHIVE -> Blocks.STRIPPED_OAK_WOOD;
case TILED_HALL, SERVICE_HALL -> Blocks.POLISHED_DIORITE;
case WORKSHOP, STORE -> Blocks.TUFF_BRICKS;
case MINE_GALLERY -> Blocks.STRIPPED_OAK_WOOD;
default -> Blocks.POLISHED_TUFF;
};
}
BlockState wallMaterial(int x, int y, int z) {
if (y <= 2) return Blocks.STONE_BRICKS.defaultBlockState();
if (y == h - 1) return (room.kind() == RuinsPlan17.Kind.ARCHIVE
? Blocks.BIRCH_PLANKS : Blocks.SMOOTH_STONE).defaultBlockState();
int along = x == 0 || x == w - 1 ? z : x;
net.minecraft.world.level.block.Block wall = switch (room.kind()) {
case ARCHIVE -> along % 4 == 0 ? Blocks.STRIPPED_OAK_WOOD : Blocks.BIRCH_PLANKS;
case WORKSHOP -> along % 4 == 0 ? Blocks.POLISHED_ANDESITE : Blocks.TUFF_BRICKS;
case STORE -> along % 4 == 0 ? Blocks.STRIPPED_OAK_WOOD : Blocks.POLISHED_GRANITE;
case MINE_GALLERY -> along % 4 == 0 ? Blocks.STRIPPED_OAK_WOOD : Blocks.TUFF;
case TILED_HALL -> y == 3 ? Blocks.POLISHED_DIORITE : Blocks.CALCITE;
case SERVICE_HALL -> y == 3 ? Blocks.BIRCH_PLANKS : Blocks.SMOOTH_STONE;
case PUMP_HALL -> along % 4 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS;
case CONTROL_ROOM -> along % 4 == 0 ? Blocks.POLISHED_ANDESITE : Blocks.SMOOTH_STONE;
default -> along % 4 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS;
};
return wall.defaultBlockState();
}
BlockState floor(int x, int z) {
boolean border = x == 1 || x == w - 2 || z == 1 || z == d - 2;
return switch (room.kind()) {
case MINE_GALLERY -> (x == cx ? Blocks.DARK_OAK_PLANKS : (x + z) % 5 == 0 ? Blocks.ANDESITE : Blocks.STONE).defaultBlockState();
case ARCHIVE -> (border ? Blocks.STRIPPED_OAK_WOOD : Blocks.BIRCH_PLANKS).defaultBlockState();
case WORKSHOP -> (border ? Blocks.POLISHED_GRANITE : Blocks.SMOOTH_STONE).defaultBlockState();
case STORE -> (border ? Blocks.TUFF_BRICKS : Blocks.OAK_PLANKS).defaultBlockState();
case TILED_HALL -> ((x / 2 + z / 2) % 2 == 0 ? Blocks.CALCITE : Blocks.POLISHED_DIORITE).defaultBlockState();
case SERVICE_HALL -> (border ? Blocks.POLISHED_DIORITE : Blocks.SMOOTH_STONE).defaultBlockState();
case PUMP_HALL, CONTROL_ROOM -> (x == cx || z == cz ? Blocks.POLISHED_ANDESITE : Blocks.SMOOTH_STONE).defaultBlockState();
default -> (border && Math.floorMod(x + z + room.seed(), 4) == 0 ? Blocks.CHISELED_TUFF_BRICKS
: border ? Blocks.TUFF_BRICKS : Blocks.POLISHED_TUFF).defaultBlockState();
};
}
void architecture() {
// High inset cornices soften the square excavation without taking away the
// three-wide walking cross. The outer stone shell stays closed and watertight.
var stair = switch (room.kind()) {
case ARCHIVE -> Blocks.OAK_STAIRS;
case MINE_GALLERY, STORE -> Blocks.OAK_STAIRS;
case TILED_HALL, SERVICE_HALL -> Blocks.POLISHED_DIORITE_STAIRS;
default -> Blocks.TUFF_BRICK_STAIRS;
};
for (int x = 2; x < w - 2; x++) {
set(x, h - 2, 1, stair.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH)
.setValue(BlockStateProperties.HALF, net.minecraft.world.level.block.state.properties.Half.TOP));
set(x, h - 2, d - 2, stair.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH)
.setValue(BlockStateProperties.HALF, net.minecraft.world.level.block.state.properties.Half.TOP));
}
for (int z = 2; z < d - 2; z++) {
set(1, h - 2, z, stair.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST)
.setValue(BlockStateProperties.HALF, net.minecraft.world.level.block.state.properties.Half.TOP));
set(w - 2, h - 2, z, stair.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST)
.setValue(BlockStateProperties.HALF, net.minecraft.world.level.block.state.properties.Half.TOP));
}
// Exposed joists read as timber construction in the archive and interrupted
// workshop; technical rooms use a repeated rib in their own mineral palette.
if (w >= 9 && d >= 9) for (int z = 3; z < d - 2; z += 4)
for (int x = 2; x < w - 2; x++) set(x, h - 2, z,
(room.kind() == RuinsPlan17.Kind.ARCHIVE || room.kind() == RuinsPlan17.Kind.WORKSHOP
? Blocks.STRIPPED_OAK_WOOD : trim()).defaultBlockState()
.trySetValue(BlockStateProperties.AXIS, Direction.Axis.X));
// Fitted thresholds and carved voussoirs distinguish doorways from random holes.
for (int direction = 0; direction < 4; direction++) if ((room.openings() & 1 << direction) != 0)
for (int side = -1; side <= 1; side++) {
int x = direction == 1 ? w - 1 : direction == 3 ? 0 : cx + side;
int z = direction == 2 ? d - 1 : direction == 0 ? 0 : cz + side;
set(x, 5, z, side == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS);
}
}
void access() {
int exit = Integer.numberOfTrailingZeros(room.openings() == 0 ? 1 : room.openings());
int wall = (exit + 2) & 3;
int x = wall == 1 ? w - 2 : wall == 3 ? 1 : cx;
int z = wall == 2 ? d - 2 : wall == 0 ? 1 : cz;
Direction facing = DIRECTIONS[wall].getOpposite();
if ((room.openings() & 1 << wall) != 0) {
// A branching shaft cannot attach its lowest ladder to another open doorway.
// This corner post leaves all three doorway cells and the central crossing usable.
x = 1; z = 2; facing = Direction.SOUTH;
for (int y = 2; y <= h - 3; y++) set(1, y, 1, Blocks.STONE_BRICKS);
}
BlockState ladder = Blocks.LADDER.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing);
for (int y = 2; y <= h - 3; y++) set(x, y, z, ladder);
for (int zz = 0; zz < d; zz++) for (int xx = 0; xx < w; xx++) {
set(xx, h - 3, zz, xx == x && zz == z ? ladder : Blocks.SMOOTH_STONE.defaultBlockState());
set(xx, h - 2, zz, Blocks.AIR); set(xx, h - 1, zz, Blocks.AIR);
}
set(x, h - 2, z, Blocks.DARK_OAK_TRAPDOOR.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, facing));
}
void corridor() {
boolean alongX = w > d;
int length = alongX ? w : d;
for (int n = 2; n < length - 1; n += 4) {
if (alongX) {
for (int y = 2; y <= h - 1; y++) { set(n, y, 0, Blocks.POLISHED_ANDESITE); set(n, y, d - 1, Blocks.POLISHED_ANDESITE); }
} else for (int y = 2; y <= h - 1; y++) { set(0, y, n, Blocks.POLISHED_ANDESITE); set(w - 1, y, n, Blocks.POLISHED_ANDESITE); }
}
}
void mine() {
for (int z : new int[] {2, d - 3}) {
for (int x : new int[] {2, w - 3}) for (int y = 2; y <= h - 2; y++) decor(x, y, z, Blocks.STRIPPED_OAK_LOG);
for (int x = 2; x <= w - 3; x++) decor(x, h - 2, z, Blocks.STRIPPED_OAK_LOG.defaultBlockState()
.setValue(BlockStateProperties.AXIS, Direction.Axis.X));
}
for (int z = 1; z < d - 1; z++) set(cx, 2, z, Blocks.RAIL);
// A short second siding ends at a buffer; the main track and cross passage remain open.
if (w >= 13) {
for (int z = 3; z < d - 2; z++) decor(w - 5, 2, z, Blocks.RAIL);
decor(w - 5, 2, d - 2, Blocks.DARK_OAK_FENCE);
}
decor(1, 2, 1, Blocks.BARREL);
decor(w - 2, 2, d - 2, Blocks.ANDESITE);
if (LostCityPalette.roll(room.seed(), 1, 0, 0, 2) == 0) decor(w - 2, 3, d - 2, Blocks.COBBLESTONE);
}
void workshop() {
decor(1, 2, 1, Blocks.CRAFTING_TABLE);
decor(2, 2, 1, Blocks.FURNACE.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
decor(1, 2, d - 3, Blocks.GRINDSTONE.defaultBlockState()
.setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR)
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST));
decor(w - 2, 2, 1, Blocks.STONECUTTER.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST));
decor(1, 2, d - 2, Blocks.CAULDRON);
decor(2, 2, d - 2, Blocks.ANVIL);
// Two grounded posts and a small planked deck preserve the trace of unfinished repairs.
for (int x = w - 3; x <= w - 2; x++) {
decor(x, 2, d - 3, Blocks.STRIPPED_OAK_LOG);
for (int z = d - 3; z <= d - 2; z++) decor(x, 3, z, Blocks.DARK_OAK_PLANKS);
}
for (int y = 2; y <= 3; y++) decor(w - 3, y, d - 4, Blocks.LADDER.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
decor(w - 2, 2, d - 2, Blocks.BARREL);
}
void store() {
for (int z = 1; z < d - 1; z++) if (!crossing(1, z)) {
decor(1, 2, z, Blocks.BARREL);
if ((z & 1) == 0) decor(1, 3, z, Blocks.BARREL);
decor(1, 4, z, Blocks.DARK_OAK_SLAB);
}
for (int x = cx + 2; x <= w - 2; x++) decor(x, 2, 1, Blocks.DARK_OAK_PLANKS);
decor(w - 2, 3, 1, Blocks.BARREL);
loot(w - 2, 2, d - 2, BuiltInLootTables.VILLAGE_PLAINS_HOUSE);
}
void tiledHall() {
// Repeated columns are interrupted by complete, centered cross passages.
for (int x : new int[] {2, w - 3}) for (int z : new int[] {2, d - 3})
for (int y = 2; y < h - 1; y++) decor(x, y, z, y == 2 || y == h - 2 ? Blocks.SMOOTH_STONE : Blocks.CALCITE);
if (w >= 13) for (int x : new int[] {3, w - 4}) for (int z = 1; z <= d - 2; z++)
if (Math.abs(z - cz) > 1) for (int y = 2; y < h - 1; y++)
decor(x, y, z, y == h - 2 ? Blocks.POLISHED_ANDESITE : Blocks.SMOOTH_STONE);
for (int x = cx - 1; x <= cx + 1; x++) {
decor(x, h - 2, 1, Blocks.POLISHED_ANDESITE);
decor(x, h - 2, d - 2, Blocks.POLISHED_ANDESITE);
}
// Mismatched surviving benches give these physically reachable side niches a former use.
decor(1, 2, 1, seat(Direction.WEST));
decor(w - 2, 2, 1, seat(Direction.EAST));
}
void archive() {
for (int z = 1; z <= d - 2; z++) if (Math.abs(z - cz) > 1)
for (int y = 2; y <= 4; y++) decor(w - 2, y, z,
y == 4 && (z & 1) == 0 ? Blocks.DARK_OAK_PLANKS : Blocks.BOOKSHELF);
decor(1, 2, 1, Blocks.BOOKSHELF);
decor(1, 2, 2, Blocks.LECTERN.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST));
decor(1, 2, d - 2, Blocks.CARTOGRAPHY_TABLE);
decor(2, 2, d - 3, Blocks.BARREL);
loot(1, 2, d - 3, BuiltInLootTables.VILLAGE_CARTOGRAPHER);
}
void service() {
for (int z = 1; z < d - 1; z++) if (Math.abs(z - cz) > 1) {
decor(1, 2, z, Blocks.POLISHED_ANDESITE);
decor(1, 3, z, Blocks.IRON_BARS);
}
for (int z : new int[] {1, d - 2}) for (int x = 1; x < w - 1; x++)
decor(x, h - 2, z, Blocks.STRIPPED_OAK_LOG.defaultBlockState().setValue(BlockStateProperties.AXIS, Direction.Axis.X));
decor(w - 2, 2, 1, Blocks.BARREL);
decor(w - 2, 2, d - 2, Blocks.CRAFTING_TABLE);
decor(2, 2, d - 2, seat(Direction.SOUTH));
}
void pumps() {
for (int z : new int[] {1, d - 2}) {
decor(1, 2, z, Blocks.DISPENSER.defaultBlockState().setValue(BlockStateProperties.FACING, Direction.EAST));
decor(2, 2, z, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 3));
decor(1, 3, z, Blocks.POLISHED_ANDESITE);
decor(1, 4, z, Blocks.IRON_BARS);
decor(w - 2, 2, z, Blocks.CAULDRON);
}
for (int z = 1; z <= d - 2; z++) decor(1, h - 2, z, Blocks.POLISHED_ANDESITE);
decor(w - 2, 2, 2, Blocks.SMOOTH_STONE);
decor(w - 2, 3, 2, Blocks.LEVER.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR)
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST));
}
void controls() {
for (int x = 1; x <= cx - 2; x++) {
decor(x, 2, 1, Blocks.SMOOTH_STONE);
decor(x, 3, 1, Blocks.LEVER.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR)
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
decor(x, 3, 0, Blocks.REDSTONE_LAMP);
}
decor(1, 2, 2, seat(Direction.WEST));
decor(w - 2, 2, 1, Blocks.CRAFTING_TABLE);
decor(w - 2, 2, d - 2, Blocks.BARREL);
// An inspection window remains backed by a complete wall: there is no opening into unverified rock or water.
for (int z = cz + 2; z < d - 1; z++) decor(0, 3, z, Blocks.GLASS);
decor(1, 2, d - 2, Blocks.CARTOGRAPHY_TABLE);
}
BlockState seat(Direction back) { return Blocks.DARK_OAK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, back); }
void lights() {
// Every lantern hangs directly from the full masonry ceiling; never from a bottom slab.
for (int[] p : new int[][] {{1, 1}, {w - 2, d - 2}})
set(p[0], h - 2, p[1], Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
}
void openings() {
for (int direction = 0; direction < 4; direction++) if ((room.openings() & 1 << direction) != 0)
for (int side = -1; side <= 1; side++) {
int x = direction == 1 ? w - 1 : direction == 3 ? 0 : cx + side;
int z = direction == 2 ? d - 1 : direction == 0 ? 0 : cz + side;
set(x, 1, z, Blocks.SMOOTH_STONE);
for (int y = 2; y <= 4; y++) set(x, y, z, Blocks.AIR);
}
}
void loot(int x, int y, int z, net.minecraft.resources.ResourceKey<net.minecraft.world.level.storage.loot.LootTable> table) {
out.chest(reverse ? w - 1 - x : x, y, reverse ? d - 1 - z : z, table, room.seed() ^ 0x17A6C41L);
}
}
}
@@ -13,9 +13,9 @@ public final class RuinsStructure17 extends Structure {
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 17 || !generator.experimentalStructures()) return Optional.empty();
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21) || !generator.experimentalStructures()) return Optional.empty();
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
if (island == null || !island.origin() || island.generation() != 17) return Optional.empty();
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return Optional.empty();
var networks = generator.ruinsPlan17().networks().stream()
.filter(n -> n.startChunkX() == context.chunkPos().x() && n.startChunkZ() == context.chunkPos().z()).toList();
if (networks.isEmpty()) return Optional.empty();
@@ -30,9 +30,9 @@ public final class RuinsStructure17 extends Structure {
return Optional.of(new GenerationStub(RuinsStructures17.locator(networks.getFirst()), pieces -> {
// Shafts come last so their surface hatches cannot be filled by a connected room.
networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() != RuinsPlan17.Kind.ACCESS)
.forEach(r -> pieces.addPiece(new RuinsPiece17(r))));
.forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS)
.forEach(r -> pieces.addPiece(new RuinsPiece17(r))));
.forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
}));
}
@Override public StructureType<?> type() { return RuinsStructures17.TYPE; }
@@ -35,6 +35,8 @@ public final class RuinsStructures17 {
SanctuaryMod.id("forgotten_networks_v17"), () -> RuinsStructure17.CODEC);
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
SanctuaryMod.id("forgotten_room_v17"), RuinsPiece17::new);
public static final StructurePieceType PIECE20 = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
SanctuaryMod.id("forgotten_room_v20"), RuinsPiece20::new);
private static final ThreadLocal<Integer> PLACEMENT_DEPTH = ThreadLocal.withInitial(() -> 0);
private RuinsStructures17() {}
@@ -53,10 +55,10 @@ public final class RuinsStructures17 {
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> level) {
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || generator.rootGeneration() != 17
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21)
|| !sanctuary.experimentalStructures()) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || island.generation() != 17) return;
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
var plan = sanctuary.ruinsPlan17();
if (plan.networks().stream().noneMatch(n -> n.startChunkX() == chunk.getPos().x()
&& n.startChunkZ() == chunk.getPos().z())) return;
@@ -76,7 +78,7 @@ public final class RuinsStructures17 {
BlockPos origin, int radius, boolean skipReferenced) {
if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 17 || !generator.experimentalStructures()
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21) || !generator.experimentalStructures()
|| !level.structureManager().shouldGenerateStructures()) return null;
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
if (!structures.contains(holder)) return null;
@@ -84,7 +86,7 @@ public final class RuinsStructures17 {
int originChunkX = origin.getX() >> 4, originChunkZ = origin.getZ() >> 4;
for (var network : generator.ruinsPlan17().networks()) {
var island = generator.islandForChunk(network.startChunkX(), network.startChunkZ());
if (island == null || !island.origin() || island.generation() != 17 || !island.status().equals("ready")) continue;
if (island == null || !island.origin() || island.generation() != generator.rootGeneration() || !island.status().equals("ready")) continue;
if (Math.abs((long) network.startChunkX() - originChunkX) <= radius
&& Math.abs((long) network.startChunkZ() - originChunkZ) <= radius) candidates.add(network);
}
@@ -97,8 +99,8 @@ public final class RuinsStructures17 {
StructureStart start = level.structureManager().getStartForStructure(holder.value(), chunk);
if (start == null || !start.isValid() || skipReferenced && !start.canBeReferenced()) continue;
if (skipReferenced) level.structureManager().addReference(start);
var entrance = start.getPieces().stream().filter(p -> p instanceof RuinsPiece17 room
&& room.room().kind() == RuinsPlan17.Kind.ACCESS).findFirst().orElse(start.getPieces().getFirst());
var entrance = start.getPieces().stream().filter(p -> (p instanceof RuinsPiece17 room && room.room().kind() == RuinsPlan17.Kind.ACCESS)
|| (p instanceof RuinsPiece20 detail && detail.room().kind() == RuinsPlan17.Kind.ACCESS)).findFirst().orElse(start.getPieces().getFirst());
return Pair.of(entrance.getLocatorPosition(), holder);
}
return null;
@@ -0,0 +1,114 @@
package fr.koka.sanctuary.worldgen.transit;
import java.util.ArrayList;
import java.util.Set;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.ResourceKey;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.loot.LootTable;
/** Saved generation21 room geometry is rendered without replanning or reading neighboring chunks. */
public final class TransitPiece21 extends StructurePiece {
private static final Set<String> MODULE_KEYS = Set.of("Schema", "Kind", "MinX", "MinY", "MinZ", "Width",
"Height", "Depth", "Facing", "Openings", "ModuleSeed", "Supports");
private static final Set<String> SUPPORT_KEYS = Set.of("X", "MinY", "Z", "MaxY");
private final TransitPlan21.Module room;
public TransitPiece21(TransitPlan21.Module room) {
super(TransitStructures21.PIECE, 0, bounds(room));
this.room = room;
setOrientation(null);
}
public TransitPiece21(StructurePieceSerializationContext context, CompoundTag tag) {
super(TransitStructures21.PIECE, tag);
room = decodeModule(tag.getCompound("SanctuaryModule")
.orElseThrow(() -> new IllegalArgumentException("Missing saved transit room")));
if (!getBoundingBox().equals(bounds(room)))
throw new IllegalArgumentException("Transit room disagrees with its saved native bounding box");
setOrientation(null);
}
public TransitPlan21.Module module() { return room; }
@Override public BlockPos getLocatorPosition() { return locator(room); }
public static BlockPos locator(TransitPlan21.Module room) {
return new BlockPos(room.minX() + room.width() / 2,
room.kind() == TransitPlan21.Kind.ACCESS ? room.surfaceY() + 1 : room.floorY() + 1,
room.kind() == TransitPlan21.Kind.ACCESS ? room.minZ() : room.minZ() + room.depth() / 2);
}
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
tag.put("SanctuaryModule", encodeModule(room));
}
public static CompoundTag encodeModule(TransitPlan21.Module room) {
var tag = new CompoundTag(); tag.putInt("Schema", 1); tag.putString("Kind", room.kind().name());
tag.putInt("MinX", room.minX()); tag.putInt("MinY", room.minY()); tag.putInt("MinZ", room.minZ());
tag.putInt("Width", room.width()); tag.putInt("Height", room.height()); tag.putInt("Depth", room.depth());
tag.putInt("Facing", room.facing()); tag.putInt("Openings", room.openings()); tag.putLong("ModuleSeed", room.seed());
var supports = new ListTag();
for (var support : room.supports()) {
var value = new CompoundTag(); value.putInt("X", support.x()); value.putInt("MinY", support.minY());
value.putInt("Z", support.z()); value.putInt("MaxY", support.maxY()); supports.add(value);
}
tag.put("Supports", supports);
return tag;
}
public static TransitPlan21.Module decodeModule(CompoundTag tag) {
if (!tag.keySet().equals(MODULE_KEYS)) throw new IllegalArgumentException("Incomplete or unknown transit room fields");
if (integer(tag, "Schema") != 1) throw new IllegalArgumentException("Unsupported transit room schema");
var kind = TransitPlan21.Kind.valueOf(tag.getString("Kind").orElseThrow(() -> new IllegalArgumentException("Missing room kind")));
type(tag, "ModuleSeed", Tag.TAG_LONG); type(tag, "Supports", Tag.TAG_LIST);
var saved = tag.getList("Supports").orElseThrow();
if (saved.size() > 64) throw new IllegalArgumentException("Too many saved transit room supports");
var supports = new ArrayList<TransitPlan21.Support>();
for (var entry : saved) {
if (!(entry instanceof CompoundTag value) || !value.keySet().equals(SUPPORT_KEYS))
throw new IllegalArgumentException("Invalid transit room support fields");
supports.add(new TransitPlan21.Support(integer(value, "X"), integer(value, "MinY"), integer(value, "Z"), integer(value, "MaxY")));
}
return new TransitPlan21.Module(kind, integer(tag, "MinX"), integer(tag, "MinY"), integer(tag, "MinZ"),
integer(tag, "Width"), integer(tag, "Height"), integer(tag, "Depth"), integer(tag, "Facing"), integer(tag, "Openings"),
tag.getLong("ModuleSeed").orElseThrow(), supports);
}
private static int integer(CompoundTag tag, String key) {
type(tag, key, Tag.TAG_INT);
return tag.getInt(key).orElseThrow();
}
private static void type(CompoundTag tag, String key, byte id) {
if (tag.get(key) == null || tag.get(key).getId() != id)
throw new IllegalArgumentException("Invalid saved transit room field: " + key);
}
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
TransitStructures21.placing(() -> TransitRenderer21.render(room, new LostCityRenderer.BlockWriter() {
private BlockPos world(int x, int y, int z) { return new BlockPos(room.minX() + x, room.minY() + y, room.minZ() + z); }
@Override public void set(int x, int y, int z, BlockState block) {
var position = world(x, y, z);
if (getBoundingBox().isInside(position) && clip.isInside(position))
placeBlock(level, block, position.getX(), position.getY(), position.getZ(), clip);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
var position = world(x, y, z);
if (getBoundingBox().isInside(position) && clip.isInside(position))
createChest(level, clip, RandomSource.create(seed), position, loot, null);
}
}));
}
public static BoundingBox bounds(TransitPlan21.Module room) {
int minY = room.minY(), maxY = Math.addExact(room.minY(), room.height() - 1);
for (var support : room.supports()) {
minY = Math.min(minY, support.minY()); maxY = Math.max(maxY, support.maxY());
}
return new BoundingBox(room.minX(), minY, room.minZ(), Math.addExact(room.minX(), room.width() - 1),
maxY, Math.addExact(room.minZ(), room.depth() - 1));
}
}
@@ -0,0 +1,86 @@
package fr.koka.sanctuary.worldgen.transit;
import java.util.HashSet;
import java.util.List;
/** Immutable final geometry. Each module owns a small native start, never an island-wide start. */
public record TransitPlan21(List<Module> modules, int samples, String diagnostic) {
public static final int MAX_SAMPLES = 3_000_000;
public static final TransitPlan21 EMPTY = new TransitPlan21(List.of(), 0, "disabled");
public enum Kind { GALLERY, JUNCTION, HALL, ACCESS }
public record Support(int x, int minY, int z, int maxY) {
public Support {
if (minY < 8 || maxY > 375 || minY > maxY || maxY - minY >= 16)
throw new IllegalArgumentException("Invalid transit footing");
}
}
/** World-aligned bounds. Openings N1/E2/S4/W8 are seven wide, local y2..8.
* Access top deck is maxY-4. Its north gate, at centerX/minZ, has a certified natural approach. */
public record Module(Kind kind, int minX, int minY, int minZ, int width, int height, int depth,
int facing, int openings, long seed, List<Support> supports) {
public Module {
supports = List.copyOf(supports);
if (kind == null || width < 9 || width > 25 || depth < 9 || depth > 25
|| (width & 1) == 0 || (depth & 1) == 0 || height < 10 || height > 160
|| minY < 16 || minY + height > 383 || facing < 0 || facing > 3
|| openings < 1 || openings > 15 || supports.size() > 64)
throw new IllegalArgumentException("Invalid transit module");
if (kind == Kind.ACCESS && (width != 17 || depth != 17 || height < 20))
throw new IllegalArgumentException("Transit stair access needs its complete 17x17 shaft");
if (kind != Kind.ACCESS && height != 10) throw new IllegalArgumentException("Unexpected gallery height");
var seen = new HashSet<Long>();
for (var s : supports) if (s.x() < minX || s.x() >= minX + width || s.z() < minZ
|| s.z() >= minZ + depth || s.maxY() != minY - 1 || !seen.add(key(s.x(), s.z())))
throw new IllegalArgumentException("Transit footing does not belong to its floor");
}
public int maxX() { return minX + width - 1; }
public int maxY() { return minY + height - 1; }
public int maxZ() { return minZ + depth - 1; }
public int centerX() { return minX + width / 2; }
public int centerZ() { return minZ + depth / 2; }
public int floorY() { return minY + 1; }
public int surfaceY() { return maxY() - 4; }
public int startChunkX() { return centerX() >> 4; }
public int startChunkZ() { return centerZ() >> 4; }
public int boundsMinY() { return supports.stream().mapToInt(Support::minY).min().orElse(minY); }
public boolean contains(int x, int y, int z) {
if (x < minX || x > maxX() || z < minZ || z > maxZ()) return false;
if (y >= minY && y <= maxY()) return true;
return supports.stream().anyMatch(s -> x == s.x() && z == s.z() && y >= s.minY() && y <= s.maxY());
}
public boolean protects(int x, int y, int z) {
if (contains(x, y, z)) return true;
if (kind == Kind.ACCESS && x >= centerX() - 2 && x <= centerX() + 2
&& z >= minZ - 3 && z < minZ && y >= surfaceY() - 2 && y <= surfaceY() + 3) return true;
// Two untouched layers carry the slab and the certified short piers.
if (x >= minX && x <= maxX() && z >= minZ && z <= maxZ() && y >= minY - 2 && y < minY) return true;
return supports.stream().anyMatch(s -> x == s.x() && z == s.z() && y >= s.minY() - 2 && y < s.minY());
}
}
public TransitPlan21 {
modules = List.copyOf(modules);
if (modules.size() > 64 || samples < 0 || samples > MAX_SAMPLES || diagnostic == null || diagnostic.length() > 1024)
throw new IllegalArgumentException("Invalid transit planning result");
if (!modules.isEmpty() && modules.stream().filter(m -> m.kind() == Kind.ACCESS).count() != 2)
throw new IllegalArgumentException("A through-gallery needs two surface accesses");
for (int i = 0; i < modules.size(); i++) for (int j = i + 1; j < modules.size(); j++) {
var a = modules.get(i); var b = modules.get(j);
if (a.minX() <= b.maxX() && a.maxX() >= b.minX() && a.minZ() <= b.maxZ() && a.maxZ() >= b.minZ())
throw new IllegalArgumentException("Transit modules overlap");
}
}
public boolean isEmpty() { return modules.isEmpty(); }
public List<Module> modulesForChunk(int x, int z) {
return modules.stream().filter(m -> m.startChunkX() == x && m.startChunkZ() == z).toList();
}
public boolean contains(int x, int y, int z) { return modules.stream().anyMatch(m -> m.contains(x, y, z)); }
public boolean protects(int x, int y, int z) { return modules.stream().anyMatch(m -> m.protects(x, y, z)); }
public boolean intersectsProtection(int x0, int y0, int z0, int x1, int y1, int z1) {
for (var m : modules) if (m.kind() == Kind.ACCESS && x0 <= m.centerX() + 2 && x1 >= m.centerX() - 2
&& z0 < m.minZ() && z1 >= m.minZ() - 3 && y0 <= m.surfaceY() + 3 && y1 >= m.surfaceY() - 2) return true;
for (var m : modules) if (x0 <= m.maxX() && x1 >= m.minX() && z0 <= m.maxZ() && z1 >= m.minZ()
&& y0 <= m.maxY() && y1 >= m.boundsMinY() - 2) return true;
return false;
}
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
}
@@ -0,0 +1,220 @@
package fr.koka.sanctuary.worldgen.transit;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Module;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Support;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/** A bounded read-only through-route. Failed candidates never publish partial corridors or shafts. */
public final class TransitPlanner21 {
public static final int MAX_COLUMNS = 12_000;
private TransitPlanner21() {}
public interface Terrain {
boolean owns(int x, int z);
boolean solid(int x, int y, int z);
boolean protectedAt(int x, int y, int z);
int surfaceY(int x, int z);
default String obstruction(int x, int y, int z) { return "protected terrain"; }
}
public static TransitPlan21 plan(long seed, int centerX, int centerZ, int diameter, Terrain terrain) {
if (diameter < 512 || diameter > 1024) throw new IllegalArgumentException("Unsupported transit island diameter");
var probe = new Probe(terrain);
int count = (int) Math.ceil(diameter * .42 / 17);
if ((count & 1) == 0) count++;
String last = "no dry supported through-route";
var choices = new ArrayList<Choice>();
// Reserve most of the native-column budget for the exact scan of selected routes.
probe.sampleLimit = 1_800_000; probe.columnLimit = 7_500;
try {
search: for (int advance : new int[]{0, -34, 34}) for (boolean alongX : new boolean[]{true, false}) for (int shift : new int[]{-diameter / 8, diameter / 8, -diameter / 16, diameter / 16, 0, -3 * diameter / 16, 3 * diameter / 16}) {
int cx = centerX + (alongX ? advance : shift), cz = centerZ + (alongX ? shift : advance);
for (int floor : new int[]{216, 200, 224, 208, 184, 232, 160}) {
var route = candidate(seed, cx, cz, count, floor, alongX, probe, false);
String label = "axis=" + (alongX ? "east-west" : "north-south") + " offset=" + shift + " advance=" + advance + " floor=" + floor;
if (route == null) {
last = probe.reason;
diagnostic("preselection reject " + label + " " + last + " columns=" + probe.columns.size());
continue;
}
int cost = route.stream().mapToInt(m -> m.supports().stream().mapToInt(pier -> pier.maxY() - pier.minY() + 1).sum()
+ (m.kind() == Kind.ACCESS ? m.surfaceY() - m.floorY() : 0)).sum();
// Try the intermediate levels first; within each level prefer the better supported axis.
int heightOrder = switch (floor) { case 216 -> 0; case 200 -> 1; case 224 -> 2; case 208 -> 3; case 184 -> 4; case 232 -> 5; default -> 6; };
choices.add(new Choice(route, floor, alongX, heightOrder * 100_000 + cost, label));
diagnostic("preselection admitted " + label + " bearing_cost=" + cost);
if (choices.size() >= 8) break search;
}
}
} catch (BudgetExceeded exhausted) { diagnostic("prescreen budget reached; retain " + choices.size() + " candidates"); }
probe.sampleLimit = TransitPlan21.MAX_SAMPLES; probe.columnLimit = MAX_COLUMNS;
choices.sort(java.util.Comparator.comparingInt(Choice::cost));
try {
for (var choice : choices) {
var finalRoute = new ArrayList<Module>();
for (var rough : choice.route()) {
var complete = certify(rough, probe, true);
if (complete == null) { last = probe.reason; break; }
finalRoute.add(complete);
}
if (finalRoute.size() != choice.route().size()) {
diagnostic("exact reject " + choice.label() + " " + last); continue;
}
addBranch(finalRoute, choice.floor(), choice.alongX(), seed, probe);
return new TransitPlan21(finalRoute, probe.samples, "accepted " + choice.label()
+ " span=" + ((count + 1) * 17) + " columns=" + probe.columns.size()
+ " candidates=" + choices.size() + " hall=" + finalRoute.stream().anyMatch(m -> m.kind() == Kind.HALL));
}
} catch (BudgetExceeded exhausted) { last = "bounded exact search exhausted: " + probe.reason; }
return new TransitPlan21(List.of(), probe.samples, last + "; candidates=" + choices.size() + "; columns=" + probe.columns.size());
}
private record Choice(List<Module> route, int floor, boolean alongX, int cost, String label) {}
private static List<Module> candidate(long seed, int cx, int cz, int count, int floor,
boolean alongX, Probe probe, boolean full) {
int first = -(count + 1) * 17 / 2;
int ax = cx + (alongX ? first : 0), az = cz + (alongX ? 0 : first);
int bx = cx - (alongX ? first : 0), bz = cz - (alongX ? 0 : first);
// Cheap route witnesses reject deep obstacles before two tall shafts are scanned in full.
for (int d = first + 17; d <= -first - 17; d += 8) for (int side : new int[]{-4, 0, 4})
for (int y : new int[]{floor - 2, floor, floor + 4, floor + 8}) {
int x = cx + (alongX ? d : side), z = cz + (alongX ? side : d);
if (probe.protectedAt(x, y, z)) {
probe.reason = "coarse route [" + x + "," + y + "," + z + "] " + probe.terrain.obstruction(x, y, z); return null;
}
}
// Certify the two indispensable exits before spending the route's column budget.
Module left = access(ax, az, floor, alongX ? 2 : 4, seed ^ 0x2101L, probe, full);
if (left == null) return null;
Module right = access(bx, bz, floor, alongX ? 8 : 1, seed ^ 0x2102L, probe, full);
if (right == null) return null;
var result = new ArrayList<Module>(); result.add(left);
int unanchoredRun = 0;
for (int n = 0; n < count; n++) {
int d = first + 17 * (n + 1);
int x = cx + (alongX ? d - 8 : -4), z = cz + (alongX ? -4 : d - 8);
var module = new Module(Kind.GALLERY, x, floor - 1, z, alongX ? 17 : 9, 10, alongX ? 9 : 17,
alongX ? 1 : 0, alongX ? 10 : 5, seed ^ (n * 0x9E3779B97F4A7C15L), List.of());
module = certify(module, probe, full);
if (module == null) return null;
unanchoredRun = probe.bearings.get(module) == 15 ? 0 : unanchoredRun + 1;
if (unanchoredRun > 2) { probe.reason = "unsupported span exceeds 34 blocks"; return null; }
result.add(module);
}
result.add(right);
return result;
}
private static Module access(int cx, int cz, int floor, int opening, long seed, Probe probe, boolean full) {
int top = -1, low = 384;
for (int x = cx - 2; x <= cx + 2; x++) for (int z = cz - 11; z <= cz - 9; z++) {
int surface = probe.surface(x, z); top = Math.max(top, surface); low = Math.min(low, surface);
}
if (low < 0 || top - low > 1) { probe.reason = "access approach relief [" + cx + "," + cz + "] " + low + ".." + top; return null; }
if (top < floor + 18 || top > floor + 145) { probe.reason = "surface access height"; return null; }
// Its visible north portico meets real ground; its three-wide approach is never an unsupported threshold.
for (int x = cx - 2; x <= cx + 2; x++) for (int z = cz - 11; z <= cz - 9; z++) {
int y = probe.surface(x, z);
if (y < top - 1 || y > top || probe.protectedAt(x, y, z)
|| probe.protectedAt(x, top + 1, z) || probe.protectedAt(x, top + 2, z)) {
probe.reason = "access approach [" + x + "," + top + "," + z + "] terrain=" + y; return null;
}
}
for (int x : new int[]{cx - 8, cx, cx + 8}) for (int z : new int[]{cz - 8, cz, cz + 8}) {
int y = probe.surface(x, z);
// The portico alone needs level terrain. A buried shaft can follow a hillside;
// its local walls and checked footings carry the deck without flattening the hill.
if (y < Math.max(floor + 8, top - 24) || y > top + 16) { probe.reason = "access hillside [" + x + "," + top + "," + z + "] terrain=" + y; return null; }
}
return certify(new Module(Kind.ACCESS, cx - 8, floor - 1, cz - 8, 17,
top - (floor - 1) + 5, 17, 0, opening, seed, List.of()), probe, full);
}
private static void addBranch(List<Module> route, int floor, boolean alongX, long seed, Probe probe) {
int index = 1 + (route.size() - 2) / 2;
var junction = route.get(index);
if (probe.bearings.getOrDefault(junction, 0) != 15) return;
for (int side : new int[]{-1, 1}) {
try {
int x = alongX ? junction.centerX() - 4 : side < 0 ? junction.minX() - 17 : junction.maxX() + 1;
int z = alongX ? side < 0 ? junction.minZ() - 17 : junction.maxZ() + 1 : junction.centerZ() - 4;
int width = alongX ? 9 : 17, depth = alongX ? 17 : 9;
var passage = certify(new Module(Kind.GALLERY, x, floor - 1, z, width, 10, depth,
alongX ? 0 : 1, alongX ? 5 : 10, seed ^ 0x21B1L ^ side, List.of()), probe, true);
if (passage == null) continue;
int hx = alongX ? junction.centerX() - 8 : side < 0 ? passage.minX() - 21 : passage.maxX() + 1;
int hz = alongX ? side < 0 ? passage.minZ() - 21 : passage.maxZ() + 1 : junction.centerZ() - 8;
int opening = alongX ? side < 0 ? 4 : 1 : side < 0 ? 2 : 8;
var hall = certify(new Module(Kind.HALL, hx, floor - 1, hz, alongX ? 17 : 21, 10,
alongX ? 21 : 17, alongX ? 0 : 1, opening, seed ^ 0x21A1L ^ side, List.of()), probe, true);
if (hall == null) continue;
int branch = alongX ? side < 0 ? 1 : 4 : side < 0 ? 8 : 2;
route.set(index, new Module(Kind.JUNCTION, junction.minX(), junction.minY(), junction.minZ(),
junction.width(), 10, junction.depth(), junction.facing(), junction.openings() | branch,
junction.seed(), junction.supports()));
route.add(passage); route.add(hall); return;
} catch (BudgetExceeded exhausted) { return; } // The already certified traversal remains complete.
}
}
private static Module certify(Module module, Probe probe, boolean full) {
var supports = new ArrayList<Support>();
int bearingQuadrants = 0;
for (int z = module.minZ();; z = Math.min(z + 4, module.maxZ())) {
for (int x = module.minX();; x = Math.min(x + 4, module.maxX())) {
if (!probe.owns(x, z)) { probe.reason = "outside island"; return null; }
if (!full) for (int y = module.minY(); y <= module.maxY(); y += 4)
if (probe.protectedAt(x, y, z)) {
probe.reason = "coarse " + module.kind() + " [" + x + "," + y + "," + z + "] " + probe.terrain.obstruction(x, y, z); return null;
}
int anchor = module.minY() - 1;
while (anchor >= module.minY() - 17 && !(probe.solid(x, anchor, z) && probe.solid(x, anchor - 1, z))) anchor--;
if (anchor >= module.minY() - 17) {
for (int y = anchor - 1; y < module.minY(); y++) if (probe.protectedAt(x, y, z)) {
probe.reason = "protected footing"; return null;
}
if (anchor < module.minY() - 1) supports.add(new Support(x, anchor + 1, z, module.minY() - 1));
bearingQuadrants |= 1 << ((x < module.centerX() ? 0 : 1) + (z < module.centerZ() ? 0 : 2));
}
if (x == module.maxX()) break;
}
if (z == module.maxZ()) break;
}
// The continuous two-block deck can span gaps between its bearings. Require
// untouched rock in each quarter, not a pier at every point above a cavity.
if (bearingQuadrants != 15 && module.kind() != Kind.GALLERY) { probe.reason = "unsupported quarters " + module.kind()
+ " [" + module.minX() + "," + module.minY() + "," + module.minZ() + "] mask=" + bearingQuadrants; return null; }
if (full) {
// Hydrology and all previous structures are checked at every edited voxel after the cheap footing test.
for (int z = module.minZ(); z <= module.maxZ(); z++) for (int x = module.minX(); x <= module.maxX(); x++) {
if (!probe.owns(x, z)) { probe.reason = "outside island"; return null; }
for (int y = module.minY() - 2; y <= module.maxY(); y++) if (probe.protectedAt(x, y, z)) {
probe.reason = "volume " + module.kind() + " [" + x + "," + y + "," + z + "] " + probe.terrain.obstruction(x, y, z); return null;
}
}
}
var certified = new Module(module.kind(), module.minX(), module.minY(), module.minZ(), module.width(), module.height(),
module.depth(), module.facing(), module.openings(), module.seed(), supports);
probe.bearings.put(certified, bearingQuadrants);
return certified;
}
private static void diagnostic(String message) {
if (Boolean.getBoolean("sanctuary.test.transit21") || Boolean.getBoolean("sanctuary.transit.plannerDiagnostics"))
System.getLogger(TransitPlanner21.class.getName()).log(System.Logger.Level.INFO, "Transit21 " + message);
}
private static final class Probe {
final Terrain terrain; final Set<Long> columns = new HashSet<>(); int samples; int sampleLimit = TransitPlan21.MAX_SAMPLES, columnLimit = MAX_COLUMNS; String reason = "budget";
final java.util.Map<Module, Integer> bearings = new java.util.HashMap<>();
Probe(Terrain terrain) { this.terrain = terrain; }
void visit(int x, int z) {
if (samples >= sampleLimit) throw new BudgetExceeded();
samples++;
long key = (long) x << 32 ^ z & 0xffffffffL;
if (!columns.contains(key) && columns.size() >= columnLimit) throw new BudgetExceeded();
columns.add(key);
}
boolean owns(int x, int z) { visit(x, z); return terrain.owns(x, z); }
boolean solid(int x, int y, int z) { visit(x, z); return terrain.solid(x, y, z); }
boolean protectedAt(int x, int y, int z) { visit(x, z); return terrain.protectedAt(x, y, z); }
int surface(int x, int z) { visit(x, z); return terrain.surfaceY(x, z); }
}
private static final class BudgetExceeded extends RuntimeException { private static final long serialVersionUID = 1L; }
}
@@ -0,0 +1,208 @@
package fr.koka.sanctuary.worldgen.transit;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Module;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.core.Direction;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.storage.loot.BuiltInLootTables;
/** A seven-wide abandoned industrial route: five roadway cells and one pavement on each side. */
public final class TransitRenderer21 {
private TransitRenderer21() {}
public record Target(String name, int x, int y, int z) {}
public static List<Target> targets(Module m) {
var result = new ArrayList<Target>();
result.add(new Target("roadway", m.width() / 2, 2, m.depth() / 2));
if ((m.openings() & 1) != 0) result.add(new Target("north_connection", m.width() / 2, 2, 0));
if ((m.openings() & 2) != 0) result.add(new Target("east_connection", m.width() - 1, 2, m.depth() / 2));
if ((m.openings() & 4) != 0) result.add(new Target("south_connection", m.width() / 2, 2, m.depth() - 1));
if ((m.openings() & 8) != 0) result.add(new Target("west_connection", 0, 2, m.depth() / 2));
if (m.kind() == Kind.ACCESS) {
result.add(new Target("surface_portico", 8, m.surfaceY() - m.minY() + 1, 0));
result.add(new Target("surface_landing", 8, m.surfaceY() - m.minY() + 1, 12));
}
if (m.kind() == Kind.HALL) {
result.add(new Target("maintenance_bench", 3, 2, 4));
result.add(new Target("shared_tool_chest", m.width() - 4, 2, m.depth() - 4));
result.add(new Target("unfinished_rock_face", m.facing() == 1 ? m.width() / 2 : 3,
2, m.facing() == 1 ? 3 : m.depth() / 2));
}
return List.copyOf(result);
}
public static void render(Module module, LostCityRenderer.BlockWriter output) {
var connected = ConnectedBlocks20.buffer(output);
new Painter(module, connected).render();
connected.flush();
}
private static final class Painter {
final Module m; final LostCityRenderer.BlockWriter out;
Painter(Module module, LostCityRenderer.BlockWriter out) { m = module; this.out = out; }
void set(int x, int y, int z, Block block) { set(x, y, z, block.defaultBlockState()); }
void set(int x, int y, int z, BlockState state) { out.set(x, y, z, state); }
void box(int x0, int y0, int z0, int x1, int y1, int z1, Block block) {
for (int x = x0; x <= x1; x++) for (int z = z0; z <= z1; z++) for (int y = y0; y <= y1; y++) set(x, y, z, block);
}
BlockState stair(Direction direction) {
return Blocks.STONE_BRICK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, direction);
}
void render() {
int w = m.width(), d = m.depth();
for (var support : m.supports()) box(support.x() - m.minX(), support.minY() - m.minY(), support.z() - m.minZ(),
support.x() - m.minX(), -1, support.z() - m.minZ(), Blocks.STONE_BRICKS);
box(0, 0, 0, w - 1, 0, d - 1, Blocks.STONE_BRICKS);
box(0, 1, 0, w - 1, 1, d - 1, Blocks.SMOOTH_STONE);
box(0, 2, 0, w - 1, m.height() - 1, d - 1, Blocks.AIR);
if (m.kind() == Kind.ACCESS) { access(); return; }
for (int x = 0; x < w; x++) for (int z = 0; z < d; z++) {
boolean edge = x == 0 || x == w - 1 || z == 0 || z == d - 1;
if (edge) for (int y = 2; y <= 8; y++) set(x, y, z,
y == 2 || y == 7 ? Blocks.TUFF_BRICKS : ((x + z) % 5 == 0 ? Blocks.STONE_BRICKS : Blocks.SMOOTH_STONE));
set(x, 9, z, Blocks.STONE_BRICKS);
boolean alongX = m.facing() == 1;
int across = alongX ? z - d / 2 : x - w / 2;
if (Math.abs(across) <= 2) set(x, 1, z, Blocks.POLISHED_ANDESITE);
else if (Math.abs(across) == 3) set(x, 2, z, Blocks.SMOOTH_STONE_SLAB);
}
openings();
if (m.kind() == Kind.JUNCTION) {
// The side branch's mouth and central crossing retain all five roadway cells.
for (int x = w / 2 - 2; x <= w / 2 + 2; x++) for (int z = 0; z < d; z++) set(x, 2, z, Blocks.AIR);
for (int z = d / 2 - 2; z <= d / 2 + 2; z++) for (int x = 0; x < w; x++) set(x, 2, z, Blocks.AIR);
}
// Ceiling ribs and luminaires are physically joined to the continuous roof slab.
for (int n = 3; n < (m.facing() == 1 ? w : d) - 2; n += 6) {
int x = m.facing() == 1 ? n : w / 2, z = m.facing() == 1 ? d / 2 : n;
set(x, 8, z, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
if (m.facing() == 1) box(x, 8, 1, x, 8, 2, Blocks.POLISHED_TUFF);
else box(1, 8, z, 2, 8, z, Blocks.POLISHED_TUFF);
}
if (m.kind() == Kind.HALL) hall();
exposedRock();
}
void openings() {
for (int y = 2; y <= 8; y++) {
if ((m.openings() & 1) != 0) box(m.width() / 2 - 3, y, 0, m.width() / 2 + 3, y, 0, Blocks.AIR);
if ((m.openings() & 4) != 0) box(m.width() / 2 - 3, y, m.depth() - 1, m.width() / 2 + 3, y, m.depth() - 1, Blocks.AIR);
if ((m.openings() & 2) != 0) box(m.width() - 1, y, m.depth() / 2 - 3, m.width() - 1, y, m.depth() / 2 + 3, Blocks.AIR);
if ((m.openings() & 8) != 0) box(0, y, m.depth() / 2 - 3, 0, y, m.depth() / 2 + 3, Blocks.AIR);
}
}
void hall() {
// Flush crossings join the work bays to the road through the raised pavements.
// They also provide an accessible route without requiring a jump over the curb.
if (m.facing() == 0) {
for (int x : new int[]{m.width() / 2 - 3, m.width() / 2 + 3})
for (int z0 : new int[]{4, m.depth() - 4}) box(x, 2, z0 - 1, x, 2, z0 + 1, Blocks.AIR);
} else {
for (int z : new int[]{m.depth() / 2 - 3, m.depth() / 2 + 3})
for (int x0 : new int[]{3, m.width() - 4}) box(x0 - 1, 2, z, x0 + 1, 2, z, Blocks.AIR);
}
// Maintenance workshop occupies side bays; the centered arrival and return remain unobstructed.
for (int z : new int[]{3, m.depth() - 4}) {
box(1, 2, z, 1, 7, z, Blocks.CHISELED_TUFF_BRICKS);
box(m.width() - 2, 2, z, m.width() - 2, 7, z, Blocks.CHISELED_TUFF_BRICKS);
box(1, 8, z, m.width() - 2, 8, z, Blocks.TUFF_BRICKS);
}
set(2, 2, 3, Blocks.CRAFTING_TABLE); set(2, 2, 4, Blocks.GRINDSTONE);
set(2, 2, 5, Blocks.FURNACE.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST));
for (int z = m.depth() - 6; z <= m.depth() - 3; z++) set(2, 2, z, Blocks.BARREL);
set(m.width() - 3, 2, 3, Blocks.CAULDRON);
out.chest(m.width() - 3, 2, m.depth() - 4, BuiltInLootTables.VILLAGE_TOOLSMITH, m.seed() ^ 0x21A1100L);
}
void exposedRock() {
// Abandoned cuts expose finite, ordinary mineable ore in the saved wall geometry.
// No ticking reward or regeneration: once mined, these blocks remain gone.
if (m.kind() != Kind.HALL && (m.kind() != Kind.GALLERY || Math.floorMod(m.seed(), 4) != 0)) return;
boolean alongX = m.facing() == 1;
int middle = alongX ? m.width() / 2 : m.depth() / 2;
Block[] ore = {Blocks.COAL_ORE, Blocks.COPPER_ORE, Blocks.IRON_ORE,
Blocks.GOLD_ORE, Blocks.REDSTONE_ORE, Blocks.LAPIS_ORE};
Block deposit = ore[Math.floorMod(m.seed() ^ 0x21CAFE, ore.length)];
for (int across = -2; across <= 2; across++) for (int y = 2; y <= 6; y++) {
int x = alongX ? middle + across : 0, z = alongX ? 0 : middle + across;
Block rock = y == 2 ? Blocks.TUFF : y >= 5 ? Blocks.STONE : Blocks.DEEPSLATE;
if (Math.abs(across) + Math.abs(y - 4) <= 1) rock = deposit;
if (m.kind() == Kind.HALL && y == 3 && across == -1) rock = Blocks.DEEPSLATE_DIAMOND_ORE;
if (m.kind() == Kind.HALL && y == 4 && across == 1) rock = Blocks.EMERALD_ORE;
set(x, y, z, rock);
}
if (m.kind() == Kind.HALL) {
// An unfinished excavation bay interrupts the masonry: irregular rock
// shoulders and a low stone arch leave its mineral face accessible.
for (int across = -3; across <= 3; across++) for (int reach = 0; reach <= 2; reach++) {
int x = alongX ? middle + across : reach, z = alongX ? reach : middle + across;
set(x, 1, z, Math.abs(across) == 3 ? Blocks.MOSSY_COBBLESTONE : Blocks.TUFF);
int ceiling = 7 + ((across + reach) & 1);
box(x, ceiling, z, x, 8, z, reach == 0 ? Blocks.DEEPSLATE : Blocks.STONE);
if (Math.abs(across) == 3) box(x, 2, z, x, 6 - reach, z,
reach == 2 ? Blocks.COBBLESTONE : Blocks.TUFF);
}
}
}
void access() {
int top = m.surfaceY() - m.minY();
// Enclosed local shaft, not a filled foundation under the whole route.
for (int n = 0; n <= 16; n++) {
box(n, 2, 0, n, top, 0, Blocks.SMOOTH_STONE);
box(n, 2, 16, n, top, 16, Blocks.SMOOTH_STONE);
box(0, 2, n, 0, top, n, Blocks.STONE_BRICKS);
box(16, 2, n, 16, top, n, Blocks.STONE_BRICKS);
}
box(1, top, 1, 15, top, 15, Blocks.SMOOTH_STONE);
// Narrow bearing walls sit beside the flights, with the bottom route crossing at z8..9.
box(2, 2, 3, 2, top - 1, 7, Blocks.STONE_BRICKS);
box(14, 2, 3, 14, top - 1, 7, Blocks.STONE_BRICKS);
openings();
int rise = top - 1;
for (int step = 1; step <= rise; step++) {
int flight = (step - 1) / 5, within = (step - 1) % 5 + 1;
boolean south = (flight & 1) == 0;
int z = south ? 2 + within : 8 - within, x0 = south ? 3 : 11, y = 1 + step;
box(x0, y, z, x0 + 2, Math.min(top + 3, y + 3), z, Blocks.AIR);
for (int x = x0; x <= x0 + 2; x++) set(x, y, z, stair(south ? Direction.SOUTH : Direction.NORTH));
if (within == 5) {
int z0 = south ? 8 : 1;
box(3, y, z0, 13, y, z0 + 1, Blocks.SMOOTH_STONE);
box(3, y + 1, z0, 13, Math.min(top + 3, y + 3), z0 + 1, Blocks.AIR);
if (y + 3 <= top) {
set(6, y + 3, z0, Blocks.SMOOTH_STONE);
set(6, y + 2, z0, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
// The bracket reaches an actual shaft wall; the last open-air landing needs no hanging lamp.
box(6, y + 3, south ? z0 : 0, 6, y + 3, south ? 16 : z0, Blocks.STONE_BRICKS);
}
}
}
// A north landing just below the surface needs a descending threshold instead
// of a slab sealing its headroom. The last flight still reaches the real top deck.
int belowNorthLanding = rise % 10;
if (belowNorthLanding >= 1 && belowNorthLanding <= 3) {
for (int z = 0; z <= belowNorthLanding; z++) for (int x = 7; x <= 9; x++) {
box(x, top - z, z, x, top + 3, z, Blocks.AIR);
set(x, top - z - 1, z, Blocks.STONE_BRICKS);
set(x, top - z, z, stair(Direction.NORTH));
}
if (belowNorthLanding == 3) {
for (int x : new int[]{6, 10}) {
set(x, top - 3, 3, Blocks.SMOOTH_STONE);
box(x, top - 2, 3, x, top - 1, 3, Blocks.AIR);
}
}
}
// A short, grounded portico identifies each naturally approachable surface gate.
for (int n = 0; n <= 16; n++) {
if (n < 6 || n > 10) set(n, top + 1, 0, Blocks.IRON_BARS);
set(n, top + 1, 16, Blocks.IRON_BARS);
set(0, top + 1, n, Blocks.IRON_BARS); set(16, top + 1, n, Blocks.IRON_BARS);
}
for (int x : new int[]{6, 10}) box(x, top + 1, 0, x, top + 3, 0, Blocks.STONE_BRICKS);
box(6, top + 4, 0, 10, top + 4, 1, Blocks.STONE_BRICK_SLAB);
}
}
}
@@ -0,0 +1,31 @@
package fr.koka.sanctuary.worldgen.transit;
import com.mojang.serialization.MapCodec;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.util.Optional;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureType;
/** A local group of neighboring modules; long routes deliberately have multiple native starts. */
public final class TransitStructure21 extends Structure {
public static final MapCodec<TransitStructure21> CODEC = simpleCodec(TransitStructure21::new);
public TransitStructure21(StructureSettings settings) { super(settings); }
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 21 || !generator.experimentalStructures()) return Optional.empty();
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
if (island == null || !island.origin() || island.generation() != 21) return Optional.empty();
var modules = generator.transitPlan21().modulesForChunk(context.chunkPos().x(), context.chunkPos().z());
if (modules.isEmpty()) return Optional.empty();
for (var module : modules) {
if (Math.abs((module.minX() >> 4) - module.startChunkX()) > 7
|| Math.abs((module.maxX() >> 4) - module.startChunkX()) > 7
|| Math.abs((module.minZ() >> 4) - module.startChunkZ()) > 7
|| Math.abs((module.maxZ() >> 4) - module.startChunkZ()) > 7)
throw new IllegalStateException("Transit module exceeds local native reference range");
}
return Optional.of(new GenerationStub(TransitPiece21.locator(modules.getFirst()), pieces ->
modules.forEach(module -> pieces.addPiece(new TransitPiece21(module)))));
}
@Override public StructureType<?> type() { return TransitStructures21.TYPE; }
}
@@ -0,0 +1,85 @@
package fr.koka.sanctuary.worldgen.transit;
import com.mojang.datafixers.util.Pair;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.util.Comparator;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.StructureType;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceType;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
public final class TransitStructures21 {
public static final ResourceKey<Structure> KEY = ResourceKey.create(Registries.STRUCTURE, SanctuaryMod.id("industrial_transit"));
public static final StructureType<TransitStructure21> TYPE = Registry.register(BuiltInRegistries.STRUCTURE_TYPE,
SanctuaryMod.id("industrial_transit_v21"), () -> TransitStructure21.CODEC);
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
SanctuaryMod.id("industrial_transit_module_v21"), TransitPiece21::new);
private static final ThreadLocal<Integer> PLACEMENT_DEPTH = ThreadLocal.withInitial(() -> 0);
private TransitStructures21() {}
public static void register() {}
public static boolean isPlacing() { return PLACEMENT_DEPTH.get() > 0; }
static void placing(Runnable action) {
int previous = PLACEMENT_DEPTH.get(); PLACEMENT_DEPTH.set(previous + 1);
try { action.run(); } finally { if (previous == 0) PLACEMENT_DEPTH.remove(); else PLACEMENT_DEPTH.set(previous); }
}
public static void generate(ExpansionChunkGenerator generator, RegistryAccess registries,
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> level) {
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || generator.rootGeneration() != 21
|| !sanctuary.experimentalStructures()) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || island.generation() != 21) return;
if (sanctuary.transitPlan21().modulesForChunk(chunk.getPos().x(), chunk.getPos().z()).isEmpty()) return;
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
StructureStart previous = manager.getStartForStructure(holder.value(), chunk);
if (previous != null && previous.isValid()) return;
var random = generator.randomState(island);
var sampler = random.createClimateSampler(SamplerContext.builder().enableCaches().build());
var start = holder.value().generate(holder, level, registries, generator, generator.getBiomeSource(), sampler,
random, templates, island.seed(), chunk.getPos(), previous == null ? 0 : previous.getReferences(),
chunk, holder.value().biomes()::contains);
if (start.isValid()) manager.setStartForStructure(holder.value(), start, chunk);
}
/** Locate selects a saved surface portico rather than teleporting the player into a deep road. */
public static Pair<BlockPos, Holder<Structure>> findNearest(ServerLevel level, HolderSet<Structure> structures,
BlockPos origin, int radius, boolean skipReferenced) {
if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 21 || !generator.experimentalStructures()
|| !level.structureManager().shouldGenerateStructures()) return null;
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
if (!structures.contains(holder)) return null;
var candidates = generator.transitPlan21().modules().stream()
.filter(m -> m.kind() == TransitPlan21.Kind.ACCESS)
.filter(m -> Math.abs((long) m.startChunkX() - (origin.getX() >> 4)) <= radius
&& Math.abs((long) m.startChunkZ() - (origin.getZ() >> 4)) <= radius)
.sorted(Comparator.comparingDouble(m -> origin.distSqr(TransitPiece21.locator(m)))).toList();
for (var module : candidates) {
var chunk = level.getChunk(module.startChunkX(), module.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
var start = level.structureManager().getStartForStructure(holder.value(), chunk);
if (start == null || !start.isValid() || skipReferenced && !start.canBeReferenced()) continue;
if (skipReferenced) level.structureManager().addReference(start);
return Pair.of(TransitPiece21.locator(module), holder);
}
return null;
}
}
@@ -0,0 +1,67 @@
package fr.koka.sanctuary.worldgen.transit;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Predicate;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.RandomState;
/** Read-only, bounded native-noise cache. Unknown columns always reject construction. */
public final class TransitTerrain21 implements TransitPlanner21.Terrain {
public static final int MAX_COLUMNS = 12_000;
private final ExpansionIsland island;
private final NoiseBasedChunkGenerator generator;
private final RandomState random;
private final LevelHeightAccessor height;
private final Predicate<BlockPos> protection;
private final Map<Long, NoiseColumn> columns = new HashMap<>();
public TransitTerrain21(ExpansionIsland island, NoiseBasedChunkGenerator generator, RandomState random,
LevelHeightAccessor height, Predicate<BlockPos> protection) {
this.island = island; this.generator = generator; this.random = random;
this.height = height; this.protection = protection;
}
private NoiseColumn column(int x, int z) {
if (!owns(x, z)) return null;
long key = ((long) x << 32) ^ (z & 0xffffffffL);
var found = columns.get(key);
if (found != null) return found;
if (columns.size() >= MAX_COLUMNS) return null;
var result = generator.getBaseColumn(x, z, height, random);
columns.put(key, result);
return result;
}
@Override public boolean owns(int x, int z) { return island.contains(x, z); }
@Override public boolean solid(int x, int y, int z) {
if (y < 0 || y >= 384) return false;
var column = column(x, z);
return column != null && column.getBlock(y).isSolid() && column.getBlock(y).getFluidState().isEmpty();
}
@Override public boolean protectedAt(int x, int y, int z) {
if (y < 0 || y >= 384) return true;
var column = column(x, z);
return column == null || !column.getBlock(y).getFluidState().isEmpty()
|| protection.test(new BlockPos(x, y, z));
}
@Override public int surfaceY(int x, int z) {
var column = column(x, z);
if (column == null) return -1;
for (int y = 380; y >= 1; y--) {
var state = column.getBlock(y);
if (!state.isAir()) return state.isSolid() && state.getFluidState().isEmpty() ? y : -1;
}
return -1;
}
@Override public String obstruction(int x, int y, int z) {
var sample = column(x, z);
if (sample == null) return "unknown/unowned column";
var state = sample.getBlock(y);
if (!state.getFluidState().isEmpty()) return "native fluid " + state;
return protection.test(new BlockPos(x, y, z)) ? "earlier structure/hydrology mask" : "none";
}
public int columnsUsed() { return columns.size(); }
}
@@ -127,5 +127,25 @@
"sanctuary.expansion.error.void_reuse_incomplete": "Void reuse is incomplete. The reservation is retained; check the server log.",
"sanctuary.world_options.experimental": "Experimental structures",
"sanctuary.world_options.experimental_hint": "Mines, strange rooms and abandoned facilities. This choice is saved with the new world.",
"structure.sanctuary.forgotten_networks": "Forgotten networks"
"structure.sanctuary.forgotten_networks": "Forgotten networks",
"sanctuary.world_options.heritage_hint": "Adds forgotten networks, buildings and abandoned routes to the new island. This choice is saved with the world.",
"structure.sanctuary.heritage_networks": "Abandoned infrastructure",
"sanctuary.heritage.kind.freight_station": "Freight station",
"sanctuary.heritage.kind.post_office": "Post office",
"sanctuary.heritage.kind.communal_depot": "Communal depot",
"sanctuary.heritage.kind.expedition_office": "Expedition office and map archive",
"sanctuary.heritage.kind.observatory": "Observatory and weather station",
"sanctuary.heritage.kind.rail_workshop": "Rail workshop",
"sanctuary.heritage.kind.pump_station": "Pump station",
"sanctuary.heritage.kind.substation": "Utility substation",
"sanctuary.heritage.kind.greenhouse": "Greenhouse and seed store",
"sanctuary.heritage.kind.void_dock": "Expedition dock",
"sanctuary.heritage.kind.radio_relay": "Radio relay",
"sanctuary.heritage.kind.photo_club": "Photography club",
"sanctuary.heritage.kind.impossible_service": "Changing rooms and strange service rooms",
"sanctuary.heritage.kind.construction_refuge": "Construction refuge",
"sanctuary.heritage.unavailable": "This survey requires a recent Sanctuary world with experimental structures enabled.",
"sanctuary.heritage.header": "Abandoned infrastructure: %s planned sites and %s links. Entrance coordinates:",
"sanctuary.heritage.entry": "%s : %s / %s / %s",
"sanctuary.transit.entry": "Industrial galleries — entrance: %s %s %s"
}
@@ -127,5 +127,25 @@
"sanctuary.expansion.error.void_reuse_incomplete": "La réutilisation du vide est incomplète. La réservation est conservée ; consultez le journal du serveur.",
"sanctuary.world_options.experimental": "Structures expérimentales",
"sanctuary.world_options.experimental_hint": "Mines, salles étranges et installations abandonnées. Ce choix reste attaché au nouveau monde.",
"structure.sanctuary.forgotten_networks": "Réseaux oubliés"
"structure.sanctuary.forgotten_networks": "Réseaux oubliés",
"sanctuary.world_options.heritage_hint": "Ajoute les anciens réseaux, bâtiments et voies abandonnées sur la nouvelle île. Ce choix est enregistré avec le monde.",
"structure.sanctuary.heritage_networks": "Anciennes infrastructures",
"sanctuary.heritage.kind.freight_station": "Gare de marchandises",
"sanctuary.heritage.kind.post_office": "Centre postal",
"sanctuary.heritage.kind.communal_depot": "Dépôt communal",
"sanctuary.heritage.kind.expedition_office": "Bureau des expéditions et cartothèque",
"sanctuary.heritage.kind.observatory": "Observatoire et station météo",
"sanctuary.heritage.kind.rail_workshop": "Atelier ferroviaire",
"sanctuary.heritage.kind.pump_station": "Station de pompage",
"sanctuary.heritage.kind.substation": "Sous-station",
"sanctuary.heritage.kind.greenhouse": "Serre et graineterie",
"sanctuary.heritage.kind.void_dock": "Quai dexpédition",
"sanctuary.heritage.kind.radio_relay": "Relais radio",
"sanctuary.heritage.kind.photo_club": "Club photo",
"sanctuary.heritage.kind.impossible_service": "Vestiaires et salles étranges",
"sanctuary.heritage.kind.construction_refuge": "Refuge de chantier",
"sanctuary.heritage.unavailable": "Ce relevé nécessite un monde Sanctuary récent avec les structures expérimentales activées.",
"sanctuary.heritage.header": "Anciennes infrastructures : %s lieux et %s liaisons prévus. Coordonnées des entrées :",
"sanctuary.heritage.entry": "%s : %s / %s / %s",
"sanctuary.transit.entry": "Galeries industrielles — entrée : %s %s %s"
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:iron_axe"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:torch",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:bread",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:iron_shovel"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:bread",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:torch",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:compass"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:paper",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:map",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:minecart"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:rail",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:coal",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:iron_hoe"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:wheat_seeds",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:pumpkin_seeds",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:clock"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:torch",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:string",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:spyglass"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:paper",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:book",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:book"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:paper",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:glass_pane",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:compass"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:paper",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:string",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:bucket"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:string",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:torch",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:compass"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:redstone",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:paper",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:iron_pickaxe"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:rail",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:coal",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:clock"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:redstone",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:copper_ingot",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:lead"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:torch",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:bread",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,55 @@
{
"replace": false,
"values": [
"#minecraft:is_overworld",
"sanctuary:cavern_bamboo_grove",
"sanctuary:cavern_birch_forest",
"sanctuary:cavern_clearing",
"sanctuary:cavern_dark_grove",
"sanctuary:cavern_dripstone_caves",
"sanctuary:cavern_dry_woodland",
"sanctuary:cavern_lush_caves",
"sanctuary:cavern_oak_forest",
"sanctuary:cavern_rocky_heath",
"sanctuary:cavern_sulfur_depths",
"sanctuary:layered_flower_meadow",
"sanctuary:layered_lower_grove",
"sanctuary:layered_meadow",
"sanctuary:layered_mossy_terraces",
"sanctuary:layered_rocky_terraces",
"sanctuary:lost_city",
"sanctuary:population_bamboo_grove",
"sanctuary:population_birch_forest",
"sanctuary:population_clearing",
"sanctuary:population_dark_grove",
"sanctuary:population_dripstone_caves",
"sanctuary:population_dry_woodland",
"sanctuary:population_lush_caves",
"sanctuary:population_oak_forest",
"sanctuary:population_rocky_heath",
"sanctuary:population_sulfur_depths",
"sanctuary:rift_bamboo_grove",
"sanctuary:rift_birch_forest",
"sanctuary:rift_clearing",
"sanctuary:rift_dark_grove",
"sanctuary:rift_dry_woodland",
"sanctuary:rift_oak_forest",
"sanctuary:rift_rocky_heath",
"sanctuary:rift_sulfur_depths",
"sanctuary:starter_forest",
"sanctuary:starter_forest_hydrology",
"sanctuary:temperate_birch_forest",
"sanctuary:temperate_dappled_forest",
"sanctuary:temperate_flower_forest",
"sanctuary:temperate_forest",
"sanctuary:temperate_plains",
"sanctuary:woodland_bamboo_grove",
"sanctuary:woodland_birch_forest",
"sanctuary:woodland_clearing",
"sanctuary:woodland_dark_grove",
"sanctuary:woodland_dry_woodland",
"sanctuary:woodland_oak_forest",
"sanctuary:woodland_rocky_heath",
"sanctuary:woodland_sulfur_depths"
]
}
@@ -0,0 +1,7 @@
{
"type": "sanctuary:heritage_networks_v19",
"biomes": "#sanctuary:has_structure/heritage_networks",
"step": "underground_structures",
"terrain_adaptation": "none",
"spawn_overrides": {}
}
@@ -0,0 +1,7 @@
{
"type": "sanctuary:industrial_transit_v21",
"biomes": "#sanctuary:has_structure/forgotten_networks",
"step": "underground_structures",
"terrain_adaptation": "none",
"spawn_overrides": {}
}
@@ -3,7 +3,7 @@
"minecraft:overworld": {
"type": "sanctuary:sanctuary_384",
"generator": {
"type": "sanctuary:island_v17",
"type": "sanctuary:island_v21",
"biome_source": {
"biomes": [
"minecraft:the_void",
@@ -0,0 +1,81 @@
{
"dimensions": {
"minecraft:overworld": {
"type": "sanctuary:sanctuary_384",
"generator": {
"type": "sanctuary:island_v17",
"biome_source": {
"biomes": [
"minecraft:the_void",
"minecraft:forest",
"minecraft:birch_forest",
"minecraft:flower_forest",
"minecraft:plains",
"minecraft:meadow",
"minecraft:taiga",
"minecraft:snowy_taiga",
"minecraft:snowy_plains",
"minecraft:grove",
"minecraft:ice_spikes",
"minecraft:frozen_peaks",
"minecraft:old_growth_spruce_taiga",
"minecraft:dark_forest",
"minecraft:swamp",
"minecraft:pale_garden",
"minecraft:jungle",
"minecraft:sparse_jungle",
"minecraft:bamboo_jungle",
"minecraft:mangrove_swamp",
"minecraft:savanna",
"minecraft:savanna_plateau",
"minecraft:desert",
"minecraft:badlands",
"minecraft:eroded_badlands",
"minecraft:wooded_badlands",
"minecraft:windswept_hills",
"minecraft:windswept_gravelly_hills",
"minecraft:stony_peaks",
"minecraft:dappled_forest",
"minecraft:cherry_grove",
"minecraft:lush_caves",
"minecraft:dripstone_caves",
"minecraft:sulfur_caves",
"sanctuary:population_oak_forest",
"sanctuary:population_birch_forest",
"sanctuary:population_clearing",
"sanctuary:population_dry_woodland",
"sanctuary:population_rocky_heath",
"sanctuary:population_dark_grove",
"sanctuary:population_bamboo_grove",
"sanctuary:population_sulfur_depths",
"sanctuary:population_lush_caves",
"sanctuary:population_dripstone_caves",
"sanctuary:lost_city"
]
},
"initial_diameter": 724
}
},
"minecraft:the_end": {
"type": "minecraft:the_end",
"generator": {
"type": "minecraft:noise",
"biome_source": {
"type": "minecraft:the_end"
},
"settings": "minecraft:end"
}
},
"minecraft:the_nether": {
"type": "minecraft:the_nether",
"generator": {
"type": "minecraft:noise",
"biome_source": {
"type": "minecraft:multi_noise",
"preset": "minecraft:nether"
},
"settings": "minecraft:nether"
}
}
}
}
@@ -0,0 +1,81 @@
{
"dimensions": {
"minecraft:overworld": {
"type": "sanctuary:sanctuary_384",
"generator": {
"type": "sanctuary:island_v19",
"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"
}
}
}
}
@@ -0,0 +1,81 @@
{
"dimensions": {
"minecraft:overworld": {
"type": "sanctuary:sanctuary_384",
"generator": {
"type": "sanctuary:island_v20",
"biome_source": {
"biomes": [
"minecraft:the_void",
"minecraft:forest",
"minecraft:birch_forest",
"minecraft:flower_forest",
"minecraft:plains",
"minecraft:meadow",
"minecraft:taiga",
"minecraft:snowy_taiga",
"minecraft:snowy_plains",
"minecraft:grove",
"minecraft:ice_spikes",
"minecraft:frozen_peaks",
"minecraft:old_growth_spruce_taiga",
"minecraft:dark_forest",
"minecraft:swamp",
"minecraft:pale_garden",
"minecraft:jungle",
"minecraft:sparse_jungle",
"minecraft:bamboo_jungle",
"minecraft:mangrove_swamp",
"minecraft:savanna",
"minecraft:savanna_plateau",
"minecraft:desert",
"minecraft:badlands",
"minecraft:eroded_badlands",
"minecraft:wooded_badlands",
"minecraft:windswept_hills",
"minecraft:windswept_gravelly_hills",
"minecraft:stony_peaks",
"minecraft:dappled_forest",
"minecraft:cherry_grove",
"minecraft:lush_caves",
"minecraft:dripstone_caves",
"minecraft:sulfur_caves",
"sanctuary:population_oak_forest",
"sanctuary:population_birch_forest",
"sanctuary:population_clearing",
"sanctuary:population_dry_woodland",
"sanctuary:population_rocky_heath",
"sanctuary:population_dark_grove",
"sanctuary:population_bamboo_grove",
"sanctuary:population_sulfur_depths",
"sanctuary:population_lush_caves",
"sanctuary:population_dripstone_caves",
"sanctuary:lost_city"
]
},
"initial_diameter": 724
}
},
"minecraft:the_end": {
"type": "minecraft:the_end",
"generator": {
"type": "minecraft:noise",
"biome_source": {
"type": "minecraft:the_end"
},
"settings": "minecraft:end"
}
},
"minecraft:the_nether": {
"type": "minecraft:the_nether",
"generator": {
"type": "minecraft:noise",
"biome_source": {
"type": "minecraft:multi_noise",
"preset": "minecraft:nether"
},
"settings": "minecraft:nether"
}
}
}
}
@@ -72,6 +72,18 @@ public final class ExpansionJournalSmoke {
require(Arrays.equals(unifiedMediumBytes, Files.readAllBytes(unifiedMediumDirectory.resolve(ExpansionJournal.FILE_NAME))),
"Rejected cross-generation read changed its journal");
for (int generation : List.of(20, 21)) {
var saved = scratch.resolve("architecture-cross-version-" + generation);
var root21 = ExpansionIsland.origin(724, SEED, generation);
var journal21 = new ExpansionJournal(saved, SEED, 724, generation);
var child = ExpansionPlacement.candidates(SEED, List.of(root21), "city21", "sanctuary", "east", "lost_city", 256, "natural", 0).getFirst();
journal21.commit(List.of(root21, child));
byte[] bytes = Files.readAllBytes(saved.resolve(ExpansionJournal.FILE_NAME));
require(new ExpansionJournal(saved, SEED, 724, generation).islands().equals(List.of(root21, child)), "Architecture journal round trip");
reject(() -> new ExpansionJournal(saved, SEED, 724, generation == 20 ? 21 : 20), "Cross-generation architecture journal");
require(Arrays.equals(bytes, Files.readAllBytes(saved.resolve(ExpansionJournal.FILE_NAME))), "Rejected upgrade altered saved journal");
}
Path cityDirectory = scratch.resolve("city-generation");
var cityRoot = ExpansionIsland.origin(724, SEED, 14);
var city = new ExpansionJournal(cityDirectory, SEED, 724, 14,
@@ -158,6 +170,37 @@ public final class ExpansionJournalSmoke {
require(Arrays.equals(experimentBytes, Files.readAllBytes(experimentDirectory.resolve(ExpansionJournal.FILE_NAME)))
&& Arrays.equals(ruinsBytes, Files.readAllBytes(ruinsDirectory.resolve(ExpansionJournal.FILE_NAME))),
"Rejected cross-generation changes preserve experiment and published16 bytes");
var heritageRoot = ExpansionIsland.origin(724, SEED, 19);
var heritageDirectory = scratch.resolve("heritage-19");
var heritageJournal = new ExpansionJournal(heritageDirectory, SEED, 724, 19);
var heritageChild = ExpansionPlacement.candidates(SEED, List.of(heritageRoot), "heritage", "sanctuary", "east", "lost_city", 128, "natural", 0).getFirst()
.withReuseToken("f".repeat(64));
heritageJournal.commit(List.of(heritageRoot, heritageChild));
require(new ExpansionJournal(heritageDirectory, SEED, 724, 19).islands().equals(List.of(heritageRoot, heritageChild)),
"Generation19 preserves the new world and its expansions across journal reload");
byte[] heritageBytes = Files.readAllBytes(heritageDirectory.resolve(ExpansionJournal.FILE_NAME));
reject(() -> new ExpansionJournal(heritageDirectory, SEED, 724, 17), "No implicit downgrade from19 to17");
reject(() -> new ExpansionJournal(experimentDirectory, SEED, 724, 19), "No implicit upgrade from17 to19");
reject(() -> heritageJournal.commit(List.of(heritageRoot, experimentChild)), "No generation17 child in19");
require(Arrays.equals(heritageBytes, Files.readAllBytes(heritageDirectory.resolve(ExpansionJournal.FILE_NAME)))
&& Arrays.equals(experimentBytes, Files.readAllBytes(experimentDirectory.resolve(ExpansionJournal.FILE_NAME))),
"Rejected migrations preserve both generation17 and19 journals");
var architectureRoot = ExpansionIsland.origin(724, SEED, 20);
var architectureDirectory = scratch.resolve("architecture-20");
var architectureJournal = new ExpansionJournal(architectureDirectory, SEED, 724, 20);
var architectureChild = ExpansionPlacement.candidates(SEED, List.of(architectureRoot), "architecture", "sanctuary", "east", "lost_city", 128, "natural", 0).getFirst()
.withReuseToken("a".repeat(64));
architectureJournal.commit(List.of(architectureRoot, architectureChild));
require(new ExpansionJournal(architectureDirectory, SEED, 724, 20).islands().equals(List.of(architectureRoot, architectureChild)),
"Generation20 and its expansions retain an independent journal on reload");
byte[] architectureBytes = Files.readAllBytes(architectureDirectory.resolve(ExpansionJournal.FILE_NAME));
reject(() -> new ExpansionJournal(architectureDirectory, SEED, 724, 19), "No implicit downgrade from20 to19");
reject(() -> new ExpansionJournal(heritageDirectory, SEED, 724, 20), "No implicit upgrade from19 to20");
reject(() -> architectureJournal.commit(List.of(architectureRoot, heritageChild)), "No generation19 child in20");
reject(() -> heritageJournal.commit(List.of(heritageRoot, architectureChild)), "No generation20 child in19");
require(Arrays.equals(architectureBytes, Files.readAllBytes(architectureDirectory.resolve(ExpansionJournal.FILE_NAME)))
&& Arrays.equals(heritageBytes, Files.readAllBytes(heritageDirectory.resolve(ExpansionJournal.FILE_NAME))),
"Cross-generation rejection preserves published19 and new20 journals");
try {
ExpansionPlacement.candidates(SEED, List.of(ExpansionIsland.origin(724, SEED, 13)), "city", "sanctuary", "east", "lost_city", 256, "natural", 0);
throw new AssertionError("Lost City must not silently activate in a saved alpha.13 world");
@@ -0,0 +1,451 @@
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 LostCityInteriors20Smoke {
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("LostCityInteriors20Smoke: 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<>();
Set<Set<BlockPos>> distinctVolumes = new HashSet<>();
var previews = new java.util.ArrayList<Object>();
for (Kind kind : KINDS) for (int floors : new int[]{2, 3, 5}) for (long seed : new long[]{0, 43}) {
Capture canonical = render(parcel(kind, floors, seed, 0));
if (floors == 2 && seed == 0) {
distinct.add(canonical.blocks);
Set<BlockPos> volume = new HashSet<>();
canonical.blocks.forEach((at, state) -> { if (!state.isAir() && at.getY() > 0
&& (at.getY() > 10 || at.getX() == 0 || at.getX() == 16 || at.getZ() == 0 || at.getZ() == 16)) volume.add(at); });
require(distinctVolumes.add(volume), kind + " only recolors an existing facade/roof volume");
}
if (floors == 3 && seed == 43 && System.getProperty("sanctuary.city20.preview") != null) {
var blocks = new java.util.ArrayList<Object>();
canonical.blocks.forEach((at, state) -> { if (!state.isAir()) blocks.add(java.util.List.of(at.getX(), at.getY(), at.getZ(), state.toString())); });
previews.add(Map.of("kind", kind.name(), "seed", seed, "floors", floors, "facing", 0, "blocks", blocks));
}
for (int facing = 0; facing < 4; facing++) {
Parcel parcel = parcel(kind, floors, seed, facing);
Capture result = facing == 0 ? canonical : render(parcel);
Capture repeated = render(parcel);
require(result.blocks.equals(repeated.blocks) && result.loot.equals(repeated.loot), "Saved parcel is not deterministic");
rotationMatches(canonical, result, facing);
require(result.blocks.values().stream().anyMatch(state -> state.is(Blocks.STONE_BRICKS)), "Missing stone-brick foundations");
require(result.blocks.values().stream().anyMatch(state -> state.is(Blocks.SMOOTH_STONE)), "Missing smooth stone");
require(result.blocks.values().stream().noneMatch(state -> state.is(Blocks.SPAWNER)
|| !state.getFluidState().isEmpty()), "Building introduced a spawner or free flowing fluid");
require(result.loot.size() == 1, "Building must keep one deferred loot chest");
var entrance = LostCityInteriors20.rooms(parcel).getFirst();
Set<BlockPos> reached = walk(result, position(entrance));
for (var room : LostCityInteriors20.rooms(parcel)) require(reached.contains(position(room)),
label(parcel) + " cannot walk from its entrance to " + room.name() + " at " + position(room));
verifyDoors(result, reached);
verifyBeds(result, reached);
verifyBathrooms(result, reached);
verifyFurnitureDirections(result);
verifyLanternSupports(result);
for (BlockPos chest : result.loot.keySet()) require(adjacentReached(chest, reached),
label(parcel) + " loot cannot be approached");
semantics(result);
if (facing == 0) verifyStoreyVariation(result);
verifyArchitecture(result);
}
}
require(distinct.size() == KINDS.length, "Building uses only labels to vary an identical interior");
boolean rejected = false;
try { LostCityInteriors20.rooms(new Parcel(Kind.PARK, 0, 0, 17, 17, 160, 2, 0, 0, 4, new int[0])); }
catch (IllegalArgumentException expected) { rejected = true; }
require(rejected, "Interior dispatch must reject non-building parcels");
String preview = System.getProperty("sanctuary.city20.preview");
if (preview != null) try {
var file = java.nio.file.Path.of(preview); if (file.getParent() != null) java.nio.file.Files.createDirectories(file.getParent());
java.nio.file.Files.writeString(file, new com.google.gson.Gson().toJson(previews));
} catch (java.io.IOException e) { throw new AssertionError("Cannot write requested architecture preview", e); }
}
private static Parcel parcel(Kind kind, int floors, long seed, int facing) {
return new Parcel(kind, -81, 119, 17, 17, 160, floors, facing, seed, 4, new int[0]);
}
private static Capture render(Parcel parcel) {
var result = new Capture(parcel);
LostCityInteriors20.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(LostCityInteriors20.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 : LostCityInteriors20.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 (LostCityInteriors20.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 : LostCityInteriors20.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(LostCityInteriors20.layout(result.parcel, floor - 1) != LostCityInteriors20.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) || state.is(Blocks.OAK_PLANKS) || state.is(Blocks.BIRCH_PLANKS)) objects.put(new BlockPos(x, up, z), state);
}
return objects;
}
private static void semantics(Capture result) {
Map<BlockState, Long> counts = new HashMap<>();
result.blocks.values().forEach(state -> counts.merge(state, 1L, Long::sum));
java.util.function.Predicate<net.minecraft.world.level.block.Block> has = block ->
counts.keySet().stream().anyMatch(state -> state.is(block));
switch (result.parcel.kind()) {
case APARTMENT -> require(has.test(Blocks.FURNACE) && has.test(Blocks.BED.white())
&& has.test(Blocks.WATER_CAULDRON), "Apartment lacks cooking, sleeping or washing fixtures");
case HOTEL -> require(has.test(Blocks.LOOM) && has.test(Blocks.BED.white())
&& has.test(Blocks.WATER_CAULDRON), "Hotel lacks guest rooms or laundry fixtures");
case LIBRARY -> require(result.blocks.values().stream().filter(state -> state.is(Blocks.BOOKSHELF)).count() >= result.parcel.floors() * 12L
&& has.test(Blocks.LECTERN), "Library is missing usable stacks and reading fixtures");
case RESTAURANT -> require(has.test(Blocks.SMOKER) && has.test(Blocks.FURNACE)
&& has.test(Blocks.CRAFTING_TABLE) && has.test(Blocks.WATER_CAULDRON), "Restaurant lacks kitchen and washroom services");
case BARRACKS -> require(has.test(Blocks.BED.white()) && has.test(Blocks.FLETCHING_TABLE)
&& has.test(Blocks.GRINDSTONE), "Barracks lacks dormitory and equipment fixtures");
case HALL -> {
require(has.test(Blocks.LECTERN) && has.test(Blocks.IRON_BARS), "Hall lacks stage or gallery guards");
require(result.block(rotate(new BlockPos(8, 5, 12), result.parcel.facing())).isAir(), "Auditorium lost its double-height volume");
}
default -> throw new AssertionError("Unexpected building");
}
}
/** Player-sized path: two clear cells above support, one-block steps and manually openable doors. */
private static Set<BlockPos> walk(Capture result, BlockPos start) {
require(standable(result, start), label(result.parcel) + " has a blocked entrance");
Set<BlockPos> seen = new HashSet<>();
ArrayDeque<BlockPos> queue = new ArrayDeque<>();
seen.add(start); queue.add(start);
while (!queue.isEmpty()) {
BlockPos current = queue.removeFirst();
for (Direction direction : HORIZONTAL) for (int dy = -1; dy <= 1; dy++) {
BlockPos next = current.relative(direction).offset(0, dy, 0);
if (dy > 0 && !clearBody(result, current.above())) continue;
if (standable(result, next) && seen.add(next)) queue.addLast(next);
}
}
return seen;
}
private static boolean standable(Capture result, BlockPos pos) {
if (pos.getX() < 0 || pos.getX() > 16 || pos.getZ() < 0 || pos.getZ() > 16
|| pos.getY() < 1 || pos.getY() > result.parcel.floors() * 5 + 4) return false;
if (!clearBody(result, pos)) return false;
var below = pos.below();
return opened(result.block(below)).getCollisionShape(EmptyBlockGetter.INSTANCE, below).toAabbs().stream()
.anyMatch(b -> b.maxY >= .5 && b.minX <= .5 && b.maxX >= .5 && b.minZ <= .5 && b.maxZ >= .5);
}
private static BlockState opened(BlockState state) {
return state.getBlock() instanceof net.minecraft.world.level.block.DoorBlock
|| state.getBlock() instanceof net.minecraft.world.level.block.TrapDoorBlock
? state.setValue(BlockStateProperties.OPEN, true) : state;
}
private static boolean clearBody(Capture result, BlockPos pos) {
var body = new AABB(pos.getX() + .2, pos.getY(), pos.getZ() + .2, pos.getX() + .8, pos.getY() + 1.8, pos.getZ() + .8);
for (var at : new BlockPos[]{pos, pos.above()}) {
var state = opened(result.block(at));
if (!state.getFluidState().isEmpty()) return false;
for (var shape : state.getCollisionShape(EmptyBlockGetter.INSTANCE, at).toAabbs())
if (shape.move(at.getX(), at.getY(), at.getZ()).intersects(body)) return false;
}
return true;
}
private static boolean clear(BlockState state) { return state.isAir() || state.is(Blocks.DARK_OAK_DOOR); }
private static void verifyArchitecture(Capture c) {
for (int x = 0; x <= 16; x++) for (int z = 0; z <= 16; z++)
require(c.block(new BlockPos(x, -1, z)).is(Blocks.STONE_BRICKS), "Foundation is not dressed stone brick");
long outsideVolume = c.blocks.entrySet().stream().filter(e -> e.getKey().getY() > 0
&& (e.getKey().getX() == 0 || e.getKey().getX() == 16 || e.getKey().getZ() == 0 || e.getKey().getZ() == 16)
&& !e.getValue().isAir()).count();
require(outsideVolume >= 40, label(c.parcel) + " has no cornice, bay, fins or entrance projection");
require(c.blocks.values().stream().anyMatch(b -> b.is(Blocks.TUFF_BRICKS)), "Missing structural tuff courses");
if (c.parcel.kind() == Kind.APARTMENT || c.parcel.kind() == Kind.LIBRARY)
require(c.block(rotate(new BlockPos(1, 2, 1), c.parcel.facing())).isAir(), "Front corner was not physically cut back");
if (c.parcel.kind() == Kind.LIBRARY) {
int roof = c.parcel.floors() * 5;
for (int x = 11; x <= 12; x++) for (int z = 4; z <= 6; z++) {
for (int y = roof - 1; y <= roof; y++)
require(c.block(rotate(new BlockPos(x, y, z), c.parcel.facing())).isAir(),
label(c.parcel) + " skylight is blocked by the roof plate or last-storey ceiling");
for (int y = roof + 1; y <= roof + 2; y++)
require(c.block(rotate(new BlockPos(x, y, z), c.parcel.facing())).is(Blocks.GLASS),
label(c.parcel) + " skylight lost its continuous transparent cap");
for (int y = roof + 3; y <= roof + 4; y++)
require(c.block(rotate(new BlockPos(x, y, z), c.parcel.facing())).isAir(),
label(c.parcel) + " another roof feature shades the light well");
}
for (int x = 10; x <= 13; x++) for (int z = 3; z <= 7; z++) {
if (x > 10 && x < 13 && z > 3 && z < 7) continue;
var base = rotate(new BlockPos(x, roof, z), c.parcel.facing());
require(c.block(base).isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, base)
&& c.block(base.above()).is(Blocks.STRIPPED_BIRCH_LOG),
label(c.parcel) + " skylight cut through its supporting perimeter or timber frame");
}
}
// Resolve every glazing edge from final neighbors, not precomputed straight strips.
for (var e : c.blocks.entrySet()) if (e.getValue().is(Blocks.GLASS_PANE)) {
for (Direction d : HORIZONTAL) {
var neighbor = c.block(e.getKey().relative(d));
if (!neighbor.isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, e.getKey().relative(d))) continue;
var property = switch(d) { case NORTH -> BlockStateProperties.NORTH; case EAST -> BlockStateProperties.EAST;
case SOUTH -> BlockStateProperties.SOUTH; case WEST -> BlockStateProperties.WEST; default -> throw new AssertionError(); };
require(e.getValue().getValue(property), "Window does not meet its actual stone or timber jamb");
}
}
}
private static boolean adjacentReached(BlockPos pos, Set<BlockPos> reached) {
for (Direction direction : HORIZONTAL) if (reached.contains(pos.relative(direction))) return true;
return false;
}
private static BlockPos rotate(BlockPos pos, int facing) {
return switch (facing) {
case 0 -> pos; case 1 -> new BlockPos(16 - pos.getZ(), pos.getY(), pos.getX());
case 2 -> new BlockPos(16 - pos.getX(), pos.getY(), 16 - pos.getZ());
default -> new BlockPos(pos.getZ(), pos.getY(), 16 - pos.getX());
};
}
private record Loot(ResourceKey<LootTable> table, long seed) {}
private static final class Capture implements LostCityRenderer.BlockWriter {
final Parcel parcel;
final Map<BlockPos, BlockState> blocks = new HashMap<>();
final Map<BlockPos, Loot> loot = new HashMap<>();
Capture(Parcel parcel) { this.parcel = parcel; }
@Override public void set(int x, int y, int z, BlockState state) {
require(x >= 0 && x < 17 && z >= 0 && z < 17 && y >= parcel.minY() - parcel.floorY()
&& y <= parcel.maxY() - parcel.floorY(), label(parcel) + " writes outside its saved bounds at " + x + "," + y + "," + z);
blocks.put(new BlockPos(x, y, z), state);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
set(x, y, z, Blocks.CHEST.defaultBlockState());
loot.put(new BlockPos(x, y, z), new Loot(table, seed));
}
BlockState block(BlockPos pos) { return blocks.getOrDefault(pos, Blocks.AIR.defaultBlockState()); }
}
private static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); }
}
@@ -0,0 +1,555 @@
package fr.koka.sanctuary.worldgen.city;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Kind;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Parcel;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.EmptyBlockGetter;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.DoorBlock;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.StairBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BedPart;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
import net.minecraft.world.level.storage.loot.LootTable;
import net.minecraft.world.phys.AABB;
/** Exercises final block geometry rather than reconstructing the painter's room allocation. */
public final class LostCityInteriors21Smoke {
private static final Kind[] KINDS = {Kind.APARTMENT, Kind.HOTEL, Kind.LIBRARY,
Kind.RESTAURANT, Kind.BARRACKS, Kind.HALL};
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
public static void main(String[] args) {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
run();
System.out.println("LostCityInteriors21Smoke: six building uses, alternating physical floor plans, four rotations, connected rooms, fully enclosed bathrooms, wall-backed beds with lateral access, native door/seat geometry, supported lanterns, touching roof supports, oak/smooth-stone palette and deferred loot passed.");
}
/** Also usable after the engine has already bootstrapped its registries. */
public static void run() {
var failures = new java.util.ArrayList<String>();
Set<Map<BlockPos, BlockState>> distinct = new HashSet<>();
Set<Set<BlockPos>> distinctVolumes = new HashSet<>();
var previews = new java.util.ArrayList<Object>();
for (Kind kind : KINDS) for (int floors : new int[]{2, 3, 5}) for (long seed : new long[]{0, 43}) {
try {
Capture canonical = render(parcel(kind, floors, seed, 0));
if (floors == 2 && seed == 0) {
distinct.add(canonical.blocks);
Set<BlockPos> volume = new HashSet<>();
canonical.blocks.forEach((at, state) -> { if (!state.isAir() && at.getY() > 0
&& (at.getY() > 10 || at.getX() == 0 || at.getX() == 16 || at.getZ() == 0 || at.getZ() == 16)) volume.add(at); });
require(distinctVolumes.add(volume), kind + " only recolors an existing facade/roof volume");
}
if (floors == 3 && seed == 43 && System.getProperty("sanctuary.city21.preview") != null) {
var blocks = new java.util.ArrayList<Object>();
canonical.blocks.forEach((at, state) -> { if (!state.isAir()) blocks.add(java.util.List.of(at.getX(), at.getY(), at.getZ(), state.toString())); });
previews.add(Map.of("kind", kind.name(), "seed", seed, "floors", floors, "facing", 0, "blocks", blocks));
}
for (int facing = 0; facing < 4; facing++) {
Parcel parcel = parcel(kind, floors, seed, facing);
Capture result = facing == 0 ? canonical : render(parcel);
Capture repeated = render(parcel);
require(result.blocks.equals(repeated.blocks) && result.loot.equals(repeated.loot), "Saved parcel is not deterministic");
rotationMatches(canonical, result, facing);
require(result.blocks.values().stream().anyMatch(state -> state.is(Blocks.STONE_BRICKS)), "Missing stone-brick foundations");
require(result.blocks.values().stream().anyMatch(state -> state.is(Blocks.SMOOTH_STONE)), "Missing smooth stone");
require(result.blocks.values().stream().noneMatch(state -> state.is(Blocks.SPAWNER)
|| !state.getFluidState().isEmpty()), "Building introduced a spawner or free flowing fluid");
require(result.loot.size() == 1, "Building must keep one deferred loot chest");
var entrance = LostCityInteriors21.rooms(parcel).getFirst();
Set<BlockPos> reached = walk(result, position(entrance));
for (var room : LostCityInteriors21.rooms(parcel)) require(reached.contains(position(room)),
label(parcel) + " cannot walk from its entrance to " + room.name() + " at " + position(room));
verifyDoors(result, reached);
verifyBeds(result, reached);
verifyBathrooms(result, reached);
verifyFurnitureDirections(result);
verifyLanternSupports(result);
for (BlockPos chest : result.loot.keySet()) require(adjacentReached(chest, reached),
label(parcel) + " loot cannot be approached");
semantics(result);
if (facing == 0) verifyStoreyVariation(result);
verifyArchitecture(result);
}
} catch (AssertionError | RuntimeException failure) {
failures.add(kind + " floors=" + floors + " seed=" + seed + ": " + failure.getMessage());
}
}
require(failures.isEmpty(), "City21 interior fixture failures (" + failures.size() + "):\n" + String.join("\n", failures));
require(distinct.size() == KINDS.length, "Building uses only labels to vary an identical interior");
boolean rejected = false;
try { LostCityInteriors21.rooms(new Parcel(Kind.PARK, 0, 0, 17, 17, 160, 2, 0, 0, 4, new int[0])); }
catch (IllegalArgumentException expected) { rejected = true; }
require(rejected, "Interior dispatch must reject non-building parcels");
String preview = System.getProperty("sanctuary.city21.preview");
if (preview != null) try {
var file = java.nio.file.Path.of(preview); if (file.getParent() != null) java.nio.file.Files.createDirectories(file.getParent());
java.nio.file.Files.writeString(file, new com.google.gson.Gson().toJson(previews));
} catch (java.io.IOException e) { throw new AssertionError("Cannot write requested architecture preview", e); }
}
private static Parcel parcel(Kind kind, int floors, long seed, int facing) {
return new Parcel(kind, -81, 119, 17, 17, 160, floors, facing, seed, 4, new int[0]);
}
private static Capture render(Parcel parcel) {
var result = new Capture(parcel);
LostCityInteriors21.render(parcel, result);
return result;
}
private static String label(Parcel p) { return p.kind() + " floors=" + p.floors() + " facing=" + p.facing() + " seed=" + p.seed(); }
private static BlockPos position(LostCityInteriors21.Room room) { return new BlockPos(room.x(), room.y(), room.z()); }
private static void rotationMatches(Capture canonical, Capture rotated, int facing) {
require(canonical.blocks.size() == rotated.blocks.size(), "Rotation changed the number of emitted positions");
Rotation rotation = switch (facing) {
case 0 -> Rotation.NONE; case 1 -> Rotation.CLOCKWISE_90;
case 2 -> Rotation.CLOCKWISE_180; default -> Rotation.COUNTERCLOCKWISE_90;
};
for (var entry : canonical.blocks.entrySet()) {
BlockPos target = rotate(entry.getKey(), facing);
// Native chest placement owns its final facing, so its loot position is checked separately.
if (canonical.loot.containsKey(entry.getKey())) continue;
require(rotated.block(target).equals(entry.getValue().rotate(rotation)), "Geometry or block-state rotation drifted at " + target);
}
for (var entry : canonical.loot.entrySet()) require(entry.getValue().equals(rotated.loot.get(rotate(entry.getKey(), facing))),
"Rotation changed deferred loot identity or position");
}
private static void verifyDoors(Capture result, Set<BlockPos> reached) {
long lowerDoors = 0, pairedLeaves = 0;
for (var entry : result.blocks.entrySet()) {
BlockState state = entry.getValue();
if (!(state.getBlock() instanceof DoorBlock)) continue;
require(state.is(Blocks.OAK_DOOR) && !state.getValue(BlockStateProperties.POWERED), "A room door cannot be opened by an ordinary player");
boolean lower = state.getValue(BlockStateProperties.DOUBLE_BLOCK_HALF) == DoubleBlockHalf.LOWER;
BlockPos partner = lower ? entry.getKey().above() : entry.getKey().below();
BlockState expected = state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF,
lower ? DoubleBlockHalf.UPPER : DoubleBlockHalf.LOWER);
require(result.block(partner).equals(expected), label(result.parcel) + " has an incomplete/mismatched door at " + entry.getKey());
if (!lower) continue;
lowerDoors++;
require(result.block(entry.getKey().below()).isSolid(), "Door has no solid threshold");
Direction normal = state.getValue(BlockStateProperties.HORIZONTAL_FACING);
require(reached.contains(entry.getKey().relative(normal)) && reached.contains(entry.getKey().relative(normal.getOpposite())),
label(result.parcel) + " door does not connect two walkable sides at " + entry.getKey());
for (Direction side : new Direction[]{normal.getClockWise(), normal.getCounterClockWise()}) {
BlockState neighbor = result.block(entry.getKey().relative(side));
if (!neighbor.is(Blocks.OAK_DOOR)
|| neighbor.getValue(BlockStateProperties.DOUBLE_BLOCK_HALF) != DoubleBlockHalf.LOWER
|| neighbor.getValue(BlockStateProperties.HORIZONTAL_FACING) != normal) continue;
AABB opened = state.setValue(BlockStateProperties.OPEN, true)
.getCollisionShape(EmptyBlockGetter.INSTANCE, entry.getKey()).bounds();
require(againstOppositeEdge(opened, side), label(result.parcel) + " double door opens into its partner at " + entry.getKey());
pairedLeaves++;
}
}
require(lowerDoors >= result.parcel.floors() * 3L + 2, "Missing actual doors between distinct rooms");
require(pairedLeaves >= 2, "Entrance must retain a double door with its hinges against the outside jambs");
for (var room : LostCityInteriors21.rooms(result.parcel)) if (room.name().endsWith("/stair_landing")) {
BlockPos landing = position(room);
boolean opensIntoStairwell = false;
for (Direction side : HORIZONTAL) {
BlockPos at = landing.relative(side);
BlockState state = result.block(at);
if (state.is(Blocks.OAK_DOOR)) opensIntoStairwell |=
at.relative(state.getValue(BlockStateProperties.HORIZONTAL_FACING)).equals(landing);
}
require(opensIntoStairwell, "Stairwell door must open into its landing, away from the central corridor");
}
}
private static boolean againstOppositeEdge(AABB box, Direction towardPartner) {
return switch (towardPartner) {
case EAST -> box.maxX <= .25; case WEST -> box.minX >= .75;
case SOUTH -> box.maxZ <= .25; case NORTH -> box.minZ >= .75;
default -> throw new AssertionError("Door partner must be horizontal");
};
}
private static void verifyFurnitureDirections(Capture result) {
int tableSeats = 0;
for (var entry : result.blocks.entrySet()) {
if (!entry.getValue().is(Blocks.OAK_FENCE)
|| !result.block(entry.getKey().above()).is(Blocks.OAK_PRESSURE_PLATE)) continue;
int aroundThisTable = 0;
for (Direction side : HORIZONTAL) {
BlockPos chair = entry.getKey().relative(side);
if (!result.block(chair).is(Blocks.OAK_STAIRS)) continue;
seatLooksToward(result, chair, side.getOpposite());
aroundThisTable++; tableSeats++;
}
require(aroundThisTable > 0, "A desk/table lost every chair facing its surface");
}
if (result.parcel.kind() != Kind.BARRACKS)
require(tableSeats >= result.parcel.floors(), "Every furnished storey needs a usable table or desk seat");
if (result.parcel.kind() == Kind.HALL) for (int x : new int[]{3, 4, 5, 11, 12, 13})
seatLooksToward(result, rotate(new BlockPos(x, 1, 11), result.parcel.facing()), rotated(Direction.SOUTH, result.parcel.facing()));
if (result.parcel.kind() == Kind.HOTEL) for (int x = 2; x <= 4; x++)
seatLooksToward(result, rotate(new BlockPos(x, 1, 14), result.parcel.facing()), rotated(Direction.NORTH, result.parcel.facing()));
if (result.parcel.kind() == Kind.BARRACKS) for (int floor = 0; floor < result.parcel.floors(); floor++)
if (LostCityInteriors21.layout(result.parcel, floor) == 0)
seatLooksToward(result, rotate(new BlockPos(13, floor * 5 + 1, 6), result.parcel.facing()), rotated(Direction.NORTH, result.parcel.facing()));
for (var entry : result.blocks.entrySet()) if (entry.getValue().is(Blocks.POLISHED_ANDESITE_STAIRS)) {
Direction look = entry.getValue().getValue(BlockStateProperties.HORIZONTAL_FACING).getOpposite();
require(clear(result.block(entry.getKey().relative(look))), "Sanitary seat faces a wall instead of its cubicle");
}
}
private static void seatLooksToward(Capture result, BlockPos chair, Direction towardUse) {
BlockState state = result.block(chair);
require(state.is(Blocks.OAK_STAIRS), "Missing chair at " + chair);
var boxes = state.getCollisionShape(EmptyBlockGetter.INSTANCE, chair).toAabbs();
double frontX = .5 + towardUse.getStepX() * .25, frontZ = .5 + towardUse.getStepZ() * .25;
double backX = .5 - towardUse.getStepX() * .25, backZ = .5 - towardUse.getStepZ() * .25;
require(boxes.stream().noneMatch(box -> box.contains(frontX, .75, frontZ))
&& boxes.stream().anyMatch(box -> box.contains(backX, .75, backZ)),
label(result.parcel) + " chair's native back faces its table/stage at " + chair);
require(result.block(chair.below()).isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, chair.below()),
"Chair does not rest on a complete floor");
}
private static Direction rotated(Direction direction, int facing) {
for (int turn = 0; turn < facing; turn++) direction = direction.getClockWise();
return direction;
}
private static void verifyLanternSupports(Capture result) {
int lanterns = 0;
for (var entry : result.blocks.entrySet()) if (entry.getValue().is(Blocks.LANTERN)) {
BlockPos support = entry.getValue().getValue(BlockStateProperties.HANGING) ? entry.getKey().above() : entry.getKey().below();
require(result.block(support).isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, support),
label(result.parcel) + " lantern floats against a slab or lacks a full support block at " + entry.getKey());
lanterns++;
}
require(lanterns == result.parcel.floors(), "A junction lost its supported lamp");
}
private static void verifyBeds(Capture result, Set<BlockPos> reached) {
for (var entry : result.blocks.entrySet()) {
BlockState state = entry.getValue();
if (!state.is(Blocks.BED.white())) continue;
Direction direction = state.getValue(BlockStateProperties.HORIZONTAL_FACING);
BedPart part = state.getValue(BlockStateProperties.BED_PART);
BlockPos partner = entry.getKey().relative(part == BedPart.FOOT ? direction : direction.getOpposite());
require(result.block(partner).equals(state.setValue(BlockStateProperties.BED_PART,
part == BedPart.FOOT ? BedPart.HEAD : BedPart.FOOT)), label(result.parcel) + " has a severed bed at " + entry.getKey());
require(result.block(entry.getKey().below()).isSolid(), "Bed does not sit on a floor");
require(adjacentReached(entry.getKey(), reached) || adjacentReached(partner, reached),
label(result.parcel) + " bed cannot be approached from its room");
if (part == BedPart.HEAD) {
var backing = entry.getKey().relative(direction);
for (int up = 0; up < 3; up++) require(result.block(backing.above(up))
.isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, backing.above(up)),
label(result.parcel) + " bed head is not against a complete wall at " + entry.getKey());
} else {
boolean lateral = false;
for (Direction side : new Direction[]{direction.getClockWise(), direction.getCounterClockWise()})
lateral |= reached.contains(entry.getKey().relative(side)) && reached.contains(partner.relative(side));
require(lateral, label(result.parcel) + " bed lacks a reachable side along its full length at " + entry.getKey());
}
}
}
private static void verifyBathrooms(Capture result, Set<BlockPos> reached) {
for (var bath : LostCityInteriors21.bathrooms(result.parcel)) {
BlockPos door = new BlockPos(bath.doorX(), bath.doorY(), bath.doorZ());
require(result.block(door).is(Blocks.OAK_DOOR), "Enclosed bathroom lost its actual door: " + bath.name());
Direction intoRoom = result.block(door).getValue(BlockStateProperties.HORIZONTAL_FACING);
require(reached.contains(door.relative(intoRoom)), "Bathroom cannot be entered through its door: " + bath.name());
require(bath.minX() == 1 || bath.minZ() == 1 || bath.maxX() == 15 || bath.maxZ() == 15,
"Bathroom enclosure is detached from the exterior room wall");
for (int x = bath.minX(); x <= bath.maxX(); x++) for (int z = bath.minZ(); z <= bath.maxZ(); z++)
for (int y = bath.minY(); y <= bath.maxY(); y++) {
boolean shell = x == bath.minX() || x == bath.maxX() || z == bath.minZ() || z == bath.maxZ()
|| y == bath.minY() || y == bath.maxY();
if (!shell) continue;
BlockPos at = new BlockPos(x, y, z);
if (at.equals(door) || at.equals(door.above())) continue;
require(result.block(at).isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, at),
label(result.parcel) + " bathroom has an incomplete floor, ceiling or connected wall at " + at + " in " + bath.name());
}
}
}
private static void verifyStoreyVariation(Capture result) {
for (int floor = 1; floor < result.parcel.floors(); floor++) {
require(LostCityInteriors21.layout(result.parcel, floor - 1) != LostCityInteriors21.layout(result.parcel, floor),
"Adjacent storeys selected the same plan");
Set<BlockPos> before = rearPartitions(result, floor - 1), after = rearPartitions(result, floor);
require(!before.equals(after), label(result.parcel) + " only changed furniture or labels between storeys " + (floor - 1) + " and " + floor);
Map<BlockPos, BlockState> beforeFixtures = furnishings(result, floor - 1), afterFixtures = furnishings(result, floor);
require(!beforeFixtures.equals(afterFixtures), label(result.parcel)
+ " copied the same furniture between storeys " + (floor - 1) + " and " + floor);
}
}
private static Set<BlockPos> rearPartitions(Capture result, int floor) {
Set<BlockPos> walls = new HashSet<>();
for (int x = 2; x <= 14; x++) for (int z = 9; z <= 14; z++) {
boolean continuous = true;
for (int y = 1; y <= 3; y++) continuous &= result.block(new BlockPos(x, floor * 5 + y, z)).is(Blocks.SMOOTH_STONE);
if (continuous) walls.add(new BlockPos(x, 0, z));
}
return walls;
}
private static Map<BlockPos, BlockState> furnishings(Capture result, int floor) {
Map<BlockPos, BlockState> objects = new HashMap<>();
for (int x = 2; x <= 14; x++) for (int z = 2; z <= 14; z++) for (int up = 1; up <= 3; up++) {
BlockState state = result.block(new BlockPos(x, floor * 5 + up, z));
if (state.is(Blocks.BED.white()) || state.is(Blocks.BARREL) || state.is(Blocks.FURNACE)
|| state.is(Blocks.BOOKSHELF) || state.is(Blocks.WATER_CAULDRON) || state.is(Blocks.OAK_FENCE)
|| state.is(Blocks.LECTERN) || state.is(Blocks.CRAFTING_TABLE) || state.is(Blocks.OAK_STAIRS)
|| state.is(Blocks.OAK_PLANKS) || state.is(Blocks.OAK_PLANKS) || state.is(Blocks.OAK_PLANKS)) objects.put(new BlockPos(x, up, z), state);
}
return objects;
}
private static void semantics(Capture result) {
Map<BlockState, Long> counts = new HashMap<>();
result.blocks.values().forEach(state -> counts.merge(state, 1L, Long::sum));
java.util.function.Predicate<net.minecraft.world.level.block.Block> has = block ->
counts.keySet().stream().anyMatch(state -> state.is(block));
switch (result.parcel.kind()) {
case APARTMENT -> require(has.test(Blocks.FURNACE) && has.test(Blocks.BED.white())
&& has.test(Blocks.WATER_CAULDRON), "Apartment lacks cooking, sleeping or washing fixtures");
case HOTEL -> require(has.test(Blocks.LOOM) && has.test(Blocks.BED.white())
&& has.test(Blocks.WATER_CAULDRON), "Hotel lacks guest rooms or laundry fixtures");
case LIBRARY -> require(result.blocks.values().stream().filter(state -> state.is(Blocks.BOOKSHELF)).count() >= result.parcel.floors() * 12L
&& has.test(Blocks.LECTERN), "Library is missing usable stacks and reading fixtures");
case RESTAURANT -> require(has.test(Blocks.SMOKER) && has.test(Blocks.FURNACE)
&& has.test(Blocks.CRAFTING_TABLE) && has.test(Blocks.WATER_CAULDRON), "Restaurant lacks kitchen and washroom services");
case BARRACKS -> require(has.test(Blocks.BED.white()) && has.test(Blocks.FLETCHING_TABLE)
&& has.test(Blocks.GRINDSTONE), "Barracks lacks dormitory and equipment fixtures");
case HALL -> {
require(has.test(Blocks.LECTERN) && has.test(Blocks.IRON_BARS), "Hall lacks stage or gallery guards");
require(result.block(rotate(new BlockPos(8, 5, 12), result.parcel.facing())).isAir(), "Auditorium lost its double-height volume");
}
default -> throw new AssertionError("Unexpected building");
}
}
/** Player-sized path: two clear cells above support, one-block steps and manually openable doors. */
private static Set<BlockPos> walk(Capture result, BlockPos start) {
require(standable(result, start), label(result.parcel) + " has a blocked entrance");
Set<BlockPos> seen = new HashSet<>();
ArrayDeque<BlockPos> queue = new ArrayDeque<>();
seen.add(start); queue.add(start);
while (!queue.isEmpty()) {
BlockPos current = queue.removeFirst();
for (Direction direction : HORIZONTAL) for (int dy = -1; dy <= 1; dy++) {
BlockPos next = current.relative(direction).offset(0, dy, 0);
if (dy > 0 && !clearBody(result, current.above())) continue;
if (standable(result, next) && seen.add(next)) queue.addLast(next);
}
}
return seen;
}
private static boolean standable(Capture result, BlockPos pos) {
if (pos.getX() < 0 || pos.getX() > 16 || pos.getZ() < 0 || pos.getZ() > 16
|| pos.getY() < 1 || pos.getY() > result.parcel.floors() * 5 + 4) return false;
if (!clearBody(result, pos)) return false;
var below = pos.below();
return opened(result.block(below)).getCollisionShape(EmptyBlockGetter.INSTANCE, below).toAabbs().stream()
.anyMatch(b -> b.maxY >= .5 && b.minX <= .5 && b.maxX >= .5 && b.minZ <= .5 && b.maxZ >= .5);
}
private static BlockState opened(BlockState state) {
return state.getBlock() instanceof net.minecraft.world.level.block.DoorBlock
|| state.getBlock() instanceof net.minecraft.world.level.block.TrapDoorBlock
? state.setValue(BlockStateProperties.OPEN, true) : state;
}
private static boolean clearBody(Capture result, BlockPos pos) {
var body = new AABB(pos.getX() + .2, pos.getY(), pos.getZ() + .2, pos.getX() + .8, pos.getY() + 1.8, pos.getZ() + .8);
for (var at : new BlockPos[]{pos, pos.above()}) {
var state = opened(result.block(at));
if (!state.getFluidState().isEmpty()) return false;
for (var shape : state.getCollisionShape(EmptyBlockGetter.INSTANCE, at).toAabbs())
if (shape.move(at.getX(), at.getY(), at.getZ()).intersects(body)) return false;
}
return true;
}
private static boolean clear(BlockState state) { return state.isAir() || state.is(Blocks.OAK_DOOR); }
private static void verifyArchitecture(Capture c) {
for (int x = 0; x <= 16; x++) for (int z = 0; z <= 16; z++)
require(c.block(new BlockPos(x, -1, z)).is(Blocks.STONE_BRICKS), "Foundation is not dressed stone brick");
long outsideVolume = c.blocks.entrySet().stream().filter(e -> e.getKey().getY() > 0
&& (e.getKey().getX() == 0 || e.getKey().getX() == 16 || e.getKey().getZ() == 0 || e.getKey().getZ() == 16)
&& !e.getValue().isAir()).count();
require(outsideVolume >= 40, label(c.parcel) + " has no cornice, bay, fins or entrance projection");
verifyPalette(c.blocks, true, label(c.parcel));
verifyGroundedGeometry(c.blocks, 0, label(c.parcel));
if (c.parcel.kind() == Kind.BARRACKS) for (int floor = 0; floor < c.parcel.floors(); floor++) {
var grinder = rotate(new BlockPos(12, floor * 5 + 1, 11), c.parcel.facing());
require(c.block(grinder).is(Blocks.GRINDSTONE)
&& c.block(grinder).getValue(BlockStateProperties.ATTACH_FACE)
== net.minecraft.world.level.block.state.properties.AttachFace.FLOOR,
label(c.parcel) + " workshop grinder lacks its floor-mounted frame");
}
if (c.parcel.kind() == Kind.HOTEL) for (int x : new int[]{12, 14}) {
var counter = rotate(new BlockPos(x, 1, 14), c.parcel.facing());
require(c.block(counter).is(Blocks.BARREL)
&& c.block(counter.above()).is(Blocks.OAK_PLANKS)
&& c.block(counter.above(2)).is(Blocks.OAK_SLAB),
label(c.parcel) + " laundry shelf lacks its counter-mounted timber frame");
}
long smooth = 0, facingMaterial = 0;
for (int x = 1; x <= 15; x++) for (int z = 1; z <= 15; z++) {
if (x != 1 && x != 15 && z != 1 && z != 15) continue;
for (int y = 1; y < c.parcel.floors() * 5; y++) {
var at = rotate(new BlockPos(x, y, z), c.parcel.facing());
var state = c.block(at);
if (state.isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, at)) {
facingMaterial++;
if (state.is(Blocks.SMOOTH_STONE)) smooth++;
}
}
}
require(smooth * 2 >= facingMaterial, label(c.parcel) + " no longer has predominantly smooth-stone facades: " + smooth + "/" + facingMaterial);
if (c.parcel.kind() == Kind.APARTMENT || c.parcel.kind() == Kind.LIBRARY)
require(c.block(rotate(new BlockPos(1, 2, 1), c.parcel.facing())).isAir(), "Front corner was not physically cut back");
if (c.parcel.kind() == Kind.LIBRARY) {
int roof = c.parcel.floors() * 5;
for (int x = 11; x <= 12; x++) for (int z = 4; z <= 6; z++) {
for (int y = roof - 1; y <= roof; y++)
require(c.block(rotate(new BlockPos(x, y, z), c.parcel.facing())).isAir(),
label(c.parcel) + " skylight is blocked by the roof plate or last-storey ceiling");
require(c.block(rotate(new BlockPos(x, roof + 1, z), c.parcel.facing())).isAir(),
label(c.parcel) + " skylight contains a redundant glazed floor under its cap");
require(c.block(rotate(new BlockPos(x, roof + 2, z), c.parcel.facing())).is(Blocks.GLASS),
label(c.parcel) + " skylight lost its continuous transparent cap");
for (int y = roof + 3; y <= roof + 4; y++)
require(c.block(rotate(new BlockPos(x, y, z), c.parcel.facing())).isAir(),
label(c.parcel) + " another roof feature shades the light well");
}
for (int x = 10; x <= 13; x++) for (int z = 3; z <= 7; z++) {
if (x > 10 && x < 13 && z > 3 && z < 7) continue;
var base = rotate(new BlockPos(x, roof, z), c.parcel.facing());
require(c.block(base).isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, base)
&& c.block(base.above()).is(Blocks.STRIPPED_OAK_LOG),
label(c.parcel) + " skylight cut through its supporting perimeter or timber frame");
}
}
if (c.parcel.kind() == Kind.HOTEL || c.parcel.kind() == Kind.BARRACKS) {
int roof = c.parcel.floors() * 5;
int x = c.parcel.kind() == Kind.HOTEL ? 9 : 1;
int z = c.parcel.kind() == Kind.HOTEL ? 3 : 2;
var top = rotate(new BlockPos(x, roof + 4, z), c.parcel.facing());
require(c.block(top).is(Blocks.STONE_BRICK_SLAB)
&& c.block(top).getValue(BlockStateProperties.SLAB_TYPE)
== net.minecraft.world.level.block.state.properties.SlabType.BOTTOM,
label(c.parcel) + " pavilion roof floats half a block above its walls");
require(c.block(top.below()).isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, top.below()),
label(c.parcel) + " pavilion lost the wall carrying its roof");
}
// Resolve every glazing edge from final neighbors, not precomputed straight strips.
for (var e : c.blocks.entrySet()) if (e.getValue().is(Blocks.GLASS_PANE)) {
for (Direction d : HORIZONTAL) {
var neighbor = c.block(e.getKey().relative(d));
if (!neighbor.isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, e.getKey().relative(d))) continue;
var property = switch(d) { case NORTH -> BlockStateProperties.NORTH; case EAST -> BlockStateProperties.EAST;
case SOUTH -> BlockStateProperties.SOUTH; case WEST -> BlockStateProperties.WEST; default -> throw new AssertionError(); };
require(e.getValue().getValue(property), "Window does not meet its actual stone or timber jamb");
}
}
}
/** The construction palette is explicit; functional fixtures and planting retain vanilla materials. */
public static void verifyPalette(Map<BlockPos, BlockState> blocks, boolean building, String label) {
for (var e : blocks.entrySet()) {
String name = e.getValue().toString();
require(!name.matches(".*(?:birch|dark_oak|spruce|jungle|acacia|mangrove|cherry|pale_oak|bamboo)_.*"),
label + " introduced a second timber palette at " + e.getKey() + ": " + name);
require(!name.contains("granite") && !name.contains("diorite"),
label + " introduced contrasting granite/diorite at " + e.getKey());
require(!name.contains("tuff") || !building && e.getKey().getY() <= 0,
label + " used tuff above the ground/subsoil at " + e.getKey());
}
}
/** Contact uses native collision volumes: merely neighboring slabs can still leave a real gap.
* Every solid fixture, roof and facade must join the foundation through touching material. */
public static void verifyGroundedGeometry(Map<BlockPos, BlockState> blocks, int groundY, String label) {
Map<BlockState, java.util.List<AABB>> shapes = new HashMap<>();
Map<BlockPos, java.util.List<AABB>> geometry = new HashMap<>();
for (var entry : blocks.entrySet()) {
// A lantern's collision omits its thin hanging chain. Its explicit native support
// is checked below instead of pretending that invisible collision gap is floating masonry.
if (entry.getValue().is(Blocks.LANTERN)) continue;
var shape = shapes.computeIfAbsent(entry.getValue(), state ->
state.getCollisionShape(EmptyBlockGetter.INSTANCE, BlockPos.ZERO).toAabbs());
if (!shape.isEmpty()) geometry.put(entry.getKey(), shape);
}
Set<BlockPos> supported = new HashSet<>();
ArrayDeque<BlockPos> queue = new ArrayDeque<>();
for (BlockPos at : geometry.keySet()) if (at.getY() <= groundY) {
supported.add(at); queue.add(at);
}
while (!queue.isEmpty()) {
var at = queue.removeFirst();
for (Direction direction : Direction.values()) {
var next = at.relative(direction);
if (supported.contains(next) || !geometry.containsKey(next)) continue;
boolean touches = false;
for (AABB a : geometry.get(at)) for (AABB b : geometry.get(next)) {
double dx = Math.min(a.maxX, b.maxX + direction.getStepX()) - Math.max(a.minX, b.minX + direction.getStepX());
double dy = Math.min(a.maxY, b.maxY + direction.getStepY()) - Math.max(a.minY, b.minY + direction.getStepY());
double dz = Math.min(a.maxZ, b.maxZ + direction.getStepZ()) - Math.max(a.minZ, b.minZ + direction.getStepZ());
if (dx >= -1e-7 && dy >= -1e-7 && dz >= -1e-7
&& (dx > 1e-7 && dy > 1e-7 || dx > 1e-7 && dz > 1e-7 || dy > 1e-7 && dz > 1e-7)) touches = true;
}
if (touches) { supported.add(next); queue.addLast(next); }
}
}
for (var at : geometry.keySet()) require(supported.contains(at),
label + " has a floating structural/fixture island at " + at + ": " + blocks.get(at));
for (var entry : blocks.entrySet()) if (entry.getValue().is(Blocks.LANTERN)) {
var anchor = entry.getValue().getValue(BlockStateProperties.HANGING) ? entry.getKey().above() : entry.getKey().below();
var state = blocks.getOrDefault(anchor, Blocks.AIR.defaultBlockState());
require(state.isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, anchor) && supported.contains(anchor),
label + " lamp lacks a full, grounded native support at " + entry.getKey());
}
}
private static boolean adjacentReached(BlockPos pos, Set<BlockPos> reached) {
for (Direction direction : HORIZONTAL) if (reached.contains(pos.relative(direction))) return true;
return false;
}
private static BlockPos rotate(BlockPos pos, int facing) {
return switch (facing) {
case 0 -> pos; case 1 -> new BlockPos(16 - pos.getZ(), pos.getY(), pos.getX());
case 2 -> new BlockPos(16 - pos.getX(), pos.getY(), 16 - pos.getZ());
default -> new BlockPos(pos.getZ(), pos.getY(), 16 - pos.getX());
};
}
private record Loot(ResourceKey<LootTable> table, long seed) {}
private static final class Capture implements LostCityRenderer.BlockWriter {
final Parcel parcel;
final Map<BlockPos, BlockState> blocks = new HashMap<>();
final Map<BlockPos, Loot> loot = new HashMap<>();
Capture(Parcel parcel) { this.parcel = parcel; }
@Override public void set(int x, int y, int z, BlockState state) {
require(x >= 0 && x < 17 && z >= 0 && z < 17 && y >= parcel.minY() - parcel.floorY()
&& y <= parcel.maxY() - parcel.floorY(), label(parcel) + " writes outside its saved bounds at " + x + "," + y + "," + z);
blocks.put(new BlockPos(x, y, z), state);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
set(x, y, z, Blocks.CHEST.defaultBlockState());
loot.put(new BlockPos(x, y, z), new Loot(table, seed));
}
BlockState block(BlockPos pos) { return blocks.getOrDefault(pos, Blocks.AIR.defaultBlockState()); }
}
private static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); }
}
@@ -0,0 +1,92 @@
package fr.koka.sanctuary.worldgen.city;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.function.IntBinaryOperator;
/** Natural extensions publish only with a supported player route; the old core/fallback is retained verbatim. */
public final class LostCityPlanner21Smoke {
public static void main(String[] args) {
int extras = 0;
for (int size : new int[]{512, 724, 1024}) for (long seed : new long[]{0, 42}) {
var terrain = new Terrain((x, z) -> 220);
var result = LostCityPlanner21.plan(seed, 0, 0, size, true, terrain); verify(result, terrain);
require(!result.core().surface().isEmpty(), "New solver lost the old starter core");
require(result.surface().districts().size() == 1 + result.connections().size(), "An isolated district was published");
extras += result.connections().size();
var repeat = LostCityPlanner21.plan(seed, 0, 0, size, true, new Terrain((x, z) -> 220));
require(signature(result).equals(signature(repeat)), "Extension depends on invocation order");
}
require(extras >= 6, "Flat fixtures do not demonstrate modest connected neighborhoods: " + extras);
var baseline = LostCityPlanner21.plan(0, 0, 0, 724, true, new Terrain((x, z) -> 220));
var core = baseline.core().surface().districts().getFirst();
Terrain cutoff = new Terrain((x, z) -> 220);
cutoff.blocked = (x, y, z) -> x < core.minX() - 2 || x > core.maxX() + 2 || z < core.minZ() - 2 || z > core.maxZ() + 2;
var stopped = LostCityPlanner21.plan(0, 0, 0, 724, true, cutoff); verify(stopped, cutoff);
require(stopped.connections().isEmpty(), "Protected outskirts acquired fictional connecting roads");
int northSouthGrades = 0;
for (int sign : new int[]{-1, 1}) for (long seed : new long[]{0, 42}) {
Terrain slope = new Terrain((x, z) -> 220 + sign * Math.floorDiv(z, 15));
northSouthGrades += verify(LostCityPlanner21.plan(seed, 0, 0, 724, true, slope), slope);
}
require(northSouthGrades > 0, "Slope witnesses never exercise a real north/south stair inside a connecting road");
Terrain empty = new Terrain((x, z) -> 0);
var fallback = LostCityPlanner21.plan(0, 0, 0, 512, true, empty);
require(fallback.connections().isEmpty() && fallback.reservation() != null && fallback.reservation().width() == 49,
"Void expanded the architectural fallback into a larger platform");
var continent = LostCityPlanner21.plan(0, 0, 0, 256, false, new Terrain((x, z) -> 220));
require(continent.connections().isEmpty() && continent.surface() != null, "Root extension policy leaked onto a continent");
System.out.println("LostCityPlanner21Smoke: preserved core, modest connected neighborhoods, bounded routes, terrain gaps, native bounds and no enlarged fallback passed.");
}
private static int verify(LostCityPlanner21.Result result, Terrain terrain) {
int northSouthGrades = 0;
require(result.columns() <= LostCityPlanner21.MAX_COLUMNS && result.volumeChecks() <= LostCityPlanner21.MAX_VOLUME_CHECKS, "Budget exceeded");
require(result.surface().districts().size() <= 3, "Town sprawls into more than three blocks");
if (!result.core().surface().isEmpty()) require(result.surface().districts().getFirst() == result.core().surface().districts().getFirst(), "Core was rewritten");
var columns = new HashMap<Long, Integer>(); var kinds = new HashMap<Long, LostCityPlan.Kind>();
var owners = new HashMap<Long, LostCityPlan.Parcel>();
for (var district : result.surface().districts()) for (var p : district.parcels()) {
require(p.width() <= 25 && p.depth() <= 25, "Road escaped parcel limits");
for (int z = p.minZ(); z <= p.maxZ(); z++) for (int x = p.minX(); x <= p.maxX(); x++) {
long key = key(x, z); Integer old = columns.putIfAbsent(key, p.surfaceY(x - p.minX(), z - p.minZ()));
require(old == null, "A road and cluster paint the same column twice"); kinds.put(key, p.kind()); owners.put(key, p);
}
}
for (var link : result.connections()) {
require(link.districtIndex() > 0 && link.districtIndex() < result.surface().districts().size(), "Connection points to an absent district");
require(link.path().size() >= 14 && link.path().size() <= 192, "Route lacks the intended natural gap or exceeds reach");
for (int i = 0; i < link.path().size(); i++) {
var p = link.path().get(i); var key = key(p.x(), p.z());
require(columns.get(key) != null && columns.get(key) == p.floorY() && kinds.get(key) == LostCityPlan.Kind.STREET, "Connection centerline has no rendered floor");
require(!terrain.blocked.test(p.x(), p.floorY(), p.z()), "Connection crosses protected terrain");
if (i > 0) {
var a = link.path().get(i - 1);
require(Math.abs(p.x() - a.x()) + Math.abs(p.z() - a.z()) == 1 && Math.abs(p.floorY() - a.floorY()) <= 1, "Player route is discontinuous");
if (p.floorY() != a.floorY()) {
require(owners.get(key) == owners.get(key(a.x(), a.z())),
"A road grade crosses a parcel boundary, so the native renderer cannot build its connecting stair");
if (p.z() != a.z()) northSouthGrades++;
}
}
}
}
return northSouthGrades;
}
private static String signature(LostCityPlanner21.Result r) {
var result = new StringBuilder().append(r.connections()).append(r.columns()).append(r.volumeChecks()).append(r.diagnostics());
for (var d : r.surface().districts()) for (var p : d.parcels()) result.append(p.kind()).append(p.minX()).append(p.minZ()).append(p.floorY()).append(java.util.Arrays.toString(p.surfaceHeights()));
return result.toString();
}
private interface Protection { boolean test(int x, int y, int z); }
private static final class Terrain implements LostCityPlanner16.Terrain {
final IntBinaryOperator height; Protection blocked = (x, y, z) -> false;
Terrain(IntBinaryOperator height) { this.height = height; }
public boolean owns(int x, int z) { return Math.abs(x) < 600 && Math.abs(z) < 600; }
public LostCityPlanner.Ground ground(int x, int z) { int h = height.applyAsInt(x, z); return new LostCityPlanner.Ground(h, h > 0 ? 24 : 0, h > 0, blocked.test(x, h, z)); }
public boolean solid(int x, int y, int z) { return y > 0 && y <= height.applyAsInt(x, z); }
public boolean protectedAt(int x, int y, int z) { return blocked.test(x, y, z); }
}
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
}
@@ -0,0 +1,229 @@
package fr.koka.sanctuary.worldgen.city;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Kind;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Parcel;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.StairBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BedPart;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.storage.loot.LootTable;
/** Real block-state geometry, without creating a world: clipping, rotations and walkable floors. */
public final class LostCitySurface20Smoke {
public static void main(String[] args) {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
run();
System.out.println("LostCitySurface20Smoke: all exterior amenities, supported lamps, basements, contained pools, excavation access, carved park variants and suspended tuff roads passed.");
}
/** Also callable from an already bootstrapped native GameTest. */
public static void run() {
for (Kind kind : Kind.values()) {
if (kind.building()) continue;
for (int facing = 0; facing < 4; facing++) {
for (long seed : new long[] {0, 42, -61872}) {
int floors = kind == Kind.HALL ? 2 : 3;
int foundation = kind == Kind.POOL || kind == Kind.EXCAVATION ? 8 : 4;
Parcel parcel = new Parcel(kind, 0, 0, 17, 17, 170, floors, facing, seed, foundation, new int[0]);
Capture result = render(parcel);
require(result.blocks.equals(render(parcel).blocks), "Renderer changed for identical saved parcel");
require(result.blocks.values().stream().noneMatch(state -> state.is(Blocks.SPAWNER)), "City introduced a spawner");
if (kind.building()) {
require(result.blocks.values().stream().anyMatch(state -> state.is(Blocks.STONE_BRICKS)), "Missing explicit stone palette");
require(result.blocks.values().stream().anyMatch(state -> state.is(Blocks.SMOOTH_STONE)), "Missing smooth stone palette");
Set<BlockPos> reached = walk(result, rotate(8, 1, 0, facing));
for (int floor = 1; floor <= floors; floor++) {
int elevation = floor * 5 + 1;
require(reached.stream().anyMatch(pos -> pos.getY() == elevation),
kind + " facing " + facing + " cannot reach floor/roof " + floor);
}
require(result.loot.size() == 1, kind + " must keep exactly one deferred, limited loot chest");
}
for (var entry : result.blocks.entrySet()) {
if (entry.getValue().is(Blocks.LANTERN)) {
var support = entry.getValue().getValue(BlockStateProperties.HANGING) ? entry.getKey().above() : entry.getKey().below();
require(result.block(support).isCollisionShapeFullBlock(net.minecraft.world.level.EmptyBlockGetter.INSTANCE, support), kind + " lantern lacks full support at " + entry.getKey());
}
BlockState state = entry.getValue();
if (state.is(Blocks.BED.white()) && state.getValue(BlockStateProperties.BED_PART) == BedPart.FOOT) {
Direction direction = state.getValue(BlockStateProperties.HORIZONTAL_FACING);
BlockState head = result.block(entry.getKey().relative(direction));
require(head.is(Blocks.BED.white()) && head.getValue(BlockStateProperties.BED_PART) == BedPart.HEAD,
"Rotated bedroom has an incomplete bed");
}
}
}
}
}
for (int facing = 0; facing < 4; facing++) {
Parcel garage = new Parcel(Kind.PARKING, 0, 0, 17, 17, 170, 1, facing, 9, 8, new int[0]);
Capture result = render(garage);
Set<BlockPos> reached = walk(result, rotate(8, 1, 0, facing));
require(reached.contains(rotate(8, -4, 10, facing)), "Underground garage is not reachable from its street ramp");
require(result.block(rotate(8, -6, 10, facing)).is(Blocks.STONE_BRICKS), "Garage excavation removed the certified rock below its floor");
Parcel pit = new Parcel(Kind.EXCAVATION, 0, 0, 17, 17, 170, 0, facing, 9, 8, new int[0]);
Capture excavation = render(pit);
require(walk(excavation, rotate(8, 1, 0, facing)).contains(rotate(8, -4, 10, facing)),
"Excavation has no walkable stairs from the street");
require(excavation.block(rotate(8, -6, 10, facing)).is(Blocks.STONE_BRICKS), "Excavation removed its certified bottom support");
Parcel pool = new Parcel(Kind.POOL, 0, 0, 17, 17, 170, 0, facing, 9, 8, new int[0]);
Capture water = render(pool);
sealedPool(water, facing);
require(walk(water, rotate(8, 1, 0, facing)).contains(rotate(8, -2, 7, facing)),
"Pool submerged steps cannot be reached from the deck");
}
// Every public type keeps a usable north approach, and its furniture does not consume the street threshold.
for (Kind kind : Kind.values()) if (!kind.building() && kind != Kind.STREET && kind != Kind.BRIDGE)
for (int facing = 0; facing < 4; facing++) {
Capture c = render(new Parcel(kind, 0, 0, 17, 17, 170, 0, facing, 42, 8, new int[0]));
var reached = walk(c, rotate(8, 1, 0, facing));
require(reached.size() >= 100, kind + " lost its public circulation");
for (var chest : c.loot.keySet()) require(java.util.Arrays.stream(new Direction[]{Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST})
.anyMatch(d -> reached.contains(chest.relative(d))), kind + " loot is not approachable");
}
exportPreviews();
Set<Map<BlockPos, BlockState>> parks = new HashSet<>();
for (Kind park : new Kind[] {Kind.PARK, Kind.PARK_GARDEN, Kind.PARK_PLAYGROUND, Kind.PARK_MEMORIAL}) {
parks.add(render(new Parcel(park, 0, 0, 17, 17, 170, 0, 0, 42, 4, new int[0])).blocks);
}
require(parks.size() == 4, "Park variants must have distinct geometry");
for (Kind kind : new Kind[] {Kind.POOL, Kind.EXCAVATION}) {
Capture fallback = render(new Parcel(kind, 0, 0, 17, 17, 170, 0, 0, 42, 4, new int[0]));
require(fallback.blocks.values().stream().noneMatch(state -> !state.getFluidState().isEmpty()),
"Uncertified ground produced a wet excavation");
require(fallback.block(new BlockPos(8, -1, 8)).is(Blocks.STONE_BRICKS), "Uncertified ground was excavated");
}
int[] heights = new int[17 * 5];
for (int z = 0; z < 17; z++) for (int x = 0; x < 5; x++) heights[z * 5 + x] = 170 + z / 4;
Parcel bridge = new Parcel(Kind.BRIDGE, 0, 0, 5, 17, 170, 0, 0, 33, 0, heights);
Capture result = render(bridge);
require(result.blocks.entrySet().stream().noneMatch(entry -> !entry.getValue().isAir()
&& entry.getKey().getY() < bridge.surfaceY(entry.getKey().getX(), entry.getKey().getZ()) - bridge.floorY()),
"Bridge grew a column below its suspended deck");
for (var e : result.blocks.entrySet()) if (e.getValue().is(Blocks.TUFF_BRICK_STAIRS)) {
var face = e.getValue().getValue(BlockStateProperties.HORIZONTAL_FACING);
var high = e.getKey().relative(face);
require(bridge.surfaceY(high.getX(), high.getZ()) - bridge.floorY() == e.getKey().getY(),
"Bridge stair fails to meet the higher deck");
}
Set<BlockPos> reached = walk(result, new BlockPos(2, 1, 0));
require(reached.contains(new BlockPos(2, 5, 16)), "Stepped bridge cannot be crossed");
}
private static void exportPreviews() {
String path = System.getProperty("sanctuary.city20.exteriorPreview");
if (path == null) return;
var examples = new java.util.ArrayList<Object>();
for (Kind kind : Kind.values()) if (!kind.building() && kind != Kind.STREET && kind != Kind.BRIDGE) {
var p = new Parcel(kind, 0, 0, 17, 17, 170, 0, 0, 42, 8, new int[0]); var c = render(p);
var blocks = new java.util.ArrayList<Object>();
c.blocks.forEach((at, state) -> { if (!state.isAir()) blocks.add(java.util.List.of(at.getX(), at.getY(), at.getZ(), state.toString())); });
examples.add(Map.of("kind", kind.name(), "seed", p.seed(), "facing", 0, "blocks", blocks));
}
try { var output = java.nio.file.Path.of(path); if (output.getParent() != null) java.nio.file.Files.createDirectories(output.getParent());
java.nio.file.Files.writeString(output, new com.google.gson.Gson().toJson(examples)); }
catch (java.io.IOException e) { throw new AssertionError("Cannot write requested geometry preview", e); }
}
private static Capture render(Parcel parcel) {
Capture result = new Capture(parcel);
LostCitySurface20.render(parcel, result);
return result;
}
/** Flood the cells a source can reach horizontally/downward, without permitting water to rise. */
private static void sealedPool(Capture result, int facing) {
Set<BlockPos> flooded = new HashSet<>();
ArrayDeque<BlockPos> queue = new ArrayDeque<>();
result.blocks.forEach((pos, state) -> {
if (!state.getFluidState().isEmpty()) { flooded.add(pos); queue.add(pos); }
});
require(flooded.size() > 150, "Pool must contain a real basin of water");
while (!queue.isEmpty()) {
BlockPos pos = queue.removeFirst();
require(pos.getY() >= -3 && pos.getY() <= -1, "Pool leaks vertically through its floor or coping");
BlockPos canonical = rotate(pos.getX(), pos.getY(), pos.getZ(), (4 - facing) % 4);
require(canonical.getX() >= 4 && canonical.getX() <= 12 && canonical.getZ() >= 5 && canonical.getZ() <= 12,
"Pool leaks outside its continuous side walls");
for (Direction direction : new Direction[] {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST, Direction.DOWN}) {
BlockPos next = pos.relative(direction);
BlockState state = result.block(next);
if ((state.isAir() || !state.getFluidState().isEmpty()) && flooded.add(next)) queue.addLast(next);
}
}
}
/** Conservative full-block walking with one-block steps and two blocks of clear headroom. */
private static Set<BlockPos> walk(Capture result, BlockPos start) {
require(standable(result, start), "Entrance is blocked: " + start);
Set<BlockPos> seen = new HashSet<>();
ArrayDeque<BlockPos> queue = new ArrayDeque<>();
queue.add(start);
seen.add(start);
while (!queue.isEmpty()) {
BlockPos current = queue.removeFirst();
for (Direction direction : new Direction[] {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}) {
for (int dy = -1; dy <= 1; dy++) {
BlockPos next = current.relative(direction).offset(0, dy, 0);
if (standable(result, next) && seen.add(next)) queue.addLast(next);
}
}
}
return seen;
}
private static boolean standable(Capture result, BlockPos pos) {
if (pos.getX() < 0 || pos.getZ() < 0 || pos.getX() >= result.parcel.width() || pos.getZ() >= result.parcel.depth()) return false;
BlockState support = result.block(pos.below());
return (support.isSolid() || support.getBlock() instanceof StairBlock)
&& clear(result.block(pos)) && clear(result.block(pos.above()));
}
private static boolean clear(BlockState state) { return state.isAir() || state.is(Blocks.WATER); }
private static BlockPos rotate(int x, int y, int z, int facing) {
return switch (facing) {
case 0 -> new BlockPos(x, y, z);
case 1 -> new BlockPos(16 - z, y, x);
case 2 -> new BlockPos(16 - x, y, 16 - z);
default -> new BlockPos(z, y, 16 - x);
};
}
private static final class Capture implements LostCityRenderer.BlockWriter {
private final Parcel parcel;
private final Map<BlockPos, BlockState> blocks = new HashMap<>();
private final Map<BlockPos, ResourceKey<LootTable>> loot = new HashMap<>();
Capture(Parcel parcel) { this.parcel = parcel; }
@Override public void set(int x, int y, int z, BlockState state) {
require(x >= 0 && z >= 0 && x < parcel.width() && z < parcel.depth()
&& y >= parcel.minY() - parcel.floorY() && y <= parcel.maxY() - parcel.floorY(),
parcel.kind() + " emitted a block outside its saved bounds at " + x + "," + y + "," + z);
blocks.put(new BlockPos(x, y, z), state);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long lootSeed) {
set(x, y, z, Blocks.CHEST.defaultBlockState());
loot.put(new BlockPos(x, y, z), table);
}
BlockState block(BlockPos pos) { return blocks.getOrDefault(pos, Blocks.AIR.defaultBlockState()); }
}
private static void require(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
}
@@ -0,0 +1,286 @@
package fr.koka.sanctuary.worldgen.city;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Kind;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Parcel;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.StairBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BedPart;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.storage.loot.LootTable;
/** Real block-state geometry, without creating a world: clipping, rotations and walkable floors. */
public final class LostCitySurface21Smoke {
public static void main(String[] args) {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
run();
System.out.println("LostCitySurface21Smoke: all exterior amenities, supported lamps, basements, contained pools, excavation access, carved park variants, grounded canopies, restrained oak palette and suspended tuff roads passed.");
}
/** Also callable from an already bootstrapped native GameTest. */
public static void run() {
var failures = new java.util.ArrayList<String>();
for (Kind kind : Kind.values()) {
if (kind.building()) continue;
for (int facing = 0; facing < 4; facing++) {
for (long seed : new long[] {0, 42, -61872}) {
try {
int floors = kind == Kind.HALL ? 2 : 3;
int foundation = kind == Kind.POOL || kind == Kind.EXCAVATION ? 8 : 4;
Parcel parcel = new Parcel(kind, 0, 0, 17, 17, 170, floors, facing, seed, foundation, new int[0]);
Capture result = render(parcel);
require(result.blocks.equals(render(parcel).blocks), "Renderer changed for identical saved parcel");
require(result.blocks.values().stream().noneMatch(state -> state.is(Blocks.SPAWNER)), "City introduced a spawner");
if (kind != Kind.STREET && kind != Kind.BRIDGE) {
LostCityInteriors21Smoke.verifyPalette(result.blocks, false, kind + " facing=" + facing);
LostCityInteriors21Smoke.verifyGroundedGeometry(result.blocks,
kind == Kind.EXCAVATION && foundation >= 8 ? -5 : kind == Kind.POOL && foundation >= 8 ? -4 : 0,
kind + " facing=" + facing + " seed=" + seed);
}
if (kind.building()) {
require(result.blocks.values().stream().anyMatch(state -> state.is(Blocks.STONE_BRICKS)), "Missing explicit stone palette");
require(result.blocks.values().stream().anyMatch(state -> state.is(Blocks.SMOOTH_STONE)), "Missing smooth stone palette");
Set<BlockPos> reached = walk(result, rotate(8, 1, 0, facing));
for (int floor = 1; floor <= floors; floor++) {
int elevation = floor * 5 + 1;
require(reached.stream().anyMatch(pos -> pos.getY() == elevation),
kind + " facing " + facing + " cannot reach floor/roof " + floor);
}
require(result.loot.size() == 1, kind + " must keep exactly one deferred, limited loot chest");
}
for (var entry : result.blocks.entrySet()) {
if (entry.getValue().is(Blocks.LANTERN)) {
var support = entry.getValue().getValue(BlockStateProperties.HANGING) ? entry.getKey().above() : entry.getKey().below();
require(result.block(support).isCollisionShapeFullBlock(net.minecraft.world.level.EmptyBlockGetter.INSTANCE, support), kind + " lantern lacks full support at " + entry.getKey());
}
BlockState state = entry.getValue();
if (state.is(Blocks.BED.white()) && state.getValue(BlockStateProperties.BED_PART) == BedPart.FOOT) {
Direction direction = state.getValue(BlockStateProperties.HORIZONTAL_FACING);
BlockState head = result.block(entry.getKey().relative(direction));
require(head.is(Blocks.BED.white()) && head.getValue(BlockStateProperties.BED_PART) == BedPart.HEAD,
"Rotated bedroom has an incomplete bed");
}
}
} catch (AssertionError | RuntimeException failure) {
failures.add(kind + " facing=" + facing + " seed=" + seed + ": " + failure.getMessage());
}
}
}
}
for (int facing = 0; facing < 4; facing++) {
try {
Parcel garage = new Parcel(Kind.PARKING, 0, 0, 17, 17, 170, 1, facing, 9, 8, new int[0]);
Capture result = render(garage);
Set<BlockPos> reached = walk(result, rotate(8, 1, 0, facing));
require(reached.contains(rotate(8, -4, 10, facing)), "Underground garage is not reachable from its street ramp");
require(result.block(rotate(8, -6, 10, facing)).is(Blocks.STONE_BRICKS), "Garage excavation removed the certified rock below its floor");
for (int z = 0; z <= 4; z++) for (int x = 6; x <= 10; x++) for (int y = -5; y < -z; y++) {
var below = rotate(x, y, z, facing);
require(result.block(below).isCollisionShapeFullBlock(net.minecraft.world.level.EmptyBlockGetter.INSTANCE, below),
"Garage ramp is suspended above its floor instead of resting on its masonry wedge");
}
LostCityInteriors21Smoke.verifyGroundedGeometry(result.blocks, -5, "Underground garage facing=" + facing);
var threshold = rotate(8, 0, 0, facing);
require(result.block(threshold).isCollisionShapeFullBlock(net.minecraft.world.level.EmptyBlockGetter.INSTANCE, threshold)
&& result.block(threshold.above()).isAir() && result.block(threshold.above(2)).isAir(),
"Garage entry lost its level, clear ramp threshold");
} catch (AssertionError | RuntimeException failure) {
failures.add("Underground garage facing=" + facing + ": " + failure.getMessage());
}
try {
Parcel pit = new Parcel(Kind.EXCAVATION, 0, 0, 17, 17, 170, 0, facing, 9, 8, new int[0]);
Capture excavation = render(pit);
require(walk(excavation, rotate(8, 1, 0, facing)).contains(rotate(8, -4, 10, facing)),
"Excavation has no walkable stairs from the street");
require(excavation.block(rotate(8, -6, 10, facing)).is(Blocks.STONE_BRICKS), "Excavation removed its certified bottom support");
LostCityInteriors21Smoke.verifyGroundedGeometry(excavation.blocks, -5, "Excavation frame facing=" + facing);
for (int x : new int[]{3, 13}) for (int z : new int[]{10, 13}) {
for (int y = -4; y <= 3; y++) require(!excavation.block(rotate(x, y, z, facing)).isAir(),
"Excavation roof lacks an unbroken post to its real floor at " + x + "," + y + "," + z);
require(excavation.block(rotate(x, 4, z, facing)).is(Blocks.OAK_SLAB)
|| excavation.block(rotate(x, 4, z, facing)).is(Blocks.OAK_PLANKS),
"Excavation's only canopy is detached from its post");
}
} catch (AssertionError | RuntimeException failure) {
failures.add("Excavation facing=" + facing + ": " + failure.getMessage());
}
try {
Parcel pool = new Parcel(Kind.POOL, 0, 0, 17, 17, 170, 0, facing, 9, 8, new int[0]);
Capture water = render(pool);
sealedPool(water, facing);
for (int step = 1; step <= 3; step++) for (int x = 7; x <= 9; x++) {
var tread = rotate(x, -step, step + 4, facing);
require(water.block(tread).getBlock() instanceof StairBlock
&& water.block(tread).getValue(BlockStateProperties.WATERLOGGED),
"Pool lost its submerged stair tread");
for (int y = -4; y < -step; y++) {
var below = rotate(x, y, step + 4, facing);
require(water.block(below).isCollisionShapeFullBlock(net.minecraft.world.level.EmptyBlockGetter.INSTANCE, below),
"Pool tread is not carried continuously by its sealed basin floor");
}
}
require(walk(water, rotate(8, 1, 0, facing)).contains(rotate(8, -2, 7, facing)),
"Pool submerged steps cannot be reached from the deck");
} catch (AssertionError | RuntimeException failure) {
failures.add("Pool facing=" + facing + ": " + failure.getMessage());
}
}
// Every public type keeps a usable north approach, and its furniture does not consume the street threshold.
for (Kind kind : Kind.values()) if (!kind.building() && kind != Kind.STREET && kind != Kind.BRIDGE)
for (int facing = 0; facing < 4; facing++) {
try {
Capture c = render(new Parcel(kind, 0, 0, 17, 17, 170, 0, facing, 42, 8, new int[0]));
var reached = walk(c, rotate(8, 1, 0, facing));
require(reached.size() >= 100, kind + " lost its public circulation");
for (var chest : c.loot.keySet()) require(java.util.Arrays.stream(new Direction[]{Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST})
.anyMatch(d -> reached.contains(chest.relative(d))), kind + " loot is not approachable");
} catch (AssertionError | RuntimeException failure) {
failures.add(kind + " public access facing=" + facing + ": " + failure.getMessage());
}
}
require(failures.isEmpty(), "City21 surface fixture failures (" + failures.size() + "):\n" + String.join("\n", failures));
exportPreviews();
Set<Map<BlockPos, BlockState>> parks = new HashSet<>();
for (Kind park : new Kind[] {Kind.PARK, Kind.PARK_GARDEN, Kind.PARK_PLAYGROUND, Kind.PARK_MEMORIAL}) {
parks.add(render(new Parcel(park, 0, 0, 17, 17, 170, 0, 0, 42, 4, new int[0])).blocks);
}
require(parks.size() == 4, "Park variants must have distinct geometry");
for (Kind kind : new Kind[] {Kind.POOL, Kind.EXCAVATION}) {
Capture fallback = render(new Parcel(kind, 0, 0, 17, 17, 170, 0, 0, 42, 4, new int[0]));
require(fallback.blocks.values().stream().noneMatch(state -> !state.getFluidState().isEmpty()),
"Uncertified ground produced a wet excavation");
require(fallback.block(new BlockPos(8, -1, 8)).is(Blocks.STONE_BRICKS), "Uncertified ground was excavated");
}
int[] heights = new int[17 * 5];
for (int z = 0; z < 17; z++) for (int x = 0; x < 5; x++) heights[z * 5 + x] = 170 + z / 4;
Parcel bridge = new Parcel(Kind.BRIDGE, 0, 0, 5, 17, 170, 0, 0, 33, 0, heights);
Capture result = render(bridge);
require(result.blocks.entrySet().stream().noneMatch(entry -> !entry.getValue().isAir()
&& entry.getKey().getY() < bridge.surfaceY(entry.getKey().getX(), entry.getKey().getZ()) - bridge.floorY()),
"Bridge grew a column below its suspended deck");
for (var e : result.blocks.entrySet()) if (e.getValue().is(Blocks.TUFF_BRICK_STAIRS)) {
var face = e.getValue().getValue(BlockStateProperties.HORIZONTAL_FACING);
var high = e.getKey().relative(face);
require(bridge.surfaceY(high.getX(), high.getZ()) - bridge.floorY() == e.getKey().getY(),
"Bridge stair fails to meet the higher deck");
}
Set<BlockPos> reached = walk(result, new BlockPos(2, 1, 0));
require(reached.contains(new BlockPos(2, 5, 16)), "Stepped bridge cannot be crossed");
}
private static void exportPreviews() {
String path = System.getProperty("sanctuary.city21.exteriorPreview");
if (path == null) return;
var examples = new java.util.ArrayList<Object>();
for (Kind kind : Kind.values()) if (!kind.building() && kind != Kind.STREET && kind != Kind.BRIDGE) {
var p = new Parcel(kind, 0, 0, 17, 17, 170, 0, 0, 42, 8, new int[0]); var c = render(p);
var blocks = new java.util.ArrayList<Object>();
c.blocks.forEach((at, state) -> { if (!state.isAir()) blocks.add(java.util.List.of(at.getX(), at.getY(), at.getZ(), state.toString())); });
examples.add(Map.of("kind", kind.name(), "seed", p.seed(), "facing", 0, "blocks", blocks));
}
try { var output = java.nio.file.Path.of(path); if (output.getParent() != null) java.nio.file.Files.createDirectories(output.getParent());
java.nio.file.Files.writeString(output, new com.google.gson.Gson().toJson(examples)); }
catch (java.io.IOException e) { throw new AssertionError("Cannot write requested geometry preview", e); }
}
private static Capture render(Parcel parcel) {
Capture result = new Capture(parcel);
LostCitySurface21.render(parcel, result);
return result;
}
/** Flood the cells a source can reach horizontally/downward, without permitting water to rise. */
private static void sealedPool(Capture result, int facing) {
Set<BlockPos> flooded = new HashSet<>();
ArrayDeque<BlockPos> queue = new ArrayDeque<>();
result.blocks.forEach((pos, state) -> {
if (!state.getFluidState().isEmpty()) { flooded.add(pos); queue.add(pos); }
});
require(flooded.size() > 150, "Pool must contain a real basin of water");
while (!queue.isEmpty()) {
BlockPos pos = queue.removeFirst();
require(pos.getY() >= -3 && pos.getY() <= -1, "Pool leaks vertically through its floor or coping");
BlockPos canonical = rotate(pos.getX(), pos.getY(), pos.getZ(), (4 - facing) % 4);
require(canonical.getX() >= 4 && canonical.getX() <= 12 && canonical.getZ() >= 5 && canonical.getZ() <= 12,
"Pool leaks outside its continuous side walls");
for (Direction direction : new Direction[] {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST, Direction.DOWN}) {
BlockPos next = pos.relative(direction);
BlockState state = result.block(next);
if ((state.isAir() || !state.getFluidState().isEmpty()) && flooded.add(next)) queue.addLast(next);
}
}
}
/** Conservative full-block walking with one-block steps and two blocks of clear headroom. */
private static Set<BlockPos> walk(Capture result, BlockPos start) {
require(standable(result, start), "Entrance is blocked: " + start);
Set<BlockPos> seen = new HashSet<>();
ArrayDeque<BlockPos> queue = new ArrayDeque<>();
queue.add(start);
seen.add(start);
while (!queue.isEmpty()) {
BlockPos current = queue.removeFirst();
for (Direction direction : new Direction[] {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}) {
for (int dy = -1; dy <= 1; dy++) {
BlockPos next = current.relative(direction).offset(0, dy, 0);
if (standable(result, next) && seen.add(next)) queue.addLast(next);
}
}
}
return seen;
}
private static boolean standable(Capture result, BlockPos pos) {
if (pos.getX() < 0 || pos.getZ() < 0 || pos.getX() >= result.parcel.width() || pos.getZ() >= result.parcel.depth()) return false;
BlockState support = result.block(pos.below());
return (support.isSolid() || support.getBlock() instanceof StairBlock)
&& clear(result.block(pos)) && clear(result.block(pos.above()));
}
private static boolean clear(BlockState state) { return state.isAir() || state.is(Blocks.WATER); }
private static BlockPos rotate(int x, int y, int z, int facing) {
return switch (facing) {
case 0 -> new BlockPos(x, y, z);
case 1 -> new BlockPos(16 - z, y, x);
case 2 -> new BlockPos(16 - x, y, 16 - z);
default -> new BlockPos(z, y, 16 - x);
};
}
private static final class Capture implements LostCityRenderer.BlockWriter {
private final Parcel parcel;
private final Map<BlockPos, BlockState> blocks = new HashMap<>();
private final Map<BlockPos, ResourceKey<LootTable>> loot = new HashMap<>();
Capture(Parcel parcel) { this.parcel = parcel; }
@Override public void set(int x, int y, int z, BlockState state) {
require(x >= 0 && z >= 0 && x < parcel.width() && z < parcel.depth()
&& y >= parcel.minY() - parcel.floorY() && y <= parcel.maxY() - parcel.floorY(),
parcel.kind() + " emitted a block outside its saved bounds at " + x + "," + y + "," + z);
blocks.put(new BlockPos(x, y, z), state);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long lootSeed) {
set(x, y, z, Blocks.CHEST.defaultBlockState());
loot.put(new BlockPos(x, y, z), table);
}
BlockState block(BlockPos pos) { return blocks.getOrDefault(pos, Blocks.AIR.defaultBlockState()); }
}
private static void require(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
}
@@ -0,0 +1,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 LostCityUnderground20Smoke {
public static void main(String[] args) {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); run();
System.out.println("LostCityUnderground20Smoke: 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()) LostCityUnderground20.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); }
}

Some files were not shown because too many files have changed in this diff Show More