Compare commits

...
Author SHA1 Message Date
koka a633d4cac9 docs: record verified alpha22 release and Prism update
Build Sanctuary / build (push) Canceled after 0s
2026-09-10 11:54:17 +02:00
koka d6e0c144d0 feat: selective surface placement and independent underground core for alpha22
Build Sanctuary / build (push) Canceled after 0s
2026-09-10 11:52:24 +02:00
koka d462f01e2b docs: record verified alpha21 release and Prism update
Build Sanctuary / build (push) Canceled after 0s
2026-09-10 10:08:01 +02:00
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
134 changed files with 22125 additions and 63 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,
+81 -6
View File
@@ -15,10 +15,85 @@ 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.22 est
publiée** : le placement des structures redevient sélectif. Les palettes,
intérieurs et filons de lalpha.21 sont conservés. Voir [les essais22](docs/testing-alpha22.md)
et [la distribution](docs/packwiz.md).
## Alpha.22 — trouver le bon terrain
La nouvelle génération préfère refuser une ville ou un bâtiment facultatif
qui ne trouve pas un emplacement adapté. Plus de ville forcée sur une réserve
artificielle ; moins de terrassement, des fondations proches de la roche et des
chemins qui suivent le relief. La grande traversée et la salle dexpansion sont
réservées en priorité et peuvent exister sans ville au-dessus. Les autres lieux
nont pas de quota garanti.
Créer un **nouveau monde Sanctuary** pour utiliser ces règles. Les tailles
512/724/1024 et le bouton natif **Personnaliser** restent inchangés ; les anciens
mondes conservent leurs générateurs. La vision retient désormais **Minecraft
comme archéologie dun autre Minecraft** : chaque ruine témoigne dune ancienne
solution, et de ceux qui lont construite puis ont disparu.
Les trois tailles natives, la réouverture, le cas expérimental OFF et la
compatibilité21 passent, ainsi que `check build assemblePack` et ses huit
GameTests généraux. Le [MRpack alpha.22](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.22/Sanctuary-0.1.0-alpha.22.mrpack)
est disponible, le canal packwiz et la même instance Prism sont synchronisés.
Les925 fichiers personnels et réglages suivis sont conservés. Voir
[la 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).
Le [MRpack alpha.21](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.21/Sanctuary-0.1.0-alpha.21.mrpack)
est disponible. Le canal packwiz et la même instance Prism sont synchronisés ;
les 925 fichiers personnels et réglages suivis sont conservés. Les tests de
création/réouverture, le cas OFF et `check build assemblePack` passent.
## 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 +484,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 +513,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).
+120
View File
@@ -717,6 +717,126 @@ 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 : livré en alpha.21 ; MRpack, release et canal packwiz publiés, même instance Prism synchronisée deux fois. Tests, sauvegardes et limites documentés dans `docs/testing-alpha21.md`.**
### WG-25 — Placement sélectif et archéologie — alpha.22
**Branche :** `codex/placement-selectif-alpha22`.
Annule pour les nouveaux mondes lassouplissement de WG-24. Chercher un terrain
vraiment adapté et accepter labsence dune structure plutôt que de forcer son
implantation. La ville ne reçoit plus de réservation artificielle de secours.
Bâtiments et chemins limitent terrassement et remblai. Exception explicite :
la grande traversée et la salle dexpansion sont des éléments de base, à placer
en priorité indépendamment de la ville ; les égouts peuvent être ailleurs.
Les galeries gardent leur génie civil et la salle reçoit un accès autonome.
Les recherches restent bornées.
Conserver les palettes, les intérieurs, les filons et le rendu21. Nouveau codec22
pour les nouveaux mondes, sans migration ni retouche des mondes précédents.
Vérifier ladmission sur les bons terrains, les refus sur les mauvais, la création
native, la réouverture et la compatibilité21. Livrer le MRpack et mettre à jour
le canal et la même instance Prism en conservant les données personnelles.
Inscrire dans la vision « Minecraft comme archéologie dun autre Minecraft » :
chaque ruine garde la trace dune solution ancienne. Les neuf personnages sont
ceux nommés dans la vision ; la découverte et les rôles restent à définir.
Ce cadre narratif ne constitue pas une nouvelle mécanique implémentée.
**État : livré en alpha.22 ; MRpack, release et canal packwiz publiés, même instance Prism synchronisée deux fois.** Voir [les essais22](testing-alpha22.md).
## 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

+162 -1
View File
@@ -13,10 +13,171 @@ 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.22 publiée — 10 septembre 2026
[Sanctuary 0.1.0-alpha.22](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.22)
est publiée depuis le commit source `d6e0c14`. Le canal stable est au commit
`0019178`. Le JAR, le
[MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.22/Sanctuary-0.1.0-alpha.22.mrpack)
et le ZIP damorçage Prism ont été vérifiés par téléchargement public.
SHA-256 du JAR :
```text
10d7ef32ac2e81a977ea4854503bafa29e0bc10929984b76df01d6f5f61f9056
```
Les bâtiments de surface recherchent des terrains adaptés ; la grande traversée
et la salle dexpansion restent des éléments de base, indépendants dune ville.
Les galeries peuvent chercher un détour et de nouveaux accès. Le hall peut
accueillir des égouts ailleurs dans l’île. Les trois tailles natives, le cas OFF,
la réouverture et la comparaison21 passent. `check build assemblePack` passe
avec75 tâches et les huit GameTests généraux. Voir [les essais22](testing-alpha22.md).
Deux synchronisations isolées puis deux dans la même instance Prism réussissent.
Un seul JAR Sanctuary alpha.22 est actif ; les **925 fichiers personnels et
réglages suivis** gardent leurs hashes. Le dossier reste
`Sanctuary-0.1.0-alpha.1`, affiché « Sanctuary Beta ». Le JAR21 et le suivi
packwiz sont sauvegardés sous `sanctuary-backups/before-0.1.0-alpha.22/`.
Reçus locaux : `build/alpha22-isolated-validation.json` et
`build/alpha22-prism-validation.json`.
Pour tester : **nouveau monde Sanctuary, Moyen, graine0**. Les portiques de la
traversée sont `-170 253 -98` et `170 254 -98` ; la trappe du hall indépendant
est `2 252 0`. `/sanctuary structures` liste les lieux. Les villes et bâtiments
facultatifs peuvent être refusés. Les graines vérifiées ne prouvent pas la
présence des infrastructures sur toutes les graines ; les recherches restent
bornées. Aucun monde personnel na été ouvert, converti ou régénéré. Aucun
essai client visuel ni essai Windows natif nest revendiqué.
## Alpha.21 publiée — 10 septembre 2026
[Sanctuary 0.1.0-alpha.21](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.21)
est publiée depuis le commit source `bb63fab`. Le canal stable utilisait cette version au commit
`8002fce`. Le JAR, le
[MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.21/Sanctuary-0.1.0-alpha.21.mrpack)
et le ZIP damorçage Prism ont été vérifiés. SHA-256 du JAR :
```text
e528e42d99f9e1ea221f780ea089b49b42a1a4e9e24e4ddd001d045183202c21
```
Palettes sobres, toitures portées, quartiers reliés, placements patrimoniaux
assouplis et premières galeries industrielles avec fronts rocheux et filons.
Voir le [catalogue](architecture-alpha21.md) et les [essais](testing-alpha21.md).
`check build assemblePack`, les huit GameTests généraux, les créations et
réouvertures natives passent. Le cas expérimental OFF passe. Les fichiers
historiques et les témoins natifs de la génération20 sont conservés.
Deux synchronisations isolées puis deux dans la même instance Prism réussissent.
Un seul JAR Sanctuary alpha.21 est actif ; les **925 fichiers personnels et
réglages suivis** gardent leurs hashes. Le dossier reste
`Sanctuary-0.1.0-alpha.1`, affiché « Sanctuary Beta ». Le JAR20 et le suivi
packwiz sont sauvegardés sous `sanctuary-backups/before-0.1.0-alpha.21/`.
Reçus locaux : `build/alpha21-isolated-validation.json` et
`build/alpha21-prism-validation.json`.
Pour tester : **nouveau monde Sanctuary, Moyen, graine0**, **Structures
expérimentales** activées dans **Personnaliser**. Entrées des galeries :
`-135 253 -178` et `-135 249 162`. `/sanctuary structures` liste les lieux.
Les raccords dépendent du relief ; certains sites restent isolés et les réseaux
souterrains ne sont pas tous reliés. Aucun monde personnel na été ouvert,
converti ou régénéré. Aucun essai Windows natif ni essai visuel client nest
revendiqué. Une sauvegarde21 nécessite le binaire21 ou un futur binaire compatible.
## 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 utilisait 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 :
+27
View File
@@ -25,6 +25,33 @@ Sources locales : [planificateur17](../mods/sanctuary/src/main/java/fr/koka/sanc
[rendu17](../mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/ruins/RuinsRenderer17.java),
[preuves alpha.17](testing-alpha17.md), [vision](vision.md).
## Ligne de conception retenue après cet audit
**Minecraft comme archéologie dun autre Minecraft.** Steve, Ari, Sunny, Kai,
Zuri, Alex, Mefe, Makena, Noor sont les personnages disparus de la partie. Ils
ont découvert quelque chose de révolutionnaire et ont continué parce que ça
marchait, jusquau moment où ça na plus marché.
> Ils nont pas ouvert quelque chose quils nauraient pas dû ouvrir. Ils ont
> construit quelque chose quils ne savaient plus refermer.
Chaque ruine doit conserver la trace dune ancienne solution : une infrastructure
dont lauteur a progressivement disparu derrière son fonctionnement. Ce canon
ne définit pas la découverte, les rôles individuels ou une catastrophe confirmée.
Il guide les prochaines structures ; il najoute aucun personnage, récit jouable
ou système de progression aux versions déjà livrées. L’état alpha.17 décrit
ci-dessus reste le constat historique de cet audit.
Pour valider la conception dun lieu, relever concrètement :
- **Un usage ancien lisible** dans les pièces, les outils et les circulations.
- **Une implantation fonctionnelle** expliquée par le terrain, une ressource
ou une destination réellement présente.
- **Des traces dentretien, dextension ou de chantier** qui montrent comment
linstallation a été utilisée et adaptée.
- **Un lien futur à la progression**, explicitement décrit comme une intention
tant que son système et ses interactions ne sont pas implémentés.
## Faire comprendre les usages avant dintroduire leurs systèmes
Chaque lieu devrait répondre à trois questions : pourquoi était-il construit
+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é.
+140
View File
@@ -0,0 +1,140 @@
# 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 release et le canal
packwiz sont publiés. Deux synchronisations isolées puis deux dans la même
instance Prism conservent les 925 fichiers personnels et réglages suivis.
Voir le [reçu de distribution](packwiz.md#alpha21-publiée--10-septembre-2026).
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
```
+153
View File
@@ -0,0 +1,153 @@
# Alpha.22 — placement sélectif
Ticket WG-25, branche `codex/placement-selectif-alpha22`.
Minecraft 26.3-pre-2, Fabric Loader 0.19.5, Fabric API 0.160.0+26.3 et Java25,
sans changement de dépendances.
## Contrat
Les bâtiments de surface redeviennent sélectifs : une ville peut être absente
si aucun emplacement nest adéquat. Deux éléments de base sont prioritaires :
**la grande traversée souterraine et la salle dexpansion**, même sans ville.
Leurs emplacements sont réservés avant les petits réseaux et le catalogue
expérimental. Une recherche bornée ne prouve pas quaucun autre emplacement
possible nexiste dans toute l’île.
- Ville : aucune réserve artificielle de secours, aucun pilier de rattrapage,
terrassement/remblai limité à deux blocs par colonne. Les fondations sont
entièrement recertifiées dans la roche. Les quartiers supplémentaires exigent
un chemin complet vers les quartiers retenus. Au plus trois propositions dans
un budget partagé de20 000 colonnes et1 120 000 contrôles de volume.
- Infrastructures : relief maximal de quatre blocs sur lemprise17×17, coupe et
remblai limités à deux blocs, deux couches de roche intacte sous chaque colonne
de fondation, sans piliers de secours. Jusqu’à24 candidats par famille, avec
les mêmes plafonds globaux de16 000 colonnes et800 000 sondages.
- Chemins patrimoniaux : coupe/remblai limité à un bloc, accès indépendant et
raccord complet requis pour publier une liaison.
- Traversée : conserve le génie civil21, avec deux accès réels, une chaussée
de cinq blocs et deux trottoirs. Les piliers sont bornés à16 blocs ; les
travées sans grille complète sont limitées à34 blocs et reposent sur les
modules adjacents. La recherche22 intervient avant les structures facultatives
pour leur réserver lemprise. Si les lignes droites sont refusées, une seconde
passe cherche des accès différents et des détours. Chaque passe est limitée à
12 000 colonnes et3 millions de sondages (24 000 et6 millions au total au plus).
Ses parois peuvent traverser des cavités existantes sans écraser les volumes
deau et de lave protégés.
- Salle : recherche autonome dun accès de surface et dune salle25×25, sans
faux quartier ni socle de ville. Une salle déjà présente sous une ville évite
un doublon. Les égouts et annexes peuvent accompagner ce lieu indépendant.
Les palettes, les plans intérieurs et les petits filons minables restent ceux
de21. Les anciens petits réseaux expérimentaux17 et les sous-sols des villes gardent
leur géométrie ; leurs protections restent respectées.
Le nouveau principe narratif est documenté dans [la vision](vision.md) et
[laudit](structures-audit.md), sans activer de système de progression.
## Sauvegardes
Le seul preset public utilise désormais `sanctuary:island_v22`. Le preset caché
`sanctuary_v21` est la copie de lancien preset public. Les anciens codecs,
planificateurs et rendus restent disponibles. Les pièces21 sont réutilisées
avec leur format existant ; les nouveaux coudes possèdent une pièce22 distincte
pour fermer leurs murs sans modifier les jonctions21 sauvegardées.
La salle indépendante possède un type de structure22
et réutilise les pièces souterraines20.
Le journal identifie la génération22 et refuse une lecture croisée avec21.
Aucun monde personnel nest ouvert, converti ou régénéré.
Les nouvelles règles nécessitent un **nouveau monde Sanctuary**. Petit512,
Moyen724 par défaut et Grand1024 restent proposés. Loption **Structures
expérimentales** reste enregistrée à la création ; en22 elle retire le catalogue
facultatif, tandis que la traversée et la salle restent des éléments de base.
Le commutateur Minecraft général des structures demeure respecté.
## Témoin natif Moyen, graine0
La création et la réouverture dans deux JVM distinctes passent. La ville est
refusée ; la traversée comporte23 modules, dont deux accès et une salle de
maintenance. Son axe s’étend sur340 blocs. Les accès sont `-170 253 -100` et
`170 254 -100`. Le hall dexpansion indépendant se rejoint par la trappe
`2 252 0` ; son réseau comprend laccès, la salle, deux passages et un égout.
La piscine est une annexe conditionnelle, absente sur ce témoin.
Les14 bâtiments patrimoniaux trouvent un terrain adapté ; sept liaisons sont
retenues. Les115 chunks de lemprise sont générés en statut FULL. Les plans,
pièces natives et une modification témoin persistent après réouverture. Le
contrôle natif de circulation vérifie la vraie trappe, les échelles soutenues,
les ouvertures et le trajet jusquau socle central. Aucun déplacement de joueur
ou chargement dun monde personnel na été utilisé pour ces essais.
## Vérifications complémentaires
**Petit512, graine42 :**17 modules, dont un coude sauvegardé avec sa pièce22,
et une salle indépendante. Les anciens alignements droits sont refusés ; le
repli trouve une paire daccès naturels et conserve une chaussée continue.
Les portiques sont `-68 250 -93` et `68 250 43`, la trappe du hall `43 244 -10`.
La marche native relie les sorties en564 déplacements et atteint53 points de
contrôle. Huit bâtiments patrimoniaux et six liaisons sont admis ; aucune ville
nest forcée. Les88 chunks de lemprise sont vérifiés en statut FULL.
**Grand1024, graine42 :** un quartier naturel est admis avec son réseau de
sous-sols : salle dexpansion, piscine, égout et parking. Aucun hall autonome
supplémentaire nest créé. La traversée comporte31 modules et s’étend sur476
blocs ; portiques `-272 249 -8` et `204 250 -8`. La marche native les relie
en740 déplacements et atteint98 points de contrôle. Quatorze bâtiments
patrimoniaux et huit liaisons sont admis. Les179 chunks de lemprise sont
vérifiés en statut FULL ; la salle et ses17 ouvertures sont accessibles.
Les tests purs des trois tailles passent sur des terrains plats adaptés ;
falaises, eau/protections, vides et appuis insuffisants sont refusés pour les
bâtiments. Le hall autonome et ses annexes sont vérifiés sans fixture de ville.
Les suites historiques restent actives. Avec loption expérimentale désactivée,
le témoin Moyen conserve ses23 modules de traversée et sa salle indépendante,
sans bâtiments patrimoniaux ni petits réseaux expérimentaux. La marche native
relie les deux sorties en548 déplacements et atteint74 points de contrôle ;
les échelles et laccès au hall restent praticables.
Les créations et réouvertures de génération21 sont identiques aux rapports de
la release21, hors identifiants de processus et dossier de test. Les391 fichiers
de production historiques qui ne nécessitent pas de raccord22 restent identiques
octet pour octet ; le preset caché21 est la copie exacte du précédent preset
public. Les masques sont vérifiés à lintérieur et hors des structures.
`check build assemblePack` passe :75 tâches, dont les huit GameTests généraux.
Le JAR, lindex packwiz, le MRpack et le ZIP Prism sont vérifiés. Aucun code de
test nest embarqué dans le JAR. SHA-256 du JAR :
```text
10d7ef32ac2e81a977ea4854503bafa29e0bc10929984b76df01d6f5f61f9056
```
SHA-256 du MRpack :
```text
0b553fcdd8b6a2ec26f72059e428d2cc0ce526f36ef8580dd3e50c1dfa40ec78
```
Les reçus de publication et de synchronisation sont suivis dans
[la distribution](packwiz.md).
Ces témoins vérifient les trois tailles, avec et sans ville ; ils ne constituent
pas une preuve de présence pour toutes les graines. La recherche reste bornée
et refuse un volume protégé ou insuffisamment porté. Aucun essai visuel client
ou essai Windows natif nest revendiqué.
## Reproduire dans le dossier de développement
Avec Java25, création du témoin Moyen puis réouverture du même monde de test :
```sh
./gradlew :sanctuary:runGameTest -PsanctuarySelective22Tests=true -PsanctuaryDensityMaps=false
./gradlew :sanctuary:runGameTest -PsanctuarySelective22Tests=true -PsanctuaryDensityMaps=false -PsanctuaryExpansionReload=true
```
Pour une nouvelle création Petit ou Grand, ajouter `-PsanctuaryTestSeed=42`
et `-PsanctuaryTestDiameter=512` ou `1024`. Le cas OFF utilise
`-PsanctuaryExperimentalStructures=false`. Chaque réouverture doit suivre la
création correspondante, avec les mêmes paramètres. Les rapports sont dans
`mods/sanctuary/build/run/gameTest/diagnostics/selective22-<graine>-<diamètre>/`.
Le témoin historique utilise `-PsanctuaryHeritage21Tests=true`, puis le même
drapeau avec `-PsanctuaryExpansionReload=true`. La vérification de livraison
reste `./gradlew check build assemblePack`. Exécuter ces JVM séquentiellement.
+21
View File
@@ -22,6 +22,27 @@ L'objectif mythologique est de progresser d'une condition très limitée, proche
- **Humour, surprise et souvenirs.** Les noms d'entités, les événements, les animaux rares et les objets insolites comptent autant que les systèmes économiques.
- **Un développement par tickets.** Livrer une petite mécanique jouable et vérifiable, puis l'ajuster avec des tickets de fonctionnalités et de bugs. Ne pas construire tous les systèmes avant de pouvoir jouer.
## Minecraft comme archéologie dun autre Minecraft
**Canon et ligne de conception retenus.** Steve, Ari, Sunny, Kai, Zuri, Alex,
Mefe, Makena, Noor sont les personnages disparus de la partie. Ils ont découvert
quelque chose de révolutionnaire et ont continué parce que ça marchait. Jusquau
moment où ça na plus marché.
> Ils nont pas ouvert quelque chose quils nauraient pas dû ouvrir. Ils ont
> construit quelque chose quils ne savaient plus refermer.
Chaque ruine doit être une trace dune ancienne solution, pas seulement la trace
dun problème : une infrastructure dont lauteur a progressivement disparu
derrière son fonctionnement. Son ancien usage, ses circulations, ses réparations,
ses extensions et ses chantiers doivent permettre de comprendre ce qui avait
été résolu ici, même quand linstallation est abandonnée.
La nature de la découverte et les rôles individuels restent à définir. Aucune
catastrophe précise nest confirmée par ce principe. **La mise en scène de cette
histoire et ses liens avec la progression restent à implémenter** ; les versions
livrées et leurs tests établissent ce que chaque structure contient effectivement.
## Périmètre de départ
Le premier incrément porte sur le socle Fabric et la génération du monde : retrouver le générateur 26.2, produire une île principale dans un vrai vide, assurer une arrivée commune et sûre, puis poser une manière reproductible de générer des continents à une distance, une direction et une taille choisies.
+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.22
pack_version=0.1.0-alpha.22
maven_group=fr.koka.sanctuary
+117 -2
View File
@@ -40,13 +40,21 @@ 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' : (providers.gradleProperty('sanctuaryHeritage21Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryLostCity21Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryTransit21Tests').map { it.toBoolean() }.getOrElse(false)) ? '21' : '22'))
systemProperty('sanctuary.test.selective22', providers.gradleProperty('sanctuarySelective22Tests').getOrElse('false'))
systemProperty('sanctuary.transit.plannerDiagnostics', providers.gradleProperty('sanctuaryTransitDiagnostics').getOrElse('false'))
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 +77,24 @@ 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 selective22 = providers.gradleProperty('sanctuarySelective22Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('selective22', selective22)
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: selective22 ? 'fr.koka.sanctuary.gametest.Selective22WorldGameTests' : 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 +437,96 @@ 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)
}
// Generation22 filters surface placement and adds an independent core hall; historical tests still run.
[
lostCity22PlannerSmoke: 'city.LostCityPlanner22Smoke',
underground22Smoke: 'city.LostCityUnderground22Smoke',
heritage22PlannerSmoke: 'heritage.HeritagePlanner22Smoke',
transit22PlannerSmoke: 'transit.TransitPlanner22Smoke',
transit22RouteSmoke: 'transit.TransitRoute22Smoke',
transit22RendererSmoke: 'transit.TransitRenderer22Smoke'
].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) }
}
@@ -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() == 22 || (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,156 @@
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.expansion.ExpansionIsland;
import fr.koka.sanctuary.worldgen.PopulationGenerationContext;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import fr.koka.sanctuary.worldgen.city.*;
import fr.koka.sanctuary.worldgen.heritage.*;
import fr.koka.sanctuary.worldgen.transit.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.BlockPos;
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.world.level.ChunkPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.LevelResource;
/** Read-only admission audit, then native starts and reload in the disposable development world. */
public final class Selective22WorldGameTests {
@GameTest(maxTicks = 12000)
public void naturalSitesAndRefusalsPersist(GameTestHelper helper) throws Exception {
var level = helper.getLevel();
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
helper.assertTrue(generator.rootGeneration() == 22, "Use a fresh22 development world");
var root = ExpansionIsland.origin(generator.initialDiameter(), level.getSeed(), 22);
var city = generator.cityPlacement21(root);
var heritage = generator.heritagePlan19();
var transit = generator.transitPlan21();
helper.assertTrue(city != null && city.reservation() == null && city.supports().isEmpty(), "No forced city envelope or rescue piers");
var binding = Objects.requireNonNull(PopulationGenerationContext.of(level));
var terrain = new HeritageTerrain21(root, binding.generator(), binding.random(), level, p -> false);
int columns = 0;
for (var district : city.surface().districts()) for (var parcel : district.parcels()) {
for (int x = parcel.minX(); x <= parcel.maxX(); x++) for (int z = parcel.minZ(); z <= parcel.maxZ(); z++) {
int floor = parcel.surfaceY(x - parcel.minX(), z - parcel.minZ());
helper.assertTrue(Math.abs(floor - terrain.surfaceY(x, z)) <= 2, "City must fit natural relief"); columns++;
}
}
// The three samplers have independent bounded caches, just like production planning.
terrain = new HeritageTerrain21(root, binding.generator(), binding.random(), level, p -> false);
for (var site : heritage.sites()) {
helper.assertTrue(site.supports().isEmpty(), "No heritage rescue piers");
for (int x = site.minX(); x <= site.maxX(); x++) for (int z = site.minZ(); z <= site.maxZ(); z++) {
helper.assertTrue(Math.abs(site.floorY() - terrain.surfaceY(x, z)) <= 2, "Building cut/fill stays within two blocks");
helper.assertTrue(terrain.solid(x, site.minY() - 1, z) && terrain.solid(x, site.minY() - 2, z), "Every foundation column rests on original rock"); columns++;
}
}
helper.assertTrue(!transit.isEmpty(), "The reference island retains its core traversal");
var hall = generator.undergroundHall22();
helper.assertTrue(!hall.isEmpty() || generator.undergroundPlan16(root).networks().stream().flatMap(n -> n.modules().stream())
.anyMatch(m -> m.kind() == LostCityUnderground16.Kind.EXPANSION_HALL), "Core hall exists with or without a surface town");
for (var module : transit.modules()) for (var support : module.supports())
helper.assertTrue(support.maxY() - support.minY() + 1 <= 16, "Core traversal retains bounded engineered supports");
if (!generator.experimentalStructures()) helper.assertTrue(heritage.isEmpty() && generator.ruinsPlan17().isEmpty(), "OFF removes optional catalogue, not core hall and traversal");
var gson = new GsonBuilder().setPrettyPrinting().create();
var encoded = ChunkGenerator.CODEC.encodeStart(RegistryOps.create(JsonOps.INSTANCE, level.registryAccess()), generator).getOrThrow();
var geometry = gson.toJsonTree(Map.of("city", city.surface().districts(), "heritage_sites", heritage.sites(),
"heritage_links", heritage.links(), "heritage_segments", heritage.segments(), "transit", transit.modules(), "independent_hall", hall.networks()));
var report = new LinkedHashMap<String, Object>();
report.put("seed", level.getSeed()); report.put("size", generator.initialDiameter());
report.put("process_id", ProcessHandle.current().pid()); report.put("generator_codec", encoded); report.put("geometry", geometry);
report.put("experimental", generator.experimentalStructures()); report.put("city_districts", city.surface().districts().size());
report.put("heritage_sites", heritage.sites().size()); report.put("heritage_links", heritage.links().size()); report.put("transit_modules", transit.modules().size());
report.put("independent_hall_networks", hall.networks().size());
report.put("core_entries", transit.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.ACCESS).map(m -> List.of(m.centerX(), m.surfaceY() + 1, m.minZ() - 2)).toList());
report.put("hall_entries", hall.networks().stream().map(n -> { var at = LostCityUndergroundStructures22.locator(n); return List.of(at.getX(), at.getY(), at.getZ()); }).toList());
report.put("natural_columns_audited", columns); report.put("city_diagnostics", city.diagnostics());
report.put("heritage_diagnostics", heritage.diagnostics()); report.put("transit_diagnostics", transit.diagnostic());
var starts = new ArrayList<StructureStart>();
var origins = new LinkedHashMap<ResourceKey<Structure>, Set<ChunkPos>>();
origins.put(LostCityStructures.KEY, new HashSet<>()); origins.put(HeritageStructures19.KEY, new HashSet<>()); origins.put(TransitStructures21.KEY, new HashSet<>()); origins.put(LostCityUndergroundStructures22.KEY, new HashSet<>());
city.surface().districts().forEach(d -> origins.get(LostCityStructures.KEY).add(new ChunkPos(d.startChunkX(), d.startChunkZ())));
heritage.sites().forEach(s -> origins.get(HeritageStructures19.KEY).add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
heritage.segments().forEach(s -> origins.get(HeritageStructures19.KEY).add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
transit.modules().forEach(m -> origins.get(TransitStructures21.KEY).add(new ChunkPos(m.startChunkX(), m.startChunkZ())));
hall.networks().forEach(n -> origins.get(LostCityUndergroundStructures22.KEY).add(new ChunkPos(n.startChunkX(), n.startChunkZ())));
var chunks = new TreeSet<ChunkPos>(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z));
var nativeTags = new TreeMap<String, String>();
var context = StructurePieceSerializationContext.fromLevel(level);
for (var entry : origins.entrySet()) for (var origin : entry.getValue()) {
var type = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(entry.getKey()).value();
var start = level.getChunk(origin.x(), origin.z(), ChunkStatus.STRUCTURE_STARTS, true).getStartForStructure(type);
helper.assertTrue(start != null && start.isValid(), "Accepted geometry must have a native start");
for (var piece : start.getPieces()) {
helper.assertTrue(!(piece instanceof LostCityFoundationPiece16), "No artificial city foundation emitted");
if (entry.getKey().equals(HeritageStructures19.KEY)) helper.assertTrue(piece instanceof HeritagePiece21, "Use published21 architecture");
if (entry.getKey().equals(LostCityUndergroundStructures22.KEY)) helper.assertTrue(piece instanceof LostCityUndergroundPiece20, "Independent hall keeps published20 room geometry");
if (entry.getKey().equals(TransitStructures21.KEY)) helper.assertTrue(piece instanceof TransitPiece21 || piece instanceof TransitPiece22, "Published21 tunnel geometry with sealed22 corners");
piece.getBoundingBox().intersectingChunks().forEach(chunks::add);
}
var tag = start.createTag(context, origin);
var loaded = StructureStart.loadStaticStart(context, tag, level.getSeed());
helper.assertTrue(loaded != null && tag.equals(loaded.createTag(context, origin)), "Native start round trip");
nativeTags.put(entry.getKey().identifier() + "/" + origin.x() + "/" + origin.z(), tag.toString()); starts.add(start);
}
if (chunks.isEmpty()) chunks.add(new ChunkPos(0, 0));
report.put("native_starts", nativeTags); report.put("full_chunks", chunks.size());
Path world = level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize();
Path proof = world.resolve("selective22-create.json");
boolean reload = Boolean.getBoolean("sanctuary.test.expansionReload");
JsonObject previous = reload ? JsonParser.parseString(Files.readString(proof)).getAsJsonObject() : null;
if (reload) {
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload follows a successful separate JVM");
helper.assertTrue(previous.get("generator_codec").equals(encoded) && previous.get("geometry").equals(geometry), "Generation, options and accepted/refused placements survive reload");
helper.assertTrue(previous.get("native_starts").equals(gson.toJsonTree(nativeTags)), "Native saved starts survive reload");
}
var iterator = chunks.iterator();
Runnable finish = () -> {
try {
// Refresh already-generated transit chunks: the earlier short FULL tickets can expire
// while the catalogue loads. The walk checks real native collision shapes, including turns.
var transitChunks = new HashSet<ChunkPos>();
transit.modules().forEach(m -> TransitPiece21.bounds(m).intersectingChunks().forEach(transitChunks::add));
for (var pos : transitChunks) level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true);
report.put("transit_native_walk", Transit21WorldGameTests.verifyAccess(helper, transit));
report.put("hall_native_access", Underground22NativeChecks.verify(helper, hall.isEmpty() ? generator.undergroundPlan16(root) : hall));
if (!heritage.sites().isEmpty()) {
var site = heritage.sites().getFirst();
var witness = new BlockPos(site.centerX(), site.floorY() - 1, site.centerZ());
if (reload) helper.assertTrue(level.getBlockState(witness).is(Blocks.GOLD_BLOCK), "Personal edit in disposable test survives reload");
else {
helper.assertTrue(level.getBlockState(witness).isSolid(), "Accepted building foundation generated in real FULL chunks");
level.setBlock(witness, Blocks.GOLD_BLOCK.defaultBlockState(), 3);
}
report.put("saved_edit", List.of(witness.getX(), witness.getY(), witness.getZ()));
}
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush native world before shutdown");
report.put("passed", true);
var directory = Path.of("diagnostics", "selective22-" + level.getSeed() + "-" + generator.initialDiameter()); Files.createDirectories(directory);
var text = gson.toJson(report) + "\n"; Files.writeString(directory.resolve(reload ? "reload.json" : "create.json"), text);
if (!reload) Files.writeString(proof, text);
helper.succeed();
} catch (Exception failure) { throw new IllegalStateException(failure); }
};
Runnable[] load = new Runnable[1];
load[0] = () -> {
if (!iterator.hasNext()) { finish.run(); return; }
var pos = iterator.next(); level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true);
helper.runAtTickTime(helper.getTick() + 1, () -> load[0].run());
};
load[0].run();
}
}
@@ -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;
}
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); }
}
}
@@ -0,0 +1,126 @@
package fr.koka.sanctuary.gametest;
import fr.koka.sanctuary.worldgen.city.LostCityUnderground16;
import fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Kind;
import fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Module;
import java.util.ArrayDeque;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.server.level.ServerLevel;
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.chunk.LevelChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
/** Native circulation proof on the already generated piece footprint. */
public final class Underground22NativeChecks {
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
private Underground22NativeChecks() {}
public static Map<String, Integer> verify(GameTestHelper helper, LostCityUnderground16.Plan plan) {
var level = helper.getLevel(); var chunks = new HashSet<Long>();
int halls = 0, ladders = 0, apertures = 0, walked = 0;
helper.assertTrue(!plan.isEmpty(), "Independent underground hall plan must be present");
for (var network : plan.networks()) {
for (var m : network.modules()) {
for (int cx = m.minX() >> 4; cx <= m.maxX() >> 4; cx++) for (int cz = m.minZ() >> 4; cz <= m.maxZ() >> 4; cz++) {
long key = ((long) cx << 32) ^ (cz & 0xffffffffL);
// Short-lived read tickets can expire during the callers asynchronous footprint load.
// Refresh only these already generated chunks before inspecting their blocks.
if (chunks.add(key)) helper.assertTrue(level.getChunk(cx, cz, ChunkStatus.FULL, true) instanceof LevelChunk,
"Underground22 piece has no loaded FULL chunk at " + cx + "," + cz);
}
for (int d = 0; d < 4; d++) if ((m.openings() & 1 << d) != 0) {
for (int side = -1; side <= 1; side++) for (int dy = 1; dy <= 3; dy++) {
var at = mouth(m, d, side, dy);
helper.assertTrue(passable(level.getBlockState(at)), "Native utility doorway blocked at " + at + " in " + m.kind());
}
apertures++;
}
}
var access = network.modules().stream().filter(m -> m.kind() == Kind.ACCESS).findFirst().orElseThrow();
BlockPos hatch = null;
for (int x = access.minX(); x <= access.maxX(); x++) for (int z = access.minZ(); z <= access.maxZ(); z++) {
var at = new BlockPos(x, access.maxY() - 1, z);
if (level.getBlockState(at).is(Blocks.DARK_OAK_TRAPDOOR)) {
helper.assertTrue(hatch == null, "The access has more than one surface hatch"); hatch = at;
}
}
helper.assertTrue(hatch != null, "Independent access has no real wooden surface hatch");
final BlockPos entrance = hatch;
var closed = level.getBlockState(entrance);
for (int y = access.floorY() + 1; y < entrance.getY(); y++) {
var at = new BlockPos(entrance.getX(), y, entrance.getZ()); var ladder = level.getBlockState(at);
helper.assertTrue(ladder.is(Blocks.LADDER), "Access ladder is interrupted at " + at);
var wall = at.relative(ladder.getValue(BlockStateProperties.HORIZONTAL_FACING).getOpposite());
helper.assertTrue(level.getBlockState(wall).isCollisionShapeFullBlock(level, wall), "Ladder has no real backing wall at " + at);
ladders++;
}
level.setBlock(entrance, closed.setValue(BlockStateProperties.OPEN, true), 2);
try {
Predicate<BlockPos> inside = pos -> network.modules().stream().anyMatch(m -> m.contains(pos.getX(), pos.getY(), pos.getZ()));
BlockPos approach = java.util.Arrays.stream(HORIZONTAL).map(entrance::relative)
.filter(inside).filter(pos -> standable(level, pos)).findFirst().orElseThrow();
var reached = walk(helper, approach, inside); walked += reached.size();
helper.assertTrue(reached.contains(entrance), "Public deck cannot reach its hatch");
for (var m : network.modules()) {
helper.assertTrue(reached.stream().anyMatch(pos -> pos.getY() == m.floorY() + 1 && pos.getX() > m.minX()
&& pos.getX() < m.maxX() && pos.getZ() > m.minZ() && pos.getZ() < m.maxZ()),
"Surface access cannot reach emitted utility module " + m.kind() + " at " + m.centerX() + "," + m.centerZ());
if (m.kind() != Kind.EXPANSION_HALL) continue;
halls++;
var plinth = new BlockPos(m.centerX(), m.minY() + 2, m.centerZ());
helper.assertTrue(level.getBlockState(plinth).is(Blocks.SMOOTH_STONE) && reached.contains(plinth.above()),
"Reserved expansion plinth must be emitted and reachable");
for (int dx : new int[]{-6, 6}) for (int dz : new int[]{-6, 6})
helper.assertTrue(reached.contains(new BlockPos(m.centerX() + dx, m.floorY() + 1, m.centerZ() + dz)),
"Expansion hall has a blocked quarter");
}
} finally { level.setBlock(entrance, closed, 2); }
}
helper.assertTrue(halls > 0, "Native networks have no usable expansion hall");
var report = new LinkedHashMap<String, Integer>(); report.put("full_chunks", chunks.size()); report.put("usable_halls", halls);
report.put("ladder_blocks", ladders); report.put("clear_openings", apertures); report.put("reached_positions", walked);
return Map.copyOf(report);
}
private static BlockPos mouth(Module m, int direction, int across, int dy) {
return new BlockPos(direction == 1 ? m.maxX() : direction == 3 ? m.minX() : m.centerX() + across,
m.floorY() + dy, direction == 0 ? m.minZ() : direction == 2 ? m.maxZ() : m.centerZ() + across);
}
/** Same finite walking/ladder rule used by the native city16/20/21 access proofs. */
private static Set<BlockPos> walk(GameTestHelper helper, BlockPos start, Predicate<BlockPos> inside) {
var level = helper.getLevel(); var reached = new HashSet<BlockPos>(); var queue = new ArrayDeque<BlockPos>();
helper.assertTrue(inside.test(start) && standable(level, start), "Blocked utility approach"); reached.add(start); queue.add(start);
while (!queue.isEmpty()) {
var pos = queue.remove();
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 && !passable(level.getBlockState(pos.above(2)))) continue;
if (standable(level, next) && reached.add(next)) queue.add(next);
}
for (int dy : new int[]{-1, 1}) {
var next = pos.offset(0, dy, 0);
if (inside.test(next) && (level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(next).is(Blocks.LADDER)
|| level.getBlockState(pos.below()).is(Blocks.LADDER)) && standable(level, next) && reached.add(next)) queue.add(next);
}
helper.assertTrue(reached.size() < 100_000, "Utility traversal exceeded its bounded footprint");
}
return reached;
}
private static boolean standable(ServerLevel level, BlockPos pos) {
var below = level.getBlockState(pos.below()); var at = level.getBlockState(pos);
return passable(at) && passable(level.getBlockState(pos.above()))
&& (below.isSolid() || below.getBlock() instanceof StairBlock || at.is(Blocks.LADDER) || below.is(Blocks.LADDER));
}
private static boolean passable(BlockState state) {
return state.isAir() || state.is(Blocks.LADDER) || state.is(Blocks.REDSTONE_WALL_TORCH)
|| state.is(Blocks.DARK_OAK_TRAPDOOR) && state.getValue(BlockStateProperties.OPEN);
}
}
@@ -45,9 +45,22 @@ 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");
var historical21 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v21")));
helper.assertTrue(historical21.value().createWorldDimensions().overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 21
&& old.source().generation() == 21 && old.experimentalStructures(), "Historical21 keeps its original placement rules");
helper.assertTrue(generator.rootGeneration() == 22 && generator.source().generation() == 22 && generator.experimentalStructures(), "The public preset uses generation22 with its new-world option");
helper.assertTrue(generator.initialDiameter() == 724, "The public preset defaults to Medium / 724 blocks");
verifyConstructionMasks(helper, (SanctuaryChunkGenerator) historical21.value().createWorldDimensions().overworld(), generator);
for (var tag : List.of(WorldPresetTags.NORMAL, WorldPresetTags.EXTENDED)) {
var visible = presets.getOrThrow(tag).stream().map(holder -> holder.getRegisteredName())
.filter(name -> name.startsWith("sanctuary:")).collect(Collectors.toSet());
@@ -56,11 +69,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() == 22 ? SanctuaryChunkGenerator.SELECTIVE22_CODEC : 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");
"Generation22 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");
@@ -103,4 +116,37 @@ final class UnifiedWorldSettingsDiagnostics {
"public_presets", List.of("sanctuary:sanctuary"), "sizes", results,
"invalid_diameters_rejected", true, "copied_biome_source", true, "passed", true)) + "\n");
}
private static void verifyConstructionMasks(GameTestHelper helper, SanctuaryChunkGenerator historical21,
SanctuaryChunkGenerator generation22) {
var outside = new net.minecraft.core.BlockPos(200_000, 192, 200_000);
// Decoding a generation must not reserve the world before any structures are planned.
// These checks also cover the old OFF contract without initializing another island.
for (var generator : List.of(historical21, historical21.withExperimentalStructures(false),
generation22, generation22.withExperimentalStructures(false))) {
helper.assertTrue(!generator.protectsCityDecoration(outside),
"Unplanned generation" + generator.rootGeneration() + " does not protect arbitrary blocks");
helper.assertTrue(!generator.intersectsExperimentalStructures(outside.getX(), outside.getY(), outside.getZ(),
outside.getX() + 1, outside.getY() + 1, outside.getZ() + 1),
"Unplanned generation" + generator.rootGeneration() + " does not reject arbitrary structure bounds");
}
// Reuse the attached world's real plans: no extra native-noise planning or chunk loads.
if (!(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator active)
|| active.rootGeneration() != 21 && active.rootGeneration() != 22) return;
helper.assertTrue(!active.protectsCityDecoration(outside)
&& !active.intersectsExperimentalStructures(outside.getX(), outside.getY(), outside.getZ(),
outside.getX() + 1, outside.getY() + 1, outside.getZ() + 1),
"Prepared construction masks remain local to their real structures");
var interior = new ArrayList<net.minecraft.core.BlockPos>();
for (var module : active.transitPlan21().modules())
interior.add(new net.minecraft.core.BlockPos(module.centerX(), module.floorY(), module.centerZ()));
for (var network : active.undergroundHall22().networks()) for (var module : network.modules())
interior.add(new net.minecraft.core.BlockPos(module.centerX(), module.floorY(), module.centerZ()));
if (active.rootGeneration() == 22 && helper.getLevel().structureManager().shouldGenerateStructures())
helper.assertTrue(!interior.isEmpty(), "The attached generation22 world has core construction to protect");
for (var position : interior) helper.assertTrue(active.protectsCityDecoration(position)
&& active.intersectsExperimentalStructures(position.getX(), position.getY(), position.getZ(),
position.getX(), position.getY(), position.getZ()),
"Real underground construction remains protected from decoration and overlapping native structures");
}
}
@@ -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.selective22") || (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", 22);
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22) throw new IllegalArgumentException("Unsupported test generation");
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 22 ? "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,17 @@ 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);
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v22"), SanctuaryChunkGenerator.SELECTIVE22_CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v22"), SanctuaryBiomeSource.SELECTIVE22_CODEC);
fr.koka.sanctuary.worldgen.transit.TransitStructures21.register();
fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.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 || generator.rootGeneration() == 22) {
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() == 22 ? "sanctuary.world_options.heritage22_hint" : (generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22) ? "sanctuary.world_options.heritage_hint" : "sanctuary.world_options.experimental_hint"), font)
.setMaxWidth(310).setCentered(true));
}
layout.addToContents(contents);
@@ -122,6 +122,9 @@ 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);
fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.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, 21 or 22 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 || generation == 22; }
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 || generator.rootGeneration() == 22)
&& fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
&& generator.protectsCityDecoration(pos)) return;
if (generator.protectsDecoration(pos) && !region.getBlockState(pos).equals(state)) {
@@ -42,6 +42,22 @@ 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)));
public static final MapCodec<SanctuaryBiomeSource> SELECTIVE22_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 22)));
private final List<Holder<Biome>> allBiomes;
private final PopulationIslandBiomeSource population;
private final boolean cities;
@@ -75,7 +91,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 == 22 ? SELECTIVE22_CODEC : 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,47 @@ 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)));
public static final MapCodec<SanctuaryChunkGenerator> SELECTIVE22_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
SanctuaryBiomeSource.SELECTIVE22_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, 22, 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;
private volatile fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan undergroundHall22 = new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
private volatile fr.koka.sanctuary.worldgen.city.LostCityProtection16 undergroundHallProtection22 = fr.koka.sanctuary.worldgen.city.LostCityProtection16.EMPTY;
private boolean undergroundHallPrepared22;
public fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan undergroundHall22() { return undergroundHall22; }
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 +121,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 || generation == 22) && 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 == 22 ? SELECTIVE22_CODEC : 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 +135,12 @@ 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 || generation == 22) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22) && heritagePlan19.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 21 || generation == 22) && transitPlan21.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| generation == 22 && undergroundHallProtection22.intersects(minX, minY, minZ, maxX, maxY, maxZ);
}
public IslandCapacity capacity() {
return switch (initialDiameter()) {
@@ -133,12 +175,18 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
// Publish one root context before any generation workers use it.
context(islands.getFirst());
prepareCities(islands);
if (generation == 22) { prepareTransit(islands); prepareUndergroundHall22(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);
if (generation == 22) { prepareTransit(islands); prepareUndergroundHall22(islands); }
prepareRuins(islands);
prepareHeritage(islands);
prepareTransit(islands);
super.setIslands(islands);
}
private void prepareCities(List<ExpansionIsland> islands) {
@@ -149,7 +197,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 || generation == 22) && !cityStructuresEnabled) {
cityProtection16 = fr.koka.sanctuary.worldgen.city.LostCityProtection16.EMPTY;
activeCityPlans = java.util.Map.of();
source().setCityPlans(java.util.Map.of());
@@ -165,7 +213,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 || generation == 22) {
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
for (var island : islands) {
protection.add(published.get(island.id()), undergroundPlan16(island));
@@ -180,11 +228,11 @@ 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 && generation != 22) || !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(),
heightAccessor, pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()));
heightAccessor, pos -> protectsCore22(pos) || protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()));
long start = System.nanoTime();
var plan = fr.koka.sanctuary.worldgen.ruins.RuinsPlanner17.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
ruinsPlan17 = plan;
@@ -192,6 +240,89 @@ 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 && generation != 22) || !experimentalStructures || !cityStructuresEnabled || heritagePrepared) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var terrain = context(root);
if (generation == 21 || generation == 22) {
var sampled = new fr.koka.sanctuary.worldgen.heritage.HeritageTerrain21(root, terrain.generator(), terrain.random(),
heightAccessor, pos -> protectsCore22(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 = generation == 22
? fr.koka.sanctuary.worldgen.heritage.HeritagePlanner22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
: fr.koka.sanctuary.worldgen.heritage.HeritagePlanner21.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
heritagePlan19 = plan;
heritagePrepared = true;
SanctuaryMod.LOGGER.info("Heritage" + generation + " 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 -> protectsCore22(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 && generation != 22) || 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 -> protectsCore22(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 = generation == 22
? fr.koka.sanctuary.worldgen.transit.TransitPlanner22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
: fr.koka.sanctuary.worldgen.transit.TransitPlanner21.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
int directColumns = 0;
if (generation == 22 && transitPlan21.isEmpty()) {
directColumns = sampled.columnsUsed();
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()));
transitPlan21 = fr.koka.sanctuary.worldgen.transit.TransitRoute22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
}
transitPrepared = true;
SanctuaryMod.LOGGER.info("Transit" + generation + " seed {}: {} modules, {} probes, {} columns in {} ms; {}", root.seed(),
transitPlan21.modules().size(), transitPlan21.samples(), directColumns + sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000, transitPlan21.diagnostic());
}
private boolean protectsCore22(net.minecraft.core.BlockPos pos) {
return generation == 22 && (transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ())
|| undergroundHallProtection22.contains(pos.getX(), pos.getY(), pos.getZ()));
}
private void prepareUndergroundHall22(List<ExpansionIsland> islands) {
if (generation != 22 || !cityStructuresEnabled || undergroundHallPrepared22) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
if (undergroundPlan16(root).networks().stream().flatMap(n -> n.modules().stream())
.anyMatch(m -> m.kind() == fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Kind.EXPANSION_HALL)) {
undergroundHallPrepared22 = true; return;
}
var terrain = context(root);
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain21(root, terrain.generator(), terrain.random(), heightAccessor,
pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()) || protectsCore22(pos));
long start = System.nanoTime();
undergroundHall22 = fr.koka.sanctuary.worldgen.city.LostCityUnderground22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(),
new fr.koka.sanctuary.worldgen.city.LostCityUnderground22.Terrain() {
@Override public int surfaceY(int x, int z) { var ground = sampled.ground(x, z); return ground.dry() ? ground.top() : -1; }
@Override public boolean owns(int x, int z) { return sampled.owns(x, z); }
@Override public boolean solid(int x, int y, int z) { return sampled.solid(x, y, z); }
@Override public boolean protectedAt(int x, int y, int z) { return sampled.protectedAt(x, y, z); }
@Override public boolean exteriorAir(int x, int y, int z, int dx, int dz) { return sampled.exteriorAir(x, y, z, dx, dz); }
});
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
protection.add(fr.koka.sanctuary.worldgen.city.LostCityPlan.EMPTY, undergroundHall22);
undergroundHallProtection22 = protection.build(); undergroundHallPrepared22 = true;
SanctuaryMod.LOGGER.info("Independent hall22 seed {}: {} networks, {} probes, {} columns in {} ms", root.seed(),
undergroundHall22.networks().size(), undergroundHall22.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000);
}
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 +333,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 +349,41 @@ 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 || generation == 22) {
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain21(island, terrain.generator(), terrain.random(),
heightAccessor, pos -> island.origin() && protectsDecoration(pos));
var placement = generation == 22
? fr.koka.sanctuary.worldgen.city.LostCityPlanner22.plan(island.seed(), island.centerX(), island.centerZ(), island.diameter(), island.origin(), sampled)
: 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 City" + generation + " {} 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("City" + generation + " 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 +454,11 @@ 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 || generation == 22) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22) && heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 21 || generation == 22) && transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ())
|| generation == 22 && undergroundHallProtection22.contains(pos.getX(), pos.getY(), pos.getZ());
if (generation == 15) return cityProtection15.contains(pos.getX(), pos.getY(), pos.getZ());
if (generation != 14) return false;
var island = islandAt(pos.getX(), pos.getZ());
@@ -311,20 +480,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 || generation == 22) && 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 && generation != 22) 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 && generation != 22) 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 && generation != 22) return null;
lostCityPlan(island);
return cityReservations16.get(cityIdentity(island));
}
@@ -334,10 +503,16 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
@Override public com.mojang.datafixers.util.Pair<net.minecraft.core.BlockPos, net.minecraft.core.Holder<net.minecraft.world.level.levelgen.structure.Structure>> findNearestMapStructure(
ServerLevel level, net.minecraft.core.HolderSet<net.minecraft.world.level.levelgen.structure.Structure> structures,
net.minecraft.core.BlockPos origin, int radius, boolean skipReferenced) {
if (generation == 22 && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.KEY))
return fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.findNearest(level, structures, origin, radius, 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 || generation == 22) && 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 || generation == 22) && 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 || generation == 22) && 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 +540,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 || generation == 22) && 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; }
}
@@ -0,0 +1,147 @@
package fr.koka.sanctuary.worldgen.city;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.District;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Kind;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Parcel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/** Generation22 only: earlier planners propose geometry, but cannot authorize artificial terrain.
* Proposals and their forced reservations are read-only; only fully recertified natural towns
* reach the returned plan. The published generation21 solver and all renderers stay unchanged. */
public final class LostCityPlanner22 {
public static final int MAX_COLUMNS = LostCityPlanner21.MAX_COLUMNS;
public static final int MAX_VOLUME_CHECKS = LostCityPlanner21.MAX_VOLUME_CHECKS;
public static final int MAX_ATTEMPTS = 3, MAX_CUT = 2, MAX_FILL = 2;
private static final LostCityPlanner.Ground INVALID = new LostCityPlanner.Ground(0, 0, false, true);
private LostCityPlanner22() {}
public static LostCityPlanner21.Result plan(long seed, int cx, int cz, int diameter, boolean origin,
LostCityPlanner16.Terrain terrain) {
if (diameter < 64 || diameter > 1024 || origin && diameter < 512)
throw new IllegalArgumentException("Unsupported natural city island size");
var probe = new Probe(terrain);
for (int attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
probe.reject("proposal-attempt");
long proposalSeed = attempt == 0 ? seed : mix(seed ^ attempt * 0x22C175L);
try {
var proposal = LostCityPlanner21.plan(proposalSeed, cx, cz, diameter, origin, probe);
if (proposal.reservation() != null) { probe.reject("forced-reservation-rejected"); continue; }
if (proposal.core().surface().isEmpty()) { probe.reject("no-natural-core"); continue; }
if (!proposal.core().supports().isEmpty()) { probe.reject("core-pier-rejected"); continue; }
boolean valid = true;
for (var district : proposal.core().surface().districts())
if (!validate(district, probe)) { valid = false; break; }
if (!valid) continue;
var districts = new ArrayList<>(proposal.core().surface().districts());
var connections = new ArrayList<LostCityPlanner21.Connection>();
for (var connection : proposal.connections()) {
try {
var district = proposal.surface().districts().get(connection.districtIndex());
if (proposal.supports().stream().anyMatch(s -> s.startChunkX() == district.startChunkX()
&& s.startChunkZ() == district.startChunkZ())) {
probe.reject("satellite-pier-rejected"); continue;
}
if (!validate(district, probe)) continue;
var retained = new ArrayList<>(districts); retained.add(district);
if (!connected(connection.path(), retained)) {
probe.reject("satellite-depends-on-rejected-road"); continue;
}
connections.add(new LostCityPlanner21.Connection(districts.size(), connection.path()));
districts.add(district);
} catch (Exhausted exhausted) { probe.reject("satellite-budget"); break; }
}
probe.reject("natural-core-accepted");
return result(proposal.core(), new LostCityPlan(districts), connections, probe);
} catch (Exhausted exhausted) {
probe.reject("natural-search-budget"); break;
} catch (IllegalArgumentException rejectedReservation) {
// The immutable16 proposal solver requires an owned fallback envelope. In22 an
// island with no such envelope is simply unsuitable; unrelated errors still fail.
String message = rejectedReservation.getMessage();
if (!"Starter terrain does not own a central 49x49 city envelope".equals(message)
&& !"Starter city reservation crosses another island's ownership".equals(message)) throw rejectedReservation;
probe.reject("unowned-reservation-rejected");
}
}
return result(new LostCityPlanner16.Result(LostCityPlan.EMPTY, List.of(), 0, 0),
LostCityPlan.EMPTY, List.of(), probe);
}
private static LostCityPlanner21.Result result(LostCityPlanner16.Result core, LostCityPlan surface,
List<LostCityPlanner21.Connection> connections, Probe probe) {
return new LostCityPlanner21.Result(core, surface, List.of(), probe.columns.size(), probe.checks,
connections, probe.diagnostics);
}
/** Validate actual emitted footprint and bearing columns, not just a few terrain samples.
* Eight-block pool/garage basements remain possible only when entirely inside native rock. */
private static boolean validate(District district, Probe probe) {
for (Parcel parcel : district.parcels()) {
if (parcel.kind() == Kind.BRIDGE) { probe.reject("suspended-bridge-rejected"); return false; }
for (int z = parcel.minZ(); z <= parcel.maxZ(); z++) for (int x = parcel.minX(); x <= parcel.maxX(); x++) {
var ground = probe.ground(x, z);
if (!ground.dry() || ground.protectedTerrain() || ground.solidDepth() < 4) {
probe.reject("not-dry-natural-ground"); return false;
}
int floor = parcel.surfaceY(x - parcel.minX(), z - parcel.minZ());
if (ground.top() - floor > MAX_CUT || floor - ground.top() > MAX_FILL) {
probe.reject("excess-cut-or-fill"); return false;
}
int bottom = floor - parcel.foundationDepth();
// Two unpainted rock layers must carry every column. The entire native part of
// each footing stays solid: no borrowed corner certificate across a cavity.
for (int y = bottom - 2; y <= Math.min(floor - 1, ground.top()); y++) {
if (!probe.solid(x, y, z)) { probe.reject("incomplete-natural-bearing"); return false; }
}
for (int y = bottom - 2; y <= parcel.maxY(); y++) {
if (probe.protectedAt(x, y, z)) { probe.reject("protected-construction-volume"); return false; }
}
}
}
return true;
}
private static boolean connected(List<LostCityPlanner21.Point> path, List<District> retained) {
for (var point : path) {
boolean found = false;
for (var district : retained) for (var parcel : district.parcels()) {
if (parcel.kind() == Kind.STREET && point.x() >= parcel.minX() && point.x() <= parcel.maxX()
&& point.z() >= parcel.minZ() && point.z() <= parcel.maxZ()
&& parcel.surfaceY(point.x() - parcel.minX(), point.z() - parcel.minZ()) == point.floorY()) found = true;
}
if (!found) return false;
}
return true;
}
private static final class Probe implements LostCityPlanner16.Terrain {
final LostCityPlanner16.Terrain terrain;
final Map<Long, LostCityPlanner.Ground> columns = new HashMap<>();
final Map<String, Integer> diagnostics = new LinkedHashMap<>();
int checks;
Probe(LostCityPlanner16.Terrain terrain) { this.terrain = java.util.Objects.requireNonNull(terrain); }
@Override public boolean owns(int x, int z) { return terrain.owns(x, z); }
@Override public LostCityPlanner.Ground ground(int x, int z) {
if (!owns(x, z)) return INVALID;
long key = (long) x << 32 ^ z & 0xffffffffL;
var cached = columns.get(key); if (cached != null) return cached;
if (columns.size() >= MAX_COLUMNS) throw new Exhausted();
var result = java.util.Objects.requireNonNull(terrain.ground(x, z)); columns.put(key, result); return result;
}
@Override public boolean solid(int x, int y, int z) {
count(); return ground(x, z) != INVALID && terrain.solid(x, y, z);
}
@Override public boolean protectedAt(int x, int y, int z) {
count(); return ground(x, z) == INVALID || terrain.protectedAt(x, y, z);
}
void count() { if (checks >= MAX_VOLUME_CHECKS) throw new Exhausted(); checks++; }
void reject(String reason) { diagnostics.merge(reason, 1, Integer::sum); }
}
private static long mix(long v) { v = (v ^ v >>> 30) * 0xbf58476d1ce4e5b9L; v = (v ^ v >>> 27) * 0x94d049bb133111ebL; return v ^ v >>> 31; }
private static final class Exhausted extends RuntimeException { private static final long serialVersionUID = 1L; }
}
@@ -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.rootGeneration() == 22) && generator.cityReservation16(island) != null
&& ((generator.rootGeneration() != 21 && generator.rootGeneration() != 22) || 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 || generator.rootGeneration() == 22)) 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 || generator.rootGeneration() == 22) ? new LostCityPiece21(parcel) : generator.rootGeneration() == 20 ? new LostCityPiece20(parcel) : (generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22) ? 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 || generator.rootGeneration() == 22)) 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 || generator.rootGeneration() == 22) ? 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 || generator.rootGeneration() == 22) ? 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,187 @@
package fr.koka.sanctuary.worldgen.city;
import fr.koka.sanctuary.worldgen.city.LostCityUnderground16.*;
import fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Module;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/** An independent public undercroft. No surface district, fake parcel or city reservation is needed.
* Only complete hall/access pairs are published; optional utility wings cannot invalidate them. */
public final class LostCityUnderground22 {
public static final int MAX_COLUMNS = 6_000, MAX_SAMPLES = LostCityUnderground16.MAX_SAMPLES;
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
private LostCityUnderground22() {}
public interface Terrain extends LostCityUnderground16.Terrain { int surfaceY(int x, int z); }
public static Plan plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
if (!List.of(512, 724, 1024).contains(diameter)) throw new IllegalArgumentException("Unsupported undercroft island size");
var probe = new Probe(terrain, cx, cz, diameter);
List<Module> accepted = null;
long acceptedSeed = seed;
try {
search: for (int attempt = 0; attempt < 40; attempt++) {
double angle = (mix(seed ^ 0x22A11L) >>> 11) * 0x1.0p-53 * Math.PI * 2 + attempt * 2.399963229728653;
double radius = attempt == 0 ? 0 : diameter * (attempt < 9 ? .09 : attempt < 21 ? .18 : .28);
int x = cx + (int) Math.round(Math.cos(angle) * radius), z = cz + (int) Math.round(Math.sin(angle) * radius);
int top = accessHeight(x, z, probe);
if (top < 0) continue;
for (int drop : new int[]{22, 32, 46, 62, 86, 110}) {
int floor = top - drop;
if (floor < 24) continue;
long networkSeed = mix(seed ^ ((long) x << 32) ^ z ^ drop);
for (int turn = 0; turn < 4; turn++) {
int direction = (int) Math.floorMod(networkSeed + turn, 4);
var access = new Module(Kind.ACCESS, x - 3, floor - 1, z - 3, 7, drop + 4, 7,
1 << direction, networkSeed, List.of());
var hall = attached(access, Kind.EXPANSION_HALL, direction, mix(networkSeed + 1));
// Cheap hall witnesses come before repeatedly scanning a tall access shaft.
if (!coarse(hall, probe)) continue;
var modules = new ArrayList<Module>();
access = certify(access, probe);
if (access == null) continue;
modules.add(access);
if (!attach(modules, 0, hall, direction, probe)) continue;
accepted = modules; acceptedSeed = networkSeed;
break search;
}
}
}
} catch (BudgetExceeded exhausted) { probe.reject("required-search-budget"); }
if (accepted != null) {
// A sewer and a pool are additional destinations, never conditions for the public hall.
int forward = Integer.numberOfTrailingZeros(accepted.getFirst().openings());
try {
for (int side : new int[]{(forward + 1) & 3, (forward + 3) & 3})
if (attach(accepted, 1, attached(accepted.get(1), Kind.SEWER, side, mix(acceptedSeed + 2)), side, probe)) break;
if (probe.samples < MAX_SAMPLES - 70_000 && probe.columns.size() < MAX_COLUMNS - 1_900)
attach(accepted, 1, attached(accepted.get(1), Kind.POOLROOM, forward, mix(acceptedSeed + 3)), forward, probe);
} catch (BudgetExceeded exhausted) { probe.reject("optional-wing-budget"); }
}
var networks = accepted == null ? List.<Network>of() : List.of(new Network(
accepted.getFirst().centerX() >> 4, accepted.getFirst().centerZ() >> 4, acceptedSeed, accepted));
System.getLogger(LostCityUnderground22.class.getName()).log(System.Logger.Level.INFO,
"Independent undercroft22 seed=" + seed + " networks=" + networks.size() + " columns=" + probe.columns.size()
+ " samples=" + probe.samples + " kinds=" + (accepted == null ? List.of() : accepted.stream().map(Module::kind).toList())
+ " admission=" + probe.rejections);
return new Plan(networks, probe.samples);
}
/** The whole visible seven-square hatch deck stays within two blocks of existing dry ground. */
private static int accessHeight(int cx, int cz, Probe p) {
int low = 384, high = -1;
for (int z = cz - 3; z <= cz + 3; z++) for (int x = cx - 3; x <= cx + 3; x++) {
int top = p.surface(x, z);
if (top < 64 || top > 365 || !p.solid(x, top, z) || p.protectedAt(x, top, z)) return p.rejectHeight("dry-access");
low = Math.min(low, top); high = Math.max(high, top);
if (high - low > 2) return p.rejectHeight("access-relief");
}
return high;
}
private static Module attached(Module parent, Kind kind, int direction, long seed) {
int width = kind == Kind.POOLROOM ? 57 : kind == Kind.EXPANSION_HALL ? 25 : 9;
int depth = kind == Kind.POOLROOM ? 31 : width;
int height = kind == Kind.POOLROOM ? 13 : kind == Kind.EXPANSION_HALL ? 10 : 7;
int offset = (DX[direction] == 0 ? parent.depth() / 2 + depth / 2 : parent.width() / 2 + width / 2) + 8;
int x = parent.centerX() + DX[direction] * offset, z = parent.centerZ() + DZ[direction] * offset;
return new Module(kind, x - width / 2, parent.floorY() - (kind == Kind.POOLROOM ? 5 : 1),
z - depth / 2, width, height, depth, 1 << ((direction + 2) & 3), seed, List.of());
}
private static boolean attach(List<Module> modules, int parentIndex, Module room, int direction, Probe p) {
var parent = modules.get(parentIndex);
int x = DX[direction] == 0 ? parent.centerX() - 2 : direction == 1 ? parent.maxX() + 1 : room.maxX() + 1;
int z = DZ[direction] == 0 ? parent.centerZ() - 2 : direction == 2 ? parent.maxZ() + 1 : room.maxZ() + 1;
var passage = new Module(Kind.PASSAGE, x, parent.floorY() - 1, z, DX[direction] == 0 ? 5 : 7, 6,
DZ[direction] == 0 ? 5 : 7, (1 << direction) | (1 << ((direction + 2) & 3)), mix(room.seed()), List.of());
for (var old : modules) if (overlaps(room, old) || overlaps(passage, old)) { p.reject("wing-collision"); return false; }
if (!coarse(room, p) || !coarse(passage, p)) return false;
room = certify(room, p); if (room == null) return false;
passage = certify(passage, p); if (passage == null) return false;
if (LostCityUnderground16.supportConflict(room, passage)) return false;
for (var old : modules) if (LostCityUnderground16.supportConflict(room, old)
|| LostCityUnderground16.supportConflict(passage, old)) return false;
modules.set(parentIndex, new Module(parent.kind(), parent.minX(), parent.minY(), parent.minZ(), parent.width(),
parent.height(), parent.depth(), parent.openings() | 1 << direction, parent.seed(), parent.supports()));
modules.add(room); modules.add(passage);
return true;
}
private static boolean overlaps(Module a, Module b) {
return a.minX() <= b.maxX() && a.maxX() >= b.minX() && a.minZ() <= b.maxZ() && a.maxZ() >= b.minZ()
&& a.boundsMinY() <= b.boundsMaxY() && a.boundsMaxY() >= b.boundsMinY();
}
private static boolean coarse(Module m, Probe p) {
int solid = 0;
for (int x : new int[]{m.minX(), m.centerX(), m.maxX()}) for (int z : new int[]{m.minZ(), m.centerZ(), m.maxZ()}) {
if (!p.owns(x, z)) { p.reject("outside-island"); return false; }
for (int y : new int[]{m.minY(), m.floorY() + 2, m.maxY()}) {
if (p.protectedAt(x, y, z)) { p.reject("coarse-protected"); return false; }
if (p.solid(x, y, z)) solid++;
}
}
if (solid < 5) { p.reject("coarse-rock"); return false; }
return true;
}
private static Module certify(Module m, Probe p) {
var supports = new ArrayList<Support>();
int stride = m.kind() == Kind.EXPANSION_HALL || m.kind() == Kind.POOLROOM ? 8 : 3;
for (int z : lattice(m.minZ(), m.maxZ(), stride)) for (int x : lattice(m.minX(), m.maxX(), stride)) {
var support = support(m, x, z, p);
if (support == null) { p.reject("no-short-anchor"); return null; }
supports.add(support);
}
int solid = 0, count = 0;
for (int z = m.minZ(); z <= m.maxZ(); z++) for (int x = m.minX(); x <= m.maxX(); x++) {
if (!p.owns(x, z)) { p.reject("outside-island"); return null; }
for (int y = m.minY(); y <= m.maxY(); y++) {
if (p.protectedAt(x, y, z)) { p.reject("protected-volume"); return null; }
if (p.solid(x, y, z)) solid++; count++;
}
}
// The constructed shell can cross a native cavity; it must still be embedded in real terrain.
if (solid * 100L < count * (m.kind() == Kind.ACCESS ? 50 : 20)) { p.reject("insufficient-rock"); return null; }
return new Module(m.kind(), m.minX(), m.minY(), m.minZ(), m.width(), m.height(), m.depth(),
m.openings(), m.seed(), supports);
}
private static Support support(Module m, int x, int z, Probe p) {
// Save even direct bearings: the existing protection builder then retains their untouched rock.
for (int distance = 1; distance <= 8; distance++) {
int y = m.minY() - distance;
if (y < 2 || p.protectedAt(x, y, z)) break;
if (p.solid(x, y, z) && p.solid(x, y - 1, z) && !p.protectedAt(x, y - 1, z))
return new Support(Type.PILLAR, x, y, z, m.minY() - 1);
}
for (int distance = 1; distance <= 8; distance++) {
int y = m.maxY() + distance;
if (y > 381 || p.protectedAt(x, y, z)) break;
if (p.solid(x, y, z) && p.solid(x, y + 1, z) && !p.protectedAt(x, y + 1, z))
return new Support(Type.CHAIN, x, m.maxY() + 1, z, y);
}
return null;
}
private static List<Integer> lattice(int min, int max, int stride) {
var result = new ArrayList<Integer>(); for (int n = min; n <= max; n += stride) result.add(n);
if (result.getLast() != max) result.add(max); return result;
}
private static final class Probe {
final Terrain terrain; final int cx, cz; final double radius;
final Map<Long, Integer> columns = new HashMap<>(); final Map<String, Integer> rejections = new LinkedHashMap<>(); int samples;
Probe(Terrain terrain, int cx, int cz, int diameter) { this.terrain = terrain; this.cx = cx; this.cz = cz; radius = diameter / 2.0 - 4; }
void count() { if (samples >= MAX_SAMPLES) throw new BudgetExceeded(); samples++; }
boolean owns(int x, int z) { return Math.hypot((double) x - cx, (double) z - cz) <= radius && terrain.owns(x, z); }
int surface(int x, int z) {
if (!owns(x, z)) return -1;
long key = ((long) x << 32) ^ (z & 0xffffffffL); var value = columns.get(key); if (value != null) return value;
if (columns.size() >= MAX_COLUMNS) throw new BudgetExceeded();
count(); int y = terrain.surfaceY(x, z); columns.put(key, y); return y;
}
boolean solid(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) < 0) 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) < 0) return true; count(); return terrain.protectedAt(x, y, z); }
void reject(String reason) { rejections.merge(reason, 1, Integer::sum); }
int rejectHeight(String reason) { reject(reason); return -1; }
}
private static final class BudgetExceeded extends RuntimeException { private static final long serialVersionUID = 1L; }
private static long mix(long x) { x = (x ^ x >>> 30) * 0xbf58476d1ce4e5b9L; x = (x ^ x >>> 27) * 0x94d049bb133111ebL; return x ^ x >>> 31; }
}
@@ -0,0 +1,88 @@
package fr.koka.sanctuary.worldgen.city;
import java.util.ArrayList;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.resources.ResourceKey;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.loot.LootTable;
/** A saved underground module includes its openings and certified supports, including their anchors. */
public final class 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,41 @@
package fr.koka.sanctuary.worldgen.city;
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;
/** An independent native start keeps a certified underground hall available without a surface city. */
public final class LostCityUndergroundStructure22 extends Structure {
public static final MapCodec<LostCityUndergroundStructure22> CODEC = simpleCodec(LostCityUndergroundStructure22::new);
public LostCityUndergroundStructure22(StructureSettings settings) { super(settings); }
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 22) return Optional.empty();
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
if (island == null || !island.origin() || island.generation() != 22) return Optional.empty();
var networks = generator.undergroundHall22().networks().stream()
.filter(n -> n.startChunkX() == context.chunkPos().x() && n.startChunkZ() == context.chunkPos().z()).toList();
if (networks.isEmpty()) return Optional.empty();
for (var network : networks) for (var module : network.modules()) {
var bounds = LostCityUndergroundPiece20.bounds(module);
if (Math.abs((long) Math.floorDiv(bounds.minX(), 16) - network.startChunkX()) > 7
|| Math.abs((long) Math.floorDiv(bounds.maxX(), 16) - network.startChunkX()) > 7
|| Math.abs((long) Math.floorDiv(bounds.minZ(), 16) - network.startChunkZ()) > 7
|| Math.abs((long) Math.floorDiv(bounds.maxZ(), 16) - network.startChunkZ()) > 7)
throw new IllegalStateException("Underground hall exceeds native structure reference range");
}
return Optional.of(new GenerationStub(LostCityUndergroundStructures22.locator(networks.getFirst()), pieces -> {
// Shafts are last so connected rooms cannot refill their surface hatches.
networks.forEach(n -> n.modules().stream().filter(m -> m.kind() != LostCityUnderground16.Kind.ACCESS)
.forEach(m -> pieces.addPiece(new LostCityUndergroundPiece20(m))));
networks.forEach(n -> n.modules().stream().filter(m -> m.kind() == LostCityUnderground16.Kind.ACCESS)
.forEach(m -> pieces.addPiece(new LostCityUndergroundPiece20(m))));
}));
}
@Override public StructureType<?> type() { return LostCityUndergroundStructures22.TYPE; }
}
@@ -0,0 +1,108 @@
package fr.koka.sanctuary.worldgen.city;
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.templatesystem.StructureTemplateManager;
/** Native lifecycle for generation22's standalone hall; its saved modules reuse the generation20 pieces. */
public final class LostCityUndergroundStructures22 {
public static final ResourceKey<Structure> KEY = ResourceKey.create(Registries.STRUCTURE,
SanctuaryMod.id("underground_hall22"));
public static final StructureType<LostCityUndergroundStructure22> TYPE = Registry.register(
BuiltInRegistries.STRUCTURE_TYPE, SanctuaryMod.id("underground_hall_v22"), () -> LostCityUndergroundStructure22.CODEC);
private LostCityUndergroundStructures22() {}
public static void register() { /* Initialize the codec before reading the dynamic structure registry. */ }
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() != 22) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || island.generation() != 22) return;
if (sanctuary.undergroundHall22().networks().stream().noneMatch(n -> n.startChunkX() == chunk.getPos().x()
&& n.startChunkZ() == 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);
}
/** Locate returns the persisted surface hatch, loading only the native structure start when necessary. */
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() != 22 || !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<LostCityUnderground16.Network>();
int originChunkX = origin.getX() >> 4, originChunkZ = origin.getZ() >> 4;
for (var network : generator.undergroundHall22().networks()) {
var island = generator.islandForChunk(network.startChunkX(), network.startChunkZ());
if (island == null || !island.origin() || island.generation() != 22 || !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;
var entrance = start.getPieces().stream()
.filter(p -> p instanceof LostCityUndergroundPiece20 piece && piece.module().kind() == LostCityUnderground16.Kind.ACCESS)
.map(p -> locator(((LostCityUndergroundPiece20) p).module()))
.min(Comparator.comparingDouble(origin::distSqr));
if (entrance.isEmpty()) continue;
if (skipReferenced) level.structureManager().addReference(start);
return Pair.of(entrance.get(), holder);
}
return null;
}
public static BlockPos locator(LostCityUnderground16.Network network) {
return locator(network.modules().stream().filter(m -> m.kind() == LostCityUnderground16.Kind.ACCESS)
.findFirst().orElseThrow(() -> new IllegalArgumentException("Underground hall has no surface access")));
}
private static BlockPos locator(LostCityUnderground16.Module access) {
int exit = Integer.numberOfTrailingZeros(access.openings() == 0 ? 1 : access.openings());
int wall = (exit + 2) & 3;
int x = wall == 1 ? access.width() - 2 : wall == 3 ? 1 : access.width() / 2;
int z = wall == 2 ? access.depth() - 2 : wall == 0 ? 1 : access.depth() / 2;
return new BlockPos(access.minX() + x, access.maxY() - 1, access.minZ() + z);
}
}
@@ -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,45 @@
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.rootGeneration() != 22) || generator.rootGeneration() != 22 && !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 || generator.rootGeneration() == 22) 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);
}
if (generator.rootGeneration() == 22) for (var network : generator.undergroundHall22().networks()) {
var at = fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.locator(network);
source.sendSuccess(() -> Component.translatable("sanctuary.underground_hall.entry", at.getX(), at.getY(), at.getZ()), false);
}
return plan.sites().size() + (generator.rootGeneration() == 22
? (int) generator.transitPlan21().modules().stream().filter(m -> m.kind() == fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind.ACCESS).count()
+ generator.undergroundHall22().networks().size() : 0);
}))));
}
}
@@ -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,612 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.*;
import fr.koka.sanctuary.worldgen.heritage.HeritagePlanner21.Terrain;
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;
/** Generation22 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 HeritagePlanner22 {
public static final int MAX_COLUMNS = HeritagePlan19.MAX_COLUMNS, MAX_SAMPLES = HeritagePlan19.MAX_SAMPLES;
// Prefer an existing platform: at most two blocks of cut/fill and four of total relief.
public static final int MAX_SITE_CUT = 2, MAX_SITE_FILL = 2, MAX_SITE_RELIEF = 4;
public static final int MAX_ROUTE_CUT = 1, MAX_ROUTE_FILL = 1;
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 HeritagePlanner22() {}
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("Heritage22 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.heritage22") || Boolean.getBoolean("sanctuary.heritage.plannerDiagnostics"))
System.getLogger(HeritagePlanner22.class.getName()).log(System.Logger.Level.INFO,
"Heritage22 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 < 24; i++) {
double turn = angle + i * Math.PI / 3;
int offset = i == 0 ? 0 : i < 5 ? 24 : i < 8 ? 48 : i < 12 ? 72 : i < 16 ? 96 : 128;
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_RELIEF) { 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 = (high - low) * 1_024 + i * 8 + (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 + MAX_SITE_FILL);
if (minimum > maximum || high - low > MAX_SITE_RELIEF) { 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>();
// Every foundation column rests directly on intact rock. Do not catch a cliff
// or a cavity with long piers just to retain this candidate.
for (int dz = 0; dz < 17; dz++) for (int dx = 0; dx < 17; dx++) {
if (!probe.solid(x + dx, base - 1, z + dz) || !probe.solid(x + dx, base - 2, z + dz)) {
probe.reject("site-no-direct-bearing"); return null;
}
for (int y = base - 2; y < base; y++) if (probe.protectedAt(x + dx, y, z + dz)) {
probe.reject("support-protected"); return null;
}
}
// 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.rootGeneration() != 22) || !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 || generator.rootGeneration() == 22) ? 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 || generator.rootGeneration() == 22) ? 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 && generator.rootGeneration() != 22)
|| !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.rootGeneration() != 22) || !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.rootGeneration() != 22) || !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 || generator.rootGeneration() == 22) ? 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 || generator.rootGeneration() == 22) ? 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 && generator.rootGeneration() != 22)
|| !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.rootGeneration() != 22) || !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,52 @@
package fr.koka.sanctuary.worldgen.transit;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
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;
/** Distinct native piece ID selects the corrected22 corner without migrating any saved21 piece. */
public final class TransitPiece22 extends StructurePiece {
private final TransitPlan21.Module room;
public TransitPiece22(TransitPlan21.Module room) {
super(TransitStructures21.PIECE22, 0, TransitPiece21.bounds(room));
this.room = room; setOrientation(null);
}
public TransitPiece22(StructurePieceSerializationContext context, CompoundTag tag) {
super(TransitStructures21.PIECE22, tag);
room = TransitPiece21.decodeModule(tag.getCompound("SanctuaryModule")
.orElseThrow(() -> new IllegalArgumentException("Missing saved transit22 room")));
if (!getBoundingBox().equals(TransitPiece21.bounds(room)))
throw new IllegalArgumentException("Transit22 room disagrees with its saved native bounding box");
setOrientation(null);
}
public TransitPlan21.Module module() { return room; }
@Override public BlockPos getLocatorPosition() { return TransitPiece21.locator(room); }
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
tag.put("SanctuaryModule", TransitPiece21.encodeModule(room));
}
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
TransitStructures21.placing(() -> TransitRenderer22.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 pos = world(x, y, z);
if (getBoundingBox().isInside(pos) && clip.isInside(pos)) placeBlock(level, block, pos.getX(), pos.getY(), pos.getZ(), clip);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
var pos = world(x, y, z);
if (getBoundingBox().isInside(pos) && clip.isInside(pos)) createChest(level, clip, RandomSource.create(seed), pos, loot, null);
}
}));
}
}
@@ -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,13 @@
package fr.koka.sanctuary.worldgen.transit;
/** The main traversal is core infrastructure in22. Reserve it before optional networks and sites.
* Its established civil engineering may bridge a short cavity with certified bearings; the stricter
* surface-building policy must not remove this underground destination. No terrain is generated here. */
public final class TransitPlanner22 {
public static final int MAX_COLUMNS = TransitPlanner21.MAX_COLUMNS;
public static final int MAX_PIER = 16, MAX_WEAK_SPAN = 34;
private TransitPlanner22() {}
public static TransitPlan21 plan(long seed, int centerX, int centerZ, int diameter, TransitPlanner21.Terrain terrain) {
return TransitPlanner21.plan(seed, centerX, centerZ, diameter, terrain);
}
}
@@ -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,24 @@
package fr.koka.sanctuary.worldgen.transit;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
import net.minecraft.world.level.block.Blocks;
/** New22 corners retain complete walls on sides that have no saved opening.
* Existing21 pieces keep their historical paint sequence and geometry. */
public final class TransitRenderer22 {
private TransitRenderer22() {}
public static void render(TransitPlan21.Module module, LostCityRenderer.BlockWriter output) {
if (module.kind() != TransitPlan21.Kind.JUNCTION) { TransitRenderer21.render(module, output); return; }
var buffer = ConnectedBlocks20.buffer(output);
TransitRenderer21.render(module, buffer);
// The roadway crossing and its two slab curbs must stop at an unopened wall.
for (int direction = 0; direction < 4; direction++) if ((module.openings() & 1 << direction) == 0)
for (int across = -3; across <= 3; across++) {
int x = direction == 1 ? module.width() - 1 : direction == 3 ? 0 : module.width() / 2 + across;
int z = direction == 0 ? 0 : direction == 2 ? module.depth() - 1 : module.depth() / 2 + across;
buffer.set(x, 2, z, Blocks.TUFF_BRICKS.defaultBlockState());
}
buffer.flush();
}
}
@@ -0,0 +1,236 @@
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 fr.koka.sanctuary.worldgen.transit.TransitPlanner21.Terrain;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/** Generation22 fallback alignments around protected volumes. It is called with a fresh terrain
* sampler after the historical straight-route search fails. No partial route is ever published. */
public final class TransitRoute22 {
public static final int MAX_COLUMNS = 12_000;
private TransitRoute22() {}
public static TransitPlan21 plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
if (!List.of(512, 724, 1024).contains(diameter)) throw new IllegalArgumentException("Unsupported transit detour diameter");
var probe = new Probe(terrain);
var choices = new ArrayList<Choice>(); String last = "no supported protected-volume detour";
int farRadius = (int) Math.ceil(diameter * .21 / 17) * 17;
int minimumRadius = (int) Math.ceil(diameter / 8.0 / 17) * 17;
var radii = new java.util.LinkedHashSet<Integer>(List.of(Math.max(minimumRadius, farRadius - 34), minimumRadius, farRadius));
// Search actual entrance pairs before repeatedly testing their identical bad porticos at seven depths.
// Fine translations also remove the previous assumption that every entrance must lie on x/z=17n.
int[][] offsets = {{0, 0}, {-17, 0}, {17, 0}, {0, -17}, {0, 17}, {-8, -8}, {8, 8}, {-8, 8}, {8, -8},
{-34, 0}, {34, 0}, {0, -34}, {0, 34}, {-34, -34}, {34, 34}, {-34, 34}, {34, -34}};
probe.columnLimit = 5_500; probe.sampleLimit = 1_200_000;
int alignments = 0;
try {
search: for (var offset : offsets) for (int radius : radii) for (int shape = 0; shape < 8; shape++)
for (boolean alongX : new boolean[]{true, false}) {
var line = alignment(cx + offset[0], cz + offset[1], radius, 0, shape, alongX);
alignments++;
int firstTop = approachTop(line.getFirst(), probe), lastTop = approachTop(line.getLast(), probe);
String geometry = "shape=" + shape + " axis=" + (alongX ? "east-west" : "north-south")
+ " radius=" + radius + " translation=" + offset[0] + "," + offset[1];
if (firstTop < 0 || lastTop < 0) { last = "no suitable pair of natural porticos"; diagnostic("portico reject " + geometry); continue; }
int selectedAtGeometry = 0;
for (int floor : floors(firstTop, lastTop)) {
var route = candidate(seed, line, floor, probe);
String label = geometry + " floor=" + floor;
if (route == null) { last = probe.reason; diagnostic("reject " + label + " " + last + " columns=" + probe.columns.size()); continue; }
int cost = route.stream().mapToInt(m -> m.supports().stream().mapToInt(p -> p.maxY() - p.minY() + 1).sum()
+ (m.kind() == Kind.ACCESS ? m.surfaceY() - m.floorY() : 0)).sum();
choices.add(new Choice(route, cost + line.size() * 4, label));
diagnostic("preselected " + label + " modules=" + route.size());
if (choices.size() >= 8) break search;
// Keep alternative footprints available if exact hydrology rejects one alignment.
if (++selectedAtGeometry >= 2) break;
}
}
} catch (BudgetExceeded exhausted) { diagnostic("prescreen budget; retain " + choices.size()); }
probe.columnLimit = MAX_COLUMNS; probe.sampleLimit = TransitPlan21.MAX_SAMPLES;
choices.sort(java.util.Comparator.comparingInt(Choice::cost));
try {
for (var choice : choices) {
var modules = new ArrayList<Module>();
for (var rough : choice.modules()) {
var complete = certify(rough, probe, true);
if (complete == null) { last = probe.reason; break; }
modules.add(complete);
}
if (modules.size() != choice.modules().size()) { diagnostic("exact reject " + choice.label() + " " + last); continue; }
int span = Math.max(Math.abs(modules.getFirst().centerX() - modules.getLast().centerX()),
Math.abs(modules.getFirst().centerZ() - modules.getLast().centerZ()));
return new TransitPlan21(modules, probe.samples, "accepted detour " + choice.label() + " span=" + span
+ " columns=" + probe.columns.size() + " bends=" + modules.stream().filter(m -> m.kind() == Kind.JUNCTION).count());
}
} catch (BudgetExceeded exhausted) { last = "bounded detour search exhausted: " + probe.reason; }
return new TransitPlan21(List.of(), probe.samples, last + "; candidates=" + choices.size() + "; alignments=" + alignments + "; columns=" + probe.columns.size());
}
private static List<Integer> floors(int a, int b) {
int lowerTop = Math.min(a, b), upperTop = Math.max(a, b);
var result = new java.util.LinkedHashSet<Integer>();
for (int y : new int[]{lowerTop - 22, lowerTop - 38, lowerTop - 54, lowerTop - 70, 216, 200, 184, 160})
if (y >= 32 && y >= upperTop - 145 && y <= lowerTop - 18) result.add(y);
return List.copyOf(result);
}
/** Height-independent portion of the existing21 portico certificate. Failure is cached per entrance. */
private static int approachTop(Point point, Probe probe) {
Integer old = probe.approaches.get(point); if (old != null) return old;
int top = -1, low = 384;
for (int x = point.x() - 2; x <= point.x() + 2; x++) for (int z = point.z() - 11; z <= point.z() - 9; z++) {
int y = probe.surface(x, z); low = Math.min(low, y); top = Math.max(top, y);
}
boolean valid = low >= 0 && top <= 378 && top - low <= 1;
if (valid) for (int x = point.x() - 2; x <= point.x() + 2; x++) for (int z = point.z() - 11; z <= point.z() - 9; z++) {
int y = probe.surface(x, z);
if (probe.protectedAt(x, y, z) || probe.protectedAt(x, top + 1, z) || probe.protectedAt(x, top + 2, z)) valid = false;
}
if (valid) for (int x : new int[]{point.x() - 8, point.x(), point.x() + 8})
for (int z : new int[]{point.z() - 8, point.z(), point.z() + 8}) {
int y = probe.surface(x, z); if (y < top - 24 || y > top + 16) valid = false;
}
probe.approaches.put(point, valid ? top : -1);
return valid ? top : -1;
}
private record Choice(List<Module> modules, int cost, String label) {}
private record Point(int x, int z) {}
private static List<Point> alignment(int cx, int cz, int radius, int shift, int shape, boolean alongX) {
var points = new ArrayList<Point>();
if (shape < 6) {
int offset = (shape / 2 + 1) * 34 * ((shape & 1) == 0 ? -1 : 1);
points.add(new Point(-radius, 0)); line(points, -radius, offset); line(points, radius, offset); line(points, radius, 0);
} else {
int offset = 68;
points.add(new Point(-radius, -offset));
if (shape == 6) line(points, radius, -offset); else line(points, -radius, offset);
line(points, radius, offset);
}
return points.stream().map(p -> new Point(cx + (alongX ? p.x() : p.z() + shift),
cz + (alongX ? p.z() + shift : p.x()))).toList();
}
private static void line(List<Point> points, int x, int z) {
var p = points.getLast(); int dx = Integer.signum(x - p.x()) * 17, dz = Integer.signum(z - p.z()) * 17;
while (p.x() != x || p.z() != z) { p = new Point(p.x() + dx, p.z() + dz); points.add(p); }
}
private static int direction(Point from, Point to) {
return from.x() == to.x() ? to.z() > from.z() ? 2 : 0 : to.x() > from.x() ? 1 : 3;
}
private static List<Module> candidate(long seed, List<Point> points, int floor, Probe probe) {
// A sparse route-body test rejects a wrong alignment before full shafts and slabs are sampled.
for (var p : points) for (int dx : new int[]{-4, 0, 4}) for (int dz : new int[]{-4, 0, 4})
for (int y : new int[]{floor - 2, floor, floor + 4, floor + 8}) if (probe.protectedAt(p.x() + dx, y, p.z() + dz)) {
probe.reason = "protected alignment [" + p.x() + "," + y + "," + p.z() + "]"; return null;
}
var first = points.getFirst(); var last = points.getLast();
var entrance = access(first.x(), first.z(), floor, 1 << direction(first, points.get(1)), seed ^ 0x2201L, probe, false);
if (entrance == null) return null;
var exit = access(last.x(), last.z(), floor, 1 << direction(last, points.get(points.size() - 2)), seed ^ 0x2202L, probe, false);
if (exit == null) return null;
var result = new ArrayList<Module>(); result.add(entrance); int unanchored = 0;
for (int i = 1; i < points.size() - 1; i++) {
var p = points.get(i); int a = direction(p, points.get(i - 1)), b = direction(p, points.get(i + 1));
boolean bend = ((a + 2) & 3) != b, alongX = a == 1 || a == 3;
int width = bend || alongX ? 17 : 9, depth = bend || !alongX ? 17 : 9;
var module = certify(new Module(bend ? Kind.JUNCTION : Kind.GALLERY, p.x() - width / 2, floor - 1,
p.z() - depth / 2, width, 10, depth, alongX ? 1 : 0, (1 << a) | (1 << b),
seed ^ (i * 0x9E3779B97F4A7C15L), List.of()), probe, false);
if (module == null) return null;
unanchored = probe.bearings.get(module) == 15 ? 0 : unanchored + 1;
if (unanchored > 2) { probe.reason = "unsupported detour span exceeds 34 blocks"; return null; }
result.add(module);
}
result.add(exit);
return List.copyOf(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 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.transit22") || Boolean.getBoolean("sanctuary.transit.plannerDiagnostics"))
System.getLogger(TransitRoute22.class.getName()).log(System.Logger.Level.INFO, "TransitRoute22 " + 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<>();
final java.util.Map<Point, Integer> approaches = 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,33 @@
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.rootGeneration() != 22) || generator.rootGeneration() != 22 && !generator.experimentalStructures()) return Optional.empty();
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
if (island == null || !island.origin() || (island.generation() != 21 && island.generation() != 22)) 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(generator.rootGeneration() == 22
&& module.kind() == TransitPlan21.Kind.JUNCTION && Integer.bitCount(module.openings()) == 2
? new TransitPiece22(module) : new TransitPiece21(module)))));
}
@Override public StructureType<?> type() { return TransitStructures21.TYPE; }
}
@@ -0,0 +1,87 @@
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);
public static final StructurePieceType PIECE22 = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
SanctuaryMod.id("industrial_transit_module_v22"), TransitPiece22::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 && generator.rootGeneration() != 22)
|| generator.rootGeneration() != 22 && !sanctuary.experimentalStructures()) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || (island.generation() != 21 && island.generation() != 22)) 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.rootGeneration() != 22) || generator.rootGeneration() != 22 && !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,27 @@
"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.underground_hall.entry": "Underground hall entrance: %s, %s, %s",
"sanctuary.world_options.heritage22_hint": "Adds experimental buildings and networks. The main traversal and underground hall belong to the island and remain without this option.",
"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,27 @@
"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.underground_hall.entry": "Accès à la salle souterraine : %s, %s, %s",
"sanctuary.world_options.heritage22_hint": "Ajoute les bâtiments et réseaux expérimentaux. La grande traversée et la salle souterraine font partie de l’île et restent présentes sans cette option.",
"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
}
}
]
}
]
}
]
}

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