feat: add detailed architecture for Sanctuary alpha20
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-10 01:18:31 +02:00
parent 2facf6f722
commit 0edac29dd8
50 changed files with 6880 additions and 70 deletions
+26
View File
@@ -1,5 +1,31 @@
# 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.
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,
+20 -5
View File
@@ -15,11 +15,26 @@ infrastructures qui permettront progressivement d'ouvrir de nouveaux continents.
Ce dépôt démarre la nouvelle base Fabric. La [vision complète](docs/vision.md)
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.19 est publiée** :
quatorze types dinfrastructures abandonnées et des liaisons de fret rejoignent
la ville et ses réseaux souterrains 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.20** reprend
larchitecture des bâtiments, leurs palettes, les blocs sculptés et les raccords
des vitres. Les villes, infrastructures et réseaux gardent leurs implantations.
Voir le [catalogue illustré](docs/architecture-alpha20.md) et l’état du canal
packwiz dans [Distribution](docs/packwiz.md).
## Alpha.20 — identité des bâtiments
La passe reprend les six bâtiments de ville, les quatorze infrastructures,
les parcs et les sous-sols. Bois de chêne et de bouleau, granit, diorite et tuf
identifient les usages ; biseaux, baies, corniches, portiques et verrières
donnent du relief aux volumes. Les routes reçoivent des bordures de briques
de tuf et des blocs ciselés. Les vitres et grilles se raccordent entre chunks.
Créer un **nouveau monde Sanctuary, Moyen, graine 0**, avec **Structures
expérimentales** activées pour retrouver le témoin. Les anciens mondes gardent
leur génération. Le bouton natif **Personnaliser** et ses trois tailles restent
identiques. Voir le [catalogue illustré](docs/architecture-alpha20.md) et les
[essais alpha.20](docs/testing-alpha20.md). `check build assemblePack`, les essais natifs et les artefacts sont vérifiés ;
la publication du canal et la synchronisation Prism sont en préparation.
## Alpha.19 — anciennes infrastructures
+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.
+29
View File
@@ -750,6 +750,35 @@ 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 : code et artefacts vérifiés ; publication en préparation.** Voir
[le contrat et les essais alpha.20](testing-alpha20.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.7 MiB

+323
View File
@@ -0,0 +1,323 @@
# Alpha.20 — identité architecturale
**État : code, essais natifs et artefacts vérifiés ; publication et synchronisation Prism en préparation.**
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 | À faire |
`./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`.
La publication et les deux synchronisations de la même instance restent à
consigner. Aucun essai client visuel ni essai Windows natif na été effectué.
+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.19
pack_version=0.1.0-alpha.19
mod_version=0.1.0-alpha.20
pack_version=0.1.0-alpha.20
maven_group=fr.koka.sanctuary
+41 -2
View File
@@ -40,13 +40,15 @@ 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' : providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false) ? '17' : '19'))
providers.gradleProperty('sanctuaryLostCityTests').map { it.toBoolean() }.getOrElse(false) ? '14' : providers.gradleProperty('sanctuaryLostCity15Tests').map { it.toBoolean() }.getOrElse(false) ? '15' : providers.gradleProperty('sanctuaryLostCity16Tests').map { it.toBoolean() }.getOrElse(false) ? '16' : providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false) ? '17' : providers.gradleProperty('sanctuaryHeritage19Tests').map { it.toBoolean() }.getOrElse(false) ? '19' : '20'))
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'))
@@ -70,12 +72,16 @@ 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 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: heritage19 ? 'fr.koka.sanctuary.gametest.Heritage19WorldGameTests' : ruins17 ? 'fr.koka.sanctuary.gametest.Ruins17WorldGameTests' : voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark
expand(gametest_entrypoint: 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')
@@ -429,3 +435,36 @@ tasks.named('check') { dependsOn('lostCityFoundation16Smoke') }
}
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)
}
@@ -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,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);
}
}
@@ -211,7 +211,7 @@ public final class PopulationDiagnostics {
void prepare() throws IOException {
boolean heritage = level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator sanctuary
&& sanctuary.rootGeneration() == 19 && sanctuary.experimentalStructures();
&& (sanctuary.rootGeneration() == 19 || sanctuary.rootGeneration() == 20) && 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.
@@ -48,8 +48,11 @@ final class UnifiedWorldSettingsDiagnostics {
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() == 19 && generator.source().generation() == 19 && generator.experimentalStructures(), "The public preset uses generation19 with its new-world option");
helper.assertTrue(generator.rootGeneration() == 20 && generator.source().generation() == 20 && generator.experimentalStructures(), "The public preset uses generation20 with its new-world option");
helper.assertTrue(generator.initialDiameter() == 724, "The public preset defaults to Medium / 724 blocks");
for (var tag : List.of(WorldPresetTags.NORMAL, WorldPresetTags.EXTENDED)) {
var visible = presets.getOrThrow(tag).stream().map(holder -> holder.getRegisteredName())
@@ -59,11 +62,11 @@ final class UnifiedWorldSettingsDiagnostics {
}
var ops = RegistryOps.create(JsonOps.INSTANCE, access);
var activeCodec = generator.rootGeneration() == 19 ? SanctuaryChunkGenerator.HERITAGE19_CODEC : generator.rootGeneration() == 17 ? SanctuaryChunkGenerator.RUINS17_CODEC : generator.rootGeneration() == 16 ? SanctuaryChunkGenerator.CITY16_CODEC : generator.rootGeneration() == 15 ? SanctuaryChunkGenerator.CITY15_CODEC : generator.rootGeneration() == 14 ? SanctuaryChunkGenerator.CITY_CODEC : SanctuaryChunkGenerator.CODEC;
var activeCodec = generator.rootGeneration() == 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(),
"Generation19 omitted experimental option defaults to enabled");
"Generation20 omitted experimental option defaults to enabled");
helper.assertTrue(!((SanctuaryChunkGenerator) historical16.overworld()).withExperimentalStructures(true).experimentalStructures(),
"The new-world switch cannot activate experimental generation in a historical16 generator");
serialized.remove("initial_diameter");
@@ -21,7 +21,8 @@ public abstract class SanctuaryGameTestServerMixin {
// generation test's deadline before its IO/workers can complete. This is test-only pacing.
@Inject(method = "waitUntilNextTick", at = @At("RETURN"))
private void sanctuary$paceExpansionWorkers(CallbackInfo ci) {
if (Boolean.getBoolean("sanctuary.test.heritage19") || Boolean.getBoolean("sanctuary.test.ruins17") || Boolean.getBoolean("sanctuary.test.expansion") || Boolean.getBoolean("sanctuary.test.lostCity")
if (Boolean.getBoolean("sanctuary.test.heritage20") || Boolean.getBoolean("sanctuary.test.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", 19);
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19) throw new IllegalArgumentException("Unsupported test generation");
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 19 ? "sanctuary" : "sanctuary_v" + generation));
int generation = Integer.getInteger("sanctuary.test.generation", 20);
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19 && generation != 20) throw new IllegalArgumentException("Unsupported test generation");
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 20 ? "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);
@@ -63,6 +63,8 @@ public final class SanctuaryMod implements ModInitializer {
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);
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);
@@ -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 || generator.rootGeneration() == 19) {
if (generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20) {
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(generator.rootGeneration() == 19 ? "sanctuary.world_options.heritage_hint" : "sanctuary.world_options.experimental_hint"), font)
contents.addChild(new MultiLineTextWidget(Component.translatable((generator.rootGeneration() == 19 || generator.rootGeneration() == 20) ? "sanctuary.world_options.heritage_hint" : "sanctuary.world_options.experimental_hint"), font)
.setMaxWidth(310).setCentered(true));
}
layout.addToContents(contents);
@@ -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, 17 or 19 expansion");
if (climate.equals("lost_city") && (!spatialGeneration(generation) || id.equals("sanctuary"))) throw new IllegalArgumentException("Lost City requires a generation 14, 15, 16, 17, 19 or 20 expansion");
if (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 || generation == 19; }
public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16 || generation == 17 || generation == 19 || generation == 20; }
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(); }
@@ -41,7 +41,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 || generator.rootGeneration() == 19)
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20)
&& fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
&& generator.protectsCityDecoration(pos)) return;
if (generator.protectsDecoration(pos) && !region.getBlockState(pos).equals(state)) {
@@ -46,6 +46,10 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
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)));
private final List<Holder<Biome>> allBiomes;
private final PopulationIslandBiomeSource population;
private final boolean cities;
@@ -79,7 +83,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
cityPlans = java.util.Map.copyOf(plans);
}
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; }
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 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);
@@ -51,6 +51,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
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)));
private final boolean experimentalStructures;
private volatile fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17 = fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.EMPTY;
private boolean ruinsPrepared;
@@ -91,12 +97,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 || generation == 19) && experimental;
this.experimentalStructures = (generation == 17 || generation == 19 || generation == 20) && 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 == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; }
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 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);
}
@@ -107,8 +113,8 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
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 || generation == 19) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| generation == 19 && heritagePlan19.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ);
return (generation == 17 || generation == 19 || generation == 20) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 19 || generation == 20) && heritagePlan19.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ);
}
public IslandCapacity capacity() {
return switch (initialDiameter()) {
@@ -161,7 +167,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
source().setCityPlans(java.util.Map.of());
return;
}
if ((generation == 16 || generation == 17 || generation == 19) && !cityStructuresEnabled) {
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20) && !cityStructuresEnabled) {
cityProtection16 = fr.koka.sanctuary.worldgen.city.LostCityProtection16.EMPTY;
activeCityPlans = java.util.Map.of();
source().setCityPlans(java.util.Map.of());
@@ -177,7 +183,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
}
cityProtection15 = protection.build();
}
if (generation == 16 || generation == 17 || generation == 19) {
if (generation == 16 || generation == 17 || generation == 19 || generation == 20) {
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
for (var island : islands) {
protection.add(published.get(island.id()), undergroundPlan16(island));
@@ -192,7 +198,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
source().setCityPlans(published);
}
private void prepareRuins(List<ExpansionIsland> islands) {
if ((generation != 17 && generation != 19) || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return;
if ((generation != 17 && generation != 19 && generation != 20) || !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(),
@@ -205,7 +211,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
root.seed(), plan.networks().size(), plan.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000);
}
private void prepareHeritage(List<ExpansionIsland> islands) {
if (generation != 19 || !experimentalStructures || !cityStructuresEnabled || heritagePrepared) return;
if ((generation != 19 && generation != 20) || !experimentalStructures || !cityStructuresEnabled || heritagePrepared) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var terrain = context(root);
var sampled = new fr.koka.sanctuary.worldgen.heritage.HeritageTerrain19(root, terrain.generator(), terrain.random(),
@@ -245,7 +251,7 @@ 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 || generation == 19) {
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(),
@@ -316,9 +322,9 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
});
}
public boolean protectsCityDecoration(net.minecraft.core.BlockPos pos) {
if (generation == 16 || generation == 17 || generation == 19) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 17 || generation == 19) && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ())
|| generation == 19 && heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ());
if (generation == 16 || generation == 17 || generation == 19 || generation == 20) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 17 || generation == 19 || generation == 20) && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 19 || generation == 20) && heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ());
if (generation == 15) return cityProtection15.contains(pos.getX(), pos.getY(), pos.getZ());
if (generation != 14) return false;
var island = islandAt(pos.getX(), pos.getZ());
@@ -340,20 +346,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 || generation == 19) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
return (generation == 16 || generation == 17 || generation == 19 || generation == 20) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
}
public fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan undergroundPlan16(ExpansionIsland island) {
if (generation != 16 && generation != 17 && generation != 19) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
if (generation != 16 && generation != 17 && generation != 19 && generation != 20) 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 && generation != 19) return List.of();
if (generation != 16 && generation != 17 && generation != 19 && generation != 20) return List.of();
lostCityPlan(island);
return citySupports16.getOrDefault(cityIdentity(island), List.of());
}
public LostCityPlanner16.Fallback cityReservation16(ExpansionIsland island) {
if (generation != 16 && generation != 17 && generation != 19) return null;
if (generation != 16 && generation != 17 && generation != 19 && generation != 20) return null;
lostCityPlan(island);
return cityReservations16.get(cityIdentity(island));
}
@@ -365,9 +371,9 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
net.minecraft.core.BlockPos origin, int radius, boolean skipReferenced) {
if (structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.city.LostCityStructures.KEY))
return fr.koka.sanctuary.worldgen.city.LostCityStructures.findNearest(level, structures, origin, radius, skipReferenced);
if ((generation == 17 || generation == 19) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.KEY))
if ((generation == 17 || generation == 19 || generation == 20) && 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 && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.KEY))
if ((generation == 19 || generation == 20) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.KEY))
return fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.findNearest(level, structures, origin, radius, skipReferenced);
return super.findNearestMapStructure(level, structures, origin, radius, skipReferenced);
}
@@ -396,7 +402,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 || generation == 19) && cityStructuresEnabled) {
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20) && 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,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());
}
}
@@ -29,17 +29,17 @@ public final class LostCityStructure extends Structure {
throw new IllegalStateException("Lost City district exceeds native structure reference range");
}
return Optional.of(new GenerationStub(LostCityStructures.locator(district), pieces -> {
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19) && generator.cityReservation16(island) != null)
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20) && generator.cityReservation16(island) != null)
pieces.addPiece(new LostCityFoundationPiece16(generator.cityReservation16(island), district.seed()));
if (generator.rootGeneration() == 15) for (var support : generator.citySupports(island)) {
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
pieces.addPiece(new LostCitySupportPiece15(support));
}
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19)) for (var support : generator.citySupports16(island)) {
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20)) 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 || generator.rootGeneration() == 19) ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
district.parcels().forEach(parcel -> pieces.addPiece(generator.rootGeneration() == 20 ? new LostCityPiece20(parcel) : (generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20) ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
? 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 +49,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 || generator.rootGeneration() == 19)) for (var network : generator.undergroundPlan16(island).networks()) {
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20)) 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 ? 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 ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
}
}));
}
@@ -40,8 +40,12 @@ 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 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,88 @@
package fr.koka.sanctuary.worldgen.city;
import fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.Half;
import net.minecraft.world.level.storage.loot.LootTable;
/** Architectural finish for the generation20 city undercroft. The certified generation16 geometry,
* stairs, wet tanks, outfalls and support positions are reused without changing their planning. */
public final class LostCityUnderground20 {
private LostCityUnderground20() {}
public static void render(LostCityUnderground16.Module module, LostCityRenderer.BlockWriter output) {
var buffer = ConnectedBlocks20.buffer(output);
LostCityUnderground16.render(module, new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) {
buffer.set(x, y, z, material(module, x, y, z, state));
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
buffer.chest(x, y, z, loot, seed);
}
});
if (module.kind() != LostCityUnderground16.Kind.ACCESS && module.kind() != LostCityUnderground16.Kind.GARAGE) {
int y = module.height() - 2;
Block molding = module.kind() == LostCityUnderground16.Kind.POOLROOM ? Blocks.POLISHED_DIORITE_STAIRS
: module.kind() == LostCityUnderground16.Kind.EXPANSION_HALL ? Blocks.OAK_STAIRS : Blocks.TUFF_BRICK_STAIRS;
// Coved edge beneath the high ceiling, with a complete external shell behind it.
// Doorway planes and floor circulation are left in their certified positions.
for (int x = 2; x < module.width() - 2; x++) {
buffer.set(x, y, 1, cove(molding, Direction.NORTH));
buffer.set(x, y, module.depth() - 2, cove(molding, Direction.SOUTH));
}
for (int z = 2; z < module.depth() - 2; z++) {
buffer.set(1, y, z, cove(molding, Direction.WEST));
buffer.set(module.width() - 2, y, z, cove(molding, Direction.EAST));
}
if (module.kind() == LostCityUnderground16.Kind.POOLROOM) {
for (int x : new int[] {8, 20, 34, 46}) {
// Buttress capitals continue the existing pool colonnade, then span as
// tall repeated ceiling ribs. The 50x25 pool and swimming volume remain open.
for (int z = 1; z < module.depth() - 1; z++)
buffer.set(x, y, z, (z == 1 || z == module.depth() - 2
? Blocks.CHISELED_TUFF_BRICKS : Blocks.POLISHED_DIORITE).defaultBlockState());
for (int z : new int[] {1, module.depth() - 2}) {
buffer.set(x - 1, y - 1, z, cove(Blocks.POLISHED_DIORITE_STAIRS, Direction.EAST));
buffer.set(x + 1, y - 1, z, cove(Blocks.POLISHED_DIORITE_STAIRS, Direction.WEST));
}
}
}
}
buffer.flush();
}
private static BlockState cove(Block block, Direction facing) {
return block.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing)
.setValue(BlockStateProperties.HALF, Half.TOP);
}
private static BlockState material(LostCityUnderground16.Module m, int x, int y, int z, BlockState state) {
if (state.is(Blocks.STONE) && (y <= 0 || m.kind() == LostCityUnderground16.Kind.GARAGE))
return Blocks.STONE_BRICKS.defaultBlockState();
boolean wall = x == 0 || z == 0 || x == m.width() - 1 || z == m.depth() - 1;
boolean masonry = state.is(Blocks.STONE_BRICKS) || state.is(Blocks.CRACKED_STONE_BRICKS)
|| state.is(Blocks.MOSSY_STONE_BRICKS);
if (wall && masonry && y > 2 && y < m.height() - 1) {
int along = x == 0 || x == m.width() - 1 ? z : x;
return switch (m.kind()) {
case POOLROOM -> (y == 7 ? Blocks.POLISHED_DIORITE : y == 8 && along % 6 == 0
? Blocks.CHISELED_TUFF_BRICKS : Blocks.CALCITE).defaultBlockState();
case EXPANSION_HALL -> (along % 5 == 0 ? Blocks.STRIPPED_OAK_WOOD
: y < 5 ? Blocks.TUFF_BRICKS : Blocks.SMOOTH_STONE).defaultBlockState();
case SEWER, SEWER_OUTLET -> (along % 6 == 0 ? Blocks.CHISELED_TUFF_BRICKS
: Blocks.TUFF_BRICKS).defaultBlockState();
case PASSAGE -> (y == 3 ? Blocks.POLISHED_GRANITE : Blocks.TUFF).defaultBlockState();
default -> state;
};
}
if (m.kind() == LostCityUnderground16.Kind.GARAGE && state.is(Blocks.SMOOTH_STONE)
&& y >= 1 && (y - 1) % 16 == 0 && (x <= 1 || z <= 1 || x >= m.width() - 2 || z >= m.depth() - 2))
return ((x + z) % 8 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS).defaultBlockState();
if (m.kind() == LostCityUnderground16.Kind.EXPANSION_HALL && state.is(Blocks.DARK_OAK_STAIRS))
return Blocks.OAK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING,
state.getValue(BlockStateProperties.HORIZONTAL_FACING));
return state;
}
}
@@ -0,0 +1,88 @@
package fr.koka.sanctuary.worldgen.city;
import java.util.ArrayList;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.resources.ResourceKey;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.loot.LootTable;
/** A saved underground module includes its openings and certified supports, including their anchors. */
public final class LostCityUndergroundPiece20 extends StructurePiece {
private final LostCityUnderground16.Module module;
public LostCityUndergroundPiece20(LostCityUnderground16.Module module) {
super(LostCityStructures.UNDERGROUND20, 0, bounds(module));
this.module = module;
setOrientation(null);
}
public LostCityUndergroundPiece20(StructurePieceSerializationContext context, CompoundTag tag) {
super(LostCityStructures.UNDERGROUND20, tag);
if (tag.getInt("SanctuarySchema").orElseThrow() != 1)
throw new IllegalArgumentException("Unsupported underground piece schema");
var supports = new ArrayList<LostCityUnderground16.Support>();
var saved = tag.getList("Supports").orElseThrow();
if (saved.size() > 128) throw new IllegalArgumentException("Too many underground supports");
for (var entry : saved) {
if (!(entry instanceof CompoundTag support)) throw new IllegalArgumentException("Invalid support");
supports.add(new LostCityUnderground16.Support(
LostCityUnderground16.Type.valueOf(support.getString("Type").orElseThrow()),
support.getInt("X").orElseThrow(), support.getInt("MinY").orElseThrow(),
support.getInt("Z").orElseThrow(), support.getInt("MaxY").orElseThrow()));
}
module = new LostCityUnderground16.Module(LostCityUnderground16.Kind.valueOf(tag.getString("Kind").orElseThrow()),
tag.getInt("MinX").orElseThrow(), tag.getInt("MinY").orElseThrow(), tag.getInt("MinZ").orElseThrow(),
tag.getInt("Width").orElseThrow(), tag.getInt("Height").orElseThrow(), tag.getInt("Depth").orElseThrow(),
tag.getInt("Openings").orElseThrow(), tag.getLong("ModuleSeed").orElseThrow(), supports);
if (!getBoundingBox().equals(bounds(module))) throw new IllegalArgumentException("Underground bounds disagree with saved geometry");
setOrientation(null);
}
public LostCityUnderground16.Module module() { return module; }
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
tag.putInt("SanctuarySchema", 1);
tag.putString("Kind", module.kind().name());
tag.putInt("MinX", module.minX()); tag.putInt("MinY", module.minY()); tag.putInt("MinZ", module.minZ());
tag.putInt("Width", module.width()); tag.putInt("Height", module.height()); tag.putInt("Depth", module.depth());
tag.putInt("Openings", module.openings()); tag.putLong("ModuleSeed", module.seed());
var supports = new ListTag();
for (var support : module.supports()) {
var value = new CompoundTag();
value.putString("Type", support.type().name()); value.putInt("X", support.x());
value.putInt("MinY", support.minY()); value.putInt("Z", support.z()); value.putInt("MaxY", support.maxY());
supports.add(value);
}
tag.put("Supports", supports);
}
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
LostCityStructures.placing(() -> LostCityUnderground20.render(module, new LostCityRenderer.BlockWriter() {
private BlockPos world(int x, int y, int z) { return new BlockPos(module.minX() + x, module.minY() + y, module.minZ() + z); }
@Override public void set(int x, int y, int z, BlockState state) {
var pos = world(x, y, z);
if (getBoundingBox().isInside(pos) && clip.isInside(pos)) placeBlock(level, state, pos.getX(), pos.getY(), pos.getZ(), clip);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
var pos = world(x, y, z);
if (getBoundingBox().isInside(pos) && clip.isInside(pos)) createChest(level, clip, RandomSource.create(seed), pos, table, null);
}
}));
}
public static BoundingBox bounds(LostCityUnderground16.Module module) {
return new BoundingBox(module.boundsMinX(), module.boundsMinY(), module.boundsMinZ(),
module.boundsMaxX(), module.boundsMaxY(), module.boundsMaxZ());
}
}
@@ -0,0 +1,170 @@
package fr.koka.sanctuary.worldgen.detail;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceKey;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.FenceBlock;
import net.minecraft.world.level.block.FenceGateBlock;
import net.minecraft.world.level.block.IronBarsBlock;
import net.minecraft.world.level.block.StairBlock;
import net.minecraft.world.level.block.WallBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.BooleanProperty;
import net.minecraft.world.level.block.state.properties.StairsShape;
import net.minecraft.world.level.block.state.properties.WallSide;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.level.storage.loot.LootTable;
import net.minecraft.world.phys.shapes.BooleanOp;
import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.VoxelShape;
/** Generation20 only: resolve the complete, already-rotated geometry before a native writer clips chunks.
* Unwritten neighbors are air. This never reads loaded terrain, changes saved older renderers or creates loot early. */
public final class ConnectedBlocks20 {
public static final int MAX_BLOCKS = 262_144;
private static final Direction[] SIDES = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
private static final VoxelShape POST_TEST = Block.column(2.0, 0.0, 16.0);
private static final Map<Direction, VoxelShape> WALL_TEST = Shapes.rotateHorizontal(Block.boxZ(2.0, 16.0, 0.0, 9.0));
private ConnectedBlocks20() {}
public static Buffer buffer(LostCityRenderer.BlockWriter destination) { return new Buffer(destination); }
public static final class Buffer implements LostCityRenderer.BlockWriter {
private final LostCityRenderer.BlockWriter destination;
private final Map<BlockPos, BlockState> blocks = new LinkedHashMap<>();
private final Map<BlockPos, Chest> chests = new LinkedHashMap<>();
private boolean flushed;
private Buffer(LostCityRenderer.BlockWriter destination) { this.destination = Objects.requireNonNull(destination); }
@Override public void set(int x, int y, int z, BlockState state) {
checkOpen(); var at = new BlockPos(x, y, z); admit(at);
blocks.put(at, Objects.requireNonNull(state)); chests.remove(at);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
checkOpen(); var at = new BlockPos(x, y, z); admit(at);
blocks.put(at, Blocks.CHEST.defaultBlockState()); chests.put(at, new Chest(Objects.requireNonNull(table), seed));
}
/** Final placement wins. Every connection sees all blocks even when the sink accepts only one chunk. */
public void flush() {
checkOpen(); flushed = true;
if (blocks.isEmpty()) return;
var view = new Geometry(blocks);
// Stair corners depend only on neighbors' facing and half, not on their previous corner shapes.
blocks.replaceAll((at, state) -> StairBlock.isStairs(state)
? state.setValue(StairBlock.SHAPE, stairsShape(state, view, at)) : state);
// A pane/fence above a wall changes the underside used for LOW/TALL. Resolve it first.
// Their horizontal wall connection uses the wall tag, independent of wall arm heights.
blocks.replaceAll((at, state) -> connected(state, view, at));
// Walls depend on the final underside and post of the wall above, so visit from top to bottom.
blocks.keySet().stream().filter(at -> blocks.get(at).getBlock() instanceof WallBlock)
.sorted(Comparator.<BlockPos>comparingInt(at -> at.getY()).reversed()).toList()
.forEach(at -> blocks.put(at, wall(blocks.get(at), view, at)));
try {
for (var entry : blocks.entrySet()) {
var at = entry.getKey(); var chest = chests.get(at);
if (chest == null) destination.set(at.getX(), at.getY(), at.getZ(), entry.getValue());
else destination.chest(at.getX(), at.getY(), at.getZ(), chest.table(), chest.seed());
}
} finally { blocks.clear(); chests.clear(); }
}
private void admit(BlockPos at) {
if (!blocks.containsKey(at) && blocks.size() >= MAX_BLOCKS)
throw new IllegalArgumentException("Generation20 connection buffer exceeded its per-piece block budget");
}
private void checkOpen() { if (flushed) throw new IllegalStateException("A generation20 geometry buffer can be flushed only once"); }
}
private record Chest(ResourceKey<LootTable> table, long seed) {}
private static BlockState connected(BlockState state, BlockGetter view, BlockPos at) {
if (!(state.getBlock() instanceof IronBarsBlock) && !(state.getBlock() instanceof FenceBlock)) return state;
for (var direction : SIDES) {
var neighborPos = at.relative(direction); var neighbor = view.getBlockState(neighborPos);
boolean sturdy = neighbor.isFaceSturdy(view, neighborPos, direction.getOpposite());
boolean connects = state.getBlock() instanceof IronBarsBlock pane ? pane.attachsTo(neighbor, sturdy)
: ((FenceBlock) state.getBlock()).connectsTo(neighbor, sturdy, direction.getOpposite());
state = state.setValue(property(direction), connects);
}
return state;
}
/** WallBlock's private MC26.3-pre-2 LOW/TALL/UP calculation, using the same native shapes. */
private static BlockState wall(BlockState state, BlockGetter view, BlockPos at) {
var abovePos = at.above(); var above = view.getBlockState(abovePos);
var support = above.getCollisionShape(view, abovePos).getFaceShape(Direction.DOWN);
for (var direction : SIDES) {
var neighborPos = at.relative(direction); var neighbor = view.getBlockState(neighborPos);
boolean sturdy = neighbor.isFaceSturdy(view, neighborPos, direction.getOpposite());
// Pane's predicate shares walls' solid/exception, pane and wall-tag rules; walls also accept gates.
boolean joins = ((IronBarsBlock) Blocks.IRON_BARS).attachsTo(neighbor, sturdy)
|| neighbor.getBlock() instanceof FenceGateBlock
&& FenceGateBlock.connectsToDirection(neighbor, direction.getOpposite());
state = state.setValue(WallBlock.PROPERTY_BY_DIRECTION.get(direction), !joins ? WallSide.NONE
: covered(support, WALL_TEST.get(direction)) ? WallSide.TALL : WallSide.LOW);
}
return state.setValue(WallBlock.UP, raisePost(state, above, support));
}
private static boolean raisePost(BlockState state, BlockState above, VoxelShape support) {
if (above.getBlock() instanceof WallBlock && above.getValue(WallBlock.UP)) return true;
var n = state.getValue(WallBlock.NORTH); var s = state.getValue(WallBlock.SOUTH);
var w = state.getValue(WallBlock.WEST); var e = state.getValue(WallBlock.EAST);
boolean noN = n == WallSide.NONE, noS = s == WallSide.NONE;
boolean noW = w == WallSide.NONE, noE = e == WallSide.NONE;
if (noN && noS && noW && noE || noN != noS || noW != noE) return true;
if (n == WallSide.TALL && s == WallSide.TALL || e == WallSide.TALL && w == WallSide.TALL) return false;
return above.is(BlockTags.WALL_POST_OVERRIDE) || covered(support, POST_TEST);
}
private static boolean covered(VoxelShape support, VoxelShape test) {
return !Shapes.joinIsNotEmpty(test, support, BooleanOp.ONLY_FIRST);
}
private static BooleanProperty property(Direction direction) {
return switch (direction) {
case NORTH -> BlockStateProperties.NORTH; case EAST -> BlockStateProperties.EAST;
case SOUTH -> BlockStateProperties.SOUTH; case WEST -> BlockStateProperties.WEST;
default -> throw new IllegalArgumentException("Connections are horizontal");
};
}
/** Matches MC26.3-pre-2 StairBlock's private shape selection, inspected in its native bytecode.
* Panes and fences call the native connection predicates directly, including tags and exceptions. */
private static StairsShape stairsShape(BlockState state, BlockGetter view, BlockPos at) {
var facing = state.getValue(StairBlock.FACING);
var front = view.getBlockState(at.relative(facing));
if (StairBlock.isStairs(front) && state.getValue(StairBlock.HALF) == front.getValue(StairBlock.HALF)) {
var other = front.getValue(StairBlock.FACING);
if (other.getAxis() != facing.getAxis() && canTakeShape(state, view, at, other.getOpposite()))
return other == facing.getCounterClockWise() ? StairsShape.OUTER_LEFT : StairsShape.OUTER_RIGHT;
}
var back = view.getBlockState(at.relative(facing.getOpposite()));
if (StairBlock.isStairs(back) && state.getValue(StairBlock.HALF) == back.getValue(StairBlock.HALF)) {
var other = back.getValue(StairBlock.FACING);
if (other.getAxis() != facing.getAxis() && canTakeShape(state, view, at, other))
return other == facing.getCounterClockWise() ? StairsShape.INNER_LEFT : StairsShape.INNER_RIGHT;
}
return StairsShape.STRAIGHT;
}
private static boolean canTakeShape(BlockState state, BlockGetter view, BlockPos at, Direction direction) {
var neighbor = view.getBlockState(at.relative(direction));
return !StairBlock.isStairs(neighbor) || neighbor.getValue(StairBlock.FACING) != state.getValue(StairBlock.FACING)
|| neighbor.getValue(StairBlock.HALF) != state.getValue(StairBlock.HALF);
}
private static final class Geometry implements BlockGetter {
private final Map<BlockPos, BlockState> blocks;
private final int low, high;
Geometry(Map<BlockPos, BlockState> blocks) {
this.blocks = blocks;
low = blocks.keySet().stream().mapToInt(at -> at.getY()).min().orElse(0);
high = blocks.keySet().stream().mapToInt(at -> at.getY()).max().orElse(0);
}
@Override public BlockState getBlockState(BlockPos at) { return blocks.getOrDefault(at, Blocks.AIR.defaultBlockState()); }
@Override public FluidState getFluidState(BlockPos at) { return getBlockState(at).getFluidState(); }
@Override public BlockEntity getBlockEntity(BlockPos at) { return null; }
@Override public int getHeight() { return Math.addExact(Math.subtractExact(high, low), 1); }
@Override public int getMinY() { return low; }
}
}
@@ -19,7 +19,7 @@ public final class HeritageCommands19 {
var level = source.getLevel();
if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 19 || !generator.experimentalStructures()) {
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20) || !generator.experimentalStructures()) {
source.sendFailure(Component.translatable("sanctuary.heritage.unavailable"));
return 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 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,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);
}
}
}
@@ -14,18 +14,18 @@ public final class HeritageStructure19 extends Structure {
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 19 || !generator.experimentalStructures()) return Optional.empty();
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20) || !generator.experimentalStructures()) return Optional.empty();
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
if (island == null || !island.origin() || island.generation() != 19) return Optional.empty();
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<HeritagePiece19>();
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(new HeritagePiece19(segment));
pieces.add(generator.rootGeneration() == 20 ? new HeritagePiece20(segment) : new HeritagePiece19(segment));
for (var site : plan.sites()) if (site.startChunkX() == x && site.startChunkZ() == z)
pieces.add(new HeritagePiece19(site));
pieces.add(generator.rootGeneration() == 20 ? new HeritagePiece20(site) : new HeritagePiece19(site));
if (pieces.isEmpty()) return Optional.empty();
for (var piece : pieces) {
var box = piece.getBoundingBox();
@@ -35,7 +35,7 @@ public final class HeritageStructure19 extends Structure {
|| 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(HeritagePiece19::isSite).findFirst().orElse(pieces.getFirst()).getLocatorPosition();
var locator = pieces.stream().filter(piece -> piece instanceof HeritagePiece19 old && old.isSite() || piece instanceof HeritagePiece20 detail && detail.isSite()).findFirst().orElse(pieces.getFirst()).getLocatorPosition();
return Optional.of(new GenerationStub(locator, builder -> pieces.forEach(builder::addPiece)));
}
@Override public StructureType<?> type() { return HeritageStructures19.TYPE; }
@@ -35,6 +35,8 @@ public final class HeritageStructures19 {
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 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() {}
@@ -53,7 +55,7 @@ public final class HeritageStructures19 {
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 instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 19 && generator.rootGeneration() != 20)
|| !sanctuary.experimentalStructures()) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
@@ -75,7 +77,7 @@ public final class HeritageStructures19 {
BlockPos origin, int radius, boolean skipReferenced) {
if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 19 || !generator.experimentalStructures()
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20) || !generator.experimentalStructures()
|| !level.structureManager().shouldGenerateStructures()) return null;
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
if (!structures.contains(holder)) return null;
@@ -96,8 +98,8 @@ public final class HeritageStructures19 {
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()).findFirst().orElseThrow();
var entrance = start.getPieces().stream().filter(p -> (p instanceof HeritagePiece19 site && site.isSite())
|| (p instanceof HeritagePiece20 detail && detail.isSite())).findFirst().orElseThrow();
return Pair.of(entrance.getLocatorPosition(), holder);
}
return null;
@@ -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,7 +13,7 @@ public final class RuinsStructure17 extends Structure {
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19) || !generator.experimentalStructures()) return Optional.empty();
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20) || !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 networks = generator.ruinsPlan17().networks().stream()
@@ -30,9 +30,9 @@ public final class RuinsStructure17 extends Structure {
return Optional.of(new GenerationStub(RuinsStructures17.locator(networks.getFirst()), pieces -> {
// 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 ? 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 ? 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,7 +55,7 @@ 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.rootGeneration() != 19)
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20)
|| !sanctuary.experimentalStructures()) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) 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.rootGeneration() != 19) || !generator.experimentalStructures()
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20) || !generator.experimentalStructures()
|| !level.structureManager().shouldGenerateStructures()) return null;
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
if (!structures.contains(holder)) return null;
@@ -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;
@@ -144,7 +144,7 @@
"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 Sanctuary alpha.19 world with experimental structures enabled.",
"sanctuary.heritage.unavailable": "This survey requires a Sanctuary alpha.19 or alpha.20 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"
}
@@ -144,7 +144,7 @@
"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 alpha.19 avec les structures expérimentales activées.",
"sanctuary.heritage.unavailable": "Ce relevé nécessite un monde Sanctuary alpha.19 ou alpha.20 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"
}
@@ -3,7 +3,7 @@
"minecraft:overworld": {
"type": "sanctuary:sanctuary_384",
"generator": {
"type": "sanctuary:island_v19",
"type": "sanctuary:island_v20",
"biome_source": {
"biomes": [
"minecraft:the_void",
@@ -0,0 +1,81 @@
{
"dimensions": {
"minecraft:overworld": {
"type": "sanctuary:sanctuary_384",
"generator": {
"type": "sanctuary:island_v19",
"biome_source": {
"biomes": [
"minecraft:the_void",
"minecraft:forest",
"minecraft:birch_forest",
"minecraft:flower_forest",
"minecraft:plains",
"minecraft:meadow",
"minecraft:taiga",
"minecraft:snowy_taiga",
"minecraft:snowy_plains",
"minecraft:grove",
"minecraft:ice_spikes",
"minecraft:frozen_peaks",
"minecraft:old_growth_spruce_taiga",
"minecraft:dark_forest",
"minecraft:swamp",
"minecraft:pale_garden",
"minecraft:jungle",
"minecraft:sparse_jungle",
"minecraft:bamboo_jungle",
"minecraft:mangrove_swamp",
"minecraft:savanna",
"minecraft:savanna_plateau",
"minecraft:desert",
"minecraft:badlands",
"minecraft:eroded_badlands",
"minecraft:wooded_badlands",
"minecraft:windswept_hills",
"minecraft:windswept_gravelly_hills",
"minecraft:stony_peaks",
"minecraft:dappled_forest",
"minecraft:cherry_grove",
"minecraft:lush_caves",
"minecraft:dripstone_caves",
"minecraft:sulfur_caves",
"sanctuary:population_oak_forest",
"sanctuary:population_birch_forest",
"sanctuary:population_clearing",
"sanctuary:population_dry_woodland",
"sanctuary:population_rocky_heath",
"sanctuary:population_dark_grove",
"sanctuary:population_bamboo_grove",
"sanctuary:population_sulfur_depths",
"sanctuary:population_lush_caves",
"sanctuary:population_dripstone_caves",
"sanctuary:lost_city"
]
},
"initial_diameter": 724
}
},
"minecraft:the_end": {
"type": "minecraft:the_end",
"generator": {
"type": "minecraft:noise",
"biome_source": {
"type": "minecraft:the_end"
},
"settings": "minecraft:end"
}
},
"minecraft:the_nether": {
"type": "minecraft:the_nether",
"generator": {
"type": "minecraft:noise",
"biome_source": {
"type": "minecraft:multi_noise",
"preset": "minecraft:nether"
},
"settings": "minecraft:nether"
}
}
}
}
@@ -173,6 +173,22 @@ public final class ExpansionJournalSmoke {
require(Arrays.equals(heritageBytes, Files.readAllBytes(heritageDirectory.resolve(ExpansionJournal.FILE_NAME)))
&& Arrays.equals(experimentBytes, Files.readAllBytes(experimentDirectory.resolve(ExpansionJournal.FILE_NAME))),
"Rejected migrations preserve both generation17 and19 journals");
var architectureRoot = ExpansionIsland.origin(724, SEED, 20);
var architectureDirectory = scratch.resolve("architecture-20");
var architectureJournal = new ExpansionJournal(architectureDirectory, SEED, 724, 20);
var architectureChild = ExpansionPlacement.candidates(SEED, List.of(architectureRoot), "architecture", "sanctuary", "east", "lost_city", 128, "natural", 0).getFirst()
.withReuseToken("a".repeat(64));
architectureJournal.commit(List.of(architectureRoot, architectureChild));
require(new ExpansionJournal(architectureDirectory, SEED, 724, 20).islands().equals(List.of(architectureRoot, architectureChild)),
"Generation20 and its expansions retain an independent journal on reload");
byte[] architectureBytes = Files.readAllBytes(architectureDirectory.resolve(ExpansionJournal.FILE_NAME));
reject(() -> new ExpansionJournal(architectureDirectory, SEED, 724, 19), "No implicit downgrade from20 to19");
reject(() -> new ExpansionJournal(heritageDirectory, SEED, 724, 20), "No implicit upgrade from19 to20");
reject(() -> architectureJournal.commit(List.of(architectureRoot, heritageChild)), "No generation19 child in20");
reject(() -> heritageJournal.commit(List.of(heritageRoot, architectureChild)), "No generation20 child in19");
require(Arrays.equals(architectureBytes, Files.readAllBytes(architectureDirectory.resolve(ExpansionJournal.FILE_NAME)))
&& Arrays.equals(heritageBytes, Files.readAllBytes(heritageDirectory.resolve(ExpansionJournal.FILE_NAME))),
"Cross-generation rejection preserves published19 and new20 journals");
try {
ExpansionPlacement.candidates(SEED, List.of(ExpansionIsland.origin(724, SEED, 13)), "city", "sanctuary", "east", "lost_city", 256, "natural", 0);
throw new AssertionError("Lost City must not silently activate in a saved alpha.13 world");
@@ -0,0 +1,451 @@
package fr.koka.sanctuary.worldgen.city;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Kind;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Parcel;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.EmptyBlockGetter;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.DoorBlock;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.StairBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BedPart;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
import net.minecraft.world.level.storage.loot.LootTable;
import net.minecraft.world.phys.AABB;
/** Exercises final block geometry rather than reconstructing the painter's room allocation. */
public final class LostCityInteriors20Smoke {
private static final Kind[] KINDS = {Kind.APARTMENT, Kind.HOTEL, Kind.LIBRARY,
Kind.RESTAURANT, Kind.BARRACKS, Kind.HALL};
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
public static void main(String[] args) {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
run();
System.out.println("LostCityInteriors20Smoke: six building uses, alternating physical floor plans, four rotations, connected rooms, fully enclosed bathrooms, wall-backed beds with lateral access, native door/seat geometry, supported lanterns and deferred loot passed.");
}
/** Also usable after the engine has already bootstrapped its registries. */
public static void run() {
Set<Map<BlockPos, BlockState>> distinct = new HashSet<>();
Set<Set<BlockPos>> distinctVolumes = new HashSet<>();
var previews = new java.util.ArrayList<Object>();
for (Kind kind : KINDS) for (int floors : new int[]{2, 3, 5}) for (long seed : new long[]{0, 43}) {
Capture canonical = render(parcel(kind, floors, seed, 0));
if (floors == 2 && seed == 0) {
distinct.add(canonical.blocks);
Set<BlockPos> volume = new HashSet<>();
canonical.blocks.forEach((at, state) -> { if (!state.isAir() && at.getY() > 0
&& (at.getY() > 10 || at.getX() == 0 || at.getX() == 16 || at.getZ() == 0 || at.getZ() == 16)) volume.add(at); });
require(distinctVolumes.add(volume), kind + " only recolors an existing facade/roof volume");
}
if (floors == 3 && seed == 43 && System.getProperty("sanctuary.city20.preview") != null) {
var blocks = new java.util.ArrayList<Object>();
canonical.blocks.forEach((at, state) -> { if (!state.isAir()) blocks.add(java.util.List.of(at.getX(), at.getY(), at.getZ(), state.toString())); });
previews.add(Map.of("kind", kind.name(), "seed", seed, "floors", floors, "facing", 0, "blocks", blocks));
}
for (int facing = 0; facing < 4; facing++) {
Parcel parcel = parcel(kind, floors, seed, facing);
Capture result = facing == 0 ? canonical : render(parcel);
Capture repeated = render(parcel);
require(result.blocks.equals(repeated.blocks) && result.loot.equals(repeated.loot), "Saved parcel is not deterministic");
rotationMatches(canonical, result, facing);
require(result.blocks.values().stream().anyMatch(state -> state.is(Blocks.STONE_BRICKS)), "Missing stone-brick foundations");
require(result.blocks.values().stream().anyMatch(state -> state.is(Blocks.SMOOTH_STONE)), "Missing smooth stone");
require(result.blocks.values().stream().noneMatch(state -> state.is(Blocks.SPAWNER)
|| !state.getFluidState().isEmpty()), "Building introduced a spawner or free flowing fluid");
require(result.loot.size() == 1, "Building must keep one deferred loot chest");
var entrance = LostCityInteriors20.rooms(parcel).getFirst();
Set<BlockPos> reached = walk(result, position(entrance));
for (var room : LostCityInteriors20.rooms(parcel)) require(reached.contains(position(room)),
label(parcel) + " cannot walk from its entrance to " + room.name() + " at " + position(room));
verifyDoors(result, reached);
verifyBeds(result, reached);
verifyBathrooms(result, reached);
verifyFurnitureDirections(result);
verifyLanternSupports(result);
for (BlockPos chest : result.loot.keySet()) require(adjacentReached(chest, reached),
label(parcel) + " loot cannot be approached");
semantics(result);
if (facing == 0) verifyStoreyVariation(result);
verifyArchitecture(result);
}
}
require(distinct.size() == KINDS.length, "Building uses only labels to vary an identical interior");
boolean rejected = false;
try { LostCityInteriors20.rooms(new Parcel(Kind.PARK, 0, 0, 17, 17, 160, 2, 0, 0, 4, new int[0])); }
catch (IllegalArgumentException expected) { rejected = true; }
require(rejected, "Interior dispatch must reject non-building parcels");
String preview = System.getProperty("sanctuary.city20.preview");
if (preview != null) try {
var file = java.nio.file.Path.of(preview); if (file.getParent() != null) java.nio.file.Files.createDirectories(file.getParent());
java.nio.file.Files.writeString(file, new com.google.gson.Gson().toJson(previews));
} catch (java.io.IOException e) { throw new AssertionError("Cannot write requested architecture preview", e); }
}
private static Parcel parcel(Kind kind, int floors, long seed, int facing) {
return new Parcel(kind, -81, 119, 17, 17, 160, floors, facing, seed, 4, new int[0]);
}
private static Capture render(Parcel parcel) {
var result = new Capture(parcel);
LostCityInteriors20.render(parcel, result);
return result;
}
private static String label(Parcel p) { return p.kind() + " floors=" + p.floors() + " facing=" + p.facing() + " seed=" + p.seed(); }
private static BlockPos position(LostCityInteriors20.Room room) { return new BlockPos(room.x(), room.y(), room.z()); }
private static void rotationMatches(Capture canonical, Capture rotated, int facing) {
require(canonical.blocks.size() == rotated.blocks.size(), "Rotation changed the number of emitted positions");
Rotation rotation = switch (facing) {
case 0 -> Rotation.NONE; case 1 -> Rotation.CLOCKWISE_90;
case 2 -> Rotation.CLOCKWISE_180; default -> Rotation.COUNTERCLOCKWISE_90;
};
for (var entry : canonical.blocks.entrySet()) {
BlockPos target = rotate(entry.getKey(), facing);
// Native chest placement owns its final facing, so its loot position is checked separately.
if (canonical.loot.containsKey(entry.getKey())) continue;
require(rotated.block(target).equals(entry.getValue().rotate(rotation)), "Geometry or block-state rotation drifted at " + target);
}
for (var entry : canonical.loot.entrySet()) require(entry.getValue().equals(rotated.loot.get(rotate(entry.getKey(), facing))),
"Rotation changed deferred loot identity or position");
}
private static void verifyDoors(Capture result, Set<BlockPos> reached) {
long lowerDoors = 0, pairedLeaves = 0;
for (var entry : result.blocks.entrySet()) {
BlockState state = entry.getValue();
if (!(state.getBlock() instanceof DoorBlock)) continue;
require(state.is(Blocks.DARK_OAK_DOOR) && !state.getValue(BlockStateProperties.POWERED), "A room door cannot be opened by an ordinary player");
boolean lower = state.getValue(BlockStateProperties.DOUBLE_BLOCK_HALF) == DoubleBlockHalf.LOWER;
BlockPos partner = lower ? entry.getKey().above() : entry.getKey().below();
BlockState expected = state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF,
lower ? DoubleBlockHalf.UPPER : DoubleBlockHalf.LOWER);
require(result.block(partner).equals(expected), label(result.parcel) + " has an incomplete/mismatched door at " + entry.getKey());
if (!lower) continue;
lowerDoors++;
require(result.block(entry.getKey().below()).isSolid(), "Door has no solid threshold");
Direction normal = state.getValue(BlockStateProperties.HORIZONTAL_FACING);
require(reached.contains(entry.getKey().relative(normal)) && reached.contains(entry.getKey().relative(normal.getOpposite())),
label(result.parcel) + " door does not connect two walkable sides at " + entry.getKey());
for (Direction side : new Direction[]{normal.getClockWise(), normal.getCounterClockWise()}) {
BlockState neighbor = result.block(entry.getKey().relative(side));
if (!neighbor.is(Blocks.DARK_OAK_DOOR)
|| neighbor.getValue(BlockStateProperties.DOUBLE_BLOCK_HALF) != DoubleBlockHalf.LOWER
|| neighbor.getValue(BlockStateProperties.HORIZONTAL_FACING) != normal) continue;
AABB opened = state.setValue(BlockStateProperties.OPEN, true)
.getCollisionShape(EmptyBlockGetter.INSTANCE, entry.getKey()).bounds();
require(againstOppositeEdge(opened, side), label(result.parcel) + " double door opens into its partner at " + entry.getKey());
pairedLeaves++;
}
}
require(lowerDoors >= result.parcel.floors() * 3L + 2, "Missing actual doors between distinct rooms");
require(pairedLeaves >= 2, "Entrance must retain a double door with its hinges against the outside jambs");
for (var room : LostCityInteriors20.rooms(result.parcel)) if (room.name().endsWith("/stair_landing")) {
BlockPos landing = position(room);
boolean opensIntoStairwell = false;
for (Direction side : HORIZONTAL) {
BlockPos at = landing.relative(side);
BlockState state = result.block(at);
if (state.is(Blocks.DARK_OAK_DOOR)) opensIntoStairwell |=
at.relative(state.getValue(BlockStateProperties.HORIZONTAL_FACING)).equals(landing);
}
require(opensIntoStairwell, "Stairwell door must open into its landing, away from the central corridor");
}
}
private static boolean againstOppositeEdge(AABB box, Direction towardPartner) {
return switch (towardPartner) {
case EAST -> box.maxX <= .25; case WEST -> box.minX >= .75;
case SOUTH -> box.maxZ <= .25; case NORTH -> box.minZ >= .75;
default -> throw new AssertionError("Door partner must be horizontal");
};
}
private static void verifyFurnitureDirections(Capture result) {
int tableSeats = 0;
for (var entry : result.blocks.entrySet()) {
if (!entry.getValue().is(Blocks.DARK_OAK_FENCE)
|| !result.block(entry.getKey().above()).is(Blocks.DARK_OAK_PRESSURE_PLATE)) continue;
int aroundThisTable = 0;
for (Direction side : HORIZONTAL) {
BlockPos chair = entry.getKey().relative(side);
if (!result.block(chair).is(Blocks.DARK_OAK_STAIRS)) continue;
seatLooksToward(result, chair, side.getOpposite());
aroundThisTable++; tableSeats++;
}
require(aroundThisTable > 0, "A desk/table lost every chair facing its surface");
}
if (result.parcel.kind() != Kind.BARRACKS)
require(tableSeats >= result.parcel.floors(), "Every furnished storey needs a usable table or desk seat");
if (result.parcel.kind() == Kind.HALL) for (int x : new int[]{3, 4, 5, 11, 12, 13})
seatLooksToward(result, rotate(new BlockPos(x, 1, 11), result.parcel.facing()), rotated(Direction.SOUTH, result.parcel.facing()));
if (result.parcel.kind() == Kind.HOTEL) for (int x = 2; x <= 4; x++)
seatLooksToward(result, rotate(new BlockPos(x, 1, 14), result.parcel.facing()), rotated(Direction.NORTH, result.parcel.facing()));
if (result.parcel.kind() == Kind.BARRACKS) for (int floor = 0; floor < result.parcel.floors(); floor++)
if (LostCityInteriors20.layout(result.parcel, floor) == 0)
seatLooksToward(result, rotate(new BlockPos(13, floor * 5 + 1, 6), result.parcel.facing()), rotated(Direction.NORTH, result.parcel.facing()));
for (var entry : result.blocks.entrySet()) if (entry.getValue().is(Blocks.POLISHED_ANDESITE_STAIRS)) {
Direction look = entry.getValue().getValue(BlockStateProperties.HORIZONTAL_FACING).getOpposite();
require(clear(result.block(entry.getKey().relative(look))), "Sanitary seat faces a wall instead of its cubicle");
}
}
private static void seatLooksToward(Capture result, BlockPos chair, Direction towardUse) {
BlockState state = result.block(chair);
require(state.is(Blocks.DARK_OAK_STAIRS), "Missing chair at " + chair);
var boxes = state.getCollisionShape(EmptyBlockGetter.INSTANCE, chair).toAabbs();
double frontX = .5 + towardUse.getStepX() * .25, frontZ = .5 + towardUse.getStepZ() * .25;
double backX = .5 - towardUse.getStepX() * .25, backZ = .5 - towardUse.getStepZ() * .25;
require(boxes.stream().noneMatch(box -> box.contains(frontX, .75, frontZ))
&& boxes.stream().anyMatch(box -> box.contains(backX, .75, backZ)),
label(result.parcel) + " chair's native back faces its table/stage at " + chair);
require(result.block(chair.below()).isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, chair.below()),
"Chair does not rest on a complete floor");
}
private static Direction rotated(Direction direction, int facing) {
for (int turn = 0; turn < facing; turn++) direction = direction.getClockWise();
return direction;
}
private static void verifyLanternSupports(Capture result) {
int lanterns = 0;
for (var entry : result.blocks.entrySet()) if (entry.getValue().is(Blocks.LANTERN)) {
BlockPos support = entry.getValue().getValue(BlockStateProperties.HANGING) ? entry.getKey().above() : entry.getKey().below();
require(result.block(support).isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, support),
label(result.parcel) + " lantern floats against a slab or lacks a full support block at " + entry.getKey());
lanterns++;
}
require(lanterns == result.parcel.floors(), "A junction lost its supported lamp");
}
private static void verifyBeds(Capture result, Set<BlockPos> reached) {
for (var entry : result.blocks.entrySet()) {
BlockState state = entry.getValue();
if (!state.is(Blocks.BED.white())) continue;
Direction direction = state.getValue(BlockStateProperties.HORIZONTAL_FACING);
BedPart part = state.getValue(BlockStateProperties.BED_PART);
BlockPos partner = entry.getKey().relative(part == BedPart.FOOT ? direction : direction.getOpposite());
require(result.block(partner).equals(state.setValue(BlockStateProperties.BED_PART,
part == BedPart.FOOT ? BedPart.HEAD : BedPart.FOOT)), label(result.parcel) + " has a severed bed at " + entry.getKey());
require(result.block(entry.getKey().below()).isSolid(), "Bed does not sit on a floor");
require(adjacentReached(entry.getKey(), reached) || adjacentReached(partner, reached),
label(result.parcel) + " bed cannot be approached from its room");
if (part == BedPart.HEAD) {
var backing = entry.getKey().relative(direction);
for (int up = 0; up < 3; up++) require(result.block(backing.above(up))
.isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, backing.above(up)),
label(result.parcel) + " bed head is not against a complete wall at " + entry.getKey());
} else {
boolean lateral = false;
for (Direction side : new Direction[]{direction.getClockWise(), direction.getCounterClockWise()})
lateral |= reached.contains(entry.getKey().relative(side)) && reached.contains(partner.relative(side));
require(lateral, label(result.parcel) + " bed lacks a reachable side along its full length at " + entry.getKey());
}
}
}
private static void verifyBathrooms(Capture result, Set<BlockPos> reached) {
for (var bath : LostCityInteriors20.bathrooms(result.parcel)) {
BlockPos door = new BlockPos(bath.doorX(), bath.doorY(), bath.doorZ());
require(result.block(door).is(Blocks.DARK_OAK_DOOR), "Enclosed bathroom lost its actual door: " + bath.name());
Direction intoRoom = result.block(door).getValue(BlockStateProperties.HORIZONTAL_FACING);
require(reached.contains(door.relative(intoRoom)), "Bathroom cannot be entered through its door: " + bath.name());
require(bath.minX() == 1 || bath.minZ() == 1 || bath.maxX() == 15 || bath.maxZ() == 15,
"Bathroom enclosure is detached from the exterior room wall");
for (int x = bath.minX(); x <= bath.maxX(); x++) for (int z = bath.minZ(); z <= bath.maxZ(); z++)
for (int y = bath.minY(); y <= bath.maxY(); y++) {
boolean shell = x == bath.minX() || x == bath.maxX() || z == bath.minZ() || z == bath.maxZ()
|| y == bath.minY() || y == bath.maxY();
if (!shell) continue;
BlockPos at = new BlockPos(x, y, z);
if (at.equals(door) || at.equals(door.above())) continue;
require(result.block(at).isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, at),
label(result.parcel) + " bathroom has an incomplete floor, ceiling or connected wall at " + at + " in " + bath.name());
}
}
}
private static void verifyStoreyVariation(Capture result) {
for (int floor = 1; floor < result.parcel.floors(); floor++) {
require(LostCityInteriors20.layout(result.parcel, floor - 1) != LostCityInteriors20.layout(result.parcel, floor),
"Adjacent storeys selected the same plan");
Set<BlockPos> before = rearPartitions(result, floor - 1), after = rearPartitions(result, floor);
require(!before.equals(after), label(result.parcel) + " only changed furniture or labels between storeys " + (floor - 1) + " and " + floor);
Map<BlockPos, BlockState> beforeFixtures = furnishings(result, floor - 1), afterFixtures = furnishings(result, floor);
require(!beforeFixtures.equals(afterFixtures), label(result.parcel)
+ " copied the same furniture between storeys " + (floor - 1) + " and " + floor);
}
}
private static Set<BlockPos> rearPartitions(Capture result, int floor) {
Set<BlockPos> walls = new HashSet<>();
for (int x = 2; x <= 14; x++) for (int z = 9; z <= 14; z++) {
boolean continuous = true;
for (int y = 1; y <= 3; y++) continuous &= result.block(new BlockPos(x, floor * 5 + y, z)).is(Blocks.SMOOTH_STONE);
if (continuous) walls.add(new BlockPos(x, 0, z));
}
return walls;
}
private static Map<BlockPos, BlockState> furnishings(Capture result, int floor) {
Map<BlockPos, BlockState> objects = new HashMap<>();
for (int x = 2; x <= 14; x++) for (int z = 2; z <= 14; z++) for (int up = 1; up <= 3; up++) {
BlockState state = result.block(new BlockPos(x, floor * 5 + up, z));
if (state.is(Blocks.BED.white()) || state.is(Blocks.BARREL) || state.is(Blocks.FURNACE)
|| state.is(Blocks.BOOKSHELF) || state.is(Blocks.WATER_CAULDRON) || state.is(Blocks.DARK_OAK_FENCE)
|| state.is(Blocks.LECTERN) || state.is(Blocks.CRAFTING_TABLE) || state.is(Blocks.DARK_OAK_STAIRS)
|| state.is(Blocks.DARK_OAK_PLANKS) || state.is(Blocks.OAK_PLANKS) || state.is(Blocks.BIRCH_PLANKS)) objects.put(new BlockPos(x, up, z), state);
}
return objects;
}
private static void semantics(Capture result) {
Map<BlockState, Long> counts = new HashMap<>();
result.blocks.values().forEach(state -> counts.merge(state, 1L, Long::sum));
java.util.function.Predicate<net.minecraft.world.level.block.Block> has = block ->
counts.keySet().stream().anyMatch(state -> state.is(block));
switch (result.parcel.kind()) {
case APARTMENT -> require(has.test(Blocks.FURNACE) && has.test(Blocks.BED.white())
&& has.test(Blocks.WATER_CAULDRON), "Apartment lacks cooking, sleeping or washing fixtures");
case HOTEL -> require(has.test(Blocks.LOOM) && has.test(Blocks.BED.white())
&& has.test(Blocks.WATER_CAULDRON), "Hotel lacks guest rooms or laundry fixtures");
case LIBRARY -> require(result.blocks.values().stream().filter(state -> state.is(Blocks.BOOKSHELF)).count() >= result.parcel.floors() * 12L
&& has.test(Blocks.LECTERN), "Library is missing usable stacks and reading fixtures");
case RESTAURANT -> require(has.test(Blocks.SMOKER) && has.test(Blocks.FURNACE)
&& has.test(Blocks.CRAFTING_TABLE) && has.test(Blocks.WATER_CAULDRON), "Restaurant lacks kitchen and washroom services");
case BARRACKS -> require(has.test(Blocks.BED.white()) && has.test(Blocks.FLETCHING_TABLE)
&& has.test(Blocks.GRINDSTONE), "Barracks lacks dormitory and equipment fixtures");
case HALL -> {
require(has.test(Blocks.LECTERN) && has.test(Blocks.IRON_BARS), "Hall lacks stage or gallery guards");
require(result.block(rotate(new BlockPos(8, 5, 12), result.parcel.facing())).isAir(), "Auditorium lost its double-height volume");
}
default -> throw new AssertionError("Unexpected building");
}
}
/** Player-sized path: two clear cells above support, one-block steps and manually openable doors. */
private static Set<BlockPos> walk(Capture result, BlockPos start) {
require(standable(result, start), label(result.parcel) + " has a blocked entrance");
Set<BlockPos> seen = new HashSet<>();
ArrayDeque<BlockPos> queue = new ArrayDeque<>();
seen.add(start); queue.add(start);
while (!queue.isEmpty()) {
BlockPos current = queue.removeFirst();
for (Direction direction : HORIZONTAL) for (int dy = -1; dy <= 1; dy++) {
BlockPos next = current.relative(direction).offset(0, dy, 0);
if (dy > 0 && !clearBody(result, current.above())) continue;
if (standable(result, next) && seen.add(next)) queue.addLast(next);
}
}
return seen;
}
private static boolean standable(Capture result, BlockPos pos) {
if (pos.getX() < 0 || pos.getX() > 16 || pos.getZ() < 0 || pos.getZ() > 16
|| pos.getY() < 1 || pos.getY() > result.parcel.floors() * 5 + 4) return false;
if (!clearBody(result, pos)) return false;
var below = pos.below();
return opened(result.block(below)).getCollisionShape(EmptyBlockGetter.INSTANCE, below).toAabbs().stream()
.anyMatch(b -> b.maxY >= .5 && b.minX <= .5 && b.maxX >= .5 && b.minZ <= .5 && b.maxZ >= .5);
}
private static BlockState opened(BlockState state) {
return state.getBlock() instanceof net.minecraft.world.level.block.DoorBlock
|| state.getBlock() instanceof net.minecraft.world.level.block.TrapDoorBlock
? state.setValue(BlockStateProperties.OPEN, true) : state;
}
private static boolean clearBody(Capture result, BlockPos pos) {
var body = new AABB(pos.getX() + .2, pos.getY(), pos.getZ() + .2, pos.getX() + .8, pos.getY() + 1.8, pos.getZ() + .8);
for (var at : new BlockPos[]{pos, pos.above()}) {
var state = opened(result.block(at));
if (!state.getFluidState().isEmpty()) return false;
for (var shape : state.getCollisionShape(EmptyBlockGetter.INSTANCE, at).toAabbs())
if (shape.move(at.getX(), at.getY(), at.getZ()).intersects(body)) return false;
}
return true;
}
private static boolean clear(BlockState state) { return state.isAir() || state.is(Blocks.DARK_OAK_DOOR); }
private static void verifyArchitecture(Capture c) {
for (int x = 0; x <= 16; x++) for (int z = 0; z <= 16; z++)
require(c.block(new BlockPos(x, -1, z)).is(Blocks.STONE_BRICKS), "Foundation is not dressed stone brick");
long outsideVolume = c.blocks.entrySet().stream().filter(e -> e.getKey().getY() > 0
&& (e.getKey().getX() == 0 || e.getKey().getX() == 16 || e.getKey().getZ() == 0 || e.getKey().getZ() == 16)
&& !e.getValue().isAir()).count();
require(outsideVolume >= 40, label(c.parcel) + " has no cornice, bay, fins or entrance projection");
require(c.blocks.values().stream().anyMatch(b -> b.is(Blocks.TUFF_BRICKS)), "Missing structural tuff courses");
if (c.parcel.kind() == Kind.APARTMENT || c.parcel.kind() == Kind.LIBRARY)
require(c.block(rotate(new BlockPos(1, 2, 1), c.parcel.facing())).isAir(), "Front corner was not physically cut back");
if (c.parcel.kind() == Kind.LIBRARY) {
int roof = c.parcel.floors() * 5;
for (int x = 11; x <= 12; x++) for (int z = 4; z <= 6; z++) {
for (int y = roof - 1; y <= roof; y++)
require(c.block(rotate(new BlockPos(x, y, z), c.parcel.facing())).isAir(),
label(c.parcel) + " skylight is blocked by the roof plate or last-storey ceiling");
for (int y = roof + 1; y <= roof + 2; y++)
require(c.block(rotate(new BlockPos(x, y, z), c.parcel.facing())).is(Blocks.GLASS),
label(c.parcel) + " skylight lost its continuous transparent cap");
for (int y = roof + 3; y <= roof + 4; y++)
require(c.block(rotate(new BlockPos(x, y, z), c.parcel.facing())).isAir(),
label(c.parcel) + " another roof feature shades the light well");
}
for (int x = 10; x <= 13; x++) for (int z = 3; z <= 7; z++) {
if (x > 10 && x < 13 && z > 3 && z < 7) continue;
var base = rotate(new BlockPos(x, roof, z), c.parcel.facing());
require(c.block(base).isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, base)
&& c.block(base.above()).is(Blocks.STRIPPED_BIRCH_LOG),
label(c.parcel) + " skylight cut through its supporting perimeter or timber frame");
}
}
// Resolve every glazing edge from final neighbors, not precomputed straight strips.
for (var e : c.blocks.entrySet()) if (e.getValue().is(Blocks.GLASS_PANE)) {
for (Direction d : HORIZONTAL) {
var neighbor = c.block(e.getKey().relative(d));
if (!neighbor.isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, e.getKey().relative(d))) continue;
var property = switch(d) { case NORTH -> BlockStateProperties.NORTH; case EAST -> BlockStateProperties.EAST;
case SOUTH -> BlockStateProperties.SOUTH; case WEST -> BlockStateProperties.WEST; default -> throw new AssertionError(); };
require(e.getValue().getValue(property), "Window does not meet its actual stone or timber jamb");
}
}
}
private static boolean adjacentReached(BlockPos pos, Set<BlockPos> reached) {
for (Direction direction : HORIZONTAL) if (reached.contains(pos.relative(direction))) return true;
return false;
}
private static BlockPos rotate(BlockPos pos, int facing) {
return switch (facing) {
case 0 -> pos; case 1 -> new BlockPos(16 - pos.getZ(), pos.getY(), pos.getX());
case 2 -> new BlockPos(16 - pos.getX(), pos.getY(), 16 - pos.getZ());
default -> new BlockPos(pos.getZ(), pos.getY(), 16 - pos.getX());
};
}
private record Loot(ResourceKey<LootTable> table, long seed) {}
private static final class Capture implements LostCityRenderer.BlockWriter {
final Parcel parcel;
final Map<BlockPos, BlockState> blocks = new HashMap<>();
final Map<BlockPos, Loot> loot = new HashMap<>();
Capture(Parcel parcel) { this.parcel = parcel; }
@Override public void set(int x, int y, int z, BlockState state) {
require(x >= 0 && x < 17 && z >= 0 && z < 17 && y >= parcel.minY() - parcel.floorY()
&& y <= parcel.maxY() - parcel.floorY(), label(parcel) + " writes outside its saved bounds at " + x + "," + y + "," + z);
blocks.put(new BlockPos(x, y, z), state);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
set(x, y, z, Blocks.CHEST.defaultBlockState());
loot.put(new BlockPos(x, y, z), new Loot(table, seed));
}
BlockState block(BlockPos pos) { return blocks.getOrDefault(pos, Blocks.AIR.defaultBlockState()); }
}
private static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); }
}
@@ -0,0 +1,229 @@
package fr.koka.sanctuary.worldgen.city;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Kind;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Parcel;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.StairBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BedPart;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.storage.loot.LootTable;
/** Real block-state geometry, without creating a world: clipping, rotations and walkable floors. */
public final class LostCitySurface20Smoke {
public static void main(String[] args) {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
run();
System.out.println("LostCitySurface20Smoke: all exterior amenities, supported lamps, basements, contained pools, excavation access, carved park variants and suspended tuff roads passed.");
}
/** Also callable from an already bootstrapped native GameTest. */
public static void run() {
for (Kind kind : Kind.values()) {
if (kind.building()) continue;
for (int facing = 0; facing < 4; facing++) {
for (long seed : new long[] {0, 42, -61872}) {
int floors = kind == Kind.HALL ? 2 : 3;
int foundation = kind == Kind.POOL || kind == Kind.EXCAVATION ? 8 : 4;
Parcel parcel = new Parcel(kind, 0, 0, 17, 17, 170, floors, facing, seed, foundation, new int[0]);
Capture result = render(parcel);
require(result.blocks.equals(render(parcel).blocks), "Renderer changed for identical saved parcel");
require(result.blocks.values().stream().noneMatch(state -> state.is(Blocks.SPAWNER)), "City introduced a spawner");
if (kind.building()) {
require(result.blocks.values().stream().anyMatch(state -> state.is(Blocks.STONE_BRICKS)), "Missing explicit stone palette");
require(result.blocks.values().stream().anyMatch(state -> state.is(Blocks.SMOOTH_STONE)), "Missing smooth stone palette");
Set<BlockPos> reached = walk(result, rotate(8, 1, 0, facing));
for (int floor = 1; floor <= floors; floor++) {
int elevation = floor * 5 + 1;
require(reached.stream().anyMatch(pos -> pos.getY() == elevation),
kind + " facing " + facing + " cannot reach floor/roof " + floor);
}
require(result.loot.size() == 1, kind + " must keep exactly one deferred, limited loot chest");
}
for (var entry : result.blocks.entrySet()) {
if (entry.getValue().is(Blocks.LANTERN)) {
var support = entry.getValue().getValue(BlockStateProperties.HANGING) ? entry.getKey().above() : entry.getKey().below();
require(result.block(support).isCollisionShapeFullBlock(net.minecraft.world.level.EmptyBlockGetter.INSTANCE, support), kind + " lantern lacks full support at " + entry.getKey());
}
BlockState state = entry.getValue();
if (state.is(Blocks.BED.white()) && state.getValue(BlockStateProperties.BED_PART) == BedPart.FOOT) {
Direction direction = state.getValue(BlockStateProperties.HORIZONTAL_FACING);
BlockState head = result.block(entry.getKey().relative(direction));
require(head.is(Blocks.BED.white()) && head.getValue(BlockStateProperties.BED_PART) == BedPart.HEAD,
"Rotated bedroom has an incomplete bed");
}
}
}
}
}
for (int facing = 0; facing < 4; facing++) {
Parcel garage = new Parcel(Kind.PARKING, 0, 0, 17, 17, 170, 1, facing, 9, 8, new int[0]);
Capture result = render(garage);
Set<BlockPos> reached = walk(result, rotate(8, 1, 0, facing));
require(reached.contains(rotate(8, -4, 10, facing)), "Underground garage is not reachable from its street ramp");
require(result.block(rotate(8, -6, 10, facing)).is(Blocks.STONE_BRICKS), "Garage excavation removed the certified rock below its floor");
Parcel pit = new Parcel(Kind.EXCAVATION, 0, 0, 17, 17, 170, 0, facing, 9, 8, new int[0]);
Capture excavation = render(pit);
require(walk(excavation, rotate(8, 1, 0, facing)).contains(rotate(8, -4, 10, facing)),
"Excavation has no walkable stairs from the street");
require(excavation.block(rotate(8, -6, 10, facing)).is(Blocks.STONE_BRICKS), "Excavation removed its certified bottom support");
Parcel pool = new Parcel(Kind.POOL, 0, 0, 17, 17, 170, 0, facing, 9, 8, new int[0]);
Capture water = render(pool);
sealedPool(water, facing);
require(walk(water, rotate(8, 1, 0, facing)).contains(rotate(8, -2, 7, facing)),
"Pool submerged steps cannot be reached from the deck");
}
// Every public type keeps a usable north approach, and its furniture does not consume the street threshold.
for (Kind kind : Kind.values()) if (!kind.building() && kind != Kind.STREET && kind != Kind.BRIDGE)
for (int facing = 0; facing < 4; facing++) {
Capture c = render(new Parcel(kind, 0, 0, 17, 17, 170, 0, facing, 42, 8, new int[0]));
var reached = walk(c, rotate(8, 1, 0, facing));
require(reached.size() >= 100, kind + " lost its public circulation");
for (var chest : c.loot.keySet()) require(java.util.Arrays.stream(new Direction[]{Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST})
.anyMatch(d -> reached.contains(chest.relative(d))), kind + " loot is not approachable");
}
exportPreviews();
Set<Map<BlockPos, BlockState>> parks = new HashSet<>();
for (Kind park : new Kind[] {Kind.PARK, Kind.PARK_GARDEN, Kind.PARK_PLAYGROUND, Kind.PARK_MEMORIAL}) {
parks.add(render(new Parcel(park, 0, 0, 17, 17, 170, 0, 0, 42, 4, new int[0])).blocks);
}
require(parks.size() == 4, "Park variants must have distinct geometry");
for (Kind kind : new Kind[] {Kind.POOL, Kind.EXCAVATION}) {
Capture fallback = render(new Parcel(kind, 0, 0, 17, 17, 170, 0, 0, 42, 4, new int[0]));
require(fallback.blocks.values().stream().noneMatch(state -> !state.getFluidState().isEmpty()),
"Uncertified ground produced a wet excavation");
require(fallback.block(new BlockPos(8, -1, 8)).is(Blocks.STONE_BRICKS), "Uncertified ground was excavated");
}
int[] heights = new int[17 * 5];
for (int z = 0; z < 17; z++) for (int x = 0; x < 5; x++) heights[z * 5 + x] = 170 + z / 4;
Parcel bridge = new Parcel(Kind.BRIDGE, 0, 0, 5, 17, 170, 0, 0, 33, 0, heights);
Capture result = render(bridge);
require(result.blocks.entrySet().stream().noneMatch(entry -> !entry.getValue().isAir()
&& entry.getKey().getY() < bridge.surfaceY(entry.getKey().getX(), entry.getKey().getZ()) - bridge.floorY()),
"Bridge grew a column below its suspended deck");
for (var e : result.blocks.entrySet()) if (e.getValue().is(Blocks.TUFF_BRICK_STAIRS)) {
var face = e.getValue().getValue(BlockStateProperties.HORIZONTAL_FACING);
var high = e.getKey().relative(face);
require(bridge.surfaceY(high.getX(), high.getZ()) - bridge.floorY() == e.getKey().getY(),
"Bridge stair fails to meet the higher deck");
}
Set<BlockPos> reached = walk(result, new BlockPos(2, 1, 0));
require(reached.contains(new BlockPos(2, 5, 16)), "Stepped bridge cannot be crossed");
}
private static void exportPreviews() {
String path = System.getProperty("sanctuary.city20.exteriorPreview");
if (path == null) return;
var examples = new java.util.ArrayList<Object>();
for (Kind kind : Kind.values()) if (!kind.building() && kind != Kind.STREET && kind != Kind.BRIDGE) {
var p = new Parcel(kind, 0, 0, 17, 17, 170, 0, 0, 42, 8, new int[0]); var c = render(p);
var blocks = new java.util.ArrayList<Object>();
c.blocks.forEach((at, state) -> { if (!state.isAir()) blocks.add(java.util.List.of(at.getX(), at.getY(), at.getZ(), state.toString())); });
examples.add(Map.of("kind", kind.name(), "seed", p.seed(), "facing", 0, "blocks", blocks));
}
try { var output = java.nio.file.Path.of(path); if (output.getParent() != null) java.nio.file.Files.createDirectories(output.getParent());
java.nio.file.Files.writeString(output, new com.google.gson.Gson().toJson(examples)); }
catch (java.io.IOException e) { throw new AssertionError("Cannot write requested geometry preview", e); }
}
private static Capture render(Parcel parcel) {
Capture result = new Capture(parcel);
LostCitySurface20.render(parcel, result);
return result;
}
/** Flood the cells a source can reach horizontally/downward, without permitting water to rise. */
private static void sealedPool(Capture result, int facing) {
Set<BlockPos> flooded = new HashSet<>();
ArrayDeque<BlockPos> queue = new ArrayDeque<>();
result.blocks.forEach((pos, state) -> {
if (!state.getFluidState().isEmpty()) { flooded.add(pos); queue.add(pos); }
});
require(flooded.size() > 150, "Pool must contain a real basin of water");
while (!queue.isEmpty()) {
BlockPos pos = queue.removeFirst();
require(pos.getY() >= -3 && pos.getY() <= -1, "Pool leaks vertically through its floor or coping");
BlockPos canonical = rotate(pos.getX(), pos.getY(), pos.getZ(), (4 - facing) % 4);
require(canonical.getX() >= 4 && canonical.getX() <= 12 && canonical.getZ() >= 5 && canonical.getZ() <= 12,
"Pool leaks outside its continuous side walls");
for (Direction direction : new Direction[] {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST, Direction.DOWN}) {
BlockPos next = pos.relative(direction);
BlockState state = result.block(next);
if ((state.isAir() || !state.getFluidState().isEmpty()) && flooded.add(next)) queue.addLast(next);
}
}
}
/** Conservative full-block walking with one-block steps and two blocks of clear headroom. */
private static Set<BlockPos> walk(Capture result, BlockPos start) {
require(standable(result, start), "Entrance is blocked: " + start);
Set<BlockPos> seen = new HashSet<>();
ArrayDeque<BlockPos> queue = new ArrayDeque<>();
queue.add(start);
seen.add(start);
while (!queue.isEmpty()) {
BlockPos current = queue.removeFirst();
for (Direction direction : new Direction[] {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}) {
for (int dy = -1; dy <= 1; dy++) {
BlockPos next = current.relative(direction).offset(0, dy, 0);
if (standable(result, next) && seen.add(next)) queue.addLast(next);
}
}
}
return seen;
}
private static boolean standable(Capture result, BlockPos pos) {
if (pos.getX() < 0 || pos.getZ() < 0 || pos.getX() >= result.parcel.width() || pos.getZ() >= result.parcel.depth()) return false;
BlockState support = result.block(pos.below());
return (support.isSolid() || support.getBlock() instanceof StairBlock)
&& clear(result.block(pos)) && clear(result.block(pos.above()));
}
private static boolean clear(BlockState state) { return state.isAir() || state.is(Blocks.WATER); }
private static BlockPos rotate(int x, int y, int z, int facing) {
return switch (facing) {
case 0 -> new BlockPos(x, y, z);
case 1 -> new BlockPos(16 - z, y, x);
case 2 -> new BlockPos(16 - x, y, 16 - z);
default -> new BlockPos(z, y, 16 - x);
};
}
private static final class Capture implements LostCityRenderer.BlockWriter {
private final Parcel parcel;
private final Map<BlockPos, BlockState> blocks = new HashMap<>();
private final Map<BlockPos, ResourceKey<LootTable>> loot = new HashMap<>();
Capture(Parcel parcel) { this.parcel = parcel; }
@Override public void set(int x, int y, int z, BlockState state) {
require(x >= 0 && z >= 0 && x < parcel.width() && z < parcel.depth()
&& y >= parcel.minY() - parcel.floorY() && y <= parcel.maxY() - parcel.floorY(),
parcel.kind() + " emitted a block outside its saved bounds at " + x + "," + y + "," + z);
blocks.put(new BlockPos(x, y, z), state);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long lootSeed) {
set(x, y, z, Blocks.CHEST.defaultBlockState());
loot.put(new BlockPos(x, y, z), table);
}
BlockState block(BlockPos pos) { return blocks.getOrDefault(pos, Blocks.AIR.defaultBlockState()); }
}
private static void require(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
}
@@ -0,0 +1,230 @@
package fr.koka.sanctuary.worldgen.city;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.StairBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.storage.loot.LootTable;
/** Actual renderer topology on disposable, finite terrain fixtures. No Minecraft save is changed. */
public final class LostCityUnderground20Smoke {
public static void main(String[] args) {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); run();
System.out.println("LostCityUnderground20Smoke: Olympic pool, deep connected parking, exterior discharge, dry hall, bounds, rock anchors and protected terrain passed.");
}
public static void run() {
var terrain = new RockTerrain(); var surface = surface();
for (long seed : new long[] {0, 42}) {
var plan = LostCityUnderground16.plan(seed, surface, terrain);
require(!plan.isEmpty(), "Finite rock island has no accessible utilities");
require(plan.equals(LostCityUnderground16.plan(seed, surface, terrain)), "Same input changed saved module parameters");
var network = plan.networks().getFirst(); var capture = render(plan, terrain);
require(capture.blocks.equals(render(plan, terrain).blocks), "Saved parameters do not reconstruct the same blocks");
for (int a = 0; a < network.modules().size(); a++) for (int b = a + 1; b < network.modules().size(); b++)
require(!LostCityUnderground16.supportConflict(network.modules().get(a), network.modules().get(b)),
"One connected room destroys another room's bearing rock");
var access = module(network, LostCityUnderground16.Kind.ACCESS);
var hatch = capture.blocks.entrySet().stream().filter(e -> e.getValue().is(Blocks.DARK_OAK_TRAPDOOR)
&& access.contains(e.getKey().getX(), e.getKey().getY(), e.getKey().getZ())).map(Map.Entry::getKey).findFirst().orElseThrow();
var reached = walk(capture, hatch.above());
for (var m : network.modules()) {
require(reached.stream().anyMatch(p -> p.getY() == m.floorY() + 1 && p.getX() > m.minX()
&& p.getX() < m.maxX() && p.getZ() > m.minZ() && p.getZ() < m.maxZ()), "No two-block-high route from the hatch to " + m.kind());
for (var support : m.supports()) {
int anchor = support.type() == LostCityUnderground16.Type.PILLAR ? support.minY() : support.maxY();
int neighbor = anchor + (support.type() == LostCityUnderground16.Type.PILLAR ? -1 : 1);
require(support.maxY() - support.minY() < 8 && terrain.solid(support.x(), anchor, support.z())
&& terrain.solid(support.x(), neighbor, support.z()), "A support has no short real rock anchor");
for (int y = support.minY(); y <= support.maxY(); y++)
require(!capture.block(new BlockPos(support.x(), y, support.z())).isAir(), "Interrupted saved support");
}
verifyLights(m, capture);
}
verifyPool(module(network, LostCityUnderground16.Kind.POOLROOM), capture, reached);
verifyGarage(module(network, LostCityUnderground16.Kind.GARAGE), capture, reached);
verifyOutlet(module(network, LostCityUnderground16.Kind.SEWER_OUTLET), capture, terrain, reached);
var hall = module(network, LostCityUnderground16.Kind.EXPANSION_HALL);
require(reached.contains(new BlockPos(hall.centerX(), hall.floorY() + 2, hall.centerZ())), "Future expansion plinth is disconnected");
capture.blocks.forEach((p, state) -> {
if (hall.contains(p.getX(), p.getY(), p.getZ())) require(state.getFluidState().isEmpty(), "Architectural expansion hall is wet");
});
require(plan.samples() <= LostCityUnderground16.MAX_SAMPLES, "Terrain sampling exceeded its cap");
}
var unsupported = new RockTerrain() { @Override public boolean solid(int x, int y, int z) { return false; } };
require(LostCityUnderground16.plan(0, surface, unsupported).isEmpty(), "Unsupported void acquired an underground city");
var protectedTerrain = new RockTerrain() { @Override public boolean protectedAt(int x, int y, int z) { return true; } };
require(LostCityUnderground16.plan(0, surface, protectedTerrain).isEmpty(), "Protected natural hydrology was overwritten");
var foreign = new RockTerrain() { @Override public boolean owns(int x, int z) { return false; } };
require(LostCityUnderground16.plan(0, surface, foreign).isEmpty(), "A network escaped chunk ownership");
var caveOnly = new RockTerrain() { @Override public boolean exteriorAir(int x, int y, int z, int dx, int dz) { return false; } };
require(LostCityUnderground16.plan(0, surface, caveOnly).networks().stream().flatMap(n -> n.modules().stream())
.noneMatch(m -> m.kind() == LostCityUnderground16.Kind.SEWER_OUTLET), "A pocket cave was treated as the island exterior");
foundationDependencies();
}
private static LostCityPlan surface() {
var street = new LostCityPlan.Parcel(LostCityPlan.Kind.STREET, -2, -12, 5, 25, 220, 0, 0, 7, 3, new int[0]);
return new LostCityPlan(List.of(new LostCityPlan.District(0, 0, 3, List.of(street))));
}
private static LostCityUnderground16.Module module(LostCityUnderground16.Network n, LostCityUnderground16.Kind kind) {
return n.modules().stream().filter(m -> m.kind() == kind).findFirst()
.orElseThrow(() -> new AssertionError("Reference island never generated " + kind + "; kinds=" + n.modules().stream().map(LostCityUnderground16.Module::kind).toList()));
}
private static void verifyPool(LostCityUnderground16.Module pool, Capture c, Set<BlockPos> reached) {
var bounds = pool.poolWaterBounds();
require(bounds.maxX() - bounds.minX() + 1 == 50 && bounds.maxZ() - bounds.minZ() + 1 == 25
&& bounds.maxY() - bounds.minY() + 1 == 4, "Pool lost its fifty-by-twenty-five four-deep tank");
int wet = 0, steps = 0;
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) for (int x = bounds.minX(); x <= bounds.maxX(); x++)
for (int y = bounds.minY(); y <= bounds.maxY(); y++) {
var p = new BlockPos(x, y, z); var state = c.block(p);
if (!state.getFluidState().isEmpty()) {
wet++;
for (var direction : new Direction[] {Direction.DOWN, Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}) {
var q = p.relative(direction); var next = c.block(q);
require(!next.isAir(), "Pool water has an unsealed horizontal or downward neighbor: " + q);
}
}
if (state.getBlock() instanceof StairBlock) {
steps++;
require(state.getValue(BlockStateProperties.WATERLOGGED)
&& state.getValue(BlockStateProperties.HORIZONTAL_FACING) == Direction.NORTH, "Pool access step cannot descend from its north promenade");
}
}
require(wet >= 4_950 && steps == 16, "Large pool was replaced by tiny basins or lost its submerged stairs: " + wet);
for (int[] point : new int[][] {{2, 2}, {2, 28}, {54, 2}, {54, 28}})
require(reached.contains(new BlockPos(pool.minX() + point[0], pool.floorY() + 1, pool.minZ() + point[1])),
"The dry promenade cannot be walked around the pool");
}
private static void verifyGarage(LostCityUnderground16.Module garage, Capture c, Set<BlockPos> reached) {
var decks = garage.parkingFloors();
require(decks.size() == 8 && decks.getLast() - decks.getFirst() == 112, "Garage did not descend through eight widely separated platforms");
require(garage.supports().size() == 9, "Open parking is missing its nine certified load-bearing columns");
for (int y : decks) {
require(reached.contains(new BlockPos(garage.centerX() + 3, y + 1, garage.centerZ())), "Garage deck is disconnected from the public stair: " + y);
require(c.block(new BlockPos(garage.minX(), y + 5, garage.centerZ())).isAir(), "Parking exterior was closed by a solid wall");
}
for (int lower : decks.subList(0, decks.size() - 1)) for (int step = 0; step < 16; step++)
for (int x = 1; x <= 2; x++) {
var p = new BlockPos(garage.minX() + x, lower + step + 1, garage.minZ() + 3 + step);
require(c.block(p).getBlock() instanceof StairBlock && c.block(p).getValue(BlockStateProperties.HORIZONTAL_FACING) == Direction.SOUTH,
"Parking stair flight has a gap or faces downhill");
require(c.block(p.above()).isAir() && c.block(p.above(2)).isAir(), "Parking stair lacks player headroom");
}
}
private static void verifyOutlet(LostCityUnderground16.Module outlet, Capture c, RockTerrain terrain, Set<BlockPos> reached) {
int[] dx = {0, 1, 0, -1}, dz = {-1, 0, 1, 0}; int direction = outlet.outletDirection();
var mouth = outlet.outletMouth(); var beyond = mouth.offset(dx[direction], 0, dz[direction]);
require(terrain.exteriorAir(beyond.getX(), beyond.getY(), beyond.getZ(), dx[direction], dz[direction]), "Sewer terminates in a cave or rock face");
require(c.block(mouth).isAir() && c.block(beyond).isAir(), "Outlet water level has a closed terminal wall");
var source = mouth.offset(-dx[direction] * 3, 0, -dz[direction] * 3);
require(c.block(source).is(Blocks.WATER), "Outflow has no real water source");
for (int distance = 1; distance <= 4; distance++) {
var p = source.offset(dx[direction] * distance, 0, dz[direction] * distance);
require(c.block(p).isAir(), "Sewer source cannot flow along the trough to the exterior");
}
require(solid(c.block(source.offset(-dx[direction], 0, -dz[direction]))), "Outlet weir allows water to flood back into the public network");
int length = Math.max(outlet.width(), outlet.depth());
var rows = new java.util.TreeMap<Integer, Integer>();
for (var support : outlet.supports()) {
int distance = dx[direction] == 0 ? direction == 0 ? outlet.maxZ() - support.z() : support.z() - outlet.minZ()
: direction == 1 ? support.x() - outlet.minX() : outlet.maxX() - support.x();
rows.merge(distance, 1, Integer::sum);
}
int prior = 0;
for (var row : rows.entrySet()) {
require(row.getValue() == 2 && row.getKey() - prior <= 12, "Sewer has a bridge span without its two short rock anchors");
prior = row.getKey();
}
require(length - 1 - prior <= 8, "Sewer outlet lip exceeds its eight-block cantilever");
require(reached.stream().anyMatch(p -> Math.abs(p.getX() - mouth.getX()) + Math.abs(p.getZ() - mouth.getZ()) <= 2
&& p.getY() == mouth.getY() + 1), "Dry sewer banks do not reach the outlet");
}
private static void verifyLights(LostCityUnderground16.Module m, Capture c) {
c.blocks.forEach((p, state) -> {
if (state.is(Blocks.LANTERN) && m.contains(p.getX(), p.getY(), p.getZ())) {
require(state.getValue(BlockStateProperties.HANGING), "Utility lantern has no suspension");
var above = p.above();
while (c.block(above).is(Blocks.IRON_CHAIN) && above.getY() < m.maxY()) above = above.above();
require(solid(c.block(above)), "A suspended lantern is not anchored to a full block");
}
});
}
private static void foundationDependencies() {
var pool = new LostCityUnderground16.Module(LostCityUnderground16.Kind.POOLROOM, 0, 150, 0, 57, 13, 31, 1, 42, List.of(new LostCityUnderground16.Support(LostCityUnderground16.Type.PILLAR, 8, 149, 8, 149)));
var below = new LostCityUnderground16.Module(LostCityUnderground16.Kind.PASSAGE, 8, 143, 8, 5, 6, 5, 5, 1, List.of());
require(LostCityUnderground16.supportConflict(pool, below), "Another tunnel can remove the pool support's rock-bearing neighbor");
var deeper = new LostCityUnderground16.Module(LostCityUnderground16.Kind.PASSAGE, 8, 142, 8, 5, 6, 5, 5, 1, List.of());
require(!LostCityUnderground16.supportConflict(pool, deeper), "Pool bearing protection escaped its certified depth");
}
/** Rock with a sloping roof, irregular deep caverns and a finite, exposed island edge. */
private static class RockTerrain implements LostCityUnderground16.Terrain {
@Override public boolean solid(int x, int y, int z) {
int top = 220 + (int) Math.floor(Math.sin(x / 37.0) * 3 * Math.min(1, Math.abs(x) / 8.0));
return Math.abs(x) <= 104 && Math.abs(z) <= 104 && y <= top && y >= 70
&& !(y < 100 && Math.floorMod(x * x + z * z, 71) < 14);
}
@Override public boolean protectedAt(int x, int y, int z) { return false; }
@Override public boolean owns(int x, int z) { return true; }
@Override public boolean exteriorAir(int x, int y, int z, int dx, int dz) {
if (Math.abs(x) <= 104 && Math.abs(z) <= 104) return false;
for (int n = 0; n <= 32; n++) if (solid(x + dx * n, y, z + dz * n)) return false;
return true;
}
}
private static Capture render(LostCityUnderground16.Plan plan, LostCityUnderground16.Terrain terrain) {
var result = new Capture(terrain);
for (var network : plan.networks()) for (var m : network.modules()) LostCityUnderground20.render(m, new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) {
var p = new BlockPos(m.minX() + x, m.minY() + y, m.minZ() + z);
require(p.getX() >= m.boundsMinX() && p.getX() <= m.boundsMaxX() && p.getY() >= m.boundsMinY()
&& p.getY() <= m.boundsMaxY() && p.getZ() >= m.boundsMinZ() && p.getZ() <= m.boundsMaxZ(), "Renderer exceeded saved bounds");
require(plan.contains(p.getX(), p.getY(), p.getZ()), "An actual block write is missing from the 3D mask");
require(!terrain.protectedAt(p.getX(), p.getY(), p.getZ()) && terrain.owns(p.getX(), p.getZ()), "Renderer entered protected or foreign terrain");
result.blocks.put(p, state);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) { throw new AssertionError("Unrequested dungeon loot"); }
});
return result;
}
private static Set<BlockPos> walk(Capture c, BlockPos start) {
var seen = new HashSet<BlockPos>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
while (!queue.isEmpty()) {
var p = queue.removeFirst();
for (Direction direction : Direction.Plane.HORIZONTAL) for (int dy : new int[] {0, 1, -1}) {
var q = p.relative(direction).offset(0, dy, 0);
if (Math.abs(q.getX() - start.getX()) > 120 || Math.abs(q.getZ() - start.getZ()) > 120 || Math.abs(q.getY() - start.getY()) > 160) continue;
if (stand(c, q) && seen.add(q)) queue.add(q);
}
for (int dy : new int[] {-1, 1}) {
var q = p.offset(0, dy, 0);
if (Math.abs(q.getY() - start.getY()) > 160) continue;
if ((climb(c.block(p)) || climb(c.block(q)) || climb(c.block(p.below()))) && stand(c, q) && seen.add(q)) queue.add(q);
}
}
return seen;
}
private static boolean stand(Capture c, BlockPos p) {
return passable(c.block(p)) && passable(c.block(p.above()))
&& (solid(c.block(p.below())) || c.block(p.below()).getBlock() instanceof StairBlock || climb(c.block(p)) || climb(c.block(p.below())));
}
private static boolean climb(BlockState b) { return b.is(Blocks.LADDER) || b.is(Blocks.DARK_OAK_TRAPDOOR); }
private static boolean passable(BlockState b) { return b.isAir() || climb(b) || b.is(Blocks.REDSTONE_WALL_TORCH); }
private static boolean solid(BlockState b) { return b.canOcclude() && b.getFluidState().isEmpty(); }
private static final class Capture {
final Map<BlockPos, BlockState> blocks = new HashMap<>(); final LostCityUnderground16.Terrain terrain;
Capture(LostCityUnderground16.Terrain terrain) { this.terrain = terrain; }
BlockState block(BlockPos p) { return blocks.getOrDefault(p, terrain.solid(p.getX(), p.getY(), p.getZ()) ? Blocks.STONE.defaultBlockState() : Blocks.AIR.defaultBlockState()); }
}
private static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); }
}
@@ -0,0 +1,95 @@
package fr.koka.sanctuary.worldgen.detail;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import java.util.HashMap;
import java.util.Map;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.StairBlock;
import net.minecraft.world.level.block.WallBlock;
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.StairsShape;
import net.minecraft.world.level.block.state.properties.WallSide;
import net.minecraft.world.level.storage.loot.BuiltInLootTables;
import net.minecraft.world.level.storage.loot.LootTable;
/** Geometry-only proof. Native world tests additionally exercise datapack-bound fence/wall tags. */
public final class ConnectedBlocks20Smoke {
public static void main(String[] args) {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); run();
System.out.println("ConnectedBlocks20Smoke: final geometry, native solid faces, panes, walls, stair corners, chunk clips and deferred loot passed.");
}
public static void run() {
var full = new Capture(); var b = ConnectedBlocks20.buffer(full);
b.set(15, 3, 8, Blocks.GLASS_PANE.defaultBlockState());
b.set(16, 3, 8, Blocks.IRON_BARS.defaultBlockState());
b.set(14, 3, 8, Blocks.STONE.defaultBlockState());
b.set(15, 3, 7, Blocks.SMOOTH_STONE_SLAB.defaultBlockState());
b.set(15, 3, 9, Blocks.STONE.defaultBlockState()); b.set(15, 3, 9, Blocks.AIR.defaultBlockState());
b.flush();
var pane = full.at(15, 3, 8);
require(pane.getValue(BlockStateProperties.EAST) && pane.getValue(BlockStateProperties.WEST), "Pane missed full geometry across a chunk boundary");
require(!pane.getValue(BlockStateProperties.NORTH) && !pane.getValue(BlockStateProperties.SOUTH), "Pane attached to a half slab or overwritten block");
require(full.at(16, 3, 8).getValue(BlockStateProperties.WEST), "Mixed panes/bars lost reciprocal connection");
var clipped = new Capture(); var clip = ConnectedBlocks20.buffer(new LostCityRenderer.BlockWriter() {
public void set(int x, int y, int z, BlockState state) { if (x < 16) clipped.set(x, y, z, state); }
public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { throw new AssertionError(); }
});
full.blocks.forEach((at, state) -> clip.set(at.getX(), at.getY(), at.getZ(), state)); clip.flush();
require(clipped.at(15, 3, 8).equals(pane) && !clipped.blocks.containsKey(new BlockPos(16, 3, 8)), "Clipping changed precomputed cross-chunk connectivity");
for (var face : new Direction[] {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}) {
for (var half : Half.values()) {
require(corner(face, half, true, true, false) == StairsShape.OUTER_LEFT, "Outer-left corner lost rotation/half");
require(corner(face, half, true, false, false) == StairsShape.OUTER_RIGHT, "Outer-right corner lost rotation/half");
require(corner(face, half, false, true, false) == StairsShape.INNER_LEFT, "Inner-left corner lost rotation/half");
require(corner(face, half, false, false, false) == StairsShape.INNER_RIGHT, "Inner-right corner lost rotation/half");
require(corner(face, half, true, true, true) == StairsShape.STRAIGHT, "Parallel stair failed to block corner");
}
}
var walls = new Capture(); var wb = ConnectedBlocks20.buffer(walls);
for (int x : new int[] {0, 4}) {
wb.set(x, 0, 0, Blocks.STONE_BRICK_WALL.defaultBlockState().setValue(WallBlock.WATERLOGGED, true));
wb.set(x - 1, 0, 0, Blocks.STONE.defaultBlockState()); wb.set(x + 1, 0, 0, Blocks.STONE.defaultBlockState());
}
wb.set(4, 1, 0, Blocks.SMOOTH_STONE.defaultBlockState()); wb.flush();
require(walls.at(0, 0, 0).getValue(WallBlock.EAST) == WallSide.LOW && !walls.at(0, 0, 0).getValue(WallBlock.UP), "Open straight wall should be low without a post");
require(walls.at(4, 0, 0).getValue(WallBlock.WEST) == WallSide.TALL && !walls.at(4, 0, 0).getValue(WallBlock.UP), "Full underside should raise both wall arms, not a redundant post");
require(walls.at(0, 0, 0).getValue(WallBlock.WATERLOGGED), "Connection resolution erased waterlogging");
var overhead = new Capture(); var ob = ConnectedBlocks20.buffer(overhead);
ob.set(0, 0, 0, Blocks.TUFF_BRICK_WALL.defaultBlockState());
ob.set(0, 0, 1, Blocks.IRON_BARS.defaultBlockState());
ob.set(0, 1, 0, Blocks.IRON_BARS.defaultBlockState());
ob.set(0, 1, 1, Blocks.IRON_BARS.defaultBlockState());
ob.flush();
require(overhead.at(0, 0, 0).getValue(WallBlock.SOUTH) == WallSide.TALL,
"Wall height used a stale unconnected underside of the bars above");
var loot = new Capture(); var lb = ConnectedBlocks20.buffer(loot);
lb.chest(0, 0, 0, BuiltInLootTables.VILLAGE_PLAINS_HOUSE, 42); lb.set(0, 0, 0, Blocks.AIR.defaultBlockState());
lb.set(1, 0, 0, Blocks.STONE.defaultBlockState()); lb.chest(1, 0, 0, BuiltInLootTables.VILLAGE_CARTOGRAPHER, Long.MIN_VALUE); lb.flush();
require(loot.chests.size() == 1 && loot.chests.get(new BlockPos(1, 0, 0)).equals("" + BuiltInLootTables.VILLAGE_CARTOGRAPHER + ":" + Long.MIN_VALUE), "Deferred chest metadata lost final-write semantics");
boolean rejected = false; try { lb.flush(); } catch (IllegalStateException expected) { rejected = true; }
require(rejected, "Repeated flush duplicated chest placement");
}
private static StairsShape corner(Direction facing, Half half, boolean front, boolean left, boolean blocker) {
var out = new Capture(); var b = ConnectedBlocks20.buffer(out); var origin = BlockPos.ZERO;
var own = Blocks.STONE_BRICK_STAIRS.defaultBlockState().setValue(StairBlock.FACING, facing).setValue(StairBlock.HALF, half);
var other = left ? facing.getCounterClockWise() : facing.getClockWise();
var neighbor = origin.relative(front ? facing : facing.getOpposite());
b.set(0, 0, 0, own); b.set(neighbor.getX(), 0, neighbor.getZ(), own.setValue(StairBlock.FACING, other));
if (blocker) { var at = origin.relative(front ? other.getOpposite() : other); b.set(at.getX(), 0, at.getZ(), own); }
b.flush(); return out.at(0, 0, 0).getValue(StairBlock.SHAPE);
}
private static final class Capture implements LostCityRenderer.BlockWriter {
final Map<BlockPos, BlockState> blocks = new HashMap<>(); final Map<BlockPos, String> chests = new HashMap<>();
public void set(int x, int y, int z, BlockState state) { blocks.put(new BlockPos(x, y, z), state); }
public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { chests.put(new BlockPos(x, y, z), "" + table + ":" + seed); }
BlockState at(int x, int y, int z) { return blocks.get(new BlockPos(x, y, z)); }
}
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
}
@@ -0,0 +1,85 @@
package fr.koka.sanctuary.worldgen.heritage;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.IntTag;
import net.minecraft.nbt.ListTag;
/** Exact, bounded independent schema20 persistence for catalogue sites and chunk-local abandoned routes. */
public final class HeritagePiece20Smoke {
public static void main(String[] args) {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); run();
System.out.println("HeritagePiece20Smoke: 14 kinds, 4 facades, every rail profile, missing rails, strict NBT and local native bounds passed.");
}
public static void run() {
for (var kind : HeritagePlan19.Kind.values()) for (int face = 0; face < 4; face++) {
int x = -32, z = 40, floor = 160;
var entry = new HeritagePlan19.Entry(face == 1 ? x + 17 : face == 3 ? x - 1 : x + 8,
floor + 1, face == 2 ? z + 17 : face == 0 ? z - 1 : z + 8, face);
var supportList = new ArrayList<HeritagePlan19.Support>();
supportList.add(new HeritagePlan19.Support(x + 1, 150, z + 1, 155));
var site = new HeritagePlan19.Site("site_" + kind.name(), kind, x, floor, z, 17, 17, 20, face,
Long.MIN_VALUE + kind.ordinal(), 4, supportList, entry);
supportList.clear();
var saved = HeritagePiece20.encodeSite(site);
require(HeritagePiece20.decodeSite(saved).equals(site) && HeritagePiece20.encodeSite(HeritagePiece20.decodeSite(saved)).equals(saved), "Site roundtrip changed geometry");
require(site.supports().size() == 1 && HeritagePiece20.bounds(site).minY() == 150, "Support input is mutable or excluded from native bounds");
require(HeritagePiece20.locator(site).getX() == entry.x() && HeritagePiece20.locator(site).getY() == entry.y()
&& HeritagePiece20.locator(site).getZ() == entry.z(), "Locate lost the rotated facade entry");
badSite(saved, t -> t.remove("Facing")); badSite(saved, t -> t.putInt("Schema", 17));
badSite(saved, t -> t.putString("Type", "ROUTE")); badSite(saved, t -> t.putString("Kind", "FUTURE"));
badSite(saved, t -> t.putDouble("FloorY", 160)); badSite(saved, t -> t.putInt("Seed", 1));
badSite(saved, t -> t.putInt("Future", 1)); badSite(saved, t -> t.putInt("MinX", Integer.MAX_VALUE));
badSite(saved, t -> t.putInt("Width", 18)); badSite(saved, t -> t.putInt("Facing", 4));
badSite(saved, t -> t.putInt("FoundationDepth", 0)); badSite(saved, t -> t.putString("Id", "x".repeat(129)));
badSite(saved, t -> t.getCompound("Entry").orElseThrow().putInt("X", entry.x() + 1));
badSite(saved, t -> t.getCompound("Entry").orElseThrow().putLong("Y", entry.y()));
badSite(saved, t -> t.getList("Supports").orElseThrow().getCompound(0).orElseThrow().putInt("MinY", 130));
badSite(saved, t -> t.getList("Supports").orElseThrow().getCompound(0).orElseThrow().putInt("X", site.maxX() + 1));
badSite(saved, t -> {
var list = t.getList("Supports").orElseThrow(); list.add(list.getFirst().copy());
});
}
var cells = new ArrayList<HeritagePlan19.Cell>();
int index = 0;
for (var rail : HeritagePlan19.Rail.values()) {
cells.add(new HeritagePlan19.Cell(-32 + index, 150 + index % 3, 32, 2 + index % 4, rail, rail != HeritagePlan19.Rail.NONE && index % 2 == 0)); index++;
}
var segment = new HeritagePlan19.Segment("freight/part0", "station", "mine", HeritagePlan19.RouteKind.FREIGHT, -2, 2, cells, Long.MAX_VALUE);
cells.clear();
var landing = new HeritagePlan19.Segment("station/landing", "station", "station", HeritagePlan19.RouteKind.PATH,
-2, 2, List.of(new HeritagePlan19.Cell(-32, 150, 32, 2, HeritagePlan19.Rail.NONE, false)), 42);
require(HeritagePiece20.decodeSegment(HeritagePiece20.encodeSegment(landing)).equals(landing), "Standalone entry landing lost its site binding");
var saved = HeritagePiece20.encodeSegment(segment);
require(HeritagePiece20.decodeSegment(saved).equals(segment) && HeritagePiece20.encodeSegment(HeritagePiece20.decodeSegment(saved)).equals(saved), "Route roundtrip changed shapes, supports or deliberate rail gaps");
require(segment.cells().size() == HeritagePlan19.Rail.values().length, "External route list mutation changed its saved cells");
require(HeritagePiece20.bounds(segment).intersectingChunks().count() == 1, "Segment escaped its one-chunk native start");
badRoute(saved, t -> t.putInt("Schema", 21)); badRoute(saved, t -> t.putString("Type", "SITE"));
badRoute(saved, t -> t.remove("FromId")); badRoute(saved, t -> t.putString("ToId", "station"));
badRoute(saved, t -> t.putString("Id", " ")); badRoute(saved, t -> t.putInt("StartChunkX", -1));
badRoute(saved, t -> t.putDouble("Seed", 42)); badRoute(saved, t -> t.put("Cells", new ListTag()));
badRoute(saved, t -> cell(t).putInt("RailPresent", 1)); badRoute(saved, t -> cell(t).putByte("RailPresent", (byte) 2));
badRoute(saved, t -> cell(t).putString("Rail", "FUTURE")); badRoute(saved, t -> cell(t).putBoolean("RailPresent", true));
badRoute(saved, t -> cell(t).putInt("X", -16)); badRoute(saved, t -> cell(t).putInt("FoundationDepth", 13));
badRoute(saved, t -> cell(t).putInt("FloorY", 2)); badRoute(saved, t -> cell(t).putInt("Extra", 0));
badRoute(saved, t -> { var list = t.getList("Cells").orElseThrow(); list.add(list.getFirst().copy()); });
badRoute(saved, t -> { var list = new ListTag(); list.add(IntTag.valueOf(1)); t.put("Cells", list); });
badRoute(saved, t -> { var list = t.getList("Cells").orElseThrow(); while (list.size() <= 256) list.add(list.getFirst().copy()); });
}
private static CompoundTag cell(CompoundTag tag) { return tag.getList("Cells").orElseThrow().getCompound(0).orElseThrow(); }
private static void badSite(CompoundTag original, Consumer<CompoundTag> mutation) {
var value = original.copy(); mutation.accept(value);
try { HeritagePiece20.decodeSite(value); } catch (IllegalArgumentException expected) { return; }
throw new AssertionError("Malformed saved site accepted: " + value);
}
private static void badRoute(CompoundTag original, Consumer<CompoundTag> mutation) {
var value = original.copy(); mutation.accept(value);
try { HeritagePiece20.decodeSegment(value); } catch (IllegalArgumentException expected) { return; }
throw new AssertionError("Malformed saved route accepted: " + value);
}
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
}
@@ -0,0 +1,420 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.EmptyBlockGetter;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.DoorBlock;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.TrapDoorBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BedPart;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
import net.minecraft.world.level.block.state.properties.Half;
import net.minecraft.world.level.storage.loot.LootTable;
import net.minecraft.world.phys.AABB;
/** Native shapes and final geometry: distinguish real rooms, reachable secrets and supported routes. */
public final class HeritageRenderer20Smoke {
private HeritageRenderer20Smoke() {}
private static final Direction[] DIRECTIONS = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
public static void main(String[] args) {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); run();
System.out.println("HeritageRenderer20Smoke passed: fourteen composed generation20 identities, four rotations, bounded chamfers and roofs, supported furnishings, reachable original targets, retained loot and connected block details");
}
public static void run() {
Set<Set<BlockPos>> silhouettes = new HashSet<>();
for (var kind : HeritagePlan19.Kind.values()) for (long seed : new long[] {0, 43}) {
var canonical = render(site(kind, 0, seed));
if (seed == 0) {
export(canonical);
Set<BlockPos> shape = new HashSet<>(); canonical.blocks.forEach((p, b) -> { if (p.getY() > 0 && !b.isAir()) shape.add(p); });
require(silhouettes.add(shape), kind + " only repaints another catalogue building");
}
for (int facing = 0; facing < 4; facing++) {
var c = facing == 0 ? canonical : render(site(kind, facing, seed));
var repeat = render(c.site);
require(c.blocks.equals(repeat.blocks) && c.loot.equals(repeat.loot), label(c) + " is not deterministic");
rotate(canonical, c);
var targets = HeritageRenderer20.targets(c.site);
var reached = walk(c, at(targets.getFirst()));
for (var target : targets) require(reached.contains(at(target)), label(c) + " cannot reach " + target.name() + " at " + at(target));
require(targets.stream().anyMatch(t -> t.name().equals("secondary_entry")), "Missing secondary physical approach");
var secondary = targets.stream().filter(t -> t.name().equals("secondary_entry")).findFirst().orElseThrow();
Set<BlockPos> returnPath = walk(c, at(secondary));
require(returnPath.contains(at(targets.getFirst())), label(c) + " service exit cannot return through the certified courtyard");
require(c.loot.size() == 1, label(c) + " must have one shared, deferred discovery chest");
var chest = c.loot.entrySet().iterator().next();
require(chest.getValue().table().identifier().toString().equals("sanctuary:chests/heritage19/" + kind.name().toLowerCase(java.util.Locale.ROOT)), "Loot table is not generation19-specific");
require(approachable(chest.getKey(), reached), label(c) + " hidden chest cannot be opened from a reachable standing cell");
for (var t : targets) if (t.name().equals("loot_approach")) require(at(t).distManhattan(chest.getKey()) == 1, "Secret witness is not next to the actual chest");
furnishings(c, reached);
refugeShaft(c, reached);
railConnection(c);
fluids(c);
require(c.blocks.values().stream().noneMatch(b -> b.is(Blocks.SPAWNER) || b.is(Blocks.LAVA)), "Heritage added an unrequested spawner or lava");
identity(c);
architecture20(c);
daylightAndCrown20(c);
}
}
require(silhouettes.size() == 14, "Catalogue does not contain fourteen physical architectures");
routes(); chunkPartition(); materials20();
}
private static HeritagePlan19.Site site(HeritagePlan19.Kind kind, int facing, long seed) {
int height = switch (kind) {
case COMMUNAL_DEPOT, VOID_DOCK, CONSTRUCTION_REFUGE -> 14;
case EXPEDITION_OFFICE, IMPOSSIBLE_SERVICE -> 12;
case OBSERVATORY -> 20; case PUMP_STATION -> 18; case RADIO_RELAY -> 22;
default -> 10;
};
int x = -43, y = 180, z = 59;
var entry = new HeritagePlan19.Entry(facing == 1 ? x + 17 : facing == 3 ? x - 1 : x + 8, y + 1,
facing == 2 ? z + 17 : facing == 0 ? z - 1 : z + 8, facing);
return new HeritagePlan19.Site("test", kind, x, y, z, 17, 17, height, facing, seed, 3,
List.of(new HeritagePlan19.Support(x, y - 7, z, y - 4)), entry);
}
private static String label(Capture c) { return c.site.kind() + " facing=" + c.site.facing() + " seed=" + c.site.seed(); }
private static Capture render(HeritagePlan19.Site site) { var c = new Capture(site); HeritageRenderer20.render(site, c); return c; }
private static BlockPos at(HeritageRenderer20.Target t) { return new BlockPos(t.x(), t.y(), t.z()); }
private static BlockPos rotate(BlockPos p, int facing) {
return switch (facing) { case 1 -> new BlockPos(16 - p.getZ(), p.getY(), p.getX());
case 2 -> new BlockPos(16 - p.getX(), p.getY(), 16 - p.getZ());
case 3 -> new BlockPos(p.getZ(), p.getY(), 16 - p.getX()); default -> p; };
}
private static void rotate(Capture base, Capture other) {
int facing = other.site.facing();
Rotation rotation = switch (facing) { case 1 -> Rotation.CLOCKWISE_90; case 2 -> Rotation.CLOCKWISE_180;
case 3 -> Rotation.COUNTERCLOCKWISE_90; default -> Rotation.NONE; };
for (var e : base.blocks.entrySet()) {
// Saved footings are already in world coordinates; they do not rotate with the facade.
if (e.getKey().getY() < -base.site.foundationDepth() || base.loot.containsKey(e.getKey())) continue;
require(other.block(rotate(e.getKey(), facing)).equals(e.getValue().rotate(rotation)), label(other) + " rotates shape and block state differently at " + e.getKey());
}
for (var e : base.loot.entrySet()) require(e.getValue().equals(other.loot.get(rotate(e.getKey(), facing))), "Rotating a site changes its deferred treasure");
}
private static void furnishings(Capture c, Set<BlockPos> reached) {
for (var e : c.blocks.entrySet()) {
var p = e.getKey(); var b = e.getValue();
if (b.getBlock() instanceof DoorBlock) {
var half = b.getValue(BlockStateProperties.DOUBLE_BLOCK_HALF);
var partner = half == DoubleBlockHalf.LOWER ? p.above() : p.below();
require(c.block(partner).equals(b.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF,
half == DoubleBlockHalf.LOWER ? DoubleBlockHalf.UPPER : DoubleBlockHalf.LOWER)), label(c) + " has a truncated door at " + p);
if (half == DoubleBlockHalf.LOWER) {
require(full(c.block(p.below()), p.below()), "Door has no full threshold");
Direction normal = b.getValue(BlockStateProperties.HORIZONTAL_FACING);
require(reached.contains(p.relative(normal)) && reached.contains(p.relative(normal.getOpposite())),
label(c) + " door does not connect two usable sides at " + p);
}
}
if (b.is(Blocks.LADDER)) require(full(c.block(p.relative(b.getValue(BlockStateProperties.HORIZONTAL_FACING).getOpposite())), p),
label(c) + " floating ladder at " + p);
if (b.is(Blocks.LANTERN)) require(b.getValue(BlockStateProperties.HANGING) && full(c.block(p.above()), p.above()),
label(c) + " lantern does not hang from a complete ceiling at " + p);
if (b.is(Blocks.LEVER) || b.is(Blocks.GRINDSTONE)) require(full(c.block(p.below()), p.below()), "Usable fixture has no full support");
if (b.is(Blocks.DARK_OAK_STAIRS)) {
var front = p.relative(b.getValue(BlockStateProperties.HORIZONTAL_FACING).getOpposite());
require(reached.contains(front), label(c) + " seat faces an obstruction at " + p);
}
if (b.hasProperty(BlockStateProperties.BED_PART)) {
Direction direction = b.getValue(BlockStateProperties.HORIZONTAL_FACING);
boolean head = b.getValue(BlockStateProperties.BED_PART) == BedPart.HEAD;
var partner = p.relative(head ? direction.getOpposite() : direction);
require(c.block(partner).equals(b.setValue(BlockStateProperties.BED_PART, head ? BedPart.FOOT : BedPart.HEAD)), "Bed halves disagree");
if (head) {
for (int y = 0; y < 3; y++) require(full(c.block(p.relative(direction).above(y)), p), "Bed head is not against its real wall");
boolean side = false;
for (Direction d : new Direction[] {direction.getClockWise(), direction.getCounterClockWise()})
side |= reached.contains(p.relative(d)) && reached.contains(partner.relative(d));
require(side, "Bed has no reachable full lateral approach");
}
}
if (b.is(Blocks.CRAFTING_TABLE) || b.is(Blocks.CARTOGRAPHY_TABLE) || b.is(Blocks.FURNACE)
|| b.is(Blocks.GRINDSTONE) || b.is(Blocks.LECTERN))
require(approachable(p, reached), label(c) + " functional furnishing cannot be approached at " + p);
}
}
private static boolean full(BlockState b, BlockPos p) {
return b.getFluidState().isEmpty() && b.getCollisionShape(EmptyBlockGetter.INSTANCE, p).toAabbs().stream()
.anyMatch(a -> a.minX == 0 && a.minY == 0 && a.minZ == 0 && a.maxX == 1 && a.maxY == 1 && a.maxZ == 1);
}
private static boolean approachable(BlockPos p, Set<BlockPos> reached) {
for (var d : DIRECTIONS) if (reached.contains(p.relative(d))) return true;
return false;
}
private static boolean clear(Capture c, BlockPos p) {
var body = new AABB(p.getX() + .2, p.getY(), p.getZ() + .2, p.getX() + .8, p.getY() + 1.8, p.getZ() + .8);
for (var at : new BlockPos[] {p, p.above()}) {
var b = opened(c.block(at)); if (!b.getFluidState().isEmpty()) return false;
for (var shape : b.getCollisionShape(EmptyBlockGetter.INSTANCE, at).toAabbs())
if (shape.move(at.getX(), at.getY(), at.getZ()).intersects(body)) return false;
}
return true;
}
private static boolean stand(Capture c, BlockPos p) {
if (p.getX() < 0 || p.getX() > 16 || p.getZ() < 0 || p.getZ() > 16
|| p.getY() < -c.site.foundationDepth() + 1 || p.getY() >= c.site.height() || !clear(c, p)) return false;
if (climb(c.block(p)) || climb(c.block(p.below()))) return true;
return opened(c.block(p.below())).getCollisionShape(EmptyBlockGetter.INSTANCE, p.below()).toAabbs().stream()
.anyMatch(a -> a.maxY >= .5 && a.minX <= .5 && a.maxX >= .5 && a.minZ <= .5 && a.maxZ >= .5);
}
private static BlockState opened(BlockState state) {
return state.getBlock() instanceof DoorBlock || state.getBlock() instanceof TrapDoorBlock
? state.setValue(BlockStateProperties.OPEN, true) : state;
}
private static void refugeShaft(Capture c, Set<BlockPos> reached) {
if (c.site.kind() != HeritagePlan19.Kind.CONSTRUCTION_REFUGE) return;
var hatch = rotate(new BlockPos(10, 5, 11), c.site.facing());
require(c.block(hatch).is(Blocks.DARK_OAK_TRAPDOOR)
&& c.block(hatch).getValue(BlockStateProperties.HALF) == Half.BOTTOM,
label(c) + " hatch is not at the climbable upper lip");
var direction = c.block(hatch).getValue(BlockStateProperties.HORIZONTAL_FACING);
require(stand(c, hatch) && reached.contains(hatch), label(c) + " open hatch has no reachable climbable support");
for (int y = 1; y <= 4; y++) {
var rung = new BlockPos(hatch.getX(), y, hatch.getZ());
require(c.block(rung).is(Blocks.LADDER)
&& c.block(rung).getValue(BlockStateProperties.HORIZONTAL_FACING) == direction,
label(c) + " hatch descent loses a matching ladder at " + rung);
require(stand(c, rung) && reached.contains(rung) && clear(c, rung.above()),
label(c) + " hatch descent cannot traverse vertically at " + rung);
}
}
private static boolean climb(BlockState b) { return b.is(Blocks.LADDER); }
private static Set<BlockPos> walk(Capture c, BlockPos start) {
require(stand(c, start), label(c) + " starts without a standing surface at " + start);
Set<BlockPos> seen = new HashSet<>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
while (!queue.isEmpty()) {
var p = queue.removeFirst();
for (var d : DIRECTIONS) for (int dy : new int[] {0, 1, -1}) {
var q = p.relative(d).offset(0, dy, 0);
if (dy > 0 && !clear(c, p.above())) continue;
if (stand(c, q) && seen.add(q)) queue.add(q);
}
for (int dy : new int[] {-1, 1}) {
var q = p.offset(0, dy, 0);
if ((climb(c.block(p)) || climb(c.block(q)) || climb(c.block(p.below()))) && stand(c, q) && seen.add(q)) queue.add(q);
}
}
return seen;
}
private static void fluids(Capture c) {
Set<BlockPos> water = new HashSet<>(); c.blocks.forEach((p, b) -> { if (!b.getFluidState().isEmpty()) water.add(p); });
int expected = c.site.kind() == HeritagePlan19.Kind.PUMP_STATION ? 60 : c.site.kind() == HeritagePlan19.Kind.GREENHOUSE ? 2 : 0;
require(water.size() == expected, label(c) + " has unexpected water quantity: " + water.size());
for (var p : water) {
require(c.block(p).is(Blocks.WATER), "Only explicitly contained water is allowed");
for (var d : new Direction[] {Direction.DOWN, Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}) {
var q = p.relative(d);
require(water.contains(q) || !c.block(q).isAir(), label(c) + " lets tank/irrigation water escape into air at " + q);
}
}
}
private static void identity(Capture c) {
switch (c.site.kind()) {
case FREIGHT_STATION -> { has(c, Blocks.RAIL); has(c, Blocks.LEVER); }
case POST_OFFICE -> { has(c, Blocks.BARREL); has(c, Blocks.CARTOGRAPHY_TABLE); }
case COMMUNAL_DEPOT -> { has(c, Blocks.BARREL); has(c, Blocks.LECTERN); }
case EXPEDITION_OFFICE -> { has(c, Blocks.CARTOGRAPHY_TABLE); has(c, Blocks.BOOKSHELF); }
case OBSERVATORY -> { has(c, Blocks.CARTOGRAPHY_TABLE); has(c, Blocks.IRON_BARS); }
case RAIL_WORKSHOP -> { has(c, Blocks.GRINDSTONE); has(c, Blocks.RAIL); has(c, Blocks.FURNACE); }
case PUMP_STATION -> { has(c, Blocks.DISPENSER); has(c, Blocks.WATER); }
case SUBSTATION -> { has(c, Blocks.LEVER); has(c, Blocks.IRON_BARS); }
case GREENHOUSE -> { has(c, Blocks.WHEAT); has(c, Blocks.FARMLAND); has(c, Blocks.COMPOSTER); }
case VOID_DOCK -> { has(c, Blocks.IRON_CHAIN); has(c, Blocks.STRIPPED_OAK_LOG); }
case RADIO_RELAY -> { has(c, Blocks.BED.white()); has(c, Blocks.IRON_BARS); }
case PHOTO_CLUB -> { has(c, Blocks.WATER_CAULDRON); has(c, Blocks.IRON_CHAIN); }
case IMPOSSIBLE_SERVICE -> { has(c, Blocks.CALCITE); has(c, Blocks.WATER_CAULDRON); }
case CONSTRUCTION_REFUGE -> { has(c, Blocks.BED.white()); has(c, Blocks.LADDER); has(c, Blocks.DARK_OAK_TRAPDOOR); }
}
}
private static void architecture20(Capture c) {
var old = new Capture(c.site); HeritageRenderer19.render(c.site, old);
long changedVolume = c.blocks.entrySet().stream().filter(e -> e.getKey().getY() > 0
&& e.getValue().isAir() != old.block(e.getKey()).isAir()).count();
require(changedVolume >= 12, label(c) + " merely substitutes a palette instead of changing its architecture: " + changedVolume);
has(c, Blocks.TUFF_BRICKS); has(c, Blocks.CHISELED_TUFF_BRICKS); has(c, Blocks.STONE_BRICKS);
// The working destinations keep exactly their historical entrances and service witnesses.
var previous = HeritageRenderer19.targets(c.site).stream().map(t -> t.name() + ":" + t.x() + ":" + t.y() + ":" + t.z()).toList();
var current = HeritageRenderer20.targets(c.site).stream().map(t -> t.name() + ":" + t.x() + ":" + t.y() + ":" + t.z()).toList();
require(previous.equals(current), label(c) + " changes the saved functional access contract");
for (var e : c.blocks.entrySet()) if (e.getKey().getY() < 0 && e.getKey().getY() >= -c.site.foundationDepth()) {
// Workshop/trench cuts are deliberate; the exterior foundation always remains brick masonry.
int x = e.getKey().getX(), z = e.getKey().getZ();
if (x == 0 || x == 16 || z == 0 || z == 16) require(e.getValue().is(Blocks.STONE_BRICKS), "Visible foundations lost the stone-brick construction");
}
}
private static void daylightAndCrown20(Capture c) {
if (c.site.kind() == HeritagePlan19.Kind.FREIGHT_STATION) {
for (int x : new int[] {7, 9}) for (int z : new int[] {6, 8}) {
require(c.block(rotate(new BlockPos(x, 9, z), c.site.facing())).is(Blocks.GLASS),
label(c) + " passenger skylight is open to the sky instead of glazed");
clearLightWell(c, x, z, 6, c.site.height());
}
}
if (c.site.kind() == HeritagePlan19.Kind.COMMUNAL_DEPOT) {
for (int x : new int[] {5, 11}) for (int z : new int[] {3, 7, 11}) {
require(c.block(rotate(new BlockPos(x, 12, z), c.site.facing())).is(Blocks.GLASS)
&& c.block(rotate(new BlockPos(x, 9, z), c.site.facing())).is(Blocks.GLASS),
label(c) + " warehouse north-light well lost a sealed transparent face");
clearLightWell(c, x, z, 8, c.site.height());
}
}
if (c.site.kind() == HeritagePlan19.Kind.OBSERVATORY) {
// The inset crown must be joined through face-adjacent masonry to the tower roof.
// This detects a detached decorative ring even when every individual voxel is in bounds.
Set<BlockPos> connected = new HashSet<>(); var pending = new java.util.ArrayDeque<BlockPos>();
c.blocks.forEach((p, block) -> {
if (p.getY() == 12 && !block.isAir()) { connected.add(p); pending.add(p); }
});
while (!pending.isEmpty()) {
var p = pending.removeFirst();
for (Direction direction : Direction.values()) {
var next = p.relative(direction);
if (next.getX() < 0 || next.getX() > 16 || next.getZ() < 0 || next.getZ() > 16
|| next.getY() < 12 || next.getY() > 17 || c.block(next).isAir()) continue;
if (connected.add(next)) pending.addLast(next);
}
}
c.blocks.forEach((p, block) -> {
if (p.getY() == 17 && block.is(Blocks.TUFF_BRICK_SLAB))
require(connected.contains(p), label(c) + " observatory crown floats above its buttresses at " + p);
});
require(c.block(rotate(new BlockPos(8, 17, 8), c.site.facing())).isAir(),
label(c) + " crown connection closes the sky-viewing slot");
}
}
private static void clearLightWell(Capture c, int x, int z, int low, int high) {
for (int y = low; y <= high; y++) {
var p = rotate(new BlockPos(x, y, z), c.site.facing());
var block = c.block(p);
require(block.isAir() || block.is(Blocks.GLASS) || block.is(Blocks.GLASS_PANE),
label(c) + " skylight is blocked by an opaque roof or ceiling at " + p + ": " + block);
}
}
private static void export(Capture c) {
String directory = System.getProperty("sanctuary.test.heritage20.export");
if (directory == null || directory.isBlank()) return;
var blocks = c.blocks.entrySet().stream().filter(e -> !e.getValue().isAir())
.sorted(java.util.Comparator.<Map.Entry<BlockPos, BlockState>>comparingInt(e -> e.getKey().getY())
.thenComparingInt(e -> e.getKey().getZ()).thenComparingInt(e -> e.getKey().getX()))
.map(e -> List.of(e.getKey().getX(), e.getKey().getY(), e.getKey().getZ(), e.getValue().toString())).toList();
var targets = HeritageRenderer20.targets(c.site).stream().map(t -> List.of(t.name(), t.x(), t.y(), t.z())).toList();
var document = Map.of("kind", c.site.kind().name(), "facing", c.site.facing(), "seed", c.site.seed(),
"width", 17, "depth", 17, "height", c.site.height(), "floor_y", 0, "blocks", blocks, "targets", targets);
try {
var root = java.nio.file.Path.of(directory); java.nio.file.Files.createDirectories(root);
java.nio.file.Files.writeString(root.resolve(c.site.kind().name().toLowerCase(java.util.Locale.ROOT) + ".json"),
new com.google.gson.GsonBuilder().setPrettyPrinting().create().toJson(document));
} catch (java.io.IOException error) { throw new AssertionError("Cannot export the requested architecture capture", error); }
}
private static void materials20() {
Set<Block> used = new HashSet<>();
for (var kind : HeritagePlan19.Kind.values()) render(site(kind, 0, 0)).blocks.values().forEach(b -> used.add(b.getBlock()));
for (Block block : new Block[] {Blocks.TUFF, Blocks.TUFF_BRICKS, Blocks.CHISELED_TUFF, Blocks.CHISELED_TUFF_BRICKS,
Blocks.CHISELED_STONE_BRICKS, Blocks.TUFF_BRICK_STAIRS, Blocks.TUFF_BRICK_SLAB, Blocks.TUFF_BRICK_WALL,
Blocks.OAK_WOOD, Blocks.BIRCH_WOOD, Blocks.OAK_PLANKS, Blocks.BIRCH_PLANKS,
Blocks.GRANITE, Blocks.DIORITE, Blocks.ANDESITE, Blocks.BIRCH_TRAPDOOR})
require(used.contains(block), "The generation20 architectural vocabulary omits " + block);
}
private static void has(Capture c, Block b) { require(c.blocks.values().stream().anyMatch(v -> v.is(b)), label(c) + " loses its " + b); }
private static void railConnection(Capture c) {
BlockPos destination = switch (c.site.kind()) {
case FREIGHT_STATION -> new BlockPos(2, 1, 8);
case RAIL_WORKSHOP -> new BlockPos(5, 1, 7);
case COMMUNAL_DEPOT -> new BlockPos(8, 1, 7);
default -> null;
};
if (destination == null) return;
var start = rotate(new BlockPos(8, 1, 0), c.site.facing());
destination = rotate(destination, c.site.facing());
Set<BlockPos> seen = new HashSet<>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
while (!queue.isEmpty()) {
var p = queue.removeFirst(); var b = c.block(p);
require(b.is(Blocks.RAIL), label(c) + " freight entry has no supported rail");
for (var d : railEnds(b)) {
var next = p.relative(d); var state = c.block(next);
if (state.is(Blocks.RAIL) && railEnds(state).contains(d.getOpposite()) && seen.add(next)) queue.add(next);
}
}
require(seen.contains(destination), label(c) + " interior rail does not connect to its real external freight entry");
}
private static List<Direction> railEnds(BlockState b) {
return switch (b.getValue(BlockStateProperties.RAIL_SHAPE)) {
case NORTH_SOUTH, ASCENDING_NORTH, ASCENDING_SOUTH -> List.of(Direction.NORTH, Direction.SOUTH);
case EAST_WEST, ASCENDING_EAST, ASCENDING_WEST -> List.of(Direction.EAST, Direction.WEST);
case SOUTH_EAST -> List.of(Direction.SOUTH, Direction.EAST);
case SOUTH_WEST -> List.of(Direction.SOUTH, Direction.WEST);
case NORTH_WEST -> List.of(Direction.NORTH, Direction.WEST);
case NORTH_EAST -> List.of(Direction.NORTH, Direction.EAST);
};
}
private static void routes() {
var cells = new ArrayList<HeritagePlan19.Cell>();
int x = -15;
for (var rail : HeritagePlan19.Rail.values()) {
cells.add(new HeritagePlan19.Cell(x++, 160, 2, 2, rail, rail != HeritagePlan19.Rail.NONE));
}
cells.add(new HeritagePlan19.Cell(-1, 161, 2, 3, HeritagePlan19.Rail.ASCENDING_EAST, false));
var segment = new HeritagePlan19.Segment("route", "a", "b", HeritagePlan19.RouteKind.FREIGHT, -1, 0, cells, 7);
Map<BlockPos, BlockState> result = new HashMap<>();
HeritageRenderer20.renderSegment(segment, new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState b) {
var p = new BlockPos(segment.minX() + x, segment.minY() + y, segment.minZ() + z);
var cell = cells.stream().filter(c -> c.x() == p.getX() && c.z() == p.getZ()).findFirst().orElseThrow();
require(p.getY() >= cell.minY() && p.getY() <= cell.maxY(), "Route escaped its saved column mask"); result.put(p, b);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { throw new AssertionError("A route generated treasure"); }
});
for (var c : cells) {
var p = new BlockPos(c.x(), c.floorY() + 1, c.z());
require(full(result.get(p.below()), p.below()), "Rail or missing-rail witness lost its ballast support");
if (c.railPresent()) require(result.get(p).is(Blocks.RAIL)
&& result.get(p).getValue(BlockStateProperties.RAIL_SHAPE).name().equals(c.rail().name()), "Saved rail shape was replaced by a straight track");
else require(result.get(p).isAir(), "An intentional abandoned-track gap was repaired by rendering");
}
}
private static void chunkPartition() {
var s = site(HeritagePlan19.Kind.CONSTRUCTION_REFUGE, 3, 43); var expected = render(s); var parts = new Capture(s);
for (int cz = s.maxZ() >> 4; cz >= s.minZ() >> 4; cz--) for (int cx = s.maxX() >> 4; cx >= s.minX() >> 4; cx--) {
final int chunkX = cx, chunkZ = cz;
HeritageRenderer20.render(s, new LostCityRenderer.BlockWriter() {
boolean owns(int x, int z) { return ((s.minX() + x) >> 4) == chunkX && ((s.minZ() + z) >> 4) == chunkZ; }
@Override public void set(int x, int y, int z, BlockState state) { if (owns(x, z)) parts.set(x, y, z, state); }
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { if (owns(x, z)) parts.chest(x, y, z, table, seed); }
});
}
require(parts.blocks.equals(expected.blocks) && parts.loot.equals(expected.loot), "Native chunk order changes a site");
}
private record Loot(ResourceKey<LootTable> table, long seed) {}
private static final class Capture implements LostCityRenderer.BlockWriter {
final HeritagePlan19.Site site;
final Map<BlockPos, BlockState> blocks = new HashMap<>();
final Map<BlockPos, Loot> loot = new HashMap<>();
Capture(HeritagePlan19.Site site) { this.site = site; }
@Override public void set(int x, int y, int z, BlockState state) {
int wy = y + site.floorY();
boolean main = x >= 0 && x <= 16 && z >= 0 && z <= 16 && wy >= site.minY() && wy <= site.maxY();
boolean footing = site.supports().stream().anyMatch(s -> s.x() == x + site.minX() && s.z() == z + site.minZ() && wy >= s.minY() && wy <= s.maxY());
require(main || footing, label(this) + " writes outside its exact body and footing mask"); blocks.put(new BlockPos(x, y, z), state);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
require(loot.put(new BlockPos(x, y, z), new Loot(table, seed)) == null, "Duplicate shared treasure"); set(x, y, z, Blocks.CHEST.defaultBlockState());
}
BlockState block(BlockPos p) { return blocks.getOrDefault(p, Blocks.AIR.defaultBlockState()); }
}
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
}
@@ -0,0 +1,219 @@
package fr.koka.sanctuary.worldgen.ruins;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.RailBlock;
import net.minecraft.world.level.block.StairBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.storage.loot.LootTable;
/** Exercises the final native block states, saved bounds and meaningful circulation of all prototypes. */
public final class RuinsRenderer20Smoke {
private RuinsRenderer20Smoke() {}
public static void main(String[] args) {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); run();
System.out.println("RuinsRenderer20Smoke passed: ten physical room types, saved seed variation, dry connected openings, usable work platform, native fixture supports, deferred loot and chunk-order reconstruction");
}
public static void run() {
Set<Map<BlockPos, BlockState>> kinds = new HashSet<>();
for (var kind : RuinsPlan17.Kind.values()) {
for (int size = 0; size < 3; size++) for (int openings : new int[] {1, 2, 4, 8, 5, 10, 15})
for (long seed : new long[] {0, 1}) {
var room = room(kind, size, openings, seed);
var c = render(room); var repeated = render(room);
require(c.blocks.equals(repeated.blocks) && c.loot.equals(repeated.loot), label(room) + " is not deterministic");
if (size == 0 && openings == 15 && seed == 0) kinds.add(c.blocks);
var start = RuinsRenderer20.targets(room).getFirst();
Set<BlockPos> reached = walk(c, position(start));
for (var target : RuinsRenderer20.targets(room)) require(reached.contains(position(target)),
label(room) + " cannot reach " + target.name() + " at " + position(target));
openings(c, reached);
supports(c);
fixtures(c, reached);
identity(c);
for (var chest : c.loot.keySet()) require(approachable(chest, reached), label(room) + " chest is inaccessible at " + chest);
require(c.blocks.values().stream().noneMatch(b -> b.is(Blocks.SPAWNER) || !b.getFluidState().isEmpty()),
label(room) + " introduced a spawner or uncontrolled fluid");
}
var a = render(room(kind, 1, 15, 0)); var b = render(room(kind, 1, 15, 1));
require(!a.blocks.equals(b.blocks), kind + " ignores its saved variation seed");
if (kind != RuinsPlan17.Kind.ACCESS && kind != RuinsPlan17.Kind.CORRIDOR)
require(!furniture(a).equals(furniture(b)), kind + " varies only stone weathering, not physical use or furniture placement");
}
require(kinds.size() == RuinsPlan17.Kind.values().length, "Room names disguise identical rendered interiors");
nativeChunkPartition();
}
private static RuinsPlan17.Room room(RuinsPlan17.Kind kind, int size, int openings, long seed) {
int width = size == 0 ? 9 : size == 1 ? 13 : 17;
int depth = size < 2 ? 9 : 13, height = size < 2 ? 7 : 9;
if (kind == RuinsPlan17.Kind.ACCESS) { width = depth = 5; height = new int[] {14, 27, 40}[size]; }
if (kind == RuinsPlan17.Kind.CORRIDOR) { width = size == 2 ? 17 : 5; depth = size == 1 ? 13 : 5; height = 6; }
int x = -43, y = 150, z = 59;
return new RuinsPlan17.Room(kind, x, y, z, width, height, depth, openings, seed,
List.of(new RuinsPlan17.Support(x, y - 5, z, y - 1),
new RuinsPlan17.Support(x + width - 1, y - 12, z + depth - 1, y - 1)));
}
private static String label(RuinsPlan17.Room r) {
return r.kind() + " " + r.width() + "x" + r.height() + "x" + r.depth() + " openings=" + r.openings() + " seed=" + r.seed();
}
private static BlockPos position(RuinsRenderer20.Target target) { return new BlockPos(target.x(), target.y(), target.z()); }
private static Capture render(RuinsPlan17.Room room) {
var result = new Capture(room); RuinsRenderer20.render(room, result); return result;
}
private static void openings(Capture c, Set<BlockPos> reached) {
var r = c.room;
for (int direction = 0; direction < 4; direction++) for (int side = -1; side <= 1; side++) {
int x = direction == 1 ? r.width() - 1 : direction == 3 ? 0 : r.width() / 2 + side;
int z = direction == 2 ? r.depth() - 1 : direction == 0 ? 0 : r.depth() / 2 + side;
var threshold = new BlockPos(x, 1, z);
require(solid(c.block(threshold)), label(r) + " has no dry threshold at " + threshold);
if ((r.openings() & 1 << direction) == 0) {
require(!c.block(threshold.above()).isAir(), label(r) + " opens an unconnected wall");
continue;
}
for (int y = 2; y <= 4; y++) require(c.block(new BlockPos(x, y, z)).isAir(),
label(r) + " obstructs its three-wide opening at " + x + "/" + z);
require(reached.contains(threshold.above()), label(r) + " opening edge is not connected to the room");
}
}
private static void supports(Capture c) {
var r = c.room;
for (var support : r.supports()) {
for (int y = support.minY(); y <= support.maxY(); y++) require(solid(c.block(new BlockPos(
support.x() - r.minX(), y - r.minY(), support.z() - r.minZ()))), label(r) + " interrupts its saved footing");
require(solid(c.block(new BlockPos(support.x() - r.minX(), 0, support.z() - r.minZ()))),
label(r) + " footing does not meet the structural floor");
}
for (int x = 0; x < r.width(); x++) for (int z = 0; z < r.depth(); z++)
require(solid(c.block(new BlockPos(x, 0, z))), label(r) + " has a perforated structural floor");
}
private static void fixtures(Capture c, Set<BlockPos> reached) {
for (var entry : c.blocks.entrySet()) {
var p = entry.getKey(); var block = entry.getValue();
if (block.is(Blocks.LANTERN)) {
require(block.getValue(BlockStateProperties.HANGING), "Lantern must hang from the ceiling");
require(solid(c.block(p.above())), label(c.room) + " floating lantern at " + p);
}
if (block.is(Blocks.LADDER)) {
Direction facing = block.getValue(BlockStateProperties.HORIZONTAL_FACING);
require(solid(c.block(p.relative(facing.getOpposite()))), label(c.room) + " ladder lacks a back wall at " + p);
}
if (block.is(Blocks.LEVER) || block.is(Blocks.GRINDSTONE)) require(solid(c.block(p.below())),
label(c.room) + " unsupported usable fixture at " + p);
if (block.is(Blocks.DARK_OAK_STAIRS)) {
Direction back = block.getValue(BlockStateProperties.HORIZONTAL_FACING);
require(solid(c.block(p.below())), label(c.room) + " seat has no floor");
require(reached.contains(p.relative(back.getOpposite())), label(c.room) + " seat faces an obstruction at " + p);
}
if (block.is(Blocks.CRAFTING_TABLE) || block.is(Blocks.FURNACE) || block.is(Blocks.GRINDSTONE)
|| block.is(Blocks.STONECUTTER) || block.is(Blocks.CARTOGRAPHY_TABLE) || block.is(Blocks.LECTERN))
require(approachable(p, reached), label(c.room) + " useful furniture cannot be approached at " + p);
}
}
private static void identity(Capture c) {
int expectedLoot = c.room.kind() == RuinsPlan17.Kind.STORE || c.room.kind() == RuinsPlan17.Kind.ARCHIVE ? 1 : 0;
require(c.loot.size() == expectedLoot, label(c.room) + " has unbounded or missing deferred loot");
switch (c.room.kind()) {
case ACCESS -> { has(c, Blocks.LADDER); has(c, Blocks.DARK_OAK_TRAPDOOR); }
case CORRIDOR -> has(c, Blocks.TUFF_BRICKS);
case MINE_GALLERY -> { has(c, Blocks.RAIL); has(c, Blocks.STRIPPED_OAK_LOG); has(c, Blocks.BARREL); }
case WORKSHOP -> { has(c, Blocks.CRAFTING_TABLE); has(c, Blocks.FURNACE); has(c, Blocks.GRINDSTONE); has(c, Blocks.STONECUTTER); has(c, Blocks.ANVIL); has(c, Blocks.LADDER); }
case STORE -> { has(c, Blocks.BARREL); has(c, Blocks.DARK_OAK_SLAB); }
case TILED_HALL -> { has(c, Blocks.CALCITE); has(c, Blocks.POLISHED_DIORITE); has(c, Blocks.DARK_OAK_STAIRS); }
case ARCHIVE -> { has(c, Blocks.BOOKSHELF); has(c, Blocks.LECTERN); has(c, Blocks.CARTOGRAPHY_TABLE); }
case SERVICE_HALL -> { has(c, Blocks.IRON_BARS); has(c, Blocks.CRAFTING_TABLE); }
case PUMP_HALL -> { has(c, Blocks.WATER_CAULDRON); has(c, Blocks.DISPENSER); has(c, Blocks.LEVER); }
case CONTROL_ROOM -> { has(c, Blocks.LEVER); has(c, Blocks.REDSTONE_LAMP); has(c, Blocks.CARTOGRAPHY_TABLE); }
}
}
private static void has(Capture c, Block block) {
require(c.blocks.values().stream().anyMatch(b -> b.is(block)), label(c.room) + " lost its recognizable " + block);
}
private static Map<BlockPos, BlockState> furniture(Capture c) {
var result = new HashMap<BlockPos, BlockState>();
c.blocks.forEach((p, b) -> {
if (p.getY() >= 2 && p.getY() < c.room.height() - 2 && !b.isAir()
&& !b.is(Blocks.STONE_BRICKS) && !b.is(Blocks.CRACKED_STONE_BRICKS)
&& !b.is(Blocks.MOSSY_STONE_BRICKS) && !b.is(Blocks.SMOOTH_STONE)) result.put(p, b);
});
return result;
}
private static boolean approachable(BlockPos p, Set<BlockPos> reached) {
for (Direction d : Direction.Plane.HORIZONTAL) if (reached.contains(p.relative(d))) return true;
return false;
}
private static Set<BlockPos> walk(Capture c, BlockPos start) {
require(stand(c, start), label(c.room) + " starts without two-block standing space at " + start);
var seen = new HashSet<BlockPos>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
while (!queue.isEmpty()) {
var p = queue.removeFirst();
for (Direction direction : Direction.Plane.HORIZONTAL) for (int dy : new int[] {0, 1, -1}) {
var q = p.relative(direction).offset(0, dy, 0);
if (stand(c, q) && seen.add(q)) queue.add(q);
}
for (int dy : new int[] {-1, 1}) {
var q = p.offset(0, dy, 0);
if ((climb(c.block(p)) || climb(c.block(q)) || climb(c.block(p.below())))
&& stand(c, q) && seen.add(q)) queue.add(q);
}
}
return seen;
}
private static boolean stand(Capture c, BlockPos p) {
if (p.getX() < 0 || p.getX() >= c.room.width() || p.getZ() < 0 || p.getZ() >= c.room.depth()
|| p.getY() < 2 || p.getY() >= c.room.height() - 1) return false;
return passable(c.block(p)) && passable(c.block(p.above()))
&& (solid(c.block(p.below())) || c.block(p.below()).getBlock() instanceof StairBlock
|| climb(c.block(p)) || climb(c.block(p.below())));
}
private static boolean climb(BlockState b) { return b.is(Blocks.LADDER) || b.is(Blocks.DARK_OAK_TRAPDOOR); }
private static boolean passable(BlockState b) { return b.isAir() || climb(b) || b.getBlock() instanceof RailBlock || b.is(Blocks.LEVER); }
private static boolean solid(BlockState b) { return b.canOcclude() && b.getFluidState().isEmpty(); }
private static void nativeChunkPartition() {
var r = room(RuinsPlan17.Kind.WORKSHOP, 2, 15, 1); var all = render(r); var chunks = new Capture(r);
for (int cz = r.maxZ() >> 4; cz >= r.minZ() >> 4; cz--) for (int cx = r.maxX() >> 4; cx >= r.minX() >> 4; cx--) {
final int chunkX = cx, chunkZ = cz;
RuinsRenderer20.render(r, new LostCityRenderer.BlockWriter() {
private boolean belongs(int x, int z) { return ((r.minX() + x) >> 4) == chunkX && ((r.minZ() + z) >> 4) == chunkZ; }
@Override public void set(int x, int y, int z, BlockState state) { if (belongs(x, z)) chunks.set(x, y, z, state); }
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { if (belongs(x, z)) chunks.chest(x, y, z, table, seed); }
});
}
require(chunks.blocks.equals(all.blocks) && chunks.loot.equals(all.loot), "Chunk order changes saved room rendering");
}
private record Loot(ResourceKey<LootTable> table, long seed) {}
private static final class Capture implements LostCityRenderer.BlockWriter {
final RuinsPlan17.Room room;
final Map<BlockPos, BlockState> blocks = new HashMap<>();
final Map<BlockPos, Loot> loot = new HashMap<>();
Capture(RuinsPlan17.Room room) { this.room = room; }
@Override public void set(int x, int y, int z, BlockState state) {
int wx = room.minX() + x, wy = room.minY() + y, wz = room.minZ() + z;
require(room.contains(wx, wy, wz), label(room) + " wrote outside its saved exact body/support mask: " + x + "/" + y + "/" + z);
require(wy >= room.boundsMinY() && wy <= room.boundsMaxY(), "Write escapes native vertical bounds");
blocks.put(new BlockPos(x, y, z), state);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
var pos = new BlockPos(x, y, z);
require(loot.put(pos, new Loot(table, seed)) == null, "Duplicate deferred chest");
set(x, y, z, Blocks.CHEST.defaultBlockState());
}
BlockState block(BlockPos p) { return blocks.getOrDefault(p, Blocks.AIR.defaultBlockState()); }
}
private static void require(boolean test, String message) { if (!test) throw new AssertionError(message); }
}
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "0.1.0-alpha.19"
version = "0.1.0-alpha.20"
pack-format = "packwiz:1.1.0"
[index]