feat: add detailed architecture for Sanctuary alpha20
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -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 d’escaliers 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,
|
||||
d’hydrologie et d’implantation 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 n’est activé.
|
||||
|
||||
## 0.1.0-alpha.19 — 2026-09-09
|
||||
|
||||
- Quatorze nouvelles infrastructures abandonnées sur l’île initiale, avec pièces,
|
||||
|
||||
@@ -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. **L’alpha.19 est publiée** :
|
||||
quatorze types d’infrastructures 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. **L’alpha.20** reprend
|
||||
l’architecture 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
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# Architecture de l’alpha.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 à l’inté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 l’option **Structures
|
||||
expérimentales** restent disponibles. Le catalogue n’impose pas que chaque lieu
|
||||
apparaisse sur chaque île.
|
||||
|
||||

|
||||
|
||||
*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 d’accè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 d’extraction 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 d’escalier coiffée d’un 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 d’attente 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 d’inventaire. |
|
||||
| Cartothèque d’expé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 d’escalier, salle d’instruments et plateforme d’observation. |
|
||||
| Atelier ferroviaire | Large couverture à nervures et vitrages, piles de granit ; deux voies, fosse d’entretien avec escalier, établi, fourneau, meule et petite réserve. |
|
||||
| Station de pompage et château d’eau | Bâtiment bas, conduites figurées par des grilles et murets, cuve haute sur quatre piles ciselées ; galerie d’entretien et réserve sèche. L’eau 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 d’entretien accessible. |
|
||||
| Serre et graineterie | Verrière haute rythmée par des nervures de bouleau ; cultures, deux points d’irrigation 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 d’une 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 d’andé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 n’ajoutent 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 d’andé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 d’eau | 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 ; l’andé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 d’escaliers à 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 d’expansion ou production électrique dédiée
|
||||
n’est 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.
|
||||
@@ -750,6 +750,35 @@ explicitement ; aucune version 18 publiée
|
||||
dans ce dépôt à l’ouverture 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 l’andé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 d’escaliers 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 l’option 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 |
@@ -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 d’emprises
|
||||
certifiées aux nouveaux rendus 20.
|
||||
|
||||
## Connexions des blocs
|
||||
|
||||
La géométrie complète d’une 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 l’ordre de chargement des chunks voisins. Aucun chunk extérieur
|
||||
n’est 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 n’est 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 d’escaliers, 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 d’or placé en `-203 255 -13` | Bloc d’or conservé |
|
||||
|
||||
La cartothèque d’expédition (`EXPEDITION_OFFICE`) n’a pas trouvé d’emplacement
|
||||
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 n’est ouverte dans ces essais.
|
||||
|
||||
Le plan des infrastructures conserve l’empreinte
|
||||
`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 n’est 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 à l’entrée, pas le centre d’un
|
||||
toit. `/sanctuary structures` liste les lieux ;
|
||||
`/locate structure sanctuary:heritage_networks`, recherché depuis `0 250 0`
|
||||
dans le test, désigne l’entré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 d’eau | `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ôt–château d’eau
|
||||
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 n’ont pas été activés. L’identifiant de test historique ne change pas
|
||||
la version 20 enregistrée dans cette expansion.
|
||||
|
||||
| Mesure | Ville initiale | Ville d’expansion |
|
||||
| --- | --- | --- |
|
||||
| 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 d’apparition 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 l’expansion se traverse de `2000 226 -613` à `2000 224 -597`.
|
||||
Aucune pile de viaduc n’est 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 d’apparition naturelle ici.
|
||||
|
||||
### Grands équipements du témoin initial
|
||||
|
||||
| Repère | Géométrie ou coordonnées constatées |
|
||||
| --- | --- |
|
||||
| Entrée de l’immeuble | `-30 252 21` |
|
||||
| Entrée de l’hôtel | `-8 252 21` |
|
||||
| Entrée de la bibliothèque | `-52 252 43` |
|
||||
| Trappe d’accès aux sous-sols, position du bloc | `-19 252 41` |
|
||||
| Grande piscine, emprise du bâtiment | De `-47 224 -26` à `9 236 4` |
|
||||
| Volume d’eau 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 d’entré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
|
||||
|
||||
L’essai 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
|
||||
l’exécution réelle des fluides par une descente d’eau observée dans chaque
|
||||
chunk concerné. Ils sont retirés avant la sauvegarde et exclus des quantités
|
||||
d’eau des structures.
|
||||
|
||||
Après cette simulation, le grand bassin garde **4 976 cellules d’eau** dans
|
||||
son enceinte, et chaque égout conserve ses cinq cellules contrôlées. Les deux
|
||||
bouches gardent leur source et produisent de l’eau à l’extérieur. Leur écoulement
|
||||
rencontre cependant du terrain : premier bloc solide sous les sorties aux
|
||||
niveaux **113** et **191**. Ce témoin prouve des sorties d’eau actives, pas une
|
||||
chute libre continue jusqu’au vide.
|
||||
|
||||
Les chiffres de connexion suivants sont ceux de l’inspection 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 n’est représenté dans cette partie du
|
||||
contrôle urbain. Le bloc d’or témoin est placé en `-38 252 21` et le journal
|
||||
est sauvegardé avec l’empreinte
|
||||
`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 d’or 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 d’eau 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 à l’ouest :
|
||||
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. À l’intérieur d’une 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
|
||||
l’oracle 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 n’est 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 l’oracle
|
||||
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
|
||||
d’infrastructures**. Ils ne garantissent pas leur présence sur toutes les îles.
|
||||
La génération native 20 Grand n’est 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 d’un client avec ses shaders ; ce n’est 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 d’eau 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 n’a été effectué.
|
||||
+2
-2
@@ -8,6 +8,6 @@ loader_version=0.19.5
|
||||
loom_version=1.17.20
|
||||
fabric_api_version=0.160.0+26.3
|
||||
|
||||
mod_version=0.1.0-alpha.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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+556
@@ -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); }
|
||||
}
|
||||
}
|
||||
+1034
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -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.
|
||||
|
||||
+6
-3
@@ -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");
|
||||
|
||||
+5
-4
@@ -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);
|
||||
|
||||
+2
-2
@@ -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(); }
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+25
-19
@@ -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();
|
||||
|
||||
+755
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+88
@@ -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;
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package fr.koka.sanctuary.worldgen.city;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
|
||||
/** A saved underground module includes its openings and certified supports, including their anchors. */
|
||||
public final class 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());
|
||||
}
|
||||
}
|
||||
+170
@@ -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; }
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
+185
@@ -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());
|
||||
}
|
||||
}
|
||||
+643
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -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; }
|
||||
|
||||
+6
-4
@@ -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; }
|
||||
|
||||
+6
-4
@@ -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"
|
||||
}
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
+81
@@ -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");
|
||||
|
||||
+451
@@ -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); }
|
||||
}
|
||||
+229
@@ -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);
|
||||
}
|
||||
}
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
package fr.koka.sanctuary.worldgen.city;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.StairBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
|
||||
/** Actual renderer topology on disposable, finite terrain fixtures. No Minecraft save is changed. */
|
||||
public final class 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); }
|
||||
}
|
||||
+95
@@ -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); }
|
||||
}
|
||||
+85
@@ -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); }
|
||||
}
|
||||
+420
@@ -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); }
|
||||
}
|
||||
+219
@@ -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
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user