Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a633d4cac9 | ||
|
|
d6e0c144d0 | ||
|
|
d462f01e2b | ||
|
|
bb63fab388 | ||
|
|
ae4c6200db | ||
|
|
0edac29dd8 | ||
|
|
2facf6f722 | ||
|
|
c23e72b8e6 |
@@ -1,5 +1,57 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.0-alpha.20 — 2026-09-10
|
||||
|
||||
- Passe architecturale sur les six bâtiments urbains, les quatorze anciennes
|
||||
infrastructures, les espaces publics et les réseaux souterrains. Palettes
|
||||
propres aux usages : chêne, bouleau, granit, diorite, andésite et tuf ;
|
||||
fondations maçonnées en briques de pierre conservées.
|
||||
- Angles coupés, baies en saillie, portiques, retraits, loggias, toitures
|
||||
nervurées et verrières avec passage lumineux. Sols et plafonds détaillés,
|
||||
corniches, encadrements, murets et blocs ciselés ; bordures de routes en
|
||||
briques de tuf et accents ciselés.
|
||||
- Vitres, barreaux, clôtures, murs bas et angles 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 ; release et canal packwiz publiés. Deux
|
||||
synchronisations isolées puis deux dans la même instance Prism passent, avec
|
||||
925 fichiers personnels et réglages suivis conservés.
|
||||
|
||||
Voir le [catalogue illustré](docs/architecture-alpha20.md) et les
|
||||
[vérifications et limites](docs/testing-alpha20.md). Les fonctions de la bêta
|
||||
restent à construire ; aucun nouveau système économique n’est activé.
|
||||
|
||||
## 0.1.0-alpha.19 — 2026-09-09
|
||||
|
||||
- Quatorze nouvelles infrastructures abandonnées sur l’île initiale, avec pièces,
|
||||
accès, mobilier, secrets et outils vanilla associés. La présence varie selon
|
||||
la graine et le relief ; les quatorze types ont été rencontrés en tests natifs.
|
||||
- Voies de fret aux rails incomplets et chemins entre destinations réelles.
|
||||
Le témoin Moyen, graine 0, contient treize lieux et sept liaisons, dont un
|
||||
fret de 499 blocs entre dépôt et pompage. Tracés enregistrés par chunk.
|
||||
- Option FR/EN « Structures expérimentales » dans le bouton natif Personnaliser,
|
||||
trois tailles conservées. Codec `sanctuary:island_v19` et journal 19 ; les
|
||||
anciens mondes gardent leurs plans et futurs chunks. Un nouveau monde avec
|
||||
l’option désactivée conserve le socle urbain sans les expériences 17/19.
|
||||
- Commande opérateur `/sanctuary structures` pour relever les entrées.
|
||||
La ville et ses sous-sols sont conservés ; les systèmes de la bêta restent
|
||||
à construire.
|
||||
|
||||
`check build assemblePack` passe, dont les huit GameTests généraux. Les trois
|
||||
formats sont vérifiés nativement ; le témoin moyen se rouvre avec ses 112 pièces,
|
||||
13 coffres, 68 cibles accessibles et la modification témoin. Les identités 17
|
||||
restent identiques à la release publiée. JAR, MRpack et ZIP Prism vérifiés ;
|
||||
release et canal packwiz publiés. Deux synchronisations isolées puis deux dans
|
||||
la même instance Prism passent, avec 767 fichiers personnels et réglages suivis
|
||||
conservés. Voir [les preuves et limites](docs/testing-alpha19.md).
|
||||
|
||||
## 0.1.0-alpha.17 — 2026-09-09
|
||||
|
||||
- Prototype de trois réseaux oubliés sur l’île initiale : mines et ateliers,
|
||||
|
||||
@@ -15,10 +15,85 @@ infrastructures qui permettront progressivement d'ouvrir de nouveaux continents.
|
||||
Ce dépôt démarre la nouvelle base Fabric. La [vision complète](docs/vision.md)
|
||||
conserve les intentions ; le [backlog](docs/backlog.md) suit les
|
||||
tickets. Les systèmes d’économie, de progression collective et les dimensions
|
||||
décrits dans la vision ne sont pas encore implémentés. **L’alpha.17 est publiée** :
|
||||
trois familles de réseaux oubliés expérimentaux rejoignent la ville et ses grands
|
||||
sous-sols sur les nouvelles îles de départ. Le canal packwiz et la même instance
|
||||
Prism sont à jour ; voir [Distribution](docs/packwiz.md).
|
||||
décrits dans la vision ne sont pas encore implémentés. **L’alpha.22 est
|
||||
publiée** : le placement des structures redevient sélectif. Les palettes,
|
||||
intérieurs et filons de l’alpha.21 sont conservés. Voir [les essais22](docs/testing-alpha22.md)
|
||||
et [la distribution](docs/packwiz.md).
|
||||
|
||||
## Alpha.22 — trouver le bon terrain
|
||||
|
||||
La nouvelle génération préfère refuser une ville ou un bâtiment facultatif
|
||||
qui ne trouve pas un emplacement adapté. Plus de ville forcée sur une réserve
|
||||
artificielle ; moins de terrassement, des fondations proches de la roche et des
|
||||
chemins qui suivent le relief. La grande traversée et la salle d’expansion sont
|
||||
réservées en priorité et peuvent exister sans ville au-dessus. Les autres lieux
|
||||
n’ont pas de quota garanti.
|
||||
|
||||
Créer un **nouveau monde Sanctuary** pour utiliser ces règles. Les tailles
|
||||
512/724/1024 et le bouton natif **Personnaliser** restent inchangés ; les anciens
|
||||
mondes conservent leurs générateurs. La vision retient désormais **Minecraft
|
||||
comme archéologie d’un autre Minecraft** : chaque ruine témoigne d’une ancienne
|
||||
solution, et de ceux qui l’ont construite puis ont disparu.
|
||||
|
||||
Les trois tailles natives, la réouverture, le cas expérimental OFF et la
|
||||
compatibilité21 passent, ainsi que `check build assemblePack` et ses huit
|
||||
GameTests généraux. Le [MRpack alpha.22](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.22/Sanctuary-0.1.0-alpha.22.mrpack)
|
||||
est disponible, le canal packwiz et la même instance Prism sont synchronisés.
|
||||
Les925 fichiers personnels et réglages suivis sont conservés. Voir
|
||||
[la distribution](docs/packwiz.md).
|
||||
|
||||
## Alpha.21 — matériaux, quartiers et traversées
|
||||
|
||||
La ville revient à la pierre lisse, pierre, briques de pierre, andésite et chêne.
|
||||
Le tuf accompagne surtout les chemins et les sous-sols. Les toits et ornements
|
||||
sont raccordés à leurs appuis ; les pièces gardent leurs aménagements. La ville
|
||||
recherche des petits quartiers séparés par du terrain naturel, avec des chemins
|
||||
vers le cœur. Les lieux patrimoniaux recherchent davantage de raccords.
|
||||
|
||||
L’option expérimentale ajoute une recherche de galeries industrielles : chaussée
|
||||
cinq blocs, deux trottoirs, salles et escaliers de surface. Les placements sur
|
||||
pente sont assouplis et les destinations isolées sont prioritaires pour les chemins.
|
||||
Créer un **nouveau monde Sanctuary** pour ce générateur21. Les anciens mondes
|
||||
conservent leur génération. Les vérifications et coordonnées du
|
||||
témoin sont consignées dans [les essais](docs/testing-alpha21.md).
|
||||
|
||||
Le [MRpack alpha.21](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.21/Sanctuary-0.1.0-alpha.21.mrpack)
|
||||
est disponible. Le canal packwiz et la même instance Prism sont synchronisés ;
|
||||
les 925 fichiers personnels et réglages suivis sont conservés. Les tests de
|
||||
création/réouverture, le cas OFF et `check build assemblePack` passent.
|
||||
|
||||
## Alpha.20 — identité des bâtiments
|
||||
|
||||
La passe reprend les six bâtiments de ville, les quatorze infrastructures,
|
||||
les parcs et les sous-sols. Bois de chêne et de bouleau, granit, diorite et tuf
|
||||
identifient les usages ; biseaux, baies, corniches, portiques et verrières
|
||||
donnent du relief aux volumes. Les routes reçoivent des bordures de briques
|
||||
de tuf et des blocs ciselés. Les vitres et grilles se raccordent entre chunks.
|
||||
|
||||
Créer un **nouveau monde Sanctuary, Moyen, graine 0**, avec **Structures
|
||||
expérimentales** activées pour retrouver le témoin. Les anciens mondes gardent
|
||||
leur génération. Le bouton natif **Personnaliser** et ses trois tailles restent
|
||||
identiques. Voir le [catalogue illustré](docs/architecture-alpha20.md) et les
|
||||
[essais alpha.20](docs/testing-alpha20.md). `check build assemblePack`, les essais natifs et les artefacts sont vérifiés ;
|
||||
la [release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.20)
|
||||
et le canal packwiz sont publiés. La même instance Prism est synchronisée
|
||||
deux fois, avec 925 fichiers personnels et réglages suivis conservés.
|
||||
|
||||
## Alpha.19 — anciennes infrastructures
|
||||
|
||||
Le chantier ajoute une première passe des quatorze lieux du
|
||||
[catalogue](docs/structures-audit.md), avec leurs accès, des petits butins vanilla
|
||||
et des voies de fret ou chemins abandonnés entre destinations réelles.
|
||||
L’option **Structures expérimentales** reste enregistrée à la création du monde.
|
||||
La nouvelle génération garde les anciennes sauvegardes intactes. Aucun système
|
||||
de progression, mailbox, caméra ou réacteur n’est activé.
|
||||
Les trois tailles sont vérifiées en génération native ; création et réouverture
|
||||
conservent les structures et les coffres. `check build assemblePack` passe.
|
||||
Pour voir le grand fret : **Moyen, graine 0**, option activée ;
|
||||
`/sanctuary structures` liste les entrées. Voir [les preuves et la carte](docs/testing-alpha19.md).
|
||||
La [release alpha.19](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.19)
|
||||
et le canal packwiz sont publiés. La même instance Prism est synchronisée deux
|
||||
fois, avec 767 fichiers personnels et réglages suivis conservés.
|
||||
|
||||
## Alpha.17 — prototype des réseaux oubliés
|
||||
|
||||
@@ -409,7 +484,7 @@ sera une mise à jour explicite, avec vérification des API et des sauvegardes.
|
||||
| Fabric API | 0.160.0+26.3 |
|
||||
| Fabric Loom | 1.17.20 |
|
||||
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
|
||||
| Sanctuary / pack | 0.1.0-alpha.17 |
|
||||
| Sanctuary / pack | 0.1.0-alpha.21 |
|
||||
|
||||
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
|
||||
uniquement la bibliothèque standard et repère aussi une installation Python
|
||||
@@ -438,7 +513,7 @@ décrits dans [Validation](docs/testing.md).
|
||||
|
||||
Résultats :
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.17.jar` : mod à installer avec
|
||||
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.21.jar` : mod à installer avec
|
||||
Fabric API sur la version Minecraft indiquée.
|
||||
- `build/packwiz/` : pack de développement complet, avec le mod construit et
|
||||
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Alpha.21 — palettes et couvertures
|
||||
|
||||
La ville revient à une base commune : pierre lisse, pierre, briques de pierre,
|
||||
andésite et chêne. Les façades, seuils, corniches et verrières gardent leurs
|
||||
formes ; les pièces conservent leurs fonctions et leur circulation. La serre
|
||||
emploie le bouleau comme bois propre. Le tuf accompagne les routes et les
|
||||
parties souterraines.
|
||||
|
||||

|
||||
|
||||
Ces vues sont calculées depuis les blocs produits par les renderers21 et les
|
||||
modèles/textures de Minecraft 26.3-pre-2. Elles montrent des variantes choisies,
|
||||
sans terrain, éclairage ni végétation de la partie. Ce ne sont pas des captures
|
||||
du client et elles ne prouvent pas les raccords avec le relief.
|
||||
|
||||
Les couvertures de l’atelier, du dépôt, de la serre, du relais radio et du refuge
|
||||
reposent sur des murs ou charpentes. L’ancien plafond opaque sous leur nouvelle
|
||||
couverture est retiré. Les vérifications contrôlent aussi les petits appuis des
|
||||
excavations, piscines, rampes de garage et ornements.
|
||||
|
||||

|
||||
|
||||
La seconde planche coupe volontairement les murs pour voir les dessous des toits.
|
||||
Les détails de validation et la graine d’essai sont dans [le protocole](testing-alpha21.md).
|
||||
+120
@@ -717,6 +717,126 @@ Aucune structure, règle de butin, version binaire ou instance Prism modifiée.
|
||||
La sélection des lieux et la réalisation des axes feront l’objet du chantier
|
||||
suivant ; ce catalogue n’est pas une liste d’ajouts tous validés.
|
||||
|
||||
### WG-22 — Anciennes infrastructures et liaisons — alpha.19
|
||||
|
||||
**Branche :** `codex/heritage-worldgen-alpha19`.
|
||||
|
||||
**Demande validée :** première passe de toutes les nouvelles structures du
|
||||
catalogue WG-21 et de leurs liaisons avant la bêta, en conservant la possibilité
|
||||
de revenir à une génération sans ce chantier.
|
||||
|
||||
- Ajouter les quatorze lieux avec architectures, entrées, pièces, mobilier et
|
||||
petits butins vanilla associés à leur usage. Présence selon le terrain ;
|
||||
relever les lieux réellement générés et donner les coordonnées des témoins.
|
||||
- Planifier des voies de fret et chemins entre destinations réelles, avec
|
||||
sections abandonnées lisibles, appuis et passages vérifiés. Rechercher les
|
||||
raccords aux anciennes installations dans un budget borné.
|
||||
- Garder une majorité naturelle à l’île ; conserver le terrain, l’hydrologie,
|
||||
la ville et les réseaux existants comme base. Première île uniquement.
|
||||
- Nouveau codec 19, option expérimentale sauvegardée à la création du monde.
|
||||
Option désactivée : socle urbain seul. Les mondes 17 et antérieurs gardent
|
||||
leur génération, leurs pièces et leurs futurs chunks. Aucune conversion,
|
||||
suppression de structures ou rétrogradation d’une sauvegarde 19.
|
||||
- Les bâtiments préparent les futurs services ; mailbox, deposit boxes,
|
||||
caméra, réacteur, progression et récompenses personnelles restent pour la
|
||||
bêta. Les coffres de cette passe sont partagés et utilisent du butin vanilla.
|
||||
- Vérifier créations/réouvertures, option désactivée, anciens mondes, supports,
|
||||
circulations et vrais placements. `check build assemblePack`, artefacts,
|
||||
publication packwiz et mise à jour de la même instance Prism.
|
||||
|
||||
**État : livré en alpha.19 ; code, artefacts et synchronisation Prism vérifiés.** Voir
|
||||
[les essais natifs et la carte des lieux](testing-alpha19.md). Version 19 demandée
|
||||
explicitement ; aucune version 18 publiée
|
||||
dans ce dépôt à 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 : livré en alpha.20 ; code, artefacts et même instance Prism vérifiés.** Voir
|
||||
[le contrat et les essais alpha.20](testing-alpha20.md).
|
||||
|
||||
### WG-24 — Palettes sobres, toits portés et galeries — alpha.21
|
||||
|
||||
**Branche :** `codex/palettes-reseaux-alpha21`.
|
||||
|
||||
Reprendre les extérieurs sans changer les plans intérieurs approuvés : smooth
|
||||
stone, stone brick, stone, andésite et chêne dominants en ville ; un bois cohérent
|
||||
par bâtiment unique et tuf surtout au sol et sous terre. Corriger les toits
|
||||
suspendus et les anciens plafonds superposés, avec appuis vérifiables.
|
||||
|
||||
Étendre la ville par au plus deux petits quartiers séparés de terrain naturel,
|
||||
reliés par des chemins praticables ; rapprocher les destinations patrimoniales
|
||||
par un graphe de chemins. Ajouter des galeries industrielles abandonnées :
|
||||
chaussée de cinq blocs, deux trottoirs, quelques grandes salles et deux accès
|
||||
par escaliers depuis la surface, en respectant les eaux et structures existantes.
|
||||
|
||||
Ajouter des fronts rocheux et de petits filons intacts dans les galeries et
|
||||
leurs salles de maintenance : ressources vanilla réellement minables, sans
|
||||
régénération de butin ni remplacement des blocs déjà extraits. Fournir un MRpack
|
||||
pour l’essai séparé demandé.
|
||||
|
||||
Assouplir les refus de placement : admettre les pentes, des volumes partiellement
|
||||
enfouis et des appuis adaptés ; rechercher des détours et prioriser les lieux
|
||||
isolés pour les chemins. Garder une ville fragmentée sans socle géant.
|
||||
|
||||
Nouvelle génération21 réservée aux nouveaux mondes ; tous les codecs et rendus
|
||||
20 et antérieurs restent disponibles. Tailles512/724/1024, Moyen par défaut ;
|
||||
option expérimentale enregistrée. Aucun monde personnel modifié. Vérifier les
|
||||
pièces natives, accès, connexions, supports, création/réouverture, ancien monde20,
|
||||
puis livrer le pack stable21 et synchroniser la même instance Prism.
|
||||
|
||||
**État : livré en alpha.21 ; MRpack, release et canal packwiz publiés, même instance Prism synchronisée deux fois. Tests, sauvegardes et limites documentés dans `docs/testing-alpha21.md`.**
|
||||
|
||||
### WG-25 — Placement sélectif et archéologie — alpha.22
|
||||
|
||||
**Branche :** `codex/placement-selectif-alpha22`.
|
||||
|
||||
Annule pour les nouveaux mondes l’assouplissement de WG-24. Chercher un terrain
|
||||
vraiment adapté et accepter l’absence d’une structure plutôt que de forcer son
|
||||
implantation. La ville ne reçoit plus de réservation artificielle de secours.
|
||||
Bâtiments et chemins limitent terrassement et remblai. Exception explicite :
|
||||
la grande traversée et la salle d’expansion sont des éléments de base, à placer
|
||||
en priorité indépendamment de la ville ; les égouts peuvent être ailleurs.
|
||||
Les galeries gardent leur génie civil et la salle reçoit un accès autonome.
|
||||
Les recherches restent bornées.
|
||||
|
||||
Conserver les palettes, les intérieurs, les filons et le rendu21. Nouveau codec22
|
||||
pour les nouveaux mondes, sans migration ni retouche des mondes précédents.
|
||||
Vérifier l’admission sur les bons terrains, les refus sur les mauvais, la création
|
||||
native, la réouverture et la compatibilité21. Livrer le MRpack et mettre à jour
|
||||
le canal et la même instance Prism en conservant les données personnelles.
|
||||
|
||||
Inscrire dans la vision « Minecraft comme archéologie d’un autre Minecraft » :
|
||||
chaque ruine garde la trace d’une solution ancienne. Les neuf personnages sont
|
||||
ceux nommés dans la vision ; la découverte et les rôles restent à définir.
|
||||
Ce cadre narratif ne constitue pas une nouvelle mécanique implémentée.
|
||||
|
||||
**État : livré en alpha.22 ; MRpack, release et canal packwiz publiés, même instance Prism synchronisée deux fois.** Voir [les essais22](testing-alpha22.md).
|
||||
|
||||
## Réserve de thèmes futurs
|
||||
|
||||
Ces thèmes servent à retrouver la vision, pas à demander leur implémentation immédiate. On en extrait un ticket seulement lorsqu'il devient utile au prochain incrément jouable.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 306 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.7 MiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 19 KiB |
+162
-1
@@ -13,10 +13,171 @@ JAR Sanctuary sont des pièces jointes de releases Gitea. Aucun JAR de mod ou
|
||||
dépendance téléchargée n'entre dans l'historique Git. Le canal Beta est distinct
|
||||
de l'ancien pack 26.2.
|
||||
|
||||
## Alpha.22 publiée — 10 septembre 2026
|
||||
|
||||
[Sanctuary 0.1.0-alpha.22](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.22)
|
||||
est publiée depuis le commit source `d6e0c14`. Le canal stable est au commit
|
||||
`0019178`. Le JAR, le
|
||||
[MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.22/Sanctuary-0.1.0-alpha.22.mrpack)
|
||||
et le ZIP d’amorçage Prism ont été vérifiés par téléchargement public.
|
||||
SHA-256 du JAR :
|
||||
|
||||
```text
|
||||
10d7ef32ac2e81a977ea4854503bafa29e0bc10929984b76df01d6f5f61f9056
|
||||
```
|
||||
|
||||
Les bâtiments de surface recherchent des terrains adaptés ; la grande traversée
|
||||
et la salle d’expansion restent des éléments de base, indépendants d’une ville.
|
||||
Les galeries peuvent chercher un détour et de nouveaux accès. Le hall peut
|
||||
accueillir des égouts ailleurs dans l’île. Les trois tailles natives, le cas OFF,
|
||||
la réouverture et la comparaison21 passent. `check build assemblePack` passe
|
||||
avec75 tâches et les huit GameTests généraux. Voir [les essais22](testing-alpha22.md).
|
||||
|
||||
Deux synchronisations isolées puis deux dans la même instance Prism réussissent.
|
||||
Un seul JAR Sanctuary alpha.22 est actif ; les **925 fichiers personnels et
|
||||
réglages suivis** gardent leurs hashes. Le dossier reste
|
||||
`Sanctuary-0.1.0-alpha.1`, affiché « Sanctuary Beta ». Le JAR21 et le suivi
|
||||
packwiz sont sauvegardés sous `sanctuary-backups/before-0.1.0-alpha.22/`.
|
||||
Reçus locaux : `build/alpha22-isolated-validation.json` et
|
||||
`build/alpha22-prism-validation.json`.
|
||||
|
||||
Pour tester : **nouveau monde Sanctuary, Moyen, graine0**. Les portiques de la
|
||||
traversée sont `-170 253 -98` et `170 254 -98` ; la trappe du hall indépendant
|
||||
est `2 252 0`. `/sanctuary structures` liste les lieux. Les villes et bâtiments
|
||||
facultatifs peuvent être refusés. Les graines vérifiées ne prouvent pas la
|
||||
présence des infrastructures sur toutes les graines ; les recherches restent
|
||||
bornées. Aucun monde personnel n’a été ouvert, converti ou régénéré. Aucun
|
||||
essai client visuel ni essai Windows natif n’est revendiqué.
|
||||
|
||||
## Alpha.21 publiée — 10 septembre 2026
|
||||
|
||||
[Sanctuary 0.1.0-alpha.21](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.21)
|
||||
est publiée depuis le commit source `bb63fab`. Le canal stable utilisait cette version au commit
|
||||
`8002fce`. Le JAR, le
|
||||
[MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.21/Sanctuary-0.1.0-alpha.21.mrpack)
|
||||
et le ZIP d’amorçage Prism ont été vérifiés. SHA-256 du JAR :
|
||||
|
||||
```text
|
||||
e528e42d99f9e1ea221f780ea089b49b42a1a4e9e24e4ddd001d045183202c21
|
||||
```
|
||||
|
||||
Palettes sobres, toitures portées, quartiers reliés, placements patrimoniaux
|
||||
assouplis et premières galeries industrielles avec fronts rocheux et filons.
|
||||
Voir le [catalogue](architecture-alpha21.md) et les [essais](testing-alpha21.md).
|
||||
`check build assemblePack`, les huit GameTests généraux, les créations et
|
||||
réouvertures natives passent. Le cas expérimental OFF passe. Les fichiers
|
||||
historiques et les témoins natifs de la génération20 sont conservés.
|
||||
|
||||
Deux synchronisations isolées puis deux dans la même instance Prism réussissent.
|
||||
Un seul JAR Sanctuary alpha.21 est actif ; les **925 fichiers personnels et
|
||||
réglages suivis** gardent leurs hashes. Le dossier reste
|
||||
`Sanctuary-0.1.0-alpha.1`, affiché « Sanctuary Beta ». Le JAR20 et le suivi
|
||||
packwiz sont sauvegardés sous `sanctuary-backups/before-0.1.0-alpha.21/`.
|
||||
Reçus locaux : `build/alpha21-isolated-validation.json` et
|
||||
`build/alpha21-prism-validation.json`.
|
||||
|
||||
Pour tester : **nouveau monde Sanctuary, Moyen, graine0**, **Structures
|
||||
expérimentales** activées dans **Personnaliser**. Entrées des galeries :
|
||||
`-135 253 -178` et `-135 249 162`. `/sanctuary structures` liste les lieux.
|
||||
Les raccords dépendent du relief ; certains sites restent isolés et les réseaux
|
||||
souterrains ne sont pas tous reliés. Aucun monde personnel n’a été ouvert,
|
||||
converti ou régénéré. Aucun essai Windows natif ni essai visuel client n’est
|
||||
revendiqué. Une sauvegarde21 nécessite le binaire21 ou un futur binaire compatible.
|
||||
|
||||
## Alpha.20 publiée — 10 septembre 2026
|
||||
|
||||
[Sanctuary 0.1.0-alpha.20](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.20)
|
||||
est publiée depuis le commit source `0edac29`. Le canal stable utilisait cette
|
||||
version au commit `4e0967d`. Le JAR, le
|
||||
[MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.20/Sanctuary-0.1.0-alpha.20.mrpack)
|
||||
et le ZIP d’amorçage Prism ont été vérifiés par téléchargement public.
|
||||
SHA-256 du JAR :
|
||||
|
||||
```text
|
||||
9ea10576e641d83c13e1277960a375448a22d416a98a6b327c4d6d2a204c423a
|
||||
```
|
||||
|
||||
Passe de détails sur six bâtiments de ville, quatorze infrastructures,
|
||||
espaces publics et réseaux souterrains : palettes propres aux usages,
|
||||
boiseries, tuf ciselé, corniches, biseaux, verrières et raccords des vitres.
|
||||
Voir le [catalogue illustré](architecture-alpha20.md).
|
||||
|
||||
`check build assemblePack` passe, avec huit GameTests généraux. Les créations
|
||||
et réouvertures natives conservent les structures, accès, supports et coffres.
|
||||
Le bassin et les sorties d’égout restent fonctionnels. Les rapports de la
|
||||
version19 restent identiques à ceux de la release publiée. Le mode désactivé
|
||||
et une petite île passent ; les quatorze infrastructures sont rencontrées sur
|
||||
les deux témoins. Les limites des raccords au terrain sont détaillées dans
|
||||
[les preuves alpha.20](testing-alpha20.md).
|
||||
|
||||
Deux synchronisations isolées puis deux dans la même instance Prism Mac
|
||||
réussissent. Un seul JAR Sanctuary alpha.20 est actif ; les **925 fichiers
|
||||
personnels et réglages suivis** conservent leurs hashes. Le dossier reste
|
||||
`Sanctuary-0.1.0-alpha.1`, affiché « Sanctuary Beta ». Le JAR19 et le suivi
|
||||
packwiz sont sauvegardés dans `sanctuary-backups/before-0.1.0-alpha.20/`.
|
||||
Reçus locaux : `build/alpha20-isolated-validation.json` et
|
||||
`build/alpha20-prism-validation.json`.
|
||||
|
||||
Pour voir cette architecture, créer un **nouveau monde Sanctuary, Moyen,
|
||||
graine 0**, avec **Structures expérimentales** activées dans **Personnaliser**.
|
||||
`/sanctuary structures` liste les entrées. Petit, graine42, possède la
|
||||
cartothèque à `-101 251 -25`. L’option désactivée d’un autre nouveau monde
|
||||
conserve la ville sans les réseaux expérimentaux. Les anciens mondes gardent
|
||||
leur génération ; aucune sauvegarde personnelle n’a été ouverte, convertie ou
|
||||
régénérée. Une sauvegarde20 ne se rétrograde pas vers le binaire19.
|
||||
|
||||
Les vues du catalogue viennent des modèles et ne sont pas des captures de
|
||||
partie. Aucun essai client visuel ni essai Windows natif n’a été effectué.
|
||||
Les nouvelles fonctions de la bêta restent à construire.
|
||||
|
||||
## Alpha.19 publiée — 9 septembre 2026
|
||||
|
||||
[Sanctuary 0.1.0-alpha.19](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.19)
|
||||
est publiée depuis le commit source `c23e72b`. Le canal stable utilisait alors
|
||||
cette version, au commit `ea883b7`. JAR, [MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.19/Sanctuary-0.1.0-alpha.19.mrpack)
|
||||
et ZIP d’amorçage Prism ont été vérifiés par téléchargement public.
|
||||
SHA-256 du JAR :
|
||||
|
||||
```text
|
||||
a159b00a2778020800e81381e0608495a4d7909f1f853f0b8812cd4017865b0b
|
||||
```
|
||||
|
||||
Première passe de quatorze architectures et de leurs voies de fret ou chemins
|
||||
abandonnés sur l’île initiale. Les trois tailles sont vérifiées nativement ;
|
||||
les quatorze types ont chacun été rencontrés et parcourus sur ces témoins.
|
||||
Créer un **nouveau monde Sanctuary, Moyen, graine 0**, puis garder
|
||||
**Structures expérimentales** activées dans **Personnaliser** : ce témoin
|
||||
contient treize lieux et sept liaisons, dont un fret de 499 blocs.
|
||||
`/sanctuary structures` relève les entrées. La présence des bâtiments et les
|
||||
raccords dépendent du relief ; voir [les preuves et la carte](testing-alpha19.md).
|
||||
|
||||
`check build assemblePack` et les huit GameTests généraux passent.
|
||||
Création et réouverture natives conservent 112 pièces et 13 coffres ; 68 cibles
|
||||
sont accessibles. Le mode désactivé passe. Les empreintes de la génération 17
|
||||
restent identiques aux preuves publiées, y compris après réouverture.
|
||||
|
||||
Deux synchronisations isolées puis deux dans la même instance Prism Mac
|
||||
réussissent. Un seul JAR Sanctuary alpha.19 est actif ; les **767 fichiers
|
||||
personnels et réglages suivis** conservent leurs hashes. Aucune sauvegarde
|
||||
personnelle n’a été ouverte, convertie ou régénérée. Le dossier reste
|
||||
`Sanctuary-0.1.0-alpha.1`, affiché « Sanctuary Beta ».
|
||||
|
||||
Le JAR 17 et le suivi packwiz sont sauvegardés dans
|
||||
`sanctuary-backups/before-0.1.0-alpha.19/`. Reçus locaux ignorés :
|
||||
`build/alpha19-isolated-validation.json` et `build/alpha19-prism-validation.json`.
|
||||
|
||||
Pour comparer, créer un autre nouveau monde avec l’option désactivée : le socle
|
||||
urbain demeure, sans les expériences 17/19. Cette option ne supprime pas de
|
||||
structures dans une sauvegarde existante. Les anciens mondes conservent leur
|
||||
génération ; une sauvegarde 19 ne doit pas être ouverte avec l’ancien binaire 17.
|
||||
Les bâtiments préparent les futurs usages, mais mailbox, caméra, réacteur et
|
||||
progression restent pour la bêta. Aucun essai visuel dans le client ni essai
|
||||
Windows natif n’a été effectué.
|
||||
|
||||
## Alpha.17 publiée — 9 septembre 2026
|
||||
|
||||
[Sanctuary 0.1.0-alpha.17](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.17)
|
||||
est publiée depuis le commit source `0969ed5`. Le canal stable utilise cette
|
||||
est publiée depuis le commit source `0969ed5`. Le canal stable utilisait alors cette
|
||||
version, au commit `6e0c297`. JAR, [MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.17/Sanctuary-0.1.0-alpha.17.mrpack)
|
||||
et ZIP d’amorçage Prism ont été vérifiés par téléchargement public.
|
||||
SHA-256 du JAR :
|
||||
|
||||
@@ -25,6 +25,33 @@ Sources locales : [planificateur17](../mods/sanctuary/src/main/java/fr/koka/sanc
|
||||
[rendu17](../mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/ruins/RuinsRenderer17.java),
|
||||
[preuves alpha.17](testing-alpha17.md), [vision](vision.md).
|
||||
|
||||
## Ligne de conception retenue après cet audit
|
||||
|
||||
**Minecraft comme archéologie d’un autre Minecraft.** Steve, Ari, Sunny, Kai,
|
||||
Zuri, Alex, Mefe, Makena, Noor sont les personnages disparus de la partie. Ils
|
||||
ont découvert quelque chose de révolutionnaire et ont continué parce que ça
|
||||
marchait, jusqu’au moment où ça n’a plus marché.
|
||||
|
||||
> Ils n’ont pas ouvert quelque chose qu’ils n’auraient pas dû ouvrir. Ils ont
|
||||
> construit quelque chose qu’ils ne savaient plus refermer.
|
||||
|
||||
Chaque ruine doit conserver la trace d’une ancienne solution : une infrastructure
|
||||
dont l’auteur a progressivement disparu derrière son fonctionnement. Ce canon
|
||||
ne définit pas la découverte, les rôles individuels ou une catastrophe confirmée.
|
||||
Il guide les prochaines structures ; il n’ajoute aucun personnage, récit jouable
|
||||
ou système de progression aux versions déjà livrées. L’état alpha.17 décrit
|
||||
ci-dessus reste le constat historique de cet audit.
|
||||
|
||||
Pour valider la conception d’un lieu, relever concrètement :
|
||||
|
||||
- **Un usage ancien lisible** dans les pièces, les outils et les circulations.
|
||||
- **Une implantation fonctionnelle** expliquée par le terrain, une ressource
|
||||
ou une destination réellement présente.
|
||||
- **Des traces d’entretien, d’extension ou de chantier** qui montrent comment
|
||||
l’installation a été utilisée et adaptée.
|
||||
- **Un lien futur à la progression**, explicitement décrit comme une intention
|
||||
tant que son système et ses interactions ne sont pas implémentés.
|
||||
|
||||
## Faire comprendre les usages avant d’introduire leurs systèmes
|
||||
|
||||
Chaque lieu devrait répondre à trois questions : pourquoi était-il construit
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
# Alpha.19 — anciennes infrastructures de Sanctuary
|
||||
|
||||
**État : alpha.19 publiée, même instance Prism synchronisée et vérifiée.** Ticket WG-22, branche
|
||||
`codex/heritage-worldgen-alpha19`. Minecraft 26.3-pre-2, mêmes dépendances que
|
||||
l’alpha.17. La numérotation 19 est demandée explicitement ; aucune 18 n’est
|
||||
intercalée dans ce dépôt.
|
||||
|
||||
## Périmètre et réversibilité
|
||||
|
||||
Première passe des quatorze lieux du [catalogue](structures-audit.md) : gare de
|
||||
marchandises, centre postal, dépôt communal, cartothèque, observatoire,
|
||||
maintenance ferroviaire, pompage, sous-station, serre, quai d’expédition,
|
||||
relais radio, club photo, vestiaires/salles étranges et refuge de chantier.
|
||||
Les plans recherchent des sites admissibles et des voies de fret ou chemins
|
||||
entre destinations réelles ; ils ne garantissent pas tous les lieux sur chaque
|
||||
graine. Les mesures et coordonnées des essais figurent ci-dessous.
|
||||
|
||||
Créer un **nouveau monde Sanctuary** et utiliser **Personnaliser** :
|
||||
Petit 512, Moyen 724 par défaut ou Grand 1 024, avec **Structures expérimentales**.
|
||||
L’option est activée par défaut et enregistrée avec le générateur 19. La désactiver
|
||||
pour un autre nouveau monde conserve le socle urbain sans les réseaux
|
||||
expérimentaux 17 et les infrastructures 19. Elle ne retire aucun bloc ni ne change
|
||||
les règles d’une sauvegarde existante.
|
||||
|
||||
Les mondes 17 et antérieurs gardent leurs codecs, plans, pièces et futurs chunks.
|
||||
Le preset 17 public est conservé octet par octet sous un identifiant historique
|
||||
caché à la création. Le générateur 19 et son journal sont distincts. Il n’existe
|
||||
aucune conversion ou rétrogradation de sauvegarde 19 vers un ancien binaire.
|
||||
L’ancienne release reste disponible pour ses propres mondes, et l’option d’un
|
||||
nouveau monde permet de comparer les deux configurations du nouveau binaire.
|
||||
|
||||
Les nouveaux lieux sont réservés à l’île initiale. La ville et les anciens
|
||||
réseaux servent de base ; leurs protections et celles de l’hydrologie restent
|
||||
respectées. Les liaisons vers de futurs continents seront construites après
|
||||
leur apparition, sans présumer leurs emplacements dans cette génération.
|
||||
|
||||
## Objets et systèmes
|
||||
|
||||
Les bâtiments suggèrent leurs usages futurs par leurs pièces et aménagements.
|
||||
Mailbox, deposit boxes, caméra, réacteur, progression et récompenses
|
||||
personnelles ne sont pas activés. Chaque nouveau lieu dispose d’une table
|
||||
de coffre propre à la génération 19, avec un outil vanilla lié à son usage et
|
||||
quelques fournitures. Les coffres sont partagés et ne se renouvellent pas.
|
||||
|
||||
La valeur des rails, équipements et blocs récupérables du décor s’ajoute au
|
||||
butin. Leur distribution doit être constatée sur les témoins, sans transformer
|
||||
cette première passe en une garantie d’équilibrage économique pour la bêta.
|
||||
|
||||
## Vérifications effectuées
|
||||
|
||||
- Placement des quatorze architectures, ouvertures, circulation, orientation
|
||||
et appuis du mobilier ; accès réel au coffre et retour possible.
|
||||
- Plans bornés sur les trois tailles, terrain non occupé, supports, continuité
|
||||
des tracés et distinction entre ballast praticable et rails retirés.
|
||||
- Pièces natives et références locales, y compris pour les longues voies ;
|
||||
géométrie, orientation et rails sauvegardés dans un schéma strict.
|
||||
- Création et réouverture d’un monde 19 avec modification témoin ; conservation
|
||||
des starts, butins différés, accès, routes et supports.
|
||||
- Option désactivée, paramètres sauvegardés, génération 17 comparée aux preuves
|
||||
de la version publiée, refus des migrations implicites de journaux.
|
||||
- `./gradlew check build assemblePack`, artefacts et index, publication immuable
|
||||
puis canal packwiz et deux synchronisations de la même instance Prism.
|
||||
|
||||
Les smokes des quatorze architectures, de leurs quatre orientations, des
|
||||
plans sur les trois tailles et des pièces natives passent. Le mode désactivé
|
||||
est vérifié dans un monde 19 neuf : aucun site ni liaison expérimental.
|
||||
`./gradlew check build assemblePack` passe, avec les huit GameTests généraux,
|
||||
125 chunks FULL, 5 923 blocs d’eau conservés et 1 800 ticks de fluides.
|
||||
Le JAR, le MRpack, les hashes du pack et le ZIP Prism sont vérifiés.
|
||||
La release et le canal packwiz sont publiés. Deux synchronisations isolées puis
|
||||
deux dans la même instance Prism passent ; 767 fichiers personnels et réglages
|
||||
suivis conservent leurs hashes. Le JAR précédent et le suivi packwiz sont
|
||||
sauvegardés dans `sanctuary-backups/before-0.1.0-alpha.19/`.
|
||||
Voir [le reçu de distribution](packwiz.md#alpha19-publiée--9-septembre-2026).
|
||||
Aucun essai visuel dans le client ni essai Windows natif n’a été effectué.
|
||||
|
||||
## Témoin natif Moyen, graine 0
|
||||
|
||||
Créer un nouveau monde avec **Structures expérimentales activées**.
|
||||
`/sanctuary structures` liste les entrées prévues sans charger de chunks ;
|
||||
`/locate structure sanctuary:heritage_networks` retrouve l’entrée d’un site.
|
||||
Le relevé ci-dessous provient des placements et parcours vérifiés par le moteur.
|
||||
|
||||

|
||||
|
||||
Treize des quatorze lieux apparaissent. **La cartothèque est absente de cette
|
||||
graine** : la recherche n’a pas trouvé d’emplacement admissible. La ville et les
|
||||
trois réseaux souterrains 17 sont conservés. Les raccords recherchés vers la
|
||||
ville et l’ancienne mine ne sont pas admis sur ce témoin.
|
||||
|
||||
Les sept liaisons comprennent un tracé de fret **dépôt → pompage de 499 blocs**,
|
||||
d’une étendue de 245 blocs sur X et 181 sur Z. Son ballast est praticable ;
|
||||
478 rails sont présents et 21 sont volontairement manquants. Les autres voies
|
||||
et chemins relient gare, dépôt, atelier, poste, club photo, serre et vestiaires.
|
||||
Le quai, l’observatoire, le relais, la sous-station et le refuge gardent leurs
|
||||
propres accès au terrain, même sans liaison avec ce réseau.
|
||||
|
||||
La création et la réouverture dans deux JVM distinctes passent chacune les deux
|
||||
GameTests : **112 pièces natives, 111 chunks FULL, 80 487 blocs contrôlés,
|
||||
2 882 contrôles de portance, 13 coffres différés et 68 cibles accessibles**.
|
||||
Trois tirages de chacune des quatorze tables de butin sont vérifiés
|
||||
séparément, sans ouvrir les coffres persistants. Une modification témoin à
|
||||
`-204 / 255 / -13` est conservée à la réouverture.
|
||||
|
||||
Empreinte stable du plan :
|
||||
`7f476c2ecca703a05bf88a550c61cc5cbeff2594024c43ccd9f3620bbc2ad3de`.
|
||||
JVM de création `36366`, de réouverture `36481`. Reçus locaux ignorés :
|
||||
`build/alpha19-evidence/heritage19-create.json` et `heritage19-reload.json`.
|
||||
|
||||
La planification supplémentaire utilise 15 144 colonnes natives, dans un plafond
|
||||
de 16 000, en 20,7 s sur le Mac de développement. Ce temps concerne ce plan,
|
||||
pas le chargement total du jeu. Les routes sont découpées par chunk ; aucun
|
||||
start ne couvre toute leur longueur ni ne charge une destination lointaine.
|
||||
|
||||
La prospection 19 consulte les plans hydrologiques de toute l’enveloppe de
|
||||
départ, avant de publier les sites. Le contrôle général utilise donc pour 19
|
||||
une limite géographique calculée : 5 régions possibles pour Petit, 9 pour
|
||||
Moyen/Grand, avec les budgets de colonnes et de sondes contrôlés séparément.
|
||||
Le témoin Moyen en consulte 5 ; les quatre régions périphériques représentent
|
||||
3,38 s de calcul hydrologique supplémentaire dans le relevé du build.
|
||||
Ces consultations ne matérialisent aucun chunk. Les générations historiques
|
||||
conservent le contrôle initial de 4 régions maximum autour du spawn.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryHeritage19Tests=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryHeritage19Tests=true -PsanctuaryExpansionReload=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false
|
||||
```
|
||||
|
||||
## Autres tailles et couverture du catalogue
|
||||
|
||||
Les deux GameTests natifs passent aussi sur Petit et Grand, graine 42.
|
||||
Les quatorze architectures ont ainsi chacune été rencontrées et parcourues
|
||||
sur le terrain réel, en complément des rotations vérifiées par les smokes.
|
||||
|
||||
| Taille et graine | Lieux | Liaisons | Pièces natives | Chunks FULL |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| Petit 512, 42 | 10 | 2 | 35 | 44 |
|
||||
| Moyen 724, 0 | 13 | 7 | 112 | 111 |
|
||||
| Grand 1 024, 42 | 13 | 4 | 47 | 61 |
|
||||
|
||||
Pour visiter la cartothèque : **Petit, graine 42**, entrée
|
||||
`-101 / 251 / -25`, ou **Grand, graine 42**, entrée `-252 / 249 / 146`.
|
||||
Le Petit testé n’a ni poste, ni observatoire, ni atelier ferroviaire, ni quai.
|
||||
Le Grand testé n’a pas de quai. Aucun grand axe n’est admis sur ces deux témoins :
|
||||
les portées maximales de leurs liaisons sont respectivement de 32 et 55 blocs.
|
||||
Le témoin Moyen, graine 0, reste celui conseillé pour découvrir le grand fret.
|
||||
Ces résultats rendent explicite la variation de cette première passe ; ils ne
|
||||
constituent pas une garantie de présence ou de connectivité pour chaque graine.
|
||||
|
||||
Reçus locaux ignorés : `build/alpha19-evidence/small42-create.json` (JVM 36591)
|
||||
et `large42-create.json` (JVM 36669).
|
||||
|
||||
## Compatibilité 17 vérifiée
|
||||
|
||||
Deux JVM de développement distinctes créent puis rouvrent le témoin 17
|
||||
**Moyen, graine 0**, avec le nouveau binaire. Les deux GameTests passent dans
|
||||
chaque exécution. Le codec, les starts natifs, les butins différés et les
|
||||
parcours restent identiques aux preuves de l’alpha.17 publiée. Le bloc d’or
|
||||
placé par le test est conservé à la réouverture.
|
||||
|
||||
Empreinte du plan 17 :
|
||||
`7358ecd7841c03be1977a7f45f37c0c3ef83a32dc27e8d3ec4f0d85d6ee5d3ef`.
|
||||
Les trois starts restent ceux documentés dans [le témoin 17](testing-alpha17.md).
|
||||
Reçus locaux ignorés : `build/alpha19-evidence/legacy17-create.json` et
|
||||
`legacy17-reload.json`.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryRuins17Tests=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryRuins17Tests=true -PsanctuaryExpansionReload=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false
|
||||
```
|
||||
|
||||
Ces commandes visent uniquement le monde de développement ignoré. La première
|
||||
le recrée ; la seconde conserve ce même monde et lit ses preuves précédentes.
|
||||
@@ -0,0 +1,327 @@
|
||||
# Alpha.20 — identité architecturale
|
||||
|
||||
**État : alpha.20 publiée, même instance Prism synchronisée et vérifiée.**
|
||||
Ticket WG-23, branche `codex/architecture-alpha20`.
|
||||
Minecraft 26.3-pre-2, dépendances identiques à la version 19 publiée.
|
||||
|
||||
## Intention
|
||||
|
||||
Chaque fonction reçoit une palette et une composition propre : bois clairs et
|
||||
grandes baies des lieux publics, pierre et tuf des installations techniques,
|
||||
loggias et retraits des logements. Les détails utilisent les blocs vanilla :
|
||||
tuf ciselé, briques de tuf ciselées, briques de pierre sculptées, bois et
|
||||
planches de chêne/bouleau, granit, diorite et andésite. Moulures, liserés,
|
||||
encadrements, murs bas et corniches utilisent escaliers, dalles et trappes.
|
||||
Les fondations restent en briques de pierre. Le [catalogue architectural](architecture-alpha20.md)
|
||||
décrit les formes et les matériaux effectivement construits pour chaque usage.
|
||||
|
||||
La passe concerne les quatorze infrastructures, la ville et ses sous-sols,
|
||||
ainsi que les petits réseaux oubliés. Elle ne déplace pas les sites ni les
|
||||
routes pour obtenir ses silhouettes. Les plans 19/17/16 servent 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 | Réussi ; deux passages isolés puis deux dans la même instance, 925 fichiers personnels et réglages préservés |
|
||||
|
||||
`./gradlew check build assemblePack --max-workers=1` passe en 4 min 39 s sur
|
||||
la machine de développement : 62 tâches, dont 55 exécutées, huit GameTests,
|
||||
125 chunks `FULL`, 5 923 blocs 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`.
|
||||
|
||||
Release immuable depuis `0edac29`, canal packwiz `4e0967d` et deux
|
||||
synchronisations isolées vérifiés. Les deux passages dans la même instance
|
||||
Prism conservent les hashes de 925 fichiers personnels et réglages. Un seul
|
||||
JAR20 est actif ; JAR19 et suivi packwiz sont sauvegardés avant remplacement.
|
||||
Voir le [reçu de distribution](packwiz.md#alpha20-publiée--10-septembre-2026).
|
||||
Aucun essai client visuel ni essai Windows natif n’a été effectué.
|
||||
@@ -0,0 +1,140 @@
|
||||
# Alpha.21 — contrat et vérification
|
||||
|
||||
Ticket WG-24, branche `codex/palettes-reseaux-alpha21`.
|
||||
Minecraft 26.3-pre-2, Fabric Loader 0.19.5, Fabric API 0.160.0+26.3,
|
||||
Java 25. Les dépendances restent celles de l’alpha.20.
|
||||
|
||||
## Résultat recherché
|
||||
|
||||
La ville reprend une palette de pierre lisse, pierre, briques de pierre,
|
||||
andésite et chêne. Les plans intérieurs sont conservés ; les corrections
|
||||
portent sur les matériaux, les appuis et les couvertures. Chaque infrastructure
|
||||
emploie un bois cohérent. Le tuf reste surtout au sol et dans les sous-sols.
|
||||
|
||||
La ville peut accueillir deux petits quartiers supplémentaires reliés à son
|
||||
cœur, avec du terrain naturel entre eux. La réserve centrale existante n’est
|
||||
pas agrandie. Les infrastructures reçoivent davantage de chemins entre des
|
||||
entrées réelles. Les recherches respectent des budgets de colonnes et refusent
|
||||
un quartier si sa liaison entière ne peut pas être construite.
|
||||
|
||||
Les galeries industrielles expérimentales ont cinq blocs de chaussée et un
|
||||
trottoir de chaque côté, des salles de maintenance et deux accès par escaliers.
|
||||
Le plan est recherché après les structures précédentes et évite leurs volumes,
|
||||
l’hydrologie. Un tablier continu peut franchir une cavité entre ses appuis ;
|
||||
les travées sans appuis complets sont limitées à 34 blocs, avec des piliers
|
||||
locaux d’au plus 16 blocs et deux couches de roche intacte sous les appuis. Chaque tronçon a une pièce native
|
||||
locale ; aucun démarrage de structure ne charge toute l’île.
|
||||
|
||||
## Sauvegardes et comparaison
|
||||
|
||||
Créer un **nouveau monde Sanctuary**, taille **Moyen**, graine **0**, avec
|
||||
**Structures expérimentales** activées. Petit 512, Moyen 724 et Grand 1024 restent
|
||||
les tailles proposées par le bouton natif **Personnaliser**.
|
||||
|
||||
Le nouveau codec est `sanctuary:island_v21`. Les codecs et pièces 20 et
|
||||
antérieurs restent disponibles, sans modifier leurs rendus. Le journal 21
|
||||
réutilise le schéma de persistance existant avec une génération distincte ;
|
||||
les ouvertures entre versions incompatibles sont refusées. Aucune conversion
|
||||
ni rénovation automatique des mondes précédents n’est effectuée.
|
||||
|
||||
Pour comparer sans les infrastructures, créer un autre monde avec l’option
|
||||
expérimentale désactivée. Une sauvegarde 21 nécessite le binaire 21 ou un futur
|
||||
binaire compatible ; elle ne se rétrograde pas en remplaçant simplement le JAR.
|
||||
|
||||
## Compatibilité vérifiée
|
||||
|
||||
373 fichiers de production historiques sont identiques au commit de publication
|
||||
20 (`0edac29`). Le preset caché `sanctuary_v20` est une copie exacte de son
|
||||
ancien preset public. Création et réouverture natives de la génération 20 donnent
|
||||
les mêmes plans, structures, appuis, accès et coffres que les rapports publiés.
|
||||
La comparaison ignore seulement l’identifiant du processus, le chemin du monde
|
||||
et deux nouveaux compteurs de détail ajoutés à l’oracle de connexion des tests.
|
||||
|
||||
## Galeries vérifiées dans Minecraft
|
||||
|
||||
Moyen, graine 0 : 23 modules dont deux accès et une salle, sur un axe de 340 blocs.
|
||||
Les entrées sont `-135 253 -178` et `-135 249 162` ; chaussée à Y 216. La portée
|
||||
sans appuis complets la plus longue du témoin est 17 blocs. Le plan emploie
|
||||
8 686 colonnes de bruit, avec un budget maximal de 12 000 ; cette recherche prend
|
||||
environ 11 secondes sur la machine de développement, hors reste du monde.
|
||||
|
||||
La création et la réouverture dans deux JVM distinctes passent : 34 chunks
|
||||
FULL, 57 635 blocs vérifiés, mêmes pièces natives, mêmes coffres différés et
|
||||
modification témoin conservée. L’oracle de collision de Minecraft vérifie un
|
||||
corps de 0,6×1,8 bloc, 74 cibles et 520 pas entre les deux portiques ; il utilise
|
||||
les vrais escaliers et les demi-marches des trottoirs.
|
||||
|
||||
Une alcôve d’excavation inachevée ouvre le parement de la salle sur une paroi
|
||||
irrégulière de tuf, pierre et deepslate. Certains tronçons exposent de petits
|
||||
filons ordinaires. Le témoin contient cinq minerais de charbon, dix-neuf de
|
||||
fer, cinq de redstone, un de diamant et un d’émeraude ajoutés dans ces fronts.
|
||||
Cuivre, or et lapis font aussi partie des variantes possibles. Ce sont des
|
||||
blocs finis, sans mécanisme de réapparition après minage. Ces quantités excluent
|
||||
les minerais naturels de l’île et ne constituent pas une garantie par graine.
|
||||
|
||||
## Autres vérifications
|
||||
|
||||
La création et la réouverture natives de la ville passent. Le témoin possède
|
||||
un cœur et deux quartiers reliés, 65 pièces de rue, trois immeubles de logements,
|
||||
un hôtel, une bibliothèque, un jardin et une place. La réserve centrale n’est
|
||||
pas étendue. Les deux nouveaux chemins sont longs de 51 et 38 cellules. Une
|
||||
expansion de test distincte est également vérifiée ; aucune expansion personnelle
|
||||
n’est activée.
|
||||
|
||||
La piscine couverte, le garage, la salle d’expansion, leurs accès et les égouts
|
||||
restent présents. Les contrôles natifs vérifient les bassins et sorties après
|
||||
exécution observée de la simulation des fluides.
|
||||
|
||||
Les quatorze types de lieux patrimoniaux apparaissent sur Moyen, graine 0,
|
||||
avec douze liaisons et 127 segments. Leur création native passe : 141 pièces,
|
||||
91 271 blocs, 140 chunks FULL et quatorze coffres différés. Les trois réseaux
|
||||
oubliés sont également parcourus. Les chemins rejoignent deux entrées de ville.
|
||||
L’observatoire et le quai du vide restent isolés sur ce témoin : leurs chemins
|
||||
n’ont pas trouvé de tracé admissible dans le budget. La commande `/sanctuary structures` indique leurs entrées ; aucun raccord
|
||||
construit complet n’est revendiqué.
|
||||
|
||||
Les smokes des renderers, plans, pièces enregistrées et raccords passent.
|
||||
La réouverture des lieux patrimoniaux conserve également le plan, les pièces,
|
||||
les coffres et la modification témoin. L’option expérimentale désactivée passe :
|
||||
elle ne publie aucun module de galerie. `check build assemblePack` passe, avec les huit GameTests généraux. Le
|
||||
MRpack, son index et le JAR embarqué sont vérifiés. La release et le canal
|
||||
packwiz sont publiés. Deux synchronisations isolées puis deux dans la même
|
||||
instance Prism conservent les 925 fichiers personnels et réglages suivis.
|
||||
Voir le [reçu de distribution](packwiz.md#alpha21-publiée--10-septembre-2026).
|
||||
|
||||
Les chemins de quartier regroupent leurs pentes à l’intérieur de rectangles ;
|
||||
leurs frontières et raccords sont des paliers plats. Les structures restent
|
||||
conditionnées par le terrain admissible, dans un budget borné. La traversée
|
||||
industrielle ne relie pas encore tous les réseaux souterrains historiques.
|
||||
|
||||
Les vues de modèles éventuelles ne remplacent pas une inspection dans le client.
|
||||
Aucune validation Windows native ni charge multijoueur n’est revendiquée.
|
||||
|
||||
|
||||
## Reproduire les contrôles
|
||||
|
||||
Depuis ce dépôt, avec Java 25, les mondes de test restent sous `mods/sanctuary/build/`.
|
||||
Pour chaque scénario, exécuter la création puis sa réouverture avant tout autre
|
||||
scénario qui nettoie ce monde de développement :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryTransit21Tests=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --max-workers=1
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryTransit21Tests=true -PsanctuaryExpansionReload=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --max-workers=1
|
||||
```
|
||||
|
||||
Remplacer `sanctuaryTransit21Tests` par `sanctuaryLostCity21Tests` pour la ville
|
||||
ou `sanctuaryHeritage21Tests` pour les infrastructures. Le cas OFF ajoute
|
||||
`-PsanctuaryExperimentalStructures=false` à une création du scénario transit.
|
||||
Les rapports de cette livraison sont conservés localement dans
|
||||
`build/alpha21-evidence/`. `./gradlew check build assemblePack` constitue la
|
||||
vérification complète avant export.
|
||||
|
||||
## Artefacts vérifiés
|
||||
|
||||
MRpack : `Sanctuary-0.1.0-alpha.21.mrpack`. Son JAR est identique au JAR du build.
|
||||
Les versions et hashes de l’index packwiz sont contrôlés, ainsi que le ZIP
|
||||
d’amorçage Prism. SHA-256 du JAR :
|
||||
|
||||
```text
|
||||
e528e42d99f9e1ea221f780ea089b49b42a1a4e9e24e4ddd001d045183202c21
|
||||
```
|
||||
@@ -0,0 +1,153 @@
|
||||
# Alpha.22 — placement sélectif
|
||||
|
||||
Ticket WG-25, branche `codex/placement-selectif-alpha22`.
|
||||
Minecraft 26.3-pre-2, Fabric Loader 0.19.5, Fabric API 0.160.0+26.3 et Java25,
|
||||
sans changement de dépendances.
|
||||
|
||||
## Contrat
|
||||
|
||||
Les bâtiments de surface redeviennent sélectifs : une ville peut être absente
|
||||
si aucun emplacement n’est adéquat. Deux éléments de base sont prioritaires :
|
||||
**la grande traversée souterraine et la salle d’expansion**, même sans ville.
|
||||
Leurs emplacements sont réservés avant les petits réseaux et le catalogue
|
||||
expérimental. Une recherche bornée ne prouve pas qu’aucun autre emplacement
|
||||
possible n’existe dans toute l’île.
|
||||
|
||||
- Ville : aucune réserve artificielle de secours, aucun pilier de rattrapage,
|
||||
terrassement/remblai limité à deux blocs par colonne. Les fondations sont
|
||||
entièrement recertifiées dans la roche. Les quartiers supplémentaires exigent
|
||||
un chemin complet vers les quartiers retenus. Au plus trois propositions dans
|
||||
un budget partagé de20 000 colonnes et1 120 000 contrôles de volume.
|
||||
- Infrastructures : relief maximal de quatre blocs sur l’emprise17×17, coupe et
|
||||
remblai limités à deux blocs, deux couches de roche intacte sous chaque colonne
|
||||
de fondation, sans piliers de secours. Jusqu’à24 candidats par famille, avec
|
||||
les mêmes plafonds globaux de16 000 colonnes et800 000 sondages.
|
||||
- Chemins patrimoniaux : coupe/remblai limité à un bloc, accès indépendant et
|
||||
raccord complet requis pour publier une liaison.
|
||||
- Traversée : conserve le génie civil21, avec deux accès réels, une chaussée
|
||||
de cinq blocs et deux trottoirs. Les piliers sont bornés à16 blocs ; les
|
||||
travées sans grille complète sont limitées à34 blocs et reposent sur les
|
||||
modules adjacents. La recherche22 intervient avant les structures facultatives
|
||||
pour leur réserver l’emprise. Si les lignes droites sont refusées, une seconde
|
||||
passe cherche des accès différents et des détours. Chaque passe est limitée à
|
||||
12 000 colonnes et3 millions de sondages (24 000 et6 millions au total au plus).
|
||||
Ses parois peuvent traverser des cavités existantes sans écraser les volumes
|
||||
d’eau et de lave protégés.
|
||||
- Salle : recherche autonome d’un accès de surface et d’une salle25×25, sans
|
||||
faux quartier ni socle de ville. Une salle déjà présente sous une ville évite
|
||||
un doublon. Les égouts et annexes peuvent accompagner ce lieu indépendant.
|
||||
|
||||
|
||||
Les palettes, les plans intérieurs et les petits filons minables restent ceux
|
||||
de21. Les anciens petits réseaux expérimentaux17 et les sous-sols des villes gardent
|
||||
leur géométrie ; leurs protections restent respectées.
|
||||
Le nouveau principe narratif est documenté dans [la vision](vision.md) et
|
||||
[l’audit](structures-audit.md), sans activer de système de progression.
|
||||
|
||||
## Sauvegardes
|
||||
|
||||
Le seul preset public utilise désormais `sanctuary:island_v22`. Le preset caché
|
||||
`sanctuary_v21` est la copie de l’ancien preset public. Les anciens codecs,
|
||||
planificateurs et rendus restent disponibles. Les pièces21 sont réutilisées
|
||||
avec leur format existant ; les nouveaux coudes possèdent une pièce22 distincte
|
||||
pour fermer leurs murs sans modifier les jonctions21 sauvegardées.
|
||||
La salle indépendante possède un type de structure22
|
||||
et réutilise les pièces souterraines20.
|
||||
Le journal identifie la génération22 et refuse une lecture croisée avec21.
|
||||
Aucun monde personnel n’est ouvert, converti ou régénéré.
|
||||
|
||||
Les nouvelles règles nécessitent un **nouveau monde Sanctuary**. Petit512,
|
||||
Moyen724 par défaut et Grand1024 restent proposés. L’option **Structures
|
||||
expérimentales** reste enregistrée à la création ; en22 elle retire le catalogue
|
||||
facultatif, tandis que la traversée et la salle restent des éléments de base.
|
||||
Le commutateur Minecraft général des structures demeure respecté.
|
||||
|
||||
## Témoin natif Moyen, graine0
|
||||
|
||||
La création et la réouverture dans deux JVM distinctes passent. La ville est
|
||||
refusée ; la traversée comporte23 modules, dont deux accès et une salle de
|
||||
maintenance. Son axe s’étend sur340 blocs. Les accès sont `-170 253 -100` et
|
||||
`170 254 -100`. Le hall d’expansion indépendant se rejoint par la trappe
|
||||
`2 252 0` ; son réseau comprend l’accès, la salle, deux passages et un égout.
|
||||
La piscine est une annexe conditionnelle, absente sur ce témoin.
|
||||
|
||||
Les14 bâtiments patrimoniaux trouvent un terrain adapté ; sept liaisons sont
|
||||
retenues. Les115 chunks de l’emprise sont générés en statut FULL. Les plans,
|
||||
pièces natives et une modification témoin persistent après réouverture. Le
|
||||
contrôle natif de circulation vérifie la vraie trappe, les échelles soutenues,
|
||||
les ouvertures et le trajet jusqu’au socle central. Aucun déplacement de joueur
|
||||
ou chargement d’un monde personnel n’a été utilisé pour ces essais.
|
||||
|
||||
## Vérifications complémentaires
|
||||
|
||||
**Petit512, graine42 :**17 modules, dont un coude sauvegardé avec sa pièce22,
|
||||
et une salle indépendante. Les anciens alignements droits sont refusés ; le
|
||||
repli trouve une paire d’accès naturels et conserve une chaussée continue.
|
||||
Les portiques sont `-68 250 -93` et `68 250 43`, la trappe du hall `43 244 -10`.
|
||||
La marche native relie les sorties en564 déplacements et atteint53 points de
|
||||
contrôle. Huit bâtiments patrimoniaux et six liaisons sont admis ; aucune ville
|
||||
n’est forcée. Les88 chunks de l’emprise sont vérifiés en statut FULL.
|
||||
|
||||
**Grand1024, graine42 :** un quartier naturel est admis avec son réseau de
|
||||
sous-sols : salle d’expansion, piscine, égout et parking. Aucun hall autonome
|
||||
supplémentaire n’est créé. La traversée comporte31 modules et s’étend sur476
|
||||
blocs ; portiques `-272 249 -8` et `204 250 -8`. La marche native les relie
|
||||
en740 déplacements et atteint98 points de contrôle. Quatorze bâtiments
|
||||
patrimoniaux et huit liaisons sont admis. Les179 chunks de l’emprise sont
|
||||
vérifiés en statut FULL ; la salle et ses17 ouvertures sont accessibles.
|
||||
|
||||
Les tests purs des trois tailles passent sur des terrains plats adaptés ;
|
||||
falaises, eau/protections, vides et appuis insuffisants sont refusés pour les
|
||||
bâtiments. Le hall autonome et ses annexes sont vérifiés sans fixture de ville.
|
||||
Les suites historiques restent actives. Avec l’option expérimentale désactivée,
|
||||
le témoin Moyen conserve ses23 modules de traversée et sa salle indépendante,
|
||||
sans bâtiments patrimoniaux ni petits réseaux expérimentaux. La marche native
|
||||
relie les deux sorties en548 déplacements et atteint74 points de contrôle ;
|
||||
les échelles et l’accès au hall restent praticables.
|
||||
|
||||
Les créations et réouvertures de génération21 sont identiques aux rapports de
|
||||
la release21, hors identifiants de processus et dossier de test. Les391 fichiers
|
||||
de production historiques qui ne nécessitent pas de raccord22 restent identiques
|
||||
octet pour octet ; le preset caché21 est la copie exacte du précédent preset
|
||||
public. Les masques sont vérifiés à l’intérieur et hors des structures.
|
||||
|
||||
`check build assemblePack` passe :75 tâches, dont les huit GameTests généraux.
|
||||
Le JAR, l’index packwiz, le MRpack et le ZIP Prism sont vérifiés. Aucun code de
|
||||
test n’est embarqué dans le JAR. SHA-256 du JAR :
|
||||
|
||||
```text
|
||||
10d7ef32ac2e81a977ea4854503bafa29e0bc10929984b76df01d6f5f61f9056
|
||||
```
|
||||
|
||||
SHA-256 du MRpack :
|
||||
|
||||
```text
|
||||
0b553fcdd8b6a2ec26f72059e428d2cc0ce526f36ef8580dd3e50c1dfa40ec78
|
||||
```
|
||||
|
||||
Les reçus de publication et de synchronisation sont suivis dans
|
||||
[la distribution](packwiz.md).
|
||||
|
||||
Ces témoins vérifient les trois tailles, avec et sans ville ; ils ne constituent
|
||||
pas une preuve de présence pour toutes les graines. La recherche reste bornée
|
||||
et refuse un volume protégé ou insuffisamment porté. Aucun essai visuel client
|
||||
ou essai Windows natif n’est revendiqué.
|
||||
|
||||
## Reproduire dans le dossier de développement
|
||||
|
||||
Avec Java25, création du témoin Moyen puis réouverture du même monde de test :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runGameTest -PsanctuarySelective22Tests=true -PsanctuaryDensityMaps=false
|
||||
./gradlew :sanctuary:runGameTest -PsanctuarySelective22Tests=true -PsanctuaryDensityMaps=false -PsanctuaryExpansionReload=true
|
||||
```
|
||||
|
||||
Pour une nouvelle création Petit ou Grand, ajouter `-PsanctuaryTestSeed=42`
|
||||
et `-PsanctuaryTestDiameter=512` ou `1024`. Le cas OFF utilise
|
||||
`-PsanctuaryExperimentalStructures=false`. Chaque réouverture doit suivre la
|
||||
création correspondante, avec les mêmes paramètres. Les rapports sont dans
|
||||
`mods/sanctuary/build/run/gameTest/diagnostics/selective22-<graine>-<diamètre>/`.
|
||||
|
||||
Le témoin historique utilise `-PsanctuaryHeritage21Tests=true`, puis le même
|
||||
drapeau avec `-PsanctuaryExpansionReload=true`. La vérification de livraison
|
||||
reste `./gradlew check build assemblePack`. Exécuter ces JVM séquentiellement.
|
||||
@@ -22,6 +22,27 @@ L'objectif mythologique est de progresser d'une condition très limitée, proche
|
||||
- **Humour, surprise et souvenirs.** Les noms d'entités, les événements, les animaux rares et les objets insolites comptent autant que les systèmes économiques.
|
||||
- **Un développement par tickets.** Livrer une petite mécanique jouable et vérifiable, puis l'ajuster avec des tickets de fonctionnalités et de bugs. Ne pas construire tous les systèmes avant de pouvoir jouer.
|
||||
|
||||
## Minecraft comme archéologie d’un autre Minecraft
|
||||
|
||||
**Canon et ligne de conception retenus.** Steve, Ari, Sunny, Kai, Zuri, Alex,
|
||||
Mefe, Makena, Noor sont les personnages disparus de la partie. Ils ont découvert
|
||||
quelque chose de révolutionnaire et ont continué parce que ça marchait. Jusqu’au
|
||||
moment où ça n’a plus marché.
|
||||
|
||||
> Ils n’ont pas ouvert quelque chose qu’ils n’auraient pas dû ouvrir. Ils ont
|
||||
> construit quelque chose qu’ils ne savaient plus refermer.
|
||||
|
||||
Chaque ruine doit être une trace d’une ancienne solution, pas seulement la trace
|
||||
d’un problème : une infrastructure dont l’auteur a progressivement disparu
|
||||
derrière son fonctionnement. Son ancien usage, ses circulations, ses réparations,
|
||||
ses extensions et ses chantiers doivent permettre de comprendre ce qui avait
|
||||
été résolu ici, même quand l’installation est abandonnée.
|
||||
|
||||
La nature de la découverte et les rôles individuels restent à définir. Aucune
|
||||
catastrophe précise n’est confirmée par ce principe. **La mise en scène de cette
|
||||
histoire et ses liens avec la progression restent à implémenter** ; les versions
|
||||
livrées et leurs tests établissent ce que chaque structure contient effectivement.
|
||||
|
||||
## Périmètre de départ
|
||||
|
||||
Le premier incrément porte sur le socle Fabric et la génération du monde : retrouver le générateur 26.2, produire une île principale dans un vrai vide, assurer une arrivée commune et sûre, puis poser une manière reproductible de générer des continents à une distance, une direction et une taille choisies.
|
||||
|
||||
+2
-2
@@ -8,6 +8,6 @@ loader_version=0.19.5
|
||||
loom_version=1.17.20
|
||||
fabric_api_version=0.160.0+26.3
|
||||
|
||||
mod_version=0.1.0-alpha.17
|
||||
pack_version=0.1.0-alpha.17
|
||||
mod_version=0.1.0-alpha.22
|
||||
pack_version=0.1.0-alpha.22
|
||||
maven_group=fr.koka.sanctuary
|
||||
|
||||
+117
-2
@@ -40,13 +40,21 @@ tasks.named('runGameTest') {
|
||||
systemProperty('sanctuary.test.unified', providers.gradleProperty('sanctuaryUnifiedTests').getOrElse('true'))
|
||||
systemProperty('sanctuary.test.diameter', providers.gradleProperty('sanctuaryTestDiameter').getOrElse('724'))
|
||||
systemProperty('sanctuary.test.generation', providers.gradleProperty('sanctuaryTestGeneration').getOrElse(
|
||||
providers.gradleProperty('sanctuaryLostCityTests').map { it.toBoolean() }.getOrElse(false) ? '14' : providers.gradleProperty('sanctuaryLostCity15Tests').map { it.toBoolean() }.getOrElse(false) ? '15' : providers.gradleProperty('sanctuaryLostCity16Tests').map { it.toBoolean() }.getOrElse(false) ? '16' : '17'))
|
||||
providers.gradleProperty('sanctuaryLostCityTests').map { it.toBoolean() }.getOrElse(false) ? '14' : providers.gradleProperty('sanctuaryLostCity15Tests').map { it.toBoolean() }.getOrElse(false) ? '15' : providers.gradleProperty('sanctuaryLostCity16Tests').map { it.toBoolean() }.getOrElse(false) ? '16' : providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false) ? '17' : providers.gradleProperty('sanctuaryHeritage19Tests').map { it.toBoolean() }.getOrElse(false) ? '19' : (providers.gradleProperty('sanctuaryHeritage20Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryLostCity20Tests').map { it.toBoolean() }.getOrElse(false)) ? '20' : (providers.gradleProperty('sanctuaryHeritage21Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryLostCity21Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryTransit21Tests').map { it.toBoolean() }.getOrElse(false)) ? '21' : '22'))
|
||||
systemProperty('sanctuary.test.selective22', providers.gradleProperty('sanctuarySelective22Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.transit.plannerDiagnostics', providers.gradleProperty('sanctuaryTransitDiagnostics').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.heritage21', providers.gradleProperty('sanctuaryHeritage21Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.lostCity21', providers.gradleProperty('sanctuaryLostCity21Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.transit21', providers.gradleProperty('sanctuaryTransit21Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.expansion', providers.gradleProperty('sanctuaryExpansionTests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.expansionReload', providers.gradleProperty('sanctuaryExpansionReload').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.lostCity', providers.gradleProperty('sanctuaryLostCityTests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.lostCity15', providers.gradleProperty('sanctuaryLostCity15Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.lostCity20', providers.gradleProperty('sanctuaryLostCity20Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.lostCity16', providers.gradleProperty('sanctuaryLostCity16Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.ruins17', providers.gradleProperty('sanctuaryRuins17Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.heritage20', providers.gradleProperty('sanctuaryHeritage20Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.heritage19', providers.gradleProperty('sanctuaryHeritage19Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.experimentalStructures', providers.gradleProperty('sanctuaryExperimentalStructures').getOrElse('true'))
|
||||
systemProperty('sanctuary.test.cityMatrix', providers.gradleProperty('sanctuaryCityMatrix').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.cityMatrixOnly', providers.gradleProperty('sanctuaryCityMatrixOnly').getOrElse('false'))
|
||||
@@ -69,10 +77,24 @@ tasks.named('processGametestResources') {
|
||||
inputs.property('lostCity16', lostCity16)
|
||||
def ruins17 = providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('ruins17', ruins17)
|
||||
def lostCity20 = providers.gradleProperty('sanctuaryLostCity20Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('lostCity20', lostCity20)
|
||||
def heritage20 = providers.gradleProperty('sanctuaryHeritage20Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('heritage20', heritage20)
|
||||
def heritage21 = providers.gradleProperty('sanctuaryHeritage21Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('heritage21', heritage21)
|
||||
def lostCity21 = providers.gradleProperty('sanctuaryLostCity21Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('lostCity21', lostCity21)
|
||||
def selective22 = providers.gradleProperty('sanctuarySelective22Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('selective22', selective22)
|
||||
def transit21 = providers.gradleProperty('sanctuaryTransit21Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('transit21', transit21)
|
||||
def heritage19 = providers.gradleProperty('sanctuaryHeritage19Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('heritage19', heritage19)
|
||||
def voidReuse = providers.gradleProperty('sanctuaryVoidReuseTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('voidReuse', voidReuse)
|
||||
filesMatching('fabric.mod.json') {
|
||||
expand(gametest_entrypoint: ruins17 ? 'fr.koka.sanctuary.gametest.Ruins17WorldGameTests' : voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark
|
||||
expand(gametest_entrypoint: selective22 ? 'fr.koka.sanctuary.gametest.Selective22WorldGameTests' : transit21 ? 'fr.koka.sanctuary.gametest.Transit21WorldGameTests' : lostCity21 ? 'fr.koka.sanctuary.gametest.LostCity21WorldGameTests' : heritage21 ? 'fr.koka.sanctuary.gametest.Heritage21WorldGameTests' : lostCity20 ? 'fr.koka.sanctuary.gametest.LostCity20WorldGameTests' : heritage20 ? 'fr.koka.sanctuary.gametest.Heritage20WorldGameTests' : heritage19 ? 'fr.koka.sanctuary.gametest.Heritage19WorldGameTests' : ruins17 ? 'fr.koka.sanctuary.gametest.Ruins17WorldGameTests' : voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark
|
||||
? 'fr.koka.sanctuary.gametest.WorldgenPerformanceGameTests'
|
||||
: unified ? 'fr.koka.sanctuary.gametest.UnifiedWorldGameTests'
|
||||
: 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests')
|
||||
@@ -415,3 +437,96 @@ tasks.named('check') { dependsOn('lostCityFoundation16Smoke') }
|
||||
}
|
||||
tasks.named('check') { dependsOn(smoke) }
|
||||
}
|
||||
|
||||
// Local native starts keep the long heritage routes bounded by Minecraft's reference range.
|
||||
['Planner', 'Renderer', 'Piece'].each { component ->
|
||||
def smoke = tasks.register("heritage19${component}Smoke", JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka.sanctuary.worldgen.heritage.Heritage${component}19Smoke"
|
||||
}
|
||||
tasks.named('check') { dependsOn(smoke) }
|
||||
}
|
||||
|
||||
// Generation20 architecture retains the previous renderers and verifies connected block states.
|
||||
[
|
||||
connectedBlocks20Smoke: 'detail.ConnectedBlocks20Smoke',
|
||||
heritage20RendererSmoke: 'heritage.HeritageRenderer20Smoke',
|
||||
heritage20PieceSmoke: 'heritage.HeritagePiece20Smoke',
|
||||
ruins20RendererSmoke: 'ruins.RuinsRenderer20Smoke',
|
||||
lostCityUnderground20Smoke: 'city.LostCityUnderground20Smoke',
|
||||
lostCitySurface20Smoke: 'city.LostCitySurface20Smoke',
|
||||
lostCityInteriors20Smoke: 'city.LostCityInteriors20Smoke'
|
||||
].each { name, harness ->
|
||||
def smoke = tasks.register(name, JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka.sanctuary.worldgen.${harness}"
|
||||
}
|
||||
tasks.named('check') { dependsOn(smoke) }
|
||||
}
|
||||
|
||||
// Optional actual block-state exports for architectural review; never enabled in ordinary checks.
|
||||
tasks.named('heritage20RendererSmoke') {
|
||||
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
|
||||
systemProperty('sanctuary.test.heritage20.export', rootProject.layout.buildDirectory.dir('alpha20-preview/heritage').get().asFile.absolutePath)
|
||||
}
|
||||
tasks.named('lostCityInteriors20Smoke') {
|
||||
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
|
||||
systemProperty('sanctuary.city20.preview', rootProject.layout.buildDirectory.file('alpha20-preview/city.json').get().asFile.absolutePath)
|
||||
}
|
||||
tasks.named('lostCitySurface20Smoke') {
|
||||
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
|
||||
systemProperty('sanctuary.city20.exteriorPreview', rootProject.layout.buildDirectory.file('alpha20-preview/exterior.json').get().asFile.absolutePath)
|
||||
}
|
||||
|
||||
// Generation21 keeps published20 renderers immutable.
|
||||
[
|
||||
heritage21RendererSmoke: 'heritage.HeritageRenderer21Smoke',
|
||||
heritage21PieceSmoke: 'heritage.HeritagePiece21Smoke',
|
||||
heritage21PlannerSmoke: 'heritage.HeritagePlanner21Smoke',
|
||||
lostCity21PlannerSmoke: 'city.LostCityPlanner21Smoke',
|
||||
lostCitySurface21Smoke: 'city.LostCitySurface21Smoke',
|
||||
lostCityInteriors21Smoke: 'city.LostCityInteriors21Smoke',
|
||||
transit21Smoke: 'transit.Transit21Smoke'
|
||||
].each { name, harness ->
|
||||
def smoke = tasks.register(name, JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka.sanctuary.worldgen.${harness}"
|
||||
}
|
||||
tasks.named('check') { dependsOn(smoke) }
|
||||
}
|
||||
tasks.named('heritage21RendererSmoke') {
|
||||
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
|
||||
systemProperty('sanctuary.test.heritage21.export', rootProject.layout.buildDirectory.dir('alpha21-preview/heritage').get().asFile.absolutePath)
|
||||
}
|
||||
tasks.named('lostCityInteriors21Smoke') {
|
||||
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
|
||||
systemProperty('sanctuary.city21.preview', rootProject.layout.buildDirectory.file('alpha21-preview/city.json').get().asFile.absolutePath)
|
||||
}
|
||||
tasks.named('lostCitySurface21Smoke') {
|
||||
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
|
||||
systemProperty('sanctuary.city21.exteriorPreview', rootProject.layout.buildDirectory.file('alpha21-preview/exterior.json').get().asFile.absolutePath)
|
||||
}
|
||||
|
||||
// Generation22 filters surface placement and adds an independent core hall; historical tests still run.
|
||||
[
|
||||
lostCity22PlannerSmoke: 'city.LostCityPlanner22Smoke',
|
||||
underground22Smoke: 'city.LostCityUnderground22Smoke',
|
||||
heritage22PlannerSmoke: 'heritage.HeritagePlanner22Smoke',
|
||||
transit22PlannerSmoke: 'transit.TransitPlanner22Smoke',
|
||||
transit22RouteSmoke: 'transit.TransitRoute22Smoke',
|
||||
transit22RendererSmoke: 'transit.TransitRenderer22Smoke'
|
||||
].each { name, harness ->
|
||||
def smoke = tasks.register(name, JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka.sanctuary.worldgen.${harness}"
|
||||
}
|
||||
tasks.named('check') { dependsOn(smoke) }
|
||||
}
|
||||
|
||||
+452
@@ -0,0 +1,452 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritagePiece19;
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19;
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritageRenderer19;
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritageStructures19;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.DoorBlock;
|
||||
import net.minecraft.world.level.block.TrapDoorBlock;
|
||||
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import net.minecraft.world.level.storage.loot.LootParams;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
|
||||
import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Native creation/reload proof confined to a disposable generation19 starter island. */
|
||||
public final class Heritage19WorldGameTests {
|
||||
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
|
||||
@GameTest(maxTicks = 12000)
|
||||
public void heritagePersistsWithRealDestinations(GameTestHelper helper) {
|
||||
helper.assertTrue(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator,
|
||||
"The heritage proof requires a Sanctuary generator");
|
||||
var generator = (SanctuaryChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
helper.assertTrue(generator.rootGeneration() == 19, "Use a fresh generation19 test world; do not convert an old save");
|
||||
new Scenario(helper, generator, Boolean.getBoolean("sanctuary.test.expansionReload")).advance();
|
||||
}
|
||||
private static final class Scenario {
|
||||
final GameTestHelper helper;
|
||||
final ServerLevel level;
|
||||
final SanctuaryChunkGenerator generator;
|
||||
final HeritagePlan19 plan;
|
||||
final boolean reload;
|
||||
final Map<String, Object> report = new LinkedHashMap<>();
|
||||
final List<ChunkPos> chunks = new ArrayList<>();
|
||||
final List<StructureStart> starts = new ArrayList<>();
|
||||
JsonObject previous;
|
||||
BlockPos gold;
|
||||
int nextChunk;
|
||||
boolean prepared;
|
||||
Scenario(GameTestHelper helper, SanctuaryChunkGenerator generator, boolean reload) {
|
||||
this.helper = helper; this.level = helper.getLevel(); this.generator = generator; this.reload = reload;
|
||||
this.plan = generator.heritagePlan19();
|
||||
report.put("seed", level.getSeed()); report.put("size", generator.initialDiameter());
|
||||
report.put("process_id", ProcessHandle.current().pid()); report.put("world_directory", world(level).toString());
|
||||
report.put("experimental_structures", generator.experimentalStructures());
|
||||
report.put("structures_enabled", level.structureManager().shouldGenerateStructures());
|
||||
report.put("scope", "Native local starts, saved sites and routes, player-sized entrances, supports and unopened chests on the starter island. "
|
||||
+ "All14 tool tables are sampled separately without consuming a saved chest. Door states are restored after traversal. "
|
||||
+ "No personal save or expansion is opened; one disposable outer-wall edit is checked in a second JVM. "
|
||||
+ "Missing rails are intentional; this proof checks walking and saved rail shapes, not an uninterrupted minecart journey.");
|
||||
}
|
||||
void advance() {
|
||||
try {
|
||||
if (!prepared) { report.put("stage", "prepare"); prepare(); prepared = true; if (plan.isEmpty()) { finishDisabled(); return; } }
|
||||
if (nextChunk < chunks.size()) {
|
||||
report.put("stage", "load_full_chunks"); var pos = chunks.get(nextChunk++);
|
||||
helper.assertTrue(level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true) instanceof LevelChunk,
|
||||
"Heritage footprint must reach native FULL: " + pos);
|
||||
if (nextChunk % 20 == 0) SanctuaryMod.LOGGER.info("Heritage19 loaded {}/{} footprint chunks", nextChunk, chunks.size());
|
||||
helper.runAtTickTime(helper.getTick() + 1, this::advance); return;
|
||||
}
|
||||
report.put("stage", "verify_native_geometry"); verifyPlaced(); report.put("passed", true);
|
||||
if (!reload) {
|
||||
helper.assertTrue(gold != null && level.getBlockState(gold).isCollisionShapeFullBlock(level, gold), "Gold witness must replace a solid wall away from circulation");
|
||||
level.setBlock(gold, Blocks.GOLD_BLOCK.defaultBlockState(), 3); report.put("gold_witness", coords(gold));
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush native starts, unopened loot and the wall edit");
|
||||
report.put("world_flushed_before_shutdown", true);
|
||||
} else {
|
||||
helper.assertTrue(level.getBlockState(gold).is(Blocks.GOLD_BLOCK), "Reload replaced the player's wall edit");
|
||||
report.put("gold_witness", coords(gold)); report.put("gold_witness_preserved", true);
|
||||
}
|
||||
write(level, report, reload); helper.succeed();
|
||||
} catch (Exception failure) {
|
||||
report.put("passed", false); report.put("failure", failure.toString());
|
||||
try { write(level, report, reload); } catch (IOException ignored) { }
|
||||
throw new IllegalStateException("Heritage19 native " + (reload ? "reload" : "creation") + " failed", failure);
|
||||
}
|
||||
}
|
||||
void prepare() throws IOException {
|
||||
var ops = RegistryOps.create(JsonOps.INSTANCE, level.registryAccess());
|
||||
var encoded = ChunkGenerator.CODEC.encodeStart(ops, generator).getOrThrow();
|
||||
var decoded = ChunkGenerator.CODEC.parse(ops, encoded).getOrThrow();
|
||||
helper.assertTrue(decoded instanceof SanctuaryChunkGenerator copy && copy.rootGeneration() == 19
|
||||
&& copy.experimentalStructures() == generator.experimentalStructures(), "Generation19 codec lost its experimental switch");
|
||||
report.put("generator_codec", encoded); report.put("plan_sha256", planHash(plan));
|
||||
report.put("planned_sites", plan.sites().stream().map(site -> Map.of("id", site.id(), "kind", site.kind().name(),
|
||||
"entry", coords(HeritagePiece19.locator(site)), "floor_y", site.floorY(), "foundation_depth", site.foundationDepth(), "footings", site.supports().size())).toList());
|
||||
report.put("planned_links", plan.links().stream().map(link -> Map.of("id", link.id(), "from", link.fromId(), "to", link.toId(), "kind", link.kind().name(), "length", link.path().size(), "path", pathCoords(link))).toList());
|
||||
report.put("planning_samples", plan.samples()); report.put("planning_columns", plan.columns()); report.put("planning_diagnostics", plan.diagnostics());
|
||||
helper.assertTrue(plan.samples() <= HeritagePlan19.MAX_SAMPLES && plan.columns() <= HeritagePlan19.MAX_COLUMNS, "Heritage planning exceeded its budget");
|
||||
verifyLootTables(helper, report);
|
||||
if (reload) {
|
||||
previous = JsonParser.parseString(Files.readString(proof(level))).getAsJsonObject();
|
||||
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("world_flushed_before_shutdown").getAsBoolean(), "Reload needs a successful flushed creation proof");
|
||||
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload must use a second JVM");
|
||||
helper.assertTrue(previous.get("seed").getAsLong() == level.getSeed() && previous.get("size").getAsInt() == generator.initialDiameter()
|
||||
&& previous.get("world_directory").getAsString().equals(world(level).toString()), "Reload selected another world");
|
||||
helper.assertTrue(previous.get("generator_codec").equals(encoded) && previous.get("plan_sha256").getAsString().equals(planHash(plan)), "Saved settings or planned identities changed on reload");
|
||||
if (previous.has("gold_witness")) gold = readPos(previous.getAsJsonArray("gold_witness"));
|
||||
}
|
||||
if (!generator.experimentalStructures() || !level.structureManager().shouldGenerateStructures()) {
|
||||
helper.assertTrue(plan.isEmpty() && plan.segments().isEmpty() && plan.links().isEmpty(), "Disabled generation published heritage geometry"); return;
|
||||
}
|
||||
helper.assertTrue(!plan.isEmpty(), "Enabled reference island has no heritage site");
|
||||
report.put("present_site_kinds", plan.sites().stream().map(s -> s.kind().name()).distinct().sorted().toList());
|
||||
report.put("missing_site_kinds", java.util.Arrays.stream(HeritagePlan19.Kind.values()).filter(k -> plan.sites().stream().noneMatch(s -> s.kind() == k)).map(Enum::name).toList());
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
|
||||
var origins = new HashSet<ChunkPos>(); var uniqueChunks = new HashSet<ChunkPos>();
|
||||
plan.sites().forEach(s -> origins.add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
|
||||
plan.segments().forEach(s -> origins.add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
|
||||
for (var origin : origins.stream().sorted(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList()) {
|
||||
var chunk = level.getChunk(origin.x(), origin.z(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
var start = chunk.getStartForStructure(structure.value());
|
||||
helper.assertTrue(start != null && start.isValid(), "Accepted site/route has no native start at " + origin); starts.add(start);
|
||||
for (var piece : start.getPieces()) {
|
||||
helper.assertTrue(piece instanceof HeritagePiece19, "Heritage loaded a foreign piece type");
|
||||
var box = piece.getBoundingBox();
|
||||
helper.assertTrue(Math.abs((long) (box.minX() >> 4) - origin.x()) <= 7 && Math.abs((long) (box.maxX() >> 4) - origin.x()) <= 7
|
||||
&& Math.abs((long) (box.minZ() >> 4) - origin.z()) <= 7 && Math.abs((long) (box.maxZ() >> 4) - origin.z()) <= 7, "Native start references a distant route endpoint");
|
||||
box.intersectingChunks().forEach(uniqueChunks::add);
|
||||
}
|
||||
}
|
||||
for (var site : plan.sites()) uniqueChunks.add(ChunkPos.containing(HeritagePiece19.locator(site)));
|
||||
for (var link : plan.links()) for (var point : link.path()) uniqueChunks.add(new ChunkPos(point.x() >> 4, point.z() >> 4));
|
||||
helper.assertTrue(uniqueChunks.size() <= 1024, "Bounded proof footprint exceeds1024 chunks");
|
||||
chunks.addAll(uniqueChunks.stream().sorted(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList());
|
||||
}
|
||||
void finishDisabled() throws IOException {
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
|
||||
var start = level.getChunk(0, 0, ChunkStatus.STRUCTURE_STARTS, true).getStartForStructure(structure.value());
|
||||
helper.assertTrue(start == null || !start.isValid(), "Disabled heritage emitted a native start");
|
||||
helper.assertTrue(generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false) == null, "Disabled heritage is locatable");
|
||||
report.put("empty_plan_verified", true); report.put("passed", true);
|
||||
if (!reload) { helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save disabled experiment setting"); report.put("world_flushed_before_shutdown", true); }
|
||||
write(level, report, reload); helper.succeed();
|
||||
}
|
||||
void verifyPlaced() {
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
|
||||
var context = StructurePieceSerializationContext.fromLevel(level); var summaries = new ArrayList<Map<String, Object>>();
|
||||
int blocks = 0, chests = 0, pieces = 0, supports = 0;
|
||||
for (var start : starts) {
|
||||
var origin = start.getChunkPos(); var saved = start.createTag(context, origin); String nativeHash = hash(saved.toString());
|
||||
var copy = StructureStart.loadStaticStart(context, saved, level.getSeed());
|
||||
helper.assertTrue(copy != null && copy.isValid() && copy.createTag(context, origin).equals(saved), "Native start failed an exact NBT roundtrip");
|
||||
var expectedPayloads = new HashSet<net.minecraft.nbt.CompoundTag>();
|
||||
plan.sites().stream().filter(s -> s.startChunkX() == origin.x() && s.startChunkZ() == origin.z()).forEach(s -> expectedPayloads.add(HeritagePiece19.encodeSite(s)));
|
||||
plan.segments().stream().filter(s -> s.startChunkX() == origin.x() && s.startChunkZ() == origin.z()).forEach(s -> expectedPayloads.add(HeritagePiece19.encodeSegment(s)));
|
||||
var actualPayloads = new HashSet<net.minecraft.nbt.CompoundTag>();
|
||||
if (reload) {
|
||||
JsonObject prior = null;
|
||||
for (var value : previous.getAsJsonArray("starts")) { var item = value.getAsJsonObject(); var at = item.getAsJsonArray("start_chunk"); if (at.get(0).getAsInt() == origin.x() && at.get(1).getAsInt() == origin.z()) prior = item; }
|
||||
helper.assertTrue(prior != null && prior.get("native_start_sha256").getAsString().equals(nativeHash), "Reload changed saved geometry or native reference count");
|
||||
}
|
||||
var loot = new ArrayList<Map<String, Object>>();
|
||||
for (var value : start.getPieces()) {
|
||||
var piece = (HeritagePiece19) value; pieces++;
|
||||
var tag = piece.createTag(context); var restored = new HeritagePiece19(context, tag);
|
||||
helper.assertTrue(restored.createTag(context).equals(tag), "Saved piece parameters changed on native load");
|
||||
actualPayloads.add(tag.getCompound("SanctuaryHeritage").orElseThrow());
|
||||
var corrupt = tag.copy(); corrupt.getCompound("SanctuaryHeritage").orElseThrow().putInt("Schema", 20);
|
||||
boolean rejected = false; try { new HeritagePiece19(context, corrupt); } catch (IllegalArgumentException expected) { rejected = true; }
|
||||
helper.assertTrue(rejected, "Unknown schema must fail closed");
|
||||
piece.getBoundingBox().intersectingChunks().forEach(at -> helper.assertTrue(level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true)
|
||||
.getReferencesForStructure(structure.value()).contains(origin.pack()), "FULL chunk lost its native structure reference"));
|
||||
var expected = rendered(piece); blocks += expected.blocks().size();
|
||||
for (var entry : expected.blocks().entrySet()) {
|
||||
var at = entry.getKey(); var wanted = entry.getValue(); helper.assertTrue(piece.getBoundingBox().isInside(at), "Renderer escaped saved bounds");
|
||||
if (reload && at.equals(gold)) continue;
|
||||
helper.assertTrue(level.getBlockState(at).is(wanted.getBlock()), "Clipping or decoration changed heritage at " + at + ": expected " + wanted + " got " + level.getBlockState(at));
|
||||
helper.assertTrue(!wanted.is(Blocks.SPAWNER), "Heritage must not create spawners");
|
||||
}
|
||||
if (piece.isSite()) {
|
||||
supports += verifySiteBearing(helper, generator, plan, piece.site());
|
||||
if (!reload && gold == null) {
|
||||
gold = expected.blocks().entrySet().stream().filter(e -> isOuterWall(piece.site(), e.getKey()) && e.getValue().isCollisionShapeFullBlock(level, e.getKey()))
|
||||
.map(Map.Entry::getKey).sorted(Comparator.<BlockPos>comparingInt(p -> p.getY()).thenComparingInt(p -> p.getX()).thenComparingInt(p -> p.getZ())).findFirst().orElse(null);
|
||||
}
|
||||
} else for (var cell : piece.segment().cells()) { supports++; assertRock(helper, new BlockPos(cell.x(), cell.minY() - 1, cell.z())); }
|
||||
for (var chest : expected.chests()) {
|
||||
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Deferred chest is missing");
|
||||
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
|
||||
helper.assertTrue(entity.getLootTable() != null && entity.getLootTable().equals(chest.table()) && entity.getLootTableSeed() == chest.seed(), "Deferred loot table/seed was lost or consumed");
|
||||
if (piece.isSite()) helper.assertTrue(chest.table().equals(lootKey(piece.site().kind())), "Site uses another kind's tool table");
|
||||
loot.add(Map.of("position", coords(chest.position()), "table", chest.table().identifier().toString(), "seed", chest.seed())); chests++;
|
||||
}
|
||||
}
|
||||
helper.assertTrue(actualPayloads.equals(expectedPayloads) && start.getPieces().size() == expectedPayloads.size(), "Native start lost or duplicated planned pieces");
|
||||
summaries.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", nativeHash, "pieces", start.getPieces().size(), "unopened_chests", loot));
|
||||
}
|
||||
report.put("starts", summaries); report.put("native_pieces", pieces); report.put("native_blocks_checked", blocks);
|
||||
report.put("full_chunks_checked", chunks.size()); report.put("rock_supports_checked", supports); report.put("unopened_deferred_chests", chests);
|
||||
report.put("stage", "verify_site_routes"); var sites = new ArrayList<Map<String, Object>>(); for (var site : plan.sites()) sites.add(verifyAccess(helper, site)); report.put("native_sites", sites);
|
||||
report.put("stage", "verify_read_only_landings");
|
||||
report.put("read_only_landmarks", verifyReadOnlyLandings(helper, generator, plan));
|
||||
report.put("stage", "verify_island_routes"); report.put("native_links", verifyLinks(helper, plan));
|
||||
int greatestSpan = plan.links().stream().mapToInt(Heritage19WorldGameTests::routeSpan).max().orElse(0);
|
||||
report.put("greatest_single_route_span", greatestSpan);
|
||||
if (level.getSeed() == 0 && generator.initialDiameter() == 724) {
|
||||
int requiredSpan = generator.initialDiameter() / 4;
|
||||
report.put("reference_minimum_route_span", requiredSpan);
|
||||
helper.assertTrue(greatestSpan >= requiredSpan, "Seed0 Medium must demonstrate an island-scale route spanning at least "
|
||||
+ requiredSpan + " blocks on X or Z; got " + greatestSpan);
|
||||
}
|
||||
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
|
||||
helper.assertTrue(located != null && located.getSecond().is(HeritageStructures19.KEY) && plan.sites().stream().anyMatch(s -> HeritagePiece19.locator(s).equals(located.getFirst())), "Locate must lead to a real exterior site entry");
|
||||
report.put("locate", coords(located.getFirst()));
|
||||
}
|
||||
}
|
||||
private record Chest(BlockPos position, ResourceKey<LootTable> table, long seed) {}
|
||||
private record Rendered(Map<BlockPos, BlockState> blocks, List<Chest> chests) {}
|
||||
private static Rendered rendered(HeritagePiece19 piece) {
|
||||
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
|
||||
int x0 = piece.isSite() ? piece.site().minX() : piece.segment().minX();
|
||||
int y0 = piece.isSite() ? piece.site().floorY() : piece.segment().minY();
|
||||
int z0 = piece.isSite() ? piece.site().minZ() : piece.segment().minZ();
|
||||
var writer = new LostCityRenderer.BlockWriter() {
|
||||
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(new BlockPos(x0 + x, y0 + y, z0 + z), state); }
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
|
||||
var at = new BlockPos(x0 + x, y0 + y, z0 + z); blocks.put(at, Blocks.CHEST.defaultBlockState()); chests.add(new Chest(at, table, RandomSource.create(seed).nextLong()));
|
||||
}
|
||||
};
|
||||
if (piece.isSite()) HeritageRenderer19.render(piece.site(), writer); else HeritageRenderer19.renderSegment(piece.segment(), writer);
|
||||
return new Rendered(blocks, chests);
|
||||
}
|
||||
private static boolean isOuterWall(HeritagePlan19.Site site, BlockPos pos) {
|
||||
return pos.getY() >= site.floorY() + 2 && pos.getY() <= site.floorY() + 3
|
||||
&& (pos.getX() == site.minX() + 2 || pos.getX() == site.maxX() - 2 || pos.getZ() == site.minZ() + 2 || pos.getZ() == site.maxZ() - 2)
|
||||
&& Math.abs(pos.getX() - site.entry().x()) + Math.abs(pos.getZ() - site.entry().z()) > 6;
|
||||
}
|
||||
private static Map<String, Object> verifyAccess(GameTestHelper helper, HeritagePlan19.Site site) {
|
||||
var level = helper.getLevel(); var expected = rendered(new HeritagePiece19(site)); var doors = new HashMap<BlockPos, BlockState>();
|
||||
expected.blocks().forEach((at, state) -> {
|
||||
if ((state.getBlock() instanceof DoorBlock || state.getBlock() instanceof TrapDoorBlock) && state.hasProperty(BlockStateProperties.OPEN)) doors.put(at, level.getBlockState(at));
|
||||
});
|
||||
doors.forEach((at, state) -> level.setBlock(at, state.setValue(BlockStateProperties.OPEN, true), 2));
|
||||
try {
|
||||
var entry = HeritagePiece19.locator(site);
|
||||
Predicate<BlockPos> inside = p -> p.equals(entry) || p.getX() >= site.minX() && p.getX() <= site.maxX()
|
||||
&& p.getZ() >= site.minZ() && p.getZ() <= site.maxZ() && p.getY() >= site.floorY() - 4 && p.getY() < site.maxY();
|
||||
var reached = walk(helper, entry, inside); int targets = 0;
|
||||
for (var target : HeritageRenderer19.targets(site)) {
|
||||
var at = new BlockPos(site.minX() + target.x(), site.floorY() + target.y(), site.minZ() + target.z());
|
||||
helper.assertTrue(reached.contains(at), "Site " + site.id() + " cannot reach " + target.name() + " at " + at); targets++;
|
||||
}
|
||||
return Map.of("id", site.id(), "kind", site.kind().name(), "entry", coords(entry), "standing_cells", reached.size(), "walkable_targets", targets);
|
||||
} finally { doors.forEach((at, state) -> level.setBlock(at, state, 2)); }
|
||||
}
|
||||
private static List<Map<String, Object>> verifyLinks(GameTestHelper helper, HeritagePlan19 plan) {
|
||||
helper.assertTrue(!plan.links().isEmpty(), "Reference island needs a real route between accepted destinations");
|
||||
var entries = new HashMap<String, HeritagePlan19.Entry>(); plan.sites().forEach(s -> entries.put(s.id(), s.entry())); plan.landmarks().forEach(s -> entries.put(s.id(), s.entry()));
|
||||
var cells = new HashMap<Long, HeritagePlan19.Cell>(); plan.segments().forEach(s -> s.cells().forEach(c -> cells.put(column(c.x(), c.z()), c)));
|
||||
var result = new ArrayList<Map<String, Object>>();
|
||||
for (var link : plan.links()) {
|
||||
var from = entries.get(link.fromId()); var to = entries.get(link.toId());
|
||||
helper.assertTrue(from != null && to != null, "Route points to an absent destination");
|
||||
var first = link.path().getFirst(); var last = link.path().getLast();
|
||||
helper.assertTrue(first.x() == from.x() && first.z() == from.z() && first.floorY() + 1 == from.y()
|
||||
&& last.x() == to.x() && last.z() == to.z() && last.floorY() + 1 == to.y(), "Route does not meet its real entrances");
|
||||
int present = 0, missing = 0;
|
||||
for (int i = 0; i < link.path().size(); i++) {
|
||||
var point = link.path().get(i); var at = new BlockPos(point.x(), point.floorY() + 1, point.z());
|
||||
helper.assertTrue(standable(helper.getLevel(), at), "Ballast route " + link.id() + " is blocked at " + at);
|
||||
if (i > 0) { var prior = link.path().get(i - 1); if (point.floorY() > prior.floorY()) helper.assertTrue(clearBody(helper.getLevel(), new BlockPos(prior.x(), prior.floorY() + 2, prior.z())), "Route slope cannot be climbed by a player"); }
|
||||
var cell = cells.get(column(point.x(), point.z()));
|
||||
if (cell != null && cell.rail() != HeritagePlan19.Rail.NONE) {
|
||||
var actual = helper.getLevel().getBlockState(at);
|
||||
if (cell.railPresent()) {
|
||||
helper.assertTrue(actual.is(Blocks.RAIL), "Saved existing rail disappeared");
|
||||
helper.assertTrue(actual.getValue(BlockStateProperties.RAIL_SHAPE).name().equals(cell.rail().name()), "Native rail update changed the intended slope or curve at " + at); present++;
|
||||
} else { helper.assertTrue(!actual.is(Blocks.RAIL), "An intentionally missing rail regenerated"); missing++; }
|
||||
}
|
||||
}
|
||||
int spanX = link.path().stream().mapToInt(HeritagePlan19.Point::x).max().orElseThrow() - link.path().stream().mapToInt(HeritagePlan19.Point::x).min().orElseThrow();
|
||||
int spanZ = link.path().stream().mapToInt(HeritagePlan19.Point::z).max().orElseThrow() - link.path().stream().mapToInt(HeritagePlan19.Point::z).min().orElseThrow();
|
||||
result.add(Map.of("id", link.id(), "from", link.fromId(), "to", link.toId(), "kind", link.kind().name(), "length", link.path().size(),
|
||||
"span_x", spanX, "span_z", spanZ, "existing_rails", present, "deliberately_missing_rails", missing, "path", pathCoords(link)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private static int routeSpan(HeritagePlan19.Link link) {
|
||||
int spanX = link.path().stream().mapToInt(HeritagePlan19.Point::x).max().orElseThrow()
|
||||
- link.path().stream().mapToInt(HeritagePlan19.Point::x).min().orElseThrow();
|
||||
int spanZ = link.path().stream().mapToInt(HeritagePlan19.Point::z).max().orElseThrow()
|
||||
- link.path().stream().mapToInt(HeritagePlan19.Point::z).min().orElseThrow();
|
||||
return Math.max(spanX, spanZ);
|
||||
}
|
||||
private static List<List<Integer>> pathCoords(HeritagePlan19.Link link) {
|
||||
return link.path().stream().map(point -> List.of(point.x(), point.floorY(), point.z())).toList();
|
||||
}
|
||||
private static int verifySiteBearing(GameTestHelper helper, SanctuaryChunkGenerator generator, HeritagePlan19 plan, HeritagePlan19.Site site) {
|
||||
int checked = 0;
|
||||
// The planner certifies a nine-point bearing grid, either directly below its slab or below a saved short footing.
|
||||
for (int dx : new int[] {1, 8, 15}) for (int dz : new int[] {1, 8, 15}) {
|
||||
int x = site.minX() + dx, z = site.minZ() + dz;
|
||||
var support = site.supports().stream().filter(s -> s.x() == x && s.z() == z).findFirst();
|
||||
int bottom = support.map(HeritagePlan19.Support::minY).orElse(site.minY());
|
||||
for (int below = 1; below <= 2; below++) {
|
||||
var at = new BlockPos(x, bottom - below, z); assertRock(helper, at);
|
||||
helper.assertTrue(plan.protects(at.getX(), at.getY(), at.getZ()) && generator.protectsCityDecoration(at),
|
||||
"Site's certified bearing layer is not published in the decoration mask: " + site.id() + " at " + at);
|
||||
helper.assertTrue(!plan.contains(at.getX(), at.getY(), at.getZ()), "Original bearing rock became a writable heritage volume: " + at);
|
||||
checked++;
|
||||
}
|
||||
}
|
||||
return checked;
|
||||
}
|
||||
private static List<Map<String, Object>> verifyReadOnlyLandings(GameTestHelper helper, SanctuaryChunkGenerator generator, HeritagePlan19 plan) {
|
||||
var used = new HashSet<String>(); plan.links().forEach(link -> { used.add(link.fromId()); used.add(link.toId()); });
|
||||
var result = new ArrayList<Map<String, Object>>();
|
||||
for (var landmark : plan.landmarks()) if (used.contains(landmark.id())) {
|
||||
var e = landmark.entry(); int checked = 0;
|
||||
for (int side = -1; side <= 1; side++) {
|
||||
int x = e.x() + ((e.facing() & 1) == 0 ? side : 0), z = e.z() + ((e.facing() & 1) == 1 ? side : 0);
|
||||
for (int y = e.y() - 2; y <= e.y() + 2; y++) {
|
||||
var at = new BlockPos(x, y, z);
|
||||
helper.assertTrue(plan.protects(x, y, z) && generator.protectsCityDecoration(at), "Read-only arrival lost its decoration mask: " + landmark.id() + " at " + at);
|
||||
helper.assertTrue(!plan.contains(x, y, z), "Heritage gained write ownership of a historical arrival: " + landmark.id() + " at " + at);
|
||||
if (y == e.y() - 1) assertRock(helper, at);
|
||||
if (y >= e.y()) helper.assertTrue(helper.getLevel().getBlockState(at).isAir(), "Decoration obstructed a certified historical arrival: " + at);
|
||||
checked++;
|
||||
}
|
||||
}
|
||||
result.add(Map.of("id", landmark.id(), "entry", List.of(e.x(), e.y(), e.z()), "read_only_cells", checked));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private static void assertRock(GameTestHelper helper, BlockPos at) {
|
||||
helper.assertTrue(helper.getLevel().getBlockState(at).isSolid() && helper.getLevel().getFluidState(at).isEmpty(), "Lost original rock bearing at " + at);
|
||||
}
|
||||
private static void verifyLootTables(GameTestHelper helper, Map<String, Object> report) {
|
||||
var result = new ArrayList<Map<String, Object>>(); var level = helper.getLevel();
|
||||
var params = new LootParams.Builder(level).withParameter(LootContextParams.ORIGIN, new Vec3(0, 160, 0)).create(LootContextParamSets.CHEST);
|
||||
for (var kind : HeritagePlan19.Kind.values()) {
|
||||
var table = level.getServer().reloadableRegistries().getLootTable(lootKey(kind));
|
||||
helper.assertTrue(table != LootTable.EMPTY, "Heritage tool table did not load: " + kind);
|
||||
for (long seed : new long[] {0, 42, -1}) helper.assertTrue(table.getRandomItems(params, seed).stream().anyMatch(stack -> stack.is(tool(kind))), "Tool table is empty or lacks its expected primary tool: " + kind);
|
||||
result.add(Map.of("kind", kind.name(), "table", lootKey(kind).identifier().toString(), "sampled_seeds", List.of(0, 42, -1)));
|
||||
}
|
||||
report.put("tool_tables_sampled_separately", result);
|
||||
}
|
||||
private static ResourceKey<LootTable> lootKey(HeritagePlan19.Kind kind) { return ResourceKey.create(Registries.LOOT_TABLE, SanctuaryMod.id("chests/heritage19/" + kind.name().toLowerCase(Locale.ROOT))); }
|
||||
private static Item tool(HeritagePlan19.Kind kind) {
|
||||
return switch (kind) {
|
||||
case OBSERVATORY -> Items.SPYGLASS;
|
||||
case POST_OFFICE, EXPEDITION_OFFICE, RADIO_RELAY -> Items.COMPASS;
|
||||
case SUBSTATION, IMPOSSIBLE_SERVICE -> Items.CLOCK;
|
||||
case FREIGHT_STATION -> Items.MINECART;
|
||||
case COMMUNAL_DEPOT -> Items.IRON_AXE;
|
||||
case RAIL_WORKSHOP -> Items.IRON_PICKAXE;
|
||||
case PUMP_STATION -> Items.BUCKET;
|
||||
case GREENHOUSE -> Items.IRON_HOE;
|
||||
case VOID_DOCK -> Items.LEAD;
|
||||
case PHOTO_CLUB -> Items.BOOK;
|
||||
case CONSTRUCTION_REFUGE -> Items.IRON_SHOVEL;
|
||||
};
|
||||
}
|
||||
private static Set<BlockPos> walk(GameTestHelper helper, BlockPos start, Predicate<BlockPos> inside) {
|
||||
var level = helper.getLevel(); helper.assertTrue(inside.test(start) && standable(level, start), "Blocked or unsupported exterior entrance: " + start);
|
||||
var seen = new HashSet<BlockPos>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
|
||||
while (!queue.isEmpty()) {
|
||||
var pos = queue.removeFirst();
|
||||
for (var direction : HORIZONTAL) for (int dy = -1; dy <= 1; dy++) {
|
||||
var next = pos.relative(direction).offset(0, dy, 0);
|
||||
if (!inside.test(next) || dy > 0 && !clearBody(level, pos.above())) continue;
|
||||
if (standable(level, next) && seen.add(next)) queue.add(next);
|
||||
}
|
||||
for (int dy : new int[] {-1, 1}) {
|
||||
var next = pos.offset(0, dy, 0); if (!inside.test(next)) continue;
|
||||
if ((level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(next).is(Blocks.LADDER) || level.getBlockState(pos.below()).is(Blocks.LADDER))
|
||||
&& standable(level, next) && seen.add(next)) queue.add(next);
|
||||
}
|
||||
helper.assertTrue(seen.size() <= 50_000, "Native site traversal escaped its bounds");
|
||||
}
|
||||
return seen;
|
||||
}
|
||||
private static boolean standable(ServerLevel level, BlockPos pos) {
|
||||
if (!clearBody(level, pos)) return false;
|
||||
if (level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(pos.below()).is(Blocks.LADDER)) return true;
|
||||
var below = pos.below(); return level.getBlockState(below).getCollisionShape(level, below).toAabbs().stream()
|
||||
.anyMatch(b -> b.maxY >= .5 && b.minX <= .5 && b.maxX >= .5 && b.minZ <= .5 && b.maxZ >= .5);
|
||||
}
|
||||
private static boolean clearBody(ServerLevel level, BlockPos pos) {
|
||||
var body = new AABB(pos.getX() + .2, pos.getY(), pos.getZ() + .2, pos.getX() + .8, pos.getY() + 1.8, pos.getZ() + .8);
|
||||
for (var at : new BlockPos[] {pos, pos.above()}) {
|
||||
if (!level.getFluidState(at).isEmpty()) return false;
|
||||
for (var shape : level.getBlockState(at).getCollisionShape(level, at).toAabbs()) if (shape.move(at.getX(), at.getY(), at.getZ()).intersects(body)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private static long column(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
|
||||
private static List<Integer> coords(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
|
||||
private static BlockPos readPos(com.google.gson.JsonArray value) { return new BlockPos(value.get(0).getAsInt(), value.get(1).getAsInt(), value.get(2).getAsInt()); }
|
||||
private static Path world(ServerLevel level) { return level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize(); }
|
||||
private static Path proof(ServerLevel level) { return world(level).resolve("heritage19-create-proof.json"); }
|
||||
private static String planHash(HeritagePlan19 plan) { return hash(plan.sites().toString() + plan.landmarks() + plan.links() + plan.segments()); }
|
||||
private static void write(ServerLevel level, Map<String, Object> report, boolean reload) throws IOException {
|
||||
var path = Path.of("diagnostics", "heritage19-" + (reload ? "reload" : "create") + ".json"); Files.createDirectories(path.getParent());
|
||||
var value = new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n"; Files.writeString(path, value); if (!reload) Files.writeString(proof(level), value);
|
||||
}
|
||||
private static String hash(String value) {
|
||||
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8))); }
|
||||
catch (NoSuchAlgorithmException impossible) { throw new AssertionError(impossible); }
|
||||
}
|
||||
}
|
||||
+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); }
|
||||
}
|
||||
}
|
||||
+554
@@ -0,0 +1,554 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritagePiece21;
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19;
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritageRenderer21;
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritageStructures19;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsPiece20;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsRenderer20;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsStructures17;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.DoorBlock;
|
||||
import net.minecraft.world.level.block.TrapDoorBlock;
|
||||
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import net.minecraft.world.level.storage.loot.LootParams;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
|
||||
import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Native creation/reload proof confined to a disposable generation21 starter island. */
|
||||
public final class Heritage21WorldGameTests {
|
||||
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
|
||||
@GameTest(maxTicks = 12000)
|
||||
public void heritagePersistsWithRealDestinations(GameTestHelper helper) {
|
||||
helper.assertTrue(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator,
|
||||
"The heritage proof requires a Sanctuary generator");
|
||||
var generator = (SanctuaryChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
helper.assertTrue(generator.rootGeneration() == 21, "Use a fresh generation21 test world; do not convert an old save");
|
||||
new Scenario(helper, generator, Boolean.getBoolean("sanctuary.test.expansionReload")).advance();
|
||||
}
|
||||
private static final class Scenario {
|
||||
final GameTestHelper helper;
|
||||
final ServerLevel level;
|
||||
final SanctuaryChunkGenerator generator;
|
||||
final HeritagePlan19 plan;
|
||||
final boolean reload;
|
||||
final Map<String, Object> report = new LinkedHashMap<>();
|
||||
final List<ChunkPos> chunks = new ArrayList<>();
|
||||
final List<StructureStart> starts = new ArrayList<>();
|
||||
final List<StructureStart> ruinsStarts = new ArrayList<>();
|
||||
JsonObject previous;
|
||||
BlockPos gold;
|
||||
int nextChunk;
|
||||
boolean prepared;
|
||||
Scenario(GameTestHelper helper, SanctuaryChunkGenerator generator, boolean reload) {
|
||||
this.helper = helper; this.level = helper.getLevel(); this.generator = generator; this.reload = reload;
|
||||
this.plan = generator.heritagePlan19();
|
||||
report.put("seed", level.getSeed()); report.put("size", generator.initialDiameter());
|
||||
report.put("process_id", ProcessHandle.current().pid()); report.put("world_directory", world(level).toString());
|
||||
report.put("experimental_structures", generator.experimentalStructures());
|
||||
report.put("structures_enabled", level.structureManager().shouldGenerateStructures());
|
||||
report.put("scope", "Native local starts, saved sites and routes, player-sized entrances, supports and unopened chests on the starter island. "
|
||||
+ "All14 tool tables are sampled separately without consuming a saved chest. Door states are restored after traversal. "
|
||||
+ "No personal save or expansion is opened; one disposable outer-wall edit is checked in a second JVM. "
|
||||
+ "Missing rails are intentional; this proof checks walking and saved rail shapes, not an uninterrupted minecart journey.");
|
||||
}
|
||||
void advance() {
|
||||
try {
|
||||
if (!prepared) { report.put("stage", "prepare"); prepare(); prepared = true; if (plan.isEmpty()) { finishDisabled(); return; } }
|
||||
if (nextChunk < chunks.size()) {
|
||||
report.put("stage", "load_full_chunks"); var pos = chunks.get(nextChunk++);
|
||||
helper.assertTrue(level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true) instanceof LevelChunk,
|
||||
"Heritage footprint must reach native FULL: " + pos);
|
||||
if (nextChunk % 20 == 0) SanctuaryMod.LOGGER.info("Heritage21 loaded {}/{} footprint chunks", nextChunk, chunks.size());
|
||||
helper.runAtTickTime(helper.getTick() + 1, this::advance); return;
|
||||
}
|
||||
report.put("stage", "verify_native_geometry"); verifyPlaced(); report.put("passed", true);
|
||||
if (!reload) {
|
||||
helper.assertTrue(gold != null && level.getBlockState(gold).isCollisionShapeFullBlock(level, gold), "Gold witness must replace a solid wall away from circulation");
|
||||
level.setBlock(gold, Blocks.GOLD_BLOCK.defaultBlockState(), 3); report.put("gold_witness", coords(gold));
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush native starts, unopened loot and the wall edit");
|
||||
report.put("world_flushed_before_shutdown", true);
|
||||
} else {
|
||||
helper.assertTrue(level.getBlockState(gold).is(Blocks.GOLD_BLOCK), "Reload replaced the player's wall edit");
|
||||
report.put("gold_witness", coords(gold)); report.put("gold_witness_preserved", true);
|
||||
}
|
||||
write(level, report, reload); helper.succeed();
|
||||
} catch (Exception failure) {
|
||||
report.put("passed", false); report.put("failure", failure.toString());
|
||||
try { write(level, report, reload); } catch (IOException ignored) { }
|
||||
throw new IllegalStateException("Heritage21 native " + (reload ? "reload" : "creation") + " failed", failure);
|
||||
}
|
||||
}
|
||||
void prepare() throws IOException {
|
||||
var ops = RegistryOps.create(JsonOps.INSTANCE, level.registryAccess());
|
||||
var encoded = ChunkGenerator.CODEC.encodeStart(ops, generator).getOrThrow();
|
||||
var decoded = ChunkGenerator.CODEC.parse(ops, encoded).getOrThrow();
|
||||
helper.assertTrue(decoded instanceof SanctuaryChunkGenerator copy && copy.rootGeneration() == 21
|
||||
&& copy.experimentalStructures() == generator.experimentalStructures(), "Generation21 codec lost its experimental switch");
|
||||
report.put("generator_codec", encoded); report.put("plan_sha256", planHash(plan));
|
||||
report.put("planned_sites", plan.sites().stream().map(site -> Map.of("id", site.id(), "kind", site.kind().name(),
|
||||
"entry", coords(HeritagePiece21.locator(site)), "floor_y", site.floorY(), "foundation_depth", site.foundationDepth(), "footings", site.supports().size())).toList());
|
||||
report.put("planned_links", plan.links().stream().map(link -> Map.of("id", link.id(), "from", link.fromId(), "to", link.toId(), "kind", link.kind().name(), "length", link.path().size(), "path", pathCoords(link))).toList());
|
||||
report.put("planning_samples", plan.samples()); report.put("planning_columns", plan.columns()); report.put("planning_diagnostics", plan.diagnostics());
|
||||
helper.assertTrue(plan.samples() <= HeritagePlan19.MAX_SAMPLES && plan.columns() <= HeritagePlan19.MAX_COLUMNS, "Heritage planning exceeded its budget");
|
||||
verifyLootTables(helper, report);
|
||||
if (reload) {
|
||||
previous = JsonParser.parseString(Files.readString(proof(level))).getAsJsonObject();
|
||||
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("world_flushed_before_shutdown").getAsBoolean(), "Reload needs a successful flushed creation proof");
|
||||
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload must use a second JVM");
|
||||
helper.assertTrue(previous.get("seed").getAsLong() == level.getSeed() && previous.get("size").getAsInt() == generator.initialDiameter()
|
||||
&& previous.get("world_directory").getAsString().equals(world(level).toString()), "Reload selected another world");
|
||||
helper.assertTrue(previous.get("generator_codec").equals(encoded) && previous.get("plan_sha256").getAsString().equals(planHash(plan)), "Saved settings or planned identities changed on reload");
|
||||
if (previous.has("gold_witness")) gold = readPos(previous.getAsJsonArray("gold_witness"));
|
||||
}
|
||||
if (!generator.experimentalStructures() || !level.structureManager().shouldGenerateStructures()) {
|
||||
helper.assertTrue(plan.isEmpty() && plan.segments().isEmpty() && plan.links().isEmpty(), "Disabled generation published heritage geometry"); return;
|
||||
}
|
||||
helper.assertTrue(!plan.isEmpty(), "Enabled reference island has no heritage site");
|
||||
report.put("present_site_kinds", plan.sites().stream().map(s -> s.kind().name()).distinct().sorted().toList());
|
||||
report.put("missing_site_kinds", java.util.Arrays.stream(HeritagePlan19.Kind.values()).filter(k -> plan.sites().stream().noneMatch(s -> s.kind() == k)).map(Enum::name).toList());
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
|
||||
var origins = new HashSet<ChunkPos>(); var uniqueChunks = new HashSet<ChunkPos>();
|
||||
plan.sites().forEach(s -> origins.add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
|
||||
plan.segments().forEach(s -> origins.add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
|
||||
for (var origin : origins.stream().sorted(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList()) {
|
||||
var chunk = level.getChunk(origin.x(), origin.z(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
var start = chunk.getStartForStructure(structure.value());
|
||||
helper.assertTrue(start != null && start.isValid(), "Accepted site/route has no native start at " + origin); starts.add(start);
|
||||
for (var piece : start.getPieces()) {
|
||||
helper.assertTrue(piece instanceof HeritagePiece21, "Heritage loaded a foreign piece type");
|
||||
var box = piece.getBoundingBox();
|
||||
helper.assertTrue(Math.abs((long) (box.minX() >> 4) - origin.x()) <= 7 && Math.abs((long) (box.maxX() >> 4) - origin.x()) <= 7
|
||||
&& Math.abs((long) (box.minZ() >> 4) - origin.z()) <= 7 && Math.abs((long) (box.maxZ() >> 4) - origin.z()) <= 7, "Native start references a distant route endpoint");
|
||||
box.intersectingChunks().forEach(uniqueChunks::add);
|
||||
}
|
||||
}
|
||||
for (var site : plan.sites()) uniqueChunks.add(ChunkPos.containing(HeritagePiece21.locator(site)));
|
||||
for (var link : plan.links()) for (var point : link.path()) uniqueChunks.add(new ChunkPos(point.x() >> 4, point.z() >> 4));
|
||||
helper.assertTrue(uniqueChunks.size() <= 1024, "Bounded proof footprint exceeds1024 chunks");
|
||||
var ruins = generator.ruinsPlan17();
|
||||
report.put("ruins_plan_sha256", hash(ruins.networks().toString()));
|
||||
if (reload) helper.assertTrue(previous.get("ruins_plan_sha256").getAsString().equals(hash(ruins.networks().toString())), "Reload changed forgotten room planning");
|
||||
helper.assertTrue(!ruins.isEmpty(), "Reference20 must demonstrate native experimental room architecture");
|
||||
var ruinsType = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(RuinsStructures17.KEY);
|
||||
var ruinsOrigins = new HashSet<ChunkPos>();
|
||||
for (var network : ruins.networks()) {
|
||||
var origin = new ChunkPos(network.startChunkX(), network.startChunkZ()); if (!ruinsOrigins.add(origin)) continue;
|
||||
var nativeStart = level.getChunk(origin.x(), origin.z(), ChunkStatus.STRUCTURE_STARTS, true).getStartForStructure(ruinsType.value());
|
||||
helper.assertTrue(nativeStart != null && nativeStart.isValid(), "Missing native20 forgotten network: " + network.id());
|
||||
ruinsStarts.add(nativeStart);
|
||||
for (var piece : nativeStart.getPieces()) {
|
||||
helper.assertTrue(piece instanceof RuinsPiece20, "Generation21 loaded a historical forgotten piece");
|
||||
piece.getBoundingBox().intersectingChunks().forEach(uniqueChunks::add);
|
||||
}
|
||||
}
|
||||
chunks.addAll(uniqueChunks.stream().sorted(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList());
|
||||
}
|
||||
void finishDisabled() throws IOException {
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
|
||||
var start = level.getChunk(0, 0, ChunkStatus.STRUCTURE_STARTS, true).getStartForStructure(structure.value());
|
||||
helper.assertTrue(start == null || !start.isValid(), "Disabled heritage emitted a native start");
|
||||
helper.assertTrue(generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false) == null, "Disabled heritage is locatable");
|
||||
report.put("empty_plan_verified", true); report.put("passed", true);
|
||||
if (!reload) { helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save disabled experiment setting"); report.put("world_flushed_before_shutdown", true); }
|
||||
write(level, report, reload); helper.succeed();
|
||||
}
|
||||
void verifyPlaced() {
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
|
||||
var context = StructurePieceSerializationContext.fromLevel(level); var summaries = new ArrayList<Map<String, Object>>();
|
||||
int blocks = 0, chests = 0, pieces = 0, supports = 0;
|
||||
var connections = NativeConnections20.Stats.empty();
|
||||
for (var start : starts) {
|
||||
var origin = start.getChunkPos(); var saved = start.createTag(context, origin); String nativeHash = hash(saved.toString());
|
||||
var copy = StructureStart.loadStaticStart(context, saved, level.getSeed());
|
||||
helper.assertTrue(copy != null && copy.isValid() && copy.createTag(context, origin).equals(saved), "Native start failed an exact NBT roundtrip");
|
||||
var expectedPayloads = new HashSet<net.minecraft.nbt.CompoundTag>();
|
||||
plan.sites().stream().filter(s -> s.startChunkX() == origin.x() && s.startChunkZ() == origin.z()).forEach(s -> expectedPayloads.add(HeritagePiece21.encodeSite(s)));
|
||||
plan.segments().stream().filter(s -> s.startChunkX() == origin.x() && s.startChunkZ() == origin.z()).forEach(s -> expectedPayloads.add(HeritagePiece21.encodeSegment(s)));
|
||||
var actualPayloads = new HashSet<net.minecraft.nbt.CompoundTag>();
|
||||
if (reload) {
|
||||
JsonObject prior = null;
|
||||
for (var value : previous.getAsJsonArray("starts")) { var item = value.getAsJsonObject(); var at = item.getAsJsonArray("start_chunk"); if (at.get(0).getAsInt() == origin.x() && at.get(1).getAsInt() == origin.z()) prior = item; }
|
||||
helper.assertTrue(prior != null && prior.get("native_start_sha256").getAsString().equals(nativeHash), "Reload changed saved geometry or native reference count");
|
||||
}
|
||||
var loot = new ArrayList<Map<String, Object>>();
|
||||
for (var value : start.getPieces()) {
|
||||
var piece = (HeritagePiece21) value; pieces++;
|
||||
var tag = piece.createTag(context); var restored = new HeritagePiece21(context, tag);
|
||||
helper.assertTrue(restored.createTag(context).equals(tag), "Saved piece parameters changed on native load");
|
||||
actualPayloads.add(tag.getCompound("SanctuaryHeritage").orElseThrow());
|
||||
var corrupt = tag.copy(); corrupt.getCompound("SanctuaryHeritage").orElseThrow().putInt("Schema", 22);
|
||||
boolean rejected = false; try { new HeritagePiece21(context, corrupt); } catch (IllegalArgumentException expected) { rejected = true; }
|
||||
helper.assertTrue(rejected, "Unknown schema must fail closed");
|
||||
piece.getBoundingBox().intersectingChunks().forEach(at -> helper.assertTrue(level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true)
|
||||
.getReferencesForStructure(structure.value()).contains(origin.pack()), "FULL chunk lost its native structure reference"));
|
||||
var expected = rendered(piece); blocks += expected.blocks().size();
|
||||
for (var entry : expected.blocks().entrySet()) {
|
||||
var at = entry.getKey(); var wanted = entry.getValue(); helper.assertTrue(piece.getBoundingBox().isInside(at), "Renderer escaped saved bounds");
|
||||
if (reload && at.equals(gold)) continue;
|
||||
helper.assertTrue(level.getBlockState(at).is(wanted.getBlock()), "Clipping or decoration changed heritage at " + at + ": expected " + wanted + " got " + level.getBlockState(at));
|
||||
helper.assertTrue(!wanted.is(Blocks.SPAWNER), "Heritage must not create spawners");
|
||||
}
|
||||
connections = connections.add(NativeConnections20.verify(helper, expected.blocks(), reload ? Set.of(gold) : Set.of()));
|
||||
if (piece.isSite()) {
|
||||
supports += verifySiteBearing(helper, generator, plan, piece.site());
|
||||
if (!reload && gold == null) {
|
||||
gold = expected.blocks().entrySet().stream().filter(e -> isOuterWall(piece.site(), e.getKey()) && e.getValue().isCollisionShapeFullBlock(level, e.getKey()))
|
||||
.map(Map.Entry::getKey).sorted(Comparator.<BlockPos>comparingInt(p -> p.getY()).thenComparingInt(p -> p.getX()).thenComparingInt(p -> p.getZ())).findFirst().orElse(null);
|
||||
}
|
||||
} else for (var cell : piece.segment().cells()) { supports++; assertRock(helper, new BlockPos(cell.x(), cell.minY() - 1, cell.z())); }
|
||||
for (var chest : expected.chests()) {
|
||||
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Deferred chest is missing");
|
||||
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
|
||||
helper.assertTrue(entity.getLootTable() != null && entity.getLootTable().equals(chest.table()) && entity.getLootTableSeed() == chest.seed(), "Deferred loot table/seed was lost or consumed");
|
||||
if (piece.isSite()) helper.assertTrue(chest.table().equals(lootKey(piece.site().kind())), "Site uses another kind's tool table");
|
||||
loot.add(Map.of("position", coords(chest.position()), "table", chest.table().identifier().toString(), "seed", chest.seed())); chests++;
|
||||
}
|
||||
}
|
||||
helper.assertTrue(actualPayloads.equals(expectedPayloads) && start.getPieces().size() == expectedPayloads.size(), "Native start lost or duplicated planned pieces");
|
||||
summaries.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", nativeHash, "pieces", start.getPieces().size(), "unopened_chests", loot));
|
||||
}
|
||||
report.put("connected_blocks", connections);
|
||||
helper.assertTrue(connections.panes() > 0 && connections.stairs() > 0 && connections.nativeOracles() > 0,
|
||||
"Reference20 needs real panes/stairs validated by vanilla neighbor rules");
|
||||
if (level.getSeed() == 0 && generator.initialDiameter() == 724)
|
||||
helper.assertTrue(connections.crossChunkPaneLinks() > 0, "Reference20 must demonstrate a real pane join across a chunk boundary");
|
||||
verifyRuins();
|
||||
report.put("starts", summaries); report.put("native_pieces", pieces); report.put("native_blocks_checked", blocks);
|
||||
report.put("full_chunks_checked", chunks.size()); report.put("rock_supports_checked", supports); report.put("unopened_deferred_chests", chests);
|
||||
report.put("stage", "verify_site_routes"); var sites = new ArrayList<Map<String, Object>>(); for (var site : plan.sites()) sites.add(verifyAccess(helper, site)); report.put("native_sites", sites);
|
||||
report.put("stage", "verify_read_only_landings");
|
||||
report.put("read_only_landmarks", verifyReadOnlyLandings(helper, generator, plan));
|
||||
report.put("stage", "verify_island_routes"); report.put("native_links", verifyLinks(helper, plan));
|
||||
int greatestSpan = plan.links().stream().mapToInt(Heritage21WorldGameTests::routeSpan).max().orElse(0);
|
||||
report.put("greatest_single_route_span", greatestSpan);
|
||||
var reachedIds = new HashSet<String>(); plan.links().forEach(l -> { reachedIds.add(l.fromId()); reachedIds.add(l.toId()); });
|
||||
report.put("connected_destination_ids", reachedIds.stream().sorted().toList());
|
||||
report.put("isolated_destination_ids", plan.sites().stream().map(HeritagePlan19.Site::id).filter(id -> !reachedIds.contains(id)).toList());
|
||||
report.put("connected_city_arrivals", plan.landmarks().stream().filter(l -> l.kind() == HeritagePlan19.LandmarkKind.CITY && reachedIds.contains(l.id())).map(HeritagePlan19.Landmark::id).toList());
|
||||
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
|
||||
helper.assertTrue(located != null && located.getSecond().is(HeritageStructures19.KEY) && plan.sites().stream().anyMatch(s -> HeritagePiece21.locator(s).equals(located.getFirst())), "Locate must lead to a real exterior site entry");
|
||||
report.put("locate", coords(located.getFirst()));
|
||||
}
|
||||
void verifyRuins() {
|
||||
var context = StructurePieceSerializationContext.fromLevel(level);
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(RuinsStructures17.KEY);
|
||||
var summaries = new ArrayList<Map<String, Object>>(); int count = 0, blockCount = 0, lootCount = 0;
|
||||
var connections = NativeConnections20.Stats.empty();
|
||||
for (var start : ruinsStarts) {
|
||||
var origin = start.getChunkPos(); var saved = start.createTag(context, origin); var digest = hash(saved.toString());
|
||||
var loaded = StructureStart.loadStaticStart(context, saved, level.getSeed());
|
||||
helper.assertTrue(loaded != null && loaded.createTag(context, origin).equals(saved), "Forgotten20 native start NBT changed");
|
||||
var expectedRooms = generator.ruinsPlan17().networks().stream().filter(n -> n.startChunkX() == origin.x() && n.startChunkZ() == origin.z()).flatMap(n -> n.rooms().stream()).toList();
|
||||
var actualRooms = start.getPieces().stream().map(RuinsPiece20.class::cast).map(RuinsPiece20::room).toList();
|
||||
helper.assertTrue(expectedRooms.size() == actualRooms.size() && new HashSet<>(expectedRooms).equals(new HashSet<>(actualRooms)), "Forgotten20 native rooms differ from plan");
|
||||
if (reload) {
|
||||
JsonObject prior = null;
|
||||
for (var value : previous.getAsJsonArray("ruins_starts")) { var item = value.getAsJsonObject(); var at = item.getAsJsonArray("start_chunk"); if (at.get(0).getAsInt() == origin.x() && at.get(1).getAsInt() == origin.z()) prior = item; }
|
||||
helper.assertTrue(prior != null && prior.get("native_start_sha256").getAsString().equals(digest), "Forgotten20 native start changed on a second JVM load");
|
||||
}
|
||||
for (var raw : start.getPieces()) {
|
||||
var piece = (RuinsPiece20) raw; var room = piece.room(); count++;
|
||||
var tag = piece.createTag(context); var roundtrip = new RuinsPiece20(context, tag);
|
||||
helper.assertTrue(roundtrip.room().equals(room) && roundtrip.createTag(context).equals(tag), "Forgotten20 piece lost saved parameters");
|
||||
piece.getBoundingBox().intersectingChunks().forEach(at -> helper.assertTrue(level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true).getReferencesForStructure(registry.value()).contains(origin.pack()), "Forgotten20 chunk lost native references"));
|
||||
var expected = rendered(room); blockCount += expected.blocks().size();
|
||||
for (var entry : expected.blocks().entrySet()) {
|
||||
helper.assertTrue(piece.getBoundingBox().isInside(entry.getKey()), "Forgotten20 renderer escaped saved bounds");
|
||||
helper.assertTrue(level.getBlockState(entry.getKey()).is(entry.getValue().getBlock()), "Forgotten20 native geometry changed at " + entry.getKey());
|
||||
}
|
||||
connections = connections.add(NativeConnections20.verify(helper, expected.blocks(), Set.of()));
|
||||
for (var support : room.supports()) assertRock(helper, new BlockPos(support.x(), support.minY() - 1, support.z()));
|
||||
for (var chest : expected.chests()) {
|
||||
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Forgotten20 missing deferred chest");
|
||||
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
|
||||
helper.assertTrue(chest.table().equals(entity.getLootTable()) && chest.seed() == entity.getLootTableSeed(), "Forgotten20 consumed/changed saved loot"); lootCount++;
|
||||
}
|
||||
}
|
||||
summaries.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", digest, "pieces", start.getPieces().size()));
|
||||
}
|
||||
report.put("ruins_starts", summaries); report.put("ruins_native_pieces", count); report.put("ruins_native_blocks_checked", blockCount);
|
||||
report.put("ruins_unopened_deferred_chests", lootCount); report.put("ruins_connections", connections);
|
||||
report.put("ruins_access", generator.ruinsPlan17().networks().stream().map(n -> verifyAccess(helper, n)).toList());
|
||||
}
|
||||
}
|
||||
private record Chest(BlockPos position, ResourceKey<LootTable> table, long seed) {}
|
||||
private record Rendered(Map<BlockPos, BlockState> blocks, List<Chest> chests) {}
|
||||
private static Rendered rendered(HeritagePiece21 piece) {
|
||||
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
|
||||
int x0 = piece.isSite() ? piece.site().minX() : piece.segment().minX();
|
||||
int y0 = piece.isSite() ? piece.site().floorY() : piece.segment().minY();
|
||||
int z0 = piece.isSite() ? piece.site().minZ() : piece.segment().minZ();
|
||||
var writer = new LostCityRenderer.BlockWriter() {
|
||||
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(new BlockPos(x0 + x, y0 + y, z0 + z), state); }
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
|
||||
var at = new BlockPos(x0 + x, y0 + y, z0 + z); blocks.put(at, Blocks.CHEST.defaultBlockState()); chests.add(new Chest(at, table, RandomSource.create(seed).nextLong()));
|
||||
}
|
||||
};
|
||||
if (piece.isSite()) HeritageRenderer21.render(piece.site(), writer); else HeritageRenderer21.renderSegment(piece.segment(), writer);
|
||||
return new Rendered(blocks, chests);
|
||||
}
|
||||
private static Rendered rendered(RuinsPlan17.Room room) {
|
||||
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
|
||||
RuinsRenderer20.render(room, new LostCityRenderer.BlockWriter() {
|
||||
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(at(room, x, y, z), state); }
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
|
||||
var position = at(room, x, y, z); blocks.put(position, Blocks.CHEST.defaultBlockState()); chests.add(new Chest(position, table, RandomSource.create(seed).nextLong()));
|
||||
}
|
||||
});
|
||||
return new Rendered(blocks, chests);
|
||||
}
|
||||
private static Map<String, Object> verifyAccess(GameTestHelper helper, RuinsPlan17.Network network) {
|
||||
var level = helper.getLevel(); var access = network.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS).findFirst().orElseThrow();
|
||||
var hatches = new HashMap<BlockPos, BlockState>();
|
||||
rendered(access).blocks().forEach((at, state) -> { if (state.is(Blocks.DARK_OAK_TRAPDOOR)) hatches.put(at, level.getBlockState(at)); });
|
||||
helper.assertTrue(hatches.size() == 1, "Each network must have one usable wooden surface hatch");
|
||||
hatches.forEach((at, state) -> {
|
||||
helper.assertTrue(state.is(Blocks.DARK_OAK_TRAPDOOR) && state.canSurvive(level, at), "Surface hatch is missing its native support");
|
||||
level.setBlock(at, state.setValue(BlockStateProperties.OPEN, true), 2);
|
||||
});
|
||||
try {
|
||||
Predicate<BlockPos> inside = pos -> network.rooms().stream().anyMatch(r -> r.contains(pos.getX(), pos.getY(), pos.getZ()));
|
||||
var start = RuinsPiece20.locator(access); var reached = walk(helper, start, inside);
|
||||
int targets = 0;
|
||||
for (var room : network.rooms()) for (var target : RuinsRenderer20.targets(room)) {
|
||||
var point = at(room, target.x(), target.y(), target.z());
|
||||
helper.assertTrue(reached.contains(point), "Native network " + network.id() + " cannot reach " + room.kind() + "/" + target.name() + " at " + point);
|
||||
targets++;
|
||||
}
|
||||
for (var hatch : hatches.keySet()) helper.assertTrue(reached.contains(hatch), "Surface approach cannot enter its ladder shaft");
|
||||
return Map.of("id", network.id(), "family", network.family().name(), "rooms", network.rooms().size(),
|
||||
"entrance", coords(start), "walkable_targets", targets, "standing_cells", reached.size());
|
||||
} finally { hatches.forEach((at, state) -> level.setBlock(at, state, 2)); }
|
||||
}
|
||||
private static BlockPos at(RuinsPlan17.Room room, int x, int y, int z) { return new BlockPos(room.minX() + x, room.minY() + y, room.minZ() + z); }
|
||||
private static boolean isOuterWall(HeritagePlan19.Site site, BlockPos pos) {
|
||||
return pos.getY() >= site.floorY() + 2 && pos.getY() <= site.floorY() + 3
|
||||
&& (pos.getX() == site.minX() + 2 || pos.getX() == site.maxX() - 2 || pos.getZ() == site.minZ() + 2 || pos.getZ() == site.maxZ() - 2)
|
||||
&& Math.abs(pos.getX() - site.entry().x()) + Math.abs(pos.getZ() - site.entry().z()) > 6;
|
||||
}
|
||||
private static Map<String, Object> verifyAccess(GameTestHelper helper, HeritagePlan19.Site site) {
|
||||
var level = helper.getLevel(); var expected = rendered(new HeritagePiece21(site)); var doors = new HashMap<BlockPos, BlockState>();
|
||||
expected.blocks().forEach((at, state) -> {
|
||||
if ((state.getBlock() instanceof DoorBlock || state.getBlock() instanceof TrapDoorBlock) && state.hasProperty(BlockStateProperties.OPEN)) doors.put(at, level.getBlockState(at));
|
||||
});
|
||||
doors.forEach((at, state) -> level.setBlock(at, state.setValue(BlockStateProperties.OPEN, true), 2));
|
||||
try {
|
||||
var entry = HeritagePiece21.locator(site);
|
||||
Predicate<BlockPos> inside = p -> p.equals(entry) || p.getX() >= site.minX() && p.getX() <= site.maxX()
|
||||
&& p.getZ() >= site.minZ() && p.getZ() <= site.maxZ() && p.getY() >= site.floorY() - 4 && p.getY() < site.maxY();
|
||||
var reached = walk(helper, entry, inside); int targets = 0;
|
||||
for (var target : HeritageRenderer21.targets(site)) {
|
||||
var at = new BlockPos(site.minX() + target.x(), site.floorY() + target.y(), site.minZ() + target.z());
|
||||
helper.assertTrue(reached.contains(at), "Site " + site.id() + " cannot reach " + target.name() + " at " + at); targets++;
|
||||
}
|
||||
return Map.of("id", site.id(), "kind", site.kind().name(), "entry", coords(entry), "standing_cells", reached.size(), "walkable_targets", targets);
|
||||
} finally { doors.forEach((at, state) -> level.setBlock(at, state, 2)); }
|
||||
}
|
||||
private static List<Map<String, Object>> verifyLinks(GameTestHelper helper, HeritagePlan19 plan) {
|
||||
helper.assertTrue(!plan.links().isEmpty(), "Reference island needs a real route between accepted destinations");
|
||||
var entries = new HashMap<String, HeritagePlan19.Entry>(); plan.sites().forEach(s -> entries.put(s.id(), s.entry())); plan.landmarks().forEach(s -> entries.put(s.id(), s.entry()));
|
||||
var cells = new HashMap<Long, HeritagePlan19.Cell>(); plan.segments().forEach(s -> s.cells().forEach(c -> cells.put(column(c.x(), c.z()), c)));
|
||||
var result = new ArrayList<Map<String, Object>>();
|
||||
for (var link : plan.links()) {
|
||||
var from = entries.get(link.fromId()); var to = entries.get(link.toId());
|
||||
helper.assertTrue(from != null && to != null, "Route points to an absent destination");
|
||||
var first = link.path().getFirst(); var last = link.path().getLast();
|
||||
helper.assertTrue(first.x() == from.x() && first.z() == from.z() && first.floorY() + 1 == from.y()
|
||||
&& last.x() == to.x() && last.z() == to.z() && last.floorY() + 1 == to.y(), "Route does not meet its real entrances");
|
||||
int present = 0, missing = 0;
|
||||
for (int i = 0; i < link.path().size(); i++) {
|
||||
var point = link.path().get(i); var at = new BlockPos(point.x(), point.floorY() + 1, point.z());
|
||||
helper.assertTrue(standable(helper.getLevel(), at), "Ballast route " + link.id() + " is blocked at " + at);
|
||||
if (i > 0) { var prior = link.path().get(i - 1); if (point.floorY() > prior.floorY()) helper.assertTrue(clearBody(helper.getLevel(), new BlockPos(prior.x(), prior.floorY() + 2, prior.z())), "Route slope cannot be climbed by a player"); }
|
||||
var cell = cells.get(column(point.x(), point.z()));
|
||||
if (cell != null && cell.rail() != HeritagePlan19.Rail.NONE) {
|
||||
var actual = helper.getLevel().getBlockState(at);
|
||||
if (cell.railPresent()) {
|
||||
helper.assertTrue(actual.is(Blocks.RAIL), "Saved existing rail disappeared");
|
||||
helper.assertTrue(actual.getValue(BlockStateProperties.RAIL_SHAPE).name().equals(cell.rail().name()), "Native rail update changed the intended slope or curve at " + at); present++;
|
||||
} else { helper.assertTrue(!actual.is(Blocks.RAIL), "An intentionally missing rail regenerated"); missing++; }
|
||||
}
|
||||
}
|
||||
int spanX = link.path().stream().mapToInt(HeritagePlan19.Point::x).max().orElseThrow() - link.path().stream().mapToInt(HeritagePlan19.Point::x).min().orElseThrow();
|
||||
int spanZ = link.path().stream().mapToInt(HeritagePlan19.Point::z).max().orElseThrow() - link.path().stream().mapToInt(HeritagePlan19.Point::z).min().orElseThrow();
|
||||
result.add(Map.of("id", link.id(), "from", link.fromId(), "to", link.toId(), "kind", link.kind().name(), "length", link.path().size(),
|
||||
"span_x", spanX, "span_z", spanZ, "existing_rails", present, "deliberately_missing_rails", missing, "path", pathCoords(link)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private static int routeSpan(HeritagePlan19.Link link) {
|
||||
int spanX = link.path().stream().mapToInt(HeritagePlan19.Point::x).max().orElseThrow()
|
||||
- link.path().stream().mapToInt(HeritagePlan19.Point::x).min().orElseThrow();
|
||||
int spanZ = link.path().stream().mapToInt(HeritagePlan19.Point::z).max().orElseThrow()
|
||||
- link.path().stream().mapToInt(HeritagePlan19.Point::z).min().orElseThrow();
|
||||
return Math.max(spanX, spanZ);
|
||||
}
|
||||
private static List<List<Integer>> pathCoords(HeritagePlan19.Link link) {
|
||||
return link.path().stream().map(point -> List.of(point.x(), point.floorY(), point.z())).toList();
|
||||
}
|
||||
private static int verifySiteBearing(GameTestHelper helper, SanctuaryChunkGenerator generator, HeritagePlan19 plan, HeritagePlan19.Site site) {
|
||||
int checked = 0;
|
||||
// The planner certifies a nine-point bearing grid, either directly below its slab or below a saved short footing.
|
||||
for (int dx : new int[] {1, 8, 15}) for (int dz : new int[] {1, 8, 15}) {
|
||||
int x = site.minX() + dx, z = site.minZ() + dz;
|
||||
var support = site.supports().stream().filter(s -> s.x() == x && s.z() == z).findFirst();
|
||||
int bottom = support.map(HeritagePlan19.Support::minY).orElse(site.minY());
|
||||
for (int below = 1; below <= 2; below++) {
|
||||
var at = new BlockPos(x, bottom - below, z); assertRock(helper, at);
|
||||
helper.assertTrue(plan.protects(at.getX(), at.getY(), at.getZ()) && generator.protectsCityDecoration(at),
|
||||
"Site's certified bearing layer is not published in the decoration mask: " + site.id() + " at " + at);
|
||||
helper.assertTrue(!plan.contains(at.getX(), at.getY(), at.getZ()), "Original bearing rock became a writable heritage volume: " + at);
|
||||
checked++;
|
||||
}
|
||||
}
|
||||
return checked;
|
||||
}
|
||||
private static List<Map<String, Object>> verifyReadOnlyLandings(GameTestHelper helper, SanctuaryChunkGenerator generator, HeritagePlan19 plan) {
|
||||
var used = new HashSet<String>(); plan.links().forEach(link -> { used.add(link.fromId()); used.add(link.toId()); });
|
||||
var result = new ArrayList<Map<String, Object>>();
|
||||
for (var landmark : plan.landmarks()) if (used.contains(landmark.id())) {
|
||||
var e = landmark.entry(); int checked = 0;
|
||||
for (int side = -1; side <= 1; side++) {
|
||||
int x = e.x() + ((e.facing() & 1) == 0 ? side : 0), z = e.z() + ((e.facing() & 1) == 1 ? side : 0);
|
||||
for (int y = e.y() - 2; y <= e.y() + 2; y++) {
|
||||
var at = new BlockPos(x, y, z);
|
||||
helper.assertTrue(plan.protects(x, y, z) && generator.protectsCityDecoration(at), "Read-only arrival lost its decoration mask: " + landmark.id() + " at " + at);
|
||||
helper.assertTrue(!plan.contains(x, y, z), "Heritage gained write ownership of a historical arrival: " + landmark.id() + " at " + at);
|
||||
if (y == e.y() - 1) assertRock(helper, at);
|
||||
if (y >= e.y()) helper.assertTrue(helper.getLevel().getBlockState(at).isAir(), "Decoration obstructed a certified historical arrival: " + at);
|
||||
checked++;
|
||||
}
|
||||
}
|
||||
result.add(Map.of("id", landmark.id(), "entry", List.of(e.x(), e.y(), e.z()), "read_only_cells", checked));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private static void assertRock(GameTestHelper helper, BlockPos at) {
|
||||
helper.assertTrue(helper.getLevel().getBlockState(at).isSolid() && helper.getLevel().getFluidState(at).isEmpty(), "Lost original rock bearing at " + at);
|
||||
}
|
||||
private static void verifyLootTables(GameTestHelper helper, Map<String, Object> report) {
|
||||
var result = new ArrayList<Map<String, Object>>(); var level = helper.getLevel();
|
||||
var params = new LootParams.Builder(level).withParameter(LootContextParams.ORIGIN, new Vec3(0, 160, 0)).create(LootContextParamSets.CHEST);
|
||||
for (var kind : HeritagePlan19.Kind.values()) {
|
||||
var table = level.getServer().reloadableRegistries().getLootTable(lootKey(kind));
|
||||
helper.assertTrue(table != LootTable.EMPTY, "Heritage tool table did not load: " + kind);
|
||||
for (long seed : new long[] {0, 42, -1}) helper.assertTrue(table.getRandomItems(params, seed).stream().anyMatch(stack -> stack.is(tool(kind))), "Tool table is empty or lacks its expected primary tool: " + kind);
|
||||
result.add(Map.of("kind", kind.name(), "table", lootKey(kind).identifier().toString(), "sampled_seeds", List.of(0, 42, -1)));
|
||||
}
|
||||
report.put("tool_tables_sampled_separately", result);
|
||||
}
|
||||
private static ResourceKey<LootTable> lootKey(HeritagePlan19.Kind kind) { return ResourceKey.create(Registries.LOOT_TABLE, SanctuaryMod.id("chests/heritage19/" + kind.name().toLowerCase(Locale.ROOT))); }
|
||||
private static Item tool(HeritagePlan19.Kind kind) {
|
||||
return switch (kind) {
|
||||
case OBSERVATORY -> Items.SPYGLASS;
|
||||
case POST_OFFICE, EXPEDITION_OFFICE, RADIO_RELAY -> Items.COMPASS;
|
||||
case SUBSTATION, IMPOSSIBLE_SERVICE -> Items.CLOCK;
|
||||
case FREIGHT_STATION -> Items.MINECART;
|
||||
case COMMUNAL_DEPOT -> Items.IRON_AXE;
|
||||
case RAIL_WORKSHOP -> Items.IRON_PICKAXE;
|
||||
case PUMP_STATION -> Items.BUCKET;
|
||||
case GREENHOUSE -> Items.IRON_HOE;
|
||||
case VOID_DOCK -> Items.LEAD;
|
||||
case PHOTO_CLUB -> Items.BOOK;
|
||||
case CONSTRUCTION_REFUGE -> Items.IRON_SHOVEL;
|
||||
};
|
||||
}
|
||||
private static Set<BlockPos> walk(GameTestHelper helper, BlockPos start, Predicate<BlockPos> inside) {
|
||||
var level = helper.getLevel(); helper.assertTrue(inside.test(start) && standable(level, start), "Blocked or unsupported exterior entrance: " + start);
|
||||
var seen = new HashSet<BlockPos>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
|
||||
while (!queue.isEmpty()) {
|
||||
var pos = queue.removeFirst();
|
||||
for (var direction : HORIZONTAL) for (int dy = -1; dy <= 1; dy++) {
|
||||
var next = pos.relative(direction).offset(0, dy, 0);
|
||||
if (!inside.test(next) || dy > 0 && !clearBody(level, pos.above())) continue;
|
||||
if (standable(level, next) && seen.add(next)) queue.add(next);
|
||||
}
|
||||
for (int dy : new int[] {-1, 1}) {
|
||||
var next = pos.offset(0, dy, 0); if (!inside.test(next)) continue;
|
||||
if ((level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(next).is(Blocks.LADDER) || level.getBlockState(pos.below()).is(Blocks.LADDER))
|
||||
&& standable(level, next) && seen.add(next)) queue.add(next);
|
||||
}
|
||||
helper.assertTrue(seen.size() <= 50_000, "Native site traversal escaped its bounds");
|
||||
}
|
||||
return seen;
|
||||
}
|
||||
private static boolean standable(ServerLevel level, BlockPos pos) {
|
||||
if (!clearBody(level, pos)) return false;
|
||||
if (level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(pos.below()).is(Blocks.LADDER)) return true;
|
||||
var below = pos.below(); return level.getBlockState(below).getCollisionShape(level, below).toAabbs().stream()
|
||||
.anyMatch(b -> b.maxY >= .5 && b.minX <= .5 && b.maxX >= .5 && b.minZ <= .5 && b.maxZ >= .5);
|
||||
}
|
||||
private static boolean clearBody(ServerLevel level, BlockPos pos) {
|
||||
var body = new AABB(pos.getX() + .2, pos.getY(), pos.getZ() + .2, pos.getX() + .8, pos.getY() + 1.8, pos.getZ() + .8);
|
||||
for (var at : new BlockPos[] {pos, pos.above()}) {
|
||||
if (!level.getFluidState(at).isEmpty()) return false;
|
||||
for (var shape : level.getBlockState(at).getCollisionShape(level, at).toAabbs()) if (shape.move(at.getX(), at.getY(), at.getZ()).intersects(body)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private static long column(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
|
||||
private static List<Integer> coords(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
|
||||
private static BlockPos readPos(com.google.gson.JsonArray value) { return new BlockPos(value.get(0).getAsInt(), value.get(1).getAsInt(), value.get(2).getAsInt()); }
|
||||
private static Path world(ServerLevel level) { return level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize(); }
|
||||
private static Path proof(ServerLevel level) { return world(level).resolve("heritage21-create-proof.json"); }
|
||||
private static String planHash(HeritagePlan19 plan) { return hash(plan.sites().toString() + plan.landmarks() + plan.links() + plan.segments()); }
|
||||
private static void write(ServerLevel level, Map<String, Object> report, boolean reload) throws IOException {
|
||||
var path = Path.of("diagnostics", "heritage21-" + (reload ? "reload" : "create") + ".json"); Files.createDirectories(path.getParent());
|
||||
var value = new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n"; Files.writeString(path, value); if (!reload) Files.writeString(proof(level), value);
|
||||
}
|
||||
private static String hash(String value) {
|
||||
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8))); }
|
||||
catch (NoSuchAlgorithmException impossible) { throw new AssertionError(impossible); }
|
||||
}
|
||||
}
|
||||
+1034
File diff suppressed because it is too large
Load Diff
+1083
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);
|
||||
}
|
||||
|
||||
}
|
||||
+24
-2
@@ -210,8 +210,30 @@ public final class PopulationDiagnostics {
|
||||
}
|
||||
|
||||
void prepare() throws IOException {
|
||||
helper.assertTrue(regionsAtInitialSpawn > 0 && regionsAtInitialSpawn <= 4,
|
||||
"Initial spawn must plan only nearby regions, not materialize the large island: " + regionsAtInitialSpawn);
|
||||
boolean heritage = level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator sanctuary
|
||||
&& (sanctuary.rootGeneration() == 22 || (sanctuary.rootGeneration() == 19 || sanctuary.rootGeneration() == 20 || sanctuary.rootGeneration() == 21) && sanctuary.experimentalStructures());
|
||||
// Generation19 explicitly prospects destinations across the root island before publishing them.
|
||||
// A consulted hydrology region is a bounded immutable plan, not a loaded/generated chunk.
|
||||
// Historical generators retain their original near-spawn contract.
|
||||
int regionLimit = 4;
|
||||
if (heritage) {
|
||||
int extent = capacity.terrainLimit();
|
||||
var first = capacity.regionAt(-extent, -extent); var last = capacity.regionAt(extent, extent);
|
||||
regionLimit = 0;
|
||||
for (int x = first.x(); x <= last.x(); x++) for (int z = first.z(); z <= last.z(); z++) {
|
||||
var region = new IslandCapacity.Region(x, z);
|
||||
double nearestX = Math.max(0, Math.max(region.minX(), -region.maxX()));
|
||||
double nearestZ = Math.max(0, Math.max(region.minZ(), -region.maxZ()));
|
||||
if (Math.hypot(nearestX, nearestZ) < extent) regionLimit++;
|
||||
}
|
||||
helper.assertTrue(regionLimit <= 9, "Heritage starter hydrology must fit within nine local regions");
|
||||
var plan = ((SanctuaryChunkGenerator) level.getChunkSource().getGenerator()).heritagePlan19();
|
||||
helper.assertTrue(plan.columns() <= fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.MAX_COLUMNS
|
||||
&& plan.samples() <= fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.MAX_SAMPLES,
|
||||
"Island-wide heritage prospecting exceeded its independent column/probe budget");
|
||||
}
|
||||
helper.assertTrue(regionsAtInitialSpawn > 0 && regionsAtInitialSpawn <= regionLimit,
|
||||
"Initial hydrology exceeded its geographic planning scope (limit " + regionLimit + "): " + regionsAtInitialSpawn);
|
||||
List<IslandCapacity.Region> targets = new ArrayList<>();
|
||||
targets.add(new IslandCapacity.Region(0,0));
|
||||
if (capacity.players() == 20) targets.add(new IslandCapacity.Region(1,0));
|
||||
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||
import fr.koka.sanctuary.worldgen.PopulationGenerationContext;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.city.*;
|
||||
import fr.koka.sanctuary.worldgen.heritage.*;
|
||||
import fr.koka.sanctuary.worldgen.transit.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
|
||||
/** Read-only admission audit, then native starts and reload in the disposable development world. */
|
||||
public final class Selective22WorldGameTests {
|
||||
@GameTest(maxTicks = 12000)
|
||||
public void naturalSitesAndRefusalsPersist(GameTestHelper helper) throws Exception {
|
||||
var level = helper.getLevel();
|
||||
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
|
||||
helper.assertTrue(generator.rootGeneration() == 22, "Use a fresh22 development world");
|
||||
var root = ExpansionIsland.origin(generator.initialDiameter(), level.getSeed(), 22);
|
||||
var city = generator.cityPlacement21(root);
|
||||
var heritage = generator.heritagePlan19();
|
||||
var transit = generator.transitPlan21();
|
||||
helper.assertTrue(city != null && city.reservation() == null && city.supports().isEmpty(), "No forced city envelope or rescue piers");
|
||||
var binding = Objects.requireNonNull(PopulationGenerationContext.of(level));
|
||||
var terrain = new HeritageTerrain21(root, binding.generator(), binding.random(), level, p -> false);
|
||||
int columns = 0;
|
||||
for (var district : city.surface().districts()) for (var parcel : district.parcels()) {
|
||||
for (int x = parcel.minX(); x <= parcel.maxX(); x++) for (int z = parcel.minZ(); z <= parcel.maxZ(); z++) {
|
||||
int floor = parcel.surfaceY(x - parcel.minX(), z - parcel.minZ());
|
||||
helper.assertTrue(Math.abs(floor - terrain.surfaceY(x, z)) <= 2, "City must fit natural relief"); columns++;
|
||||
}
|
||||
}
|
||||
// The three samplers have independent bounded caches, just like production planning.
|
||||
terrain = new HeritageTerrain21(root, binding.generator(), binding.random(), level, p -> false);
|
||||
for (var site : heritage.sites()) {
|
||||
helper.assertTrue(site.supports().isEmpty(), "No heritage rescue piers");
|
||||
for (int x = site.minX(); x <= site.maxX(); x++) for (int z = site.minZ(); z <= site.maxZ(); z++) {
|
||||
helper.assertTrue(Math.abs(site.floorY() - terrain.surfaceY(x, z)) <= 2, "Building cut/fill stays within two blocks");
|
||||
helper.assertTrue(terrain.solid(x, site.minY() - 1, z) && terrain.solid(x, site.minY() - 2, z), "Every foundation column rests on original rock"); columns++;
|
||||
}
|
||||
}
|
||||
helper.assertTrue(!transit.isEmpty(), "The reference island retains its core traversal");
|
||||
var hall = generator.undergroundHall22();
|
||||
helper.assertTrue(!hall.isEmpty() || generator.undergroundPlan16(root).networks().stream().flatMap(n -> n.modules().stream())
|
||||
.anyMatch(m -> m.kind() == LostCityUnderground16.Kind.EXPANSION_HALL), "Core hall exists with or without a surface town");
|
||||
for (var module : transit.modules()) for (var support : module.supports())
|
||||
helper.assertTrue(support.maxY() - support.minY() + 1 <= 16, "Core traversal retains bounded engineered supports");
|
||||
if (!generator.experimentalStructures()) helper.assertTrue(heritage.isEmpty() && generator.ruinsPlan17().isEmpty(), "OFF removes optional catalogue, not core hall and traversal");
|
||||
var gson = new GsonBuilder().setPrettyPrinting().create();
|
||||
var encoded = ChunkGenerator.CODEC.encodeStart(RegistryOps.create(JsonOps.INSTANCE, level.registryAccess()), generator).getOrThrow();
|
||||
var geometry = gson.toJsonTree(Map.of("city", city.surface().districts(), "heritage_sites", heritage.sites(),
|
||||
"heritage_links", heritage.links(), "heritage_segments", heritage.segments(), "transit", transit.modules(), "independent_hall", hall.networks()));
|
||||
var report = new LinkedHashMap<String, Object>();
|
||||
report.put("seed", level.getSeed()); report.put("size", generator.initialDiameter());
|
||||
report.put("process_id", ProcessHandle.current().pid()); report.put("generator_codec", encoded); report.put("geometry", geometry);
|
||||
report.put("experimental", generator.experimentalStructures()); report.put("city_districts", city.surface().districts().size());
|
||||
report.put("heritage_sites", heritage.sites().size()); report.put("heritage_links", heritage.links().size()); report.put("transit_modules", transit.modules().size());
|
||||
report.put("independent_hall_networks", hall.networks().size());
|
||||
report.put("core_entries", transit.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.ACCESS).map(m -> List.of(m.centerX(), m.surfaceY() + 1, m.minZ() - 2)).toList());
|
||||
report.put("hall_entries", hall.networks().stream().map(n -> { var at = LostCityUndergroundStructures22.locator(n); return List.of(at.getX(), at.getY(), at.getZ()); }).toList());
|
||||
report.put("natural_columns_audited", columns); report.put("city_diagnostics", city.diagnostics());
|
||||
report.put("heritage_diagnostics", heritage.diagnostics()); report.put("transit_diagnostics", transit.diagnostic());
|
||||
var starts = new ArrayList<StructureStart>();
|
||||
var origins = new LinkedHashMap<ResourceKey<Structure>, Set<ChunkPos>>();
|
||||
origins.put(LostCityStructures.KEY, new HashSet<>()); origins.put(HeritageStructures19.KEY, new HashSet<>()); origins.put(TransitStructures21.KEY, new HashSet<>()); origins.put(LostCityUndergroundStructures22.KEY, new HashSet<>());
|
||||
city.surface().districts().forEach(d -> origins.get(LostCityStructures.KEY).add(new ChunkPos(d.startChunkX(), d.startChunkZ())));
|
||||
heritage.sites().forEach(s -> origins.get(HeritageStructures19.KEY).add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
|
||||
heritage.segments().forEach(s -> origins.get(HeritageStructures19.KEY).add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
|
||||
transit.modules().forEach(m -> origins.get(TransitStructures21.KEY).add(new ChunkPos(m.startChunkX(), m.startChunkZ())));
|
||||
hall.networks().forEach(n -> origins.get(LostCityUndergroundStructures22.KEY).add(new ChunkPos(n.startChunkX(), n.startChunkZ())));
|
||||
var chunks = new TreeSet<ChunkPos>(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z));
|
||||
var nativeTags = new TreeMap<String, String>();
|
||||
var context = StructurePieceSerializationContext.fromLevel(level);
|
||||
for (var entry : origins.entrySet()) for (var origin : entry.getValue()) {
|
||||
var type = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(entry.getKey()).value();
|
||||
var start = level.getChunk(origin.x(), origin.z(), ChunkStatus.STRUCTURE_STARTS, true).getStartForStructure(type);
|
||||
helper.assertTrue(start != null && start.isValid(), "Accepted geometry must have a native start");
|
||||
for (var piece : start.getPieces()) {
|
||||
helper.assertTrue(!(piece instanceof LostCityFoundationPiece16), "No artificial city foundation emitted");
|
||||
if (entry.getKey().equals(HeritageStructures19.KEY)) helper.assertTrue(piece instanceof HeritagePiece21, "Use published21 architecture");
|
||||
if (entry.getKey().equals(LostCityUndergroundStructures22.KEY)) helper.assertTrue(piece instanceof LostCityUndergroundPiece20, "Independent hall keeps published20 room geometry");
|
||||
if (entry.getKey().equals(TransitStructures21.KEY)) helper.assertTrue(piece instanceof TransitPiece21 || piece instanceof TransitPiece22, "Published21 tunnel geometry with sealed22 corners");
|
||||
piece.getBoundingBox().intersectingChunks().forEach(chunks::add);
|
||||
}
|
||||
var tag = start.createTag(context, origin);
|
||||
var loaded = StructureStart.loadStaticStart(context, tag, level.getSeed());
|
||||
helper.assertTrue(loaded != null && tag.equals(loaded.createTag(context, origin)), "Native start round trip");
|
||||
nativeTags.put(entry.getKey().identifier() + "/" + origin.x() + "/" + origin.z(), tag.toString()); starts.add(start);
|
||||
}
|
||||
if (chunks.isEmpty()) chunks.add(new ChunkPos(0, 0));
|
||||
report.put("native_starts", nativeTags); report.put("full_chunks", chunks.size());
|
||||
Path world = level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize();
|
||||
Path proof = world.resolve("selective22-create.json");
|
||||
boolean reload = Boolean.getBoolean("sanctuary.test.expansionReload");
|
||||
JsonObject previous = reload ? JsonParser.parseString(Files.readString(proof)).getAsJsonObject() : null;
|
||||
if (reload) {
|
||||
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload follows a successful separate JVM");
|
||||
helper.assertTrue(previous.get("generator_codec").equals(encoded) && previous.get("geometry").equals(geometry), "Generation, options and accepted/refused placements survive reload");
|
||||
helper.assertTrue(previous.get("native_starts").equals(gson.toJsonTree(nativeTags)), "Native saved starts survive reload");
|
||||
}
|
||||
var iterator = chunks.iterator();
|
||||
Runnable finish = () -> {
|
||||
try {
|
||||
// Refresh already-generated transit chunks: the earlier short FULL tickets can expire
|
||||
// while the catalogue loads. The walk checks real native collision shapes, including turns.
|
||||
var transitChunks = new HashSet<ChunkPos>();
|
||||
transit.modules().forEach(m -> TransitPiece21.bounds(m).intersectingChunks().forEach(transitChunks::add));
|
||||
for (var pos : transitChunks) level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true);
|
||||
report.put("transit_native_walk", Transit21WorldGameTests.verifyAccess(helper, transit));
|
||||
report.put("hall_native_access", Underground22NativeChecks.verify(helper, hall.isEmpty() ? generator.undergroundPlan16(root) : hall));
|
||||
if (!heritage.sites().isEmpty()) {
|
||||
var site = heritage.sites().getFirst();
|
||||
var witness = new BlockPos(site.centerX(), site.floorY() - 1, site.centerZ());
|
||||
if (reload) helper.assertTrue(level.getBlockState(witness).is(Blocks.GOLD_BLOCK), "Personal edit in disposable test survives reload");
|
||||
else {
|
||||
helper.assertTrue(level.getBlockState(witness).isSolid(), "Accepted building foundation generated in real FULL chunks");
|
||||
level.setBlock(witness, Blocks.GOLD_BLOCK.defaultBlockState(), 3);
|
||||
}
|
||||
report.put("saved_edit", List.of(witness.getX(), witness.getY(), witness.getZ()));
|
||||
}
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush native world before shutdown");
|
||||
report.put("passed", true);
|
||||
var directory = Path.of("diagnostics", "selective22-" + level.getSeed() + "-" + generator.initialDiameter()); Files.createDirectories(directory);
|
||||
var text = gson.toJson(report) + "\n"; Files.writeString(directory.resolve(reload ? "reload.json" : "create.json"), text);
|
||||
if (!reload) Files.writeString(proof, text);
|
||||
helper.succeed();
|
||||
} catch (Exception failure) { throw new IllegalStateException(failure); }
|
||||
};
|
||||
Runnable[] load = new Runnable[1];
|
||||
load[0] = () -> {
|
||||
if (!iterator.hasNext()) { finish.run(); return; }
|
||||
var pos = iterator.next(); level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true);
|
||||
helper.runAtTickTime(helper.getTick() + 1, () -> load[0].run());
|
||||
};
|
||||
load[0].run();
|
||||
}
|
||||
}
|
||||
+453
@@ -0,0 +1,453 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import fr.koka.sanctuary.worldgen.transit.TransitPiece21;
|
||||
import fr.koka.sanctuary.worldgen.transit.TransitPlan21;
|
||||
import fr.koka.sanctuary.worldgen.transit.TransitRenderer21;
|
||||
import fr.koka.sanctuary.worldgen.transit.TransitStructures21;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.StairBlock;
|
||||
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
|
||||
/** Opt-in native proof confined to the new generation21 starter island in a disposable development world. */
|
||||
public final class Transit21WorldGameTests {
|
||||
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
|
||||
|
||||
@GameTest(maxTicks = 12000)
|
||||
public void industrialTransitPersistsNatively(GameTestHelper helper) {
|
||||
helper.assertTrue(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator,
|
||||
"The industrial-transits proof requires a Sanctuary generator");
|
||||
var generator = (SanctuaryChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
helper.assertTrue(generator.rootGeneration() == 21, "Use a fresh generation21 test world; do not convert old saves");
|
||||
new Scenario(helper, generator, Boolean.getBoolean("sanctuary.test.expansionReload")).advance();
|
||||
}
|
||||
|
||||
private static final class Scenario {
|
||||
final GameTestHelper helper;
|
||||
final ServerLevel level;
|
||||
final SanctuaryChunkGenerator generator;
|
||||
final TransitPlan21 plan;
|
||||
final boolean reload;
|
||||
final Map<String, Object> report = new LinkedHashMap<>();
|
||||
final List<ChunkPos> chunks = new ArrayList<>();
|
||||
final List<StructureStart> starts = new ArrayList<>();
|
||||
final Map<BlockPos, BlockState> allGeometry = new HashMap<>();
|
||||
JsonObject previous;
|
||||
BlockPos gold;
|
||||
int nextChunk;
|
||||
boolean prepared;
|
||||
|
||||
Scenario(GameTestHelper helper, SanctuaryChunkGenerator generator, boolean reload) {
|
||||
this.helper = helper; this.level = helper.getLevel(); this.generator = generator; this.reload = reload;
|
||||
this.plan = generator.transitPlan21();
|
||||
report.put("seed", level.getSeed()); report.put("size", generator.initialDiameter());
|
||||
report.put("process_id", ProcessHandle.current().pid()); report.put("world_directory", world(level).toString());
|
||||
report.put("experimental_structures", generator.experimentalStructures());
|
||||
report.put("structures_enabled", level.structureManager().shouldGenerateStructures());
|
||||
report.put("planning_diagnostic", plan.diagnostic());
|
||||
report.put("scope", "Native starts, references, geometry, rock bearings, unopened loot and a player-sized stair walk between two surface gates. "
|
||||
+ "No expansion is created and no personal save is opened. A single outer-wall gold edit in this disposable world "
|
||||
+ "is saved and checked in a second JVM. Traversal remains inside the modules and certified portico approaches; underground one-block rises need real correctly oriented stairs. Only the certified natural portico approaches permit an ordinary one-block jump. No flight, ladder or external terrain shortcut is allowed. No fluid simulation is claimed.");
|
||||
}
|
||||
void advance() {
|
||||
try {
|
||||
if (!prepared) {
|
||||
prepare(); prepared = true;
|
||||
if (plan.isEmpty()) { finishDisabled(); return; }
|
||||
}
|
||||
if (nextChunk < chunks.size()) {
|
||||
var pos = chunks.get(nextChunk++);
|
||||
var loaded = level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true);
|
||||
helper.assertTrue(loaded instanceof LevelChunk, "Every room footprint must reach native FULL: " + pos);
|
||||
if (nextChunk % 10 == 0) SanctuaryMod.LOGGER.info("Industrial transit21: loaded {}/{} footprint chunks", nextChunk, chunks.size());
|
||||
helper.runAtTickTime(helper.getTick() + 1, this::advance); return;
|
||||
}
|
||||
verifyPlaced();
|
||||
report.put("passed", true);
|
||||
if (!reload) {
|
||||
helper.assertTrue(gold != null && level.getBlockState(gold).isCollisionShapeFullBlock(level, gold),
|
||||
"Gameplay witness must replace a solid outer wall away from every doorway");
|
||||
level.setBlock(gold, Blocks.GOLD_BLOCK.defaultBlockState(), 3);
|
||||
report.put("gold_witness", coords(gold));
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush native structures, deferred loot and the gold edit");
|
||||
report.put("world_flushed_before_shutdown", true);
|
||||
} else {
|
||||
helper.assertTrue(level.getBlockState(gold).is(Blocks.GOLD_BLOCK), "Reload replaced the existing wall edit");
|
||||
report.put("gold_witness", coords(gold)); report.put("gold_witness_preserved", true);
|
||||
}
|
||||
write(level, report, reload); helper.succeed();
|
||||
} catch (Exception failure) {
|
||||
report.put("passed", false); report.put("failure", failure.toString());
|
||||
try { write(level, report, reload); } catch (IOException ignored) { }
|
||||
throw new IllegalStateException("Industrial transit21 native " + (reload ? "reload" : "creation") + " failed", failure);
|
||||
}
|
||||
}
|
||||
void prepare() throws IOException {
|
||||
var ops = RegistryOps.create(JsonOps.INSTANCE, level.registryAccess());
|
||||
var encoded = ChunkGenerator.CODEC.encodeStart(ops, generator).getOrThrow();
|
||||
var decoded = ChunkGenerator.CODEC.parse(ops, encoded).getOrThrow();
|
||||
helper.assertTrue(decoded instanceof SanctuaryChunkGenerator copy && copy.rootGeneration() == 21
|
||||
&& copy.experimentalStructures() == generator.experimentalStructures(), "The generation21 codec must preserve its experimental switch");
|
||||
report.put("generator_codec", encoded); report.put("plan_sha256", planHash(plan)); report.put("planning_samples", plan.samples());
|
||||
helper.assertTrue(plan.samples() <= TransitPlan21.MAX_SAMPLES, "Transit planning exceeded its budget");
|
||||
if (reload) {
|
||||
previous = JsonParser.parseString(Files.readString(proof(level))).getAsJsonObject();
|
||||
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("world_flushed_before_shutdown").getAsBoolean(),
|
||||
"Reload requires the successful, flushed creation proof");
|
||||
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload must run in a separate JVM");
|
||||
helper.assertTrue(previous.get("seed").getAsLong() == level.getSeed()
|
||||
&& previous.get("size").getAsInt() == generator.initialDiameter()
|
||||
&& previous.get("world_directory").getAsString().equals(world(level).toString()), "Reload used another world, seed or size");
|
||||
helper.assertTrue(previous.get("generator_codec").equals(encoded) && previous.get("plan_sha256").getAsString().equals(planHash(plan)),
|
||||
"Saved switch or planned generation21 room identities changed on reload");
|
||||
if (previous.has("gold_witness")) gold = readPos(previous.getAsJsonArray("gold_witness"));
|
||||
}
|
||||
boolean enabled = generator.experimentalStructures() && level.structureManager().shouldGenerateStructures();
|
||||
if (!enabled) {
|
||||
helper.assertTrue(plan.isEmpty(), "Experimental=false or generateStructures=false must publish no industrial transit");
|
||||
return;
|
||||
}
|
||||
helper.assertTrue(!plan.isEmpty(), "Enabled reference island has no experimental network");
|
||||
helper.assertTrue(plan.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.ACCESS).count() == 2,
|
||||
"A through-route needs two independent saved surface stairs");
|
||||
int spanX = plan.modules().stream().mapToInt(TransitPlan21.Module::maxX).max().orElseThrow()
|
||||
- plan.modules().stream().mapToInt(TransitPlan21.Module::minX).min().orElseThrow() + 1;
|
||||
int spanZ = plan.modules().stream().mapToInt(TransitPlan21.Module::maxZ).max().orElseThrow()
|
||||
- plan.modules().stream().mapToInt(TransitPlan21.Module::minZ).min().orElseThrow() + 1;
|
||||
report.put("span_x", spanX); report.put("span_z", spanZ);
|
||||
helper.assertTrue(Math.max(spanX, spanZ) >= generator.initialDiameter() * .4,
|
||||
"The native transit reference must cross at least40% of the island's nominal diameter");
|
||||
report.put("modules", plan.modules().size());
|
||||
report.put("halls", plan.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.HALL).count());
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(TransitStructures21.KEY);
|
||||
var origins = new HashSet<Long>(); var uniqueChunks = new HashSet<ChunkPos>();
|
||||
for (var network : plan.modules()) {
|
||||
long origin = new ChunkPos(network.startChunkX(), network.startChunkZ()).pack();
|
||||
if (!origins.add(origin)) continue;
|
||||
var chunk = level.getChunk(network.startChunkX(), network.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
var start = chunk.getStartForStructure(structure.value());
|
||||
helper.assertTrue(start != null && start.isValid(), "Accepted experimental network has no native start: " + network.kind() + " at " + network.centerX() + "," + network.centerZ());
|
||||
starts.add(start);
|
||||
for (var piece : start.getPieces()) {
|
||||
helper.assertTrue(piece instanceof TransitPiece21, "Industrial transit loaded a foreign or historical piece type");
|
||||
piece.getBoundingBox().intersectingChunks().forEach(at -> {
|
||||
helper.assertTrue(Math.abs(at.x() - network.startChunkX()) <= 7 && Math.abs(at.z() - network.startChunkZ()) <= 7,
|
||||
"Transit native start exceeds its local reference range");
|
||||
uniqueChunks.add(at);
|
||||
});
|
||||
}
|
||||
}
|
||||
helper.assertTrue(uniqueChunks.size() <= 256, "The bounded root-only proof unexpectedly covers more than256 chunks");
|
||||
chunks.addAll(uniqueChunks.stream().sorted(java.util.Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList());
|
||||
}
|
||||
void finishDisabled() throws IOException {
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(TransitStructures21.KEY);
|
||||
var chunk = level.getChunk(0, 0, ChunkStatus.STRUCTURE_STARTS, true);
|
||||
var start = chunk.getStartForStructure(structure.value());
|
||||
helper.assertTrue(start == null || !start.isValid(), "Disabled experimental generation emitted a native start");
|
||||
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
|
||||
helper.assertTrue(located == null, "Disabled experimental structures must not be located or prepared");
|
||||
report.put("empty_plan_verified", true); report.put("passed", true);
|
||||
if (!reload) {
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save the disabled experiment setting");
|
||||
report.put("world_flushed_before_shutdown", true);
|
||||
}
|
||||
write(level, report, reload); helper.succeed();
|
||||
}
|
||||
void verifyPlaced() {
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(TransitStructures21.KEY);
|
||||
var context = StructurePieceSerializationContext.fromLevel(level);
|
||||
var sites = new ArrayList<Map<String, Object>>();
|
||||
var modules = new ArrayList<Map<String, Object>>();
|
||||
int blocks = 0, supports = 0, chests = 0, pieces = 0;
|
||||
var bearingMasks = new HashMap<TransitPlan21.Module, Integer>();
|
||||
for (var start : starts) {
|
||||
var origin = start.getChunkPos(); var saved = start.createTag(context, origin);
|
||||
var copy = StructureStart.loadStaticStart(context, saved, level.getSeed());
|
||||
helper.assertTrue(copy != null && copy.isValid() && copy.createTag(context, origin).equals(saved), "The native start changed on an NBT roundtrip");
|
||||
var expectedRooms = plan.modulesForChunk(origin.x(), origin.z());
|
||||
var actualRooms = start.getPieces().stream().map(TransitPiece21.class::cast).map(TransitPiece21::module).toList();
|
||||
helper.assertTrue(actualRooms.size() == expectedRooms.size() && new HashSet<>(actualRooms).equals(new HashSet<>(expectedRooms)),
|
||||
"Native start did not persist every planned room exactly once");
|
||||
String nativeHash = hash(saved.toString());
|
||||
if (reload) {
|
||||
JsonObject prior = null;
|
||||
for (var value : previous.getAsJsonArray("starts")) {
|
||||
var site = value.getAsJsonObject(); var at = site.getAsJsonArray("start_chunk");
|
||||
if (at.get(0).getAsInt() == origin.x() && at.get(1).getAsInt() == origin.z()) prior = site;
|
||||
}
|
||||
helper.assertTrue(prior != null && prior.get("native_start_sha256").getAsString().equals(nativeHash),
|
||||
"Reload altered native room parameters, supports or reference count");
|
||||
}
|
||||
var loot = new ArrayList<Map<String, Object>>();
|
||||
for (var piece : start.getPieces()) {
|
||||
var roomPiece = (TransitPiece21) piece; var room = roomPiece.module(); pieces++;
|
||||
piece.getBoundingBox().intersectingChunks().forEach(at -> {
|
||||
var full = level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true);
|
||||
helper.assertTrue(full.getReferencesForStructure(structure.value()).contains(origin.pack()), "A saved FULL chunk lost the native structure reference");
|
||||
});
|
||||
var roomTag = roomPiece.createTag(context);
|
||||
var roundtrip = new TransitPiece21(context, roomTag);
|
||||
helper.assertTrue(roundtrip.module().equals(room) && roundtrip.createTag(context).equals(roomTag), "The native room failed its exact saved-field roundtrip");
|
||||
var corrupt = roomTag.copy(); var payload = corrupt.getCompound("SanctuaryModule").orElseThrow(); payload.putInt("Schema", 99);
|
||||
boolean rejected = false;
|
||||
try { new TransitPiece21(context, corrupt); } catch (IllegalArgumentException expected) { rejected = true; }
|
||||
helper.assertTrue(rejected, "An unknown generation21 piece schema must be refused");
|
||||
var expected = rendered(room); blocks += expected.blocks().size();
|
||||
expected.blocks().forEach((at, state) -> helper.assertTrue(allGeometry.putIfAbsent(at, state) == null,
|
||||
"Two saved transit modules paint the same position: " + at));
|
||||
modules.add(Map.of("kind", room.kind().name(), "min", List.of(room.minX(), room.minY(), room.minZ()),
|
||||
"max", List.of(room.maxX(), room.maxY(), room.maxZ()), "floor_y", room.floorY(),
|
||||
"surface_y", room.surfaceY(), "openings", room.openings(), "supports", room.supports().size(),
|
||||
"start_chunk", List.of(room.startChunkX(), room.startChunkZ())));
|
||||
for (var entry : expected.blocks().entrySet()) {
|
||||
var at = entry.getKey(); var wanted = entry.getValue();
|
||||
helper.assertTrue(TransitPiece21.bounds(room).isInside(at), "Renderer escaped its saved room/support bounds");
|
||||
helper.assertTrue(plan.protects(at.getX(), at.getY(), at.getZ()) && generator.protectsCityDecoration(at),
|
||||
"Transit construction/cleared air is missing its published decoration protection at " + at);
|
||||
if (reload && at.equals(gold)) continue;
|
||||
helper.assertTrue(level.getBlockState(at).is(wanted.getBlock()),
|
||||
"Native clipping or later decoration changed " + room.kind() + " at " + at + ": expected " + wanted + " got " + level.getBlockState(at));
|
||||
helper.assertTrue(!level.getBlockState(at).is(Blocks.SPAWNER), "Experimental architecture must not create a spawner");
|
||||
}
|
||||
supports += verifyBearings(helper, generator, room, bearingMasks);
|
||||
for (var entry : expected.blocks().entrySet()) if (entry.getValue().is(Blocks.LANTERN)) {
|
||||
var at = entry.getKey(); var state = level.getBlockState(at);
|
||||
helper.assertTrue(state.canSurvive(level, at) && level.getBlockState(at.above()).isFaceSturdy(level, at.above(), Direction.DOWN),
|
||||
"Transit lamp lost its complete ceiling bearing at " + at);
|
||||
}
|
||||
for (var chest : expected.chests()) {
|
||||
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Deferred native chest block entity is missing");
|
||||
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
|
||||
helper.assertTrue(chest.table().equals(entity.getLootTable()) && entity.getLootTableSeed() == chest.seed(),
|
||||
"Native chest loot must remain deferred with its exact table and seed");
|
||||
loot.add(Map.of("position", coords(chest.position()), "table", chest.table().identifier().toString(), "seed", chest.seed())); chests++;
|
||||
}
|
||||
if (!reload && gold == null && room.kind() != TransitPlan21.Kind.ACCESS && room.kind() != TransitPlan21.Kind.JUNCTION)
|
||||
gold = new BlockPos(room.minX(), room.floorY() + 1, room.minZ() + 1);
|
||||
}
|
||||
sites.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", nativeHash,
|
||||
"pieces", start.getPieces().size(), "unopened_chests", loot));
|
||||
}
|
||||
report.put("module_coordinates", modules);
|
||||
report.put("starts", sites);
|
||||
int weakSpan = 0, longestSpan = 0;
|
||||
for (var room : plan.modules()) {
|
||||
weakSpan = bearingMasks.get(room) == 15 ? 0 : weakSpan + 1;
|
||||
longestSpan = Math.max(longestSpan, weakSpan);
|
||||
helper.assertTrue(weakSpan <= 2, "Native deck spans more than 34 blocks without distributed rock bearings");
|
||||
}
|
||||
report.put("longest_span_without_full_bearings", longestSpan * 17);
|
||||
var ores = new java.util.TreeMap<String, Integer>();
|
||||
allGeometry.values().forEach(state -> {
|
||||
var id = net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString();
|
||||
if (id.endsWith("_ore")) ores.merge(id, 1, Integer::sum);
|
||||
});
|
||||
report.put("finite_exposed_ores", ores);
|
||||
var traversal = verifyAccess(helper, plan);
|
||||
report.put("native_connections", NativeConnections20.verify(helper, allGeometry,
|
||||
reload && gold != null ? Set.of(gold) : Set.of()));
|
||||
report.put("module_coordinates", modules);
|
||||
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
|
||||
helper.assertTrue(located != null && located.getSecond().is(TransitStructures21.KEY), "Native locate cannot find a saved industrial transit");
|
||||
helper.assertTrue(plan.modules().stream().filter(r -> r.kind() == TransitPlan21.Kind.ACCESS)
|
||||
.anyMatch(r -> TransitPiece21.locator(r).equals(located.getFirst())), "Locate must lead to a surface entrance rather than an underground floor");
|
||||
report.put("starts", sites); report.put("native_pieces", pieces); report.put("native_blocks_checked", blocks);
|
||||
report.put("full_chunks_checked", chunks.size()); report.put("rock_supports_checked", supports);
|
||||
report.put("unopened_deferred_chests", chests); report.put("native_routes", traversal); report.put("locate", coords(located.getFirst()));
|
||||
}
|
||||
}
|
||||
private record Chest(BlockPos position, ResourceKey<LootTable> table, long seed) {}
|
||||
private record Rendered(Map<BlockPos, BlockState> blocks, List<Chest> chests) {}
|
||||
private static Rendered rendered(TransitPlan21.Module room) {
|
||||
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
|
||||
TransitRenderer21.render(room, new LostCityRenderer.BlockWriter() {
|
||||
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(at(room, x, y, z), state); }
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
|
||||
var position = at(room, x, y, z); blocks.put(position, Blocks.CHEST.defaultBlockState()); chests.add(new Chest(position, table, RandomSource.create(seed).nextLong()));
|
||||
}
|
||||
});
|
||||
return new Rendered(blocks, chests);
|
||||
}
|
||||
private static int verifyBearings(GameTestHelper helper, SanctuaryChunkGenerator generator, TransitPlan21.Module room, Map<TransitPlan21.Module, Integer> masks) {
|
||||
var level = helper.getLevel(); int checked = 0, quarters = 0;
|
||||
for (int z = room.minZ();; z = Math.min(z + 4, room.maxZ())) {
|
||||
for (int x = room.minX();; x = Math.min(x + 4, room.maxX())) {
|
||||
int anchor = room.minY() - 1;
|
||||
for (var support : room.supports()) if (support.x() == x && support.z() == z) anchor = support.minY() - 1;
|
||||
var bearing = new BlockPos(x, anchor, z);
|
||||
boolean supported = level.getBlockState(bearing).isSolid() && level.getFluidState(bearing).isEmpty()
|
||||
&& level.getBlockState(bearing.below()).isSolid() && level.getFluidState(bearing.below()).isEmpty();
|
||||
if (supported) {
|
||||
quarters |= 1 << ((x < room.centerX() ? 0 : 1) + (z < room.centerZ() ? 0 : 2));
|
||||
for (int dy = 0; dy >= -1; dy--) {
|
||||
var at = new BlockPos(x, anchor + dy, z);
|
||||
helper.assertTrue(level.getBlockState(at).isSolid() && level.getFluidState(at).isEmpty(),
|
||||
"Transit bearing grid lost one of its two untouched rock layers at " + at);
|
||||
helper.assertTrue(generator.protectsCityDecoration(at), "A certified transit rock dependency is unprotected at " + at);
|
||||
checked++;
|
||||
}
|
||||
}
|
||||
if (x == room.maxX()) break;
|
||||
}
|
||||
if (z == room.maxZ()) break;
|
||||
}
|
||||
helper.assertTrue(room.kind() == TransitPlan21.Kind.GALLERY || room.kind() == TransitPlan21.Kind.JUNCTION || quarters == 15, "The continuous deck lost its distributed rock bearings");
|
||||
masks.put(room, quarters);
|
||||
return checked;
|
||||
}
|
||||
static Map<String, Object> verifyAccess(GameTestHelper helper, TransitPlan21 plan) {
|
||||
var level = helper.getLevel();
|
||||
var accesses = plan.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.ACCESS).toList();
|
||||
helper.assertTrue(accesses.size() == 2, "Native transit requires exactly two surface gates");
|
||||
Predicate<BlockPos> inside = pos -> plan.contains(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| accesses.stream().anyMatch(m -> pos.getX() >= m.centerX() - 2 && pos.getX() <= m.centerX() + 2
|
||||
&& pos.getZ() >= m.minZ() - 3 && pos.getZ() < m.minZ()
|
||||
&& pos.getY() >= m.surfaceY() && pos.getY() <= m.surfaceY() + 2);
|
||||
var start = TransitPiece21.locator(accesses.getFirst());
|
||||
Predicate<BlockPos> naturalApproach = pos -> accesses.stream().anyMatch(m ->
|
||||
pos.getX() >= m.centerX() - 2 && pos.getX() <= m.centerX() + 2
|
||||
&& pos.getZ() >= m.minZ() - 3 && pos.getZ() < m.minZ()
|
||||
&& pos.getY() >= m.surfaceY() && pos.getY() <= m.surfaceY() + 2);
|
||||
var reached = walk(helper, start, inside, naturalApproach); int targets = 0;
|
||||
var arrivals = new ArrayList<Map<String, Object>>();
|
||||
for (var module : plan.modules()) for (var target : TransitRenderer21.targets(module)) {
|
||||
var point = at(module, target.x(), target.y(), target.z());
|
||||
helper.assertTrue(reached.containsKey(Foot.at(point)), "Native transit cannot walk from its first portico to "
|
||||
+ module.kind() + "/" + target.name() + " at " + point);
|
||||
targets++;
|
||||
}
|
||||
for (var access : accesses) {
|
||||
var gate = TransitPiece21.locator(access);
|
||||
helper.assertTrue(reached.containsKey(Foot.at(gate)), "The second independent surface gate is not connected by real stairs");
|
||||
int approaches = 0;
|
||||
for (int x = access.centerX() - 2; x <= access.centerX() + 2; x++) for (int z = access.minZ() - 3; z < access.minZ(); z++) {
|
||||
var high = new BlockPos(x, access.surfaceY() + 1, z);
|
||||
var low = high.below(); var position = standable(level, high) ? high : low;
|
||||
helper.assertTrue(standable(level, position) && reached.containsKey(Foot.at(position)),
|
||||
"The certified natural approach no longer joins its portico at " + position);
|
||||
helper.assertTrue(level.getBlockState(position.below()).isSolid()
|
||||
&& level.getFluidState(position.below()).isEmpty(), "A surface arrival is suspended above air or water");
|
||||
approaches++;
|
||||
}
|
||||
arrivals.add(Map.of("gate", coords(gate), "roadway", coords(at(access, 8, 2, 8)),
|
||||
"rise", access.surfaceY() - access.floorY(), "steps_from_first_gate", reached.get(Foot.at(gate)),
|
||||
"natural_approach_cells", approaches));
|
||||
}
|
||||
return Map.of("surface_accesses", arrivals, "walkable_targets", targets, "standing_cells", reached.size(),
|
||||
"surface_to_surface_steps", reached.get(Foot.at(TransitPiece21.locator(accesses.getLast()))),
|
||||
"traversal_policy", "0.6 by1.8 native collision volume, half-block sidewalk steps; underground rises require a real forward-facing stair; natural portico approaches alone permit a normal one-block jump; no vertical moves");
|
||||
}
|
||||
/** Half-block standing elevations retain real sidewalk slabs without pretending an integer cell can hover over them. */
|
||||
private record Foot(int x, int halfY, int z) {
|
||||
static Foot at(BlockPos pos) { return new Foot(pos.getX(), pos.getY() * 2, pos.getZ()); }
|
||||
double y() { return halfY / 2.0; }
|
||||
BlockPos block() { return new BlockPos(x, Math.floorDiv(halfY, 2), z); }
|
||||
BlockPos bearing() { return new BlockPos(x, Math.floorDiv(halfY - 1, 2), z); }
|
||||
Foot rise(int halves) { return new Foot(x, halfY + halves, z); }
|
||||
Foot move(Direction direction, int halves) { return new Foot(x + direction.getStepX(), halfY + halves, z + direction.getStepZ()); }
|
||||
}
|
||||
private static Map<Foot, Integer> walk(GameTestHelper helper, BlockPos start, Predicate<BlockPos> inside, Predicate<BlockPos> naturalApproach) {
|
||||
var level = helper.getLevel(); var first = Foot.at(start);
|
||||
helper.assertTrue(inside.test(start) && standable(level, first), "Blocked native surface portico: " + start);
|
||||
var seen = new HashMap<Foot, Integer>(); var queue = new ArrayDeque<Foot>(); seen.put(first, 0); queue.add(first);
|
||||
while (!queue.isEmpty()) {
|
||||
var pos = queue.removeFirst();
|
||||
for (var direction : HORIZONTAL) for (int dy = -2; dy <= 2; dy++) {
|
||||
var next = pos.move(direction, dy);
|
||||
if (!inside.test(next.block()) || seen.containsKey(next)) continue;
|
||||
boolean ordinaryThreshold = naturalApproach.test(pos.block()) || naturalApproach.test(next.block());
|
||||
// Native step height is enough for a half slab; a full block needs the two treads of a stair.
|
||||
if (dy > 0 && (!clearBody(level, pos.rise(dy))
|
||||
|| dy == 2 && !ordinaryThreshold && !ascendingStair(level, next.bearing(), direction))) continue;
|
||||
if (dy < 0 && (!clearBody(level, next.rise(-dy))
|
||||
|| dy == -2 && !ordinaryThreshold && !ascendingStair(level, pos.bearing(), direction.getOpposite()))) continue;
|
||||
if (standable(level, next)) { seen.put(next, seen.get(pos) + 1); queue.add(next); }
|
||||
}
|
||||
helper.assertTrue(seen.size() <= 120_000, "Native transit traversal exceeded its bounded room volumes");
|
||||
}
|
||||
return seen;
|
||||
}
|
||||
private static boolean ascendingStair(ServerLevel level, BlockPos pos, Direction direction) {
|
||||
var state = level.getBlockState(pos);
|
||||
return state.getBlock() instanceof StairBlock
|
||||
&& state.getValue(BlockStateProperties.HORIZONTAL_FACING) == direction
|
||||
&& state.getValue(BlockStateProperties.HALF) == net.minecraft.world.level.block.state.properties.Half.BOTTOM;
|
||||
}
|
||||
private static boolean standable(ServerLevel level, BlockPos pos) { return standable(level, Foot.at(pos)); }
|
||||
private static boolean standable(ServerLevel level, Foot pos) {
|
||||
if (!clearBody(level, pos)) return false;
|
||||
var below = pos.bearing(); double top = pos.y() - below.getY();
|
||||
return level.getBlockState(below).getCollisionShape(level, below).toAabbs().stream()
|
||||
.anyMatch(b -> Math.abs(b.maxY - top) < .0001 && b.minX <= .5 && b.maxX >= .5 && b.minZ <= .5 && b.maxZ >= .5);
|
||||
}
|
||||
private static boolean clearBody(ServerLevel level, Foot pos) {
|
||||
var body = new AABB(pos.x() + .2, pos.y(), pos.z() + .2, pos.x() + .8, pos.y() + 1.8, pos.z() + .8);
|
||||
for (int y = (int) Math.floor(pos.y()); y <= (int) Math.floor(pos.y() + 1.8 - .0001); y++) {
|
||||
var at = new BlockPos(pos.x(), y, pos.z());
|
||||
if (!level.getFluidState(at).isEmpty()) return false;
|
||||
for (var shape : level.getBlockState(at).getCollisionShape(level, at).toAabbs())
|
||||
if (shape.move(at.getX(), at.getY(), at.getZ()).intersects(body)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private static BlockPos at(TransitPlan21.Module room, int x, int y, int z) { return new BlockPos(room.minX() + x, room.minY() + y, room.minZ() + z); }
|
||||
private static List<Integer> coords(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
|
||||
private static BlockPos readPos(com.google.gson.JsonArray value) { return new BlockPos(value.get(0).getAsInt(), value.get(1).getAsInt(), value.get(2).getAsInt()); }
|
||||
private static Path world(ServerLevel level) { return level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize(); }
|
||||
private static Path proof(ServerLevel level) { return world(level).resolve("transit21-create-proof.json"); }
|
||||
private static String planHash(TransitPlan21 plan) { return hash(plan.modules().toString()); }
|
||||
private static void write(ServerLevel level, Map<String, Object> report, boolean reload) throws IOException {
|
||||
var path = Path.of("diagnostics", "transit21-" + (reload ? "reload" : "create") + ".json"); Files.createDirectories(path.getParent());
|
||||
var value = new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n";
|
||||
Files.writeString(path, value);
|
||||
if (!reload) Files.writeString(proof(level), value);
|
||||
}
|
||||
private static String hash(String value) {
|
||||
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8))); }
|
||||
catch (NoSuchAlgorithmException impossible) { throw new AssertionError(impossible); }
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityUnderground16;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Kind;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Module;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.StairBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
|
||||
/** Native circulation proof on the already generated piece footprint. */
|
||||
public final class Underground22NativeChecks {
|
||||
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
|
||||
private Underground22NativeChecks() {}
|
||||
public static Map<String, Integer> verify(GameTestHelper helper, LostCityUnderground16.Plan plan) {
|
||||
var level = helper.getLevel(); var chunks = new HashSet<Long>();
|
||||
int halls = 0, ladders = 0, apertures = 0, walked = 0;
|
||||
helper.assertTrue(!plan.isEmpty(), "Independent underground hall plan must be present");
|
||||
for (var network : plan.networks()) {
|
||||
for (var m : network.modules()) {
|
||||
for (int cx = m.minX() >> 4; cx <= m.maxX() >> 4; cx++) for (int cz = m.minZ() >> 4; cz <= m.maxZ() >> 4; cz++) {
|
||||
long key = ((long) cx << 32) ^ (cz & 0xffffffffL);
|
||||
// Short-lived read tickets can expire during the caller’s asynchronous footprint load.
|
||||
// Refresh only these already generated chunks before inspecting their blocks.
|
||||
if (chunks.add(key)) helper.assertTrue(level.getChunk(cx, cz, ChunkStatus.FULL, true) instanceof LevelChunk,
|
||||
"Underground22 piece has no loaded FULL chunk at " + cx + "," + cz);
|
||||
}
|
||||
for (int d = 0; d < 4; d++) if ((m.openings() & 1 << d) != 0) {
|
||||
for (int side = -1; side <= 1; side++) for (int dy = 1; dy <= 3; dy++) {
|
||||
var at = mouth(m, d, side, dy);
|
||||
helper.assertTrue(passable(level.getBlockState(at)), "Native utility doorway blocked at " + at + " in " + m.kind());
|
||||
}
|
||||
apertures++;
|
||||
}
|
||||
}
|
||||
var access = network.modules().stream().filter(m -> m.kind() == Kind.ACCESS).findFirst().orElseThrow();
|
||||
BlockPos hatch = null;
|
||||
for (int x = access.minX(); x <= access.maxX(); x++) for (int z = access.minZ(); z <= access.maxZ(); z++) {
|
||||
var at = new BlockPos(x, access.maxY() - 1, z);
|
||||
if (level.getBlockState(at).is(Blocks.DARK_OAK_TRAPDOOR)) {
|
||||
helper.assertTrue(hatch == null, "The access has more than one surface hatch"); hatch = at;
|
||||
}
|
||||
}
|
||||
helper.assertTrue(hatch != null, "Independent access has no real wooden surface hatch");
|
||||
final BlockPos entrance = hatch;
|
||||
var closed = level.getBlockState(entrance);
|
||||
for (int y = access.floorY() + 1; y < entrance.getY(); y++) {
|
||||
var at = new BlockPos(entrance.getX(), y, entrance.getZ()); var ladder = level.getBlockState(at);
|
||||
helper.assertTrue(ladder.is(Blocks.LADDER), "Access ladder is interrupted at " + at);
|
||||
var wall = at.relative(ladder.getValue(BlockStateProperties.HORIZONTAL_FACING).getOpposite());
|
||||
helper.assertTrue(level.getBlockState(wall).isCollisionShapeFullBlock(level, wall), "Ladder has no real backing wall at " + at);
|
||||
ladders++;
|
||||
}
|
||||
level.setBlock(entrance, closed.setValue(BlockStateProperties.OPEN, true), 2);
|
||||
try {
|
||||
Predicate<BlockPos> inside = pos -> network.modules().stream().anyMatch(m -> m.contains(pos.getX(), pos.getY(), pos.getZ()));
|
||||
BlockPos approach = java.util.Arrays.stream(HORIZONTAL).map(entrance::relative)
|
||||
.filter(inside).filter(pos -> standable(level, pos)).findFirst().orElseThrow();
|
||||
var reached = walk(helper, approach, inside); walked += reached.size();
|
||||
helper.assertTrue(reached.contains(entrance), "Public deck cannot reach its hatch");
|
||||
for (var m : network.modules()) {
|
||||
helper.assertTrue(reached.stream().anyMatch(pos -> pos.getY() == m.floorY() + 1 && pos.getX() > m.minX()
|
||||
&& pos.getX() < m.maxX() && pos.getZ() > m.minZ() && pos.getZ() < m.maxZ()),
|
||||
"Surface access cannot reach emitted utility module " + m.kind() + " at " + m.centerX() + "," + m.centerZ());
|
||||
if (m.kind() != Kind.EXPANSION_HALL) continue;
|
||||
halls++;
|
||||
var plinth = new BlockPos(m.centerX(), m.minY() + 2, m.centerZ());
|
||||
helper.assertTrue(level.getBlockState(plinth).is(Blocks.SMOOTH_STONE) && reached.contains(plinth.above()),
|
||||
"Reserved expansion plinth must be emitted and reachable");
|
||||
for (int dx : new int[]{-6, 6}) for (int dz : new int[]{-6, 6})
|
||||
helper.assertTrue(reached.contains(new BlockPos(m.centerX() + dx, m.floorY() + 1, m.centerZ() + dz)),
|
||||
"Expansion hall has a blocked quarter");
|
||||
}
|
||||
} finally { level.setBlock(entrance, closed, 2); }
|
||||
}
|
||||
helper.assertTrue(halls > 0, "Native networks have no usable expansion hall");
|
||||
var report = new LinkedHashMap<String, Integer>(); report.put("full_chunks", chunks.size()); report.put("usable_halls", halls);
|
||||
report.put("ladder_blocks", ladders); report.put("clear_openings", apertures); report.put("reached_positions", walked);
|
||||
return Map.copyOf(report);
|
||||
}
|
||||
private static BlockPos mouth(Module m, int direction, int across, int dy) {
|
||||
return new BlockPos(direction == 1 ? m.maxX() : direction == 3 ? m.minX() : m.centerX() + across,
|
||||
m.floorY() + dy, direction == 0 ? m.minZ() : direction == 2 ? m.maxZ() : m.centerZ() + across);
|
||||
}
|
||||
/** Same finite walking/ladder rule used by the native city16/20/21 access proofs. */
|
||||
private static Set<BlockPos> walk(GameTestHelper helper, BlockPos start, Predicate<BlockPos> inside) {
|
||||
var level = helper.getLevel(); var reached = new HashSet<BlockPos>(); var queue = new ArrayDeque<BlockPos>();
|
||||
helper.assertTrue(inside.test(start) && standable(level, start), "Blocked utility approach"); reached.add(start); queue.add(start);
|
||||
while (!queue.isEmpty()) {
|
||||
var pos = queue.remove();
|
||||
for (var direction : HORIZONTAL) for (int dy = -1; dy <= 1; dy++) {
|
||||
var next = pos.relative(direction).offset(0, dy, 0);
|
||||
if (!inside.test(next) || dy > 0 && !passable(level.getBlockState(pos.above(2)))) continue;
|
||||
if (standable(level, next) && reached.add(next)) queue.add(next);
|
||||
}
|
||||
for (int dy : new int[]{-1, 1}) {
|
||||
var next = pos.offset(0, dy, 0);
|
||||
if (inside.test(next) && (level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(next).is(Blocks.LADDER)
|
||||
|| level.getBlockState(pos.below()).is(Blocks.LADDER)) && standable(level, next) && reached.add(next)) queue.add(next);
|
||||
}
|
||||
helper.assertTrue(reached.size() < 100_000, "Utility traversal exceeded its bounded footprint");
|
||||
}
|
||||
return reached;
|
||||
}
|
||||
private static boolean standable(ServerLevel level, BlockPos pos) {
|
||||
var below = level.getBlockState(pos.below()); var at = level.getBlockState(pos);
|
||||
return passable(at) && passable(level.getBlockState(pos.above()))
|
||||
&& (below.isSolid() || below.getBlock() instanceof StairBlock || at.is(Blocks.LADDER) || below.is(Blocks.LADDER));
|
||||
}
|
||||
private static boolean passable(BlockState state) {
|
||||
return state.isAir() || state.is(Blocks.LADDER) || state.is(Blocks.REDSTONE_WALL_TORCH)
|
||||
|| state.is(Blocks.DARK_OAK_TRAPDOOR) && state.getValue(BlockStateProperties.OPEN);
|
||||
}
|
||||
}
|
||||
+49
-3
@@ -45,9 +45,22 @@ final class UnifiedWorldSettingsDiagnostics {
|
||||
var historical16 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v16"))).value().createWorldDimensions();
|
||||
helper.assertTrue(historical16.overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 16
|
||||
&& old.source().generation() == 16 && !old.experimentalStructures(), "Historical16 never gains experimental structures");
|
||||
var historical17 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v17"))).value().createWorldDimensions();
|
||||
helper.assertTrue(historical17.overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 17
|
||||
&& old.source().generation() == 17 && old.experimentalStructures(), "Historical17 keeps its original experimental generator");
|
||||
var historical19 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v19"))).value().createWorldDimensions();
|
||||
helper.assertTrue(historical19.overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 19
|
||||
&& old.source().generation() == 19 && old.experimentalStructures(), "Historical19 keeps its original renderer and world settings");
|
||||
var generator = (SanctuaryChunkGenerator) standard.overworld();
|
||||
helper.assertTrue(generator.rootGeneration() == 17 && generator.source().generation() == 17 && generator.experimentalStructures(), "The public preset uses generation17 with its new-world option");
|
||||
var historical20 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v20")));
|
||||
helper.assertTrue(historical20.value().createWorldDimensions().overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 20
|
||||
&& old.source().generation() == 20 && old.experimentalStructures(), "Historical20 keeps its original architecture");
|
||||
var historical21 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v21")));
|
||||
helper.assertTrue(historical21.value().createWorldDimensions().overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 21
|
||||
&& old.source().generation() == 21 && old.experimentalStructures(), "Historical21 keeps its original placement rules");
|
||||
helper.assertTrue(generator.rootGeneration() == 22 && generator.source().generation() == 22 && generator.experimentalStructures(), "The public preset uses generation22 with its new-world option");
|
||||
helper.assertTrue(generator.initialDiameter() == 724, "The public preset defaults to Medium / 724 blocks");
|
||||
verifyConstructionMasks(helper, (SanctuaryChunkGenerator) historical21.value().createWorldDimensions().overworld(), generator);
|
||||
for (var tag : List.of(WorldPresetTags.NORMAL, WorldPresetTags.EXTENDED)) {
|
||||
var visible = presets.getOrThrow(tag).stream().map(holder -> holder.getRegisteredName())
|
||||
.filter(name -> name.startsWith("sanctuary:")).collect(Collectors.toSet());
|
||||
@@ -56,11 +69,11 @@ final class UnifiedWorldSettingsDiagnostics {
|
||||
}
|
||||
|
||||
var ops = RegistryOps.create(JsonOps.INSTANCE, access);
|
||||
var activeCodec = generator.rootGeneration() == 17 ? SanctuaryChunkGenerator.RUINS17_CODEC : generator.rootGeneration() == 16 ? SanctuaryChunkGenerator.CITY16_CODEC : generator.rootGeneration() == 15 ? SanctuaryChunkGenerator.CITY15_CODEC : generator.rootGeneration() == 14 ? SanctuaryChunkGenerator.CITY_CODEC : SanctuaryChunkGenerator.CODEC;
|
||||
var activeCodec = generator.rootGeneration() == 22 ? SanctuaryChunkGenerator.SELECTIVE22_CODEC : generator.rootGeneration() == 21 ? SanctuaryChunkGenerator.NETWORK21_CODEC : generator.rootGeneration() == 20 ? SanctuaryChunkGenerator.ARCHITECTURE20_CODEC : generator.rootGeneration() == 19 ? SanctuaryChunkGenerator.HERITAGE19_CODEC : generator.rootGeneration() == 17 ? SanctuaryChunkGenerator.RUINS17_CODEC : generator.rootGeneration() == 16 ? SanctuaryChunkGenerator.CITY16_CODEC : generator.rootGeneration() == 15 ? SanctuaryChunkGenerator.CITY15_CODEC : generator.rootGeneration() == 14 ? SanctuaryChunkGenerator.CITY_CODEC : SanctuaryChunkGenerator.CODEC;
|
||||
var serialized = activeCodec.codec().encodeStart(ops, generator).getOrThrow().getAsJsonObject();
|
||||
serialized.remove("experimental_structures");
|
||||
helper.assertTrue(activeCodec.codec().parse(ops, serialized).getOrThrow().experimentalStructures(),
|
||||
"Generation17 omitted experimental option defaults to enabled");
|
||||
"Generation22 omitted experimental option defaults to enabled");
|
||||
helper.assertTrue(!((SanctuaryChunkGenerator) historical16.overworld()).withExperimentalStructures(true).experimentalStructures(),
|
||||
"The new-world switch cannot activate experimental generation in a historical16 generator");
|
||||
serialized.remove("initial_diameter");
|
||||
@@ -103,4 +116,37 @@ final class UnifiedWorldSettingsDiagnostics {
|
||||
"public_presets", List.of("sanctuary:sanctuary"), "sizes", results,
|
||||
"invalid_diameters_rejected", true, "copied_biome_source", true, "passed", true)) + "\n");
|
||||
}
|
||||
|
||||
private static void verifyConstructionMasks(GameTestHelper helper, SanctuaryChunkGenerator historical21,
|
||||
SanctuaryChunkGenerator generation22) {
|
||||
var outside = new net.minecraft.core.BlockPos(200_000, 192, 200_000);
|
||||
// Decoding a generation must not reserve the world before any structures are planned.
|
||||
// These checks also cover the old OFF contract without initializing another island.
|
||||
for (var generator : List.of(historical21, historical21.withExperimentalStructures(false),
|
||||
generation22, generation22.withExperimentalStructures(false))) {
|
||||
helper.assertTrue(!generator.protectsCityDecoration(outside),
|
||||
"Unplanned generation" + generator.rootGeneration() + " does not protect arbitrary blocks");
|
||||
helper.assertTrue(!generator.intersectsExperimentalStructures(outside.getX(), outside.getY(), outside.getZ(),
|
||||
outside.getX() + 1, outside.getY() + 1, outside.getZ() + 1),
|
||||
"Unplanned generation" + generator.rootGeneration() + " does not reject arbitrary structure bounds");
|
||||
}
|
||||
// Reuse the attached world's real plans: no extra native-noise planning or chunk loads.
|
||||
if (!(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator active)
|
||||
|| active.rootGeneration() != 21 && active.rootGeneration() != 22) return;
|
||||
helper.assertTrue(!active.protectsCityDecoration(outside)
|
||||
&& !active.intersectsExperimentalStructures(outside.getX(), outside.getY(), outside.getZ(),
|
||||
outside.getX() + 1, outside.getY() + 1, outside.getZ() + 1),
|
||||
"Prepared construction masks remain local to their real structures");
|
||||
var interior = new ArrayList<net.minecraft.core.BlockPos>();
|
||||
for (var module : active.transitPlan21().modules())
|
||||
interior.add(new net.minecraft.core.BlockPos(module.centerX(), module.floorY(), module.centerZ()));
|
||||
for (var network : active.undergroundHall22().networks()) for (var module : network.modules())
|
||||
interior.add(new net.minecraft.core.BlockPos(module.centerX(), module.floorY(), module.centerZ()));
|
||||
if (active.rootGeneration() == 22 && helper.getLevel().structureManager().shouldGenerateStructures())
|
||||
helper.assertTrue(!interior.isEmpty(), "The attached generation22 world has core construction to protect");
|
||||
for (var position : interior) helper.assertTrue(active.protectsCityDecoration(position)
|
||||
&& active.intersectsExperimentalStructures(position.getX(), position.getY(), position.getZ(),
|
||||
position.getX(), position.getY(), position.getZ()),
|
||||
"Real underground construction remains protected from decoration and overlapping native structures");
|
||||
}
|
||||
}
|
||||
|
||||
+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.ruins17") || Boolean.getBoolean("sanctuary.test.expansion") || Boolean.getBoolean("sanctuary.test.lostCity")
|
||||
if (Boolean.getBoolean("sanctuary.test.selective22") || (Boolean.getBoolean("sanctuary.test.heritage20") || Boolean.getBoolean("sanctuary.test.heritage21") || Boolean.getBoolean("sanctuary.test.lostCity21") || Boolean.getBoolean("sanctuary.test.transit21")) || Boolean.getBoolean("sanctuary.test.lostCity20")
|
||||
|| Boolean.getBoolean("sanctuary.test.heritage19") || Boolean.getBoolean("sanctuary.test.ruins17") || Boolean.getBoolean("sanctuary.test.expansion") || Boolean.getBoolean("sanctuary.test.lostCity")
|
||||
|| Boolean.getBoolean("sanctuary.test.lostCity16") || Boolean.getBoolean("sanctuary.test.lostCity15") || Boolean.getBoolean("sanctuary.test.voidReuse")) {
|
||||
long until = System.nanoTime() + 20_000_000L;
|
||||
long remaining;
|
||||
@@ -46,9 +47,9 @@ public abstract class SanctuaryGameTestServerMixin {
|
||||
"Lnet/minecraft/world/level/levelgen/presets/WorldPresets;FLAT_ALL_DIMENSIONS:Lnet/minecraft/resources/ResourceKey;"))
|
||||
private static ResourceKey<WorldPreset> sanctuary$selectActualPreset() {
|
||||
if (Boolean.getBoolean("sanctuary.test.unified")) {
|
||||
int generation = Integer.getInteger("sanctuary.test.generation", 17);
|
||||
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17) throw new IllegalArgumentException("Unsupported test generation");
|
||||
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 17 ? "sanctuary" : "sanctuary_v" + generation));
|
||||
int generation = Integer.getInteger("sanctuary.test.generation", 22);
|
||||
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22) throw new IllegalArgumentException("Unsupported test generation");
|
||||
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 22 ? "sanctuary" : "sanctuary_v" + generation));
|
||||
}
|
||||
if (Boolean.getBoolean("sanctuary.test.expansion")) return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("expansion_lab"));
|
||||
int players = Integer.getInteger("sanctuary.test.players", 10);
|
||||
|
||||
@@ -48,6 +48,7 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("expansion_island"), fr.koka.sanctuary.expansion.ExpansionDensity.CODEC);
|
||||
fr.koka.sanctuary.expansion.ExpansionRuntime.register();
|
||||
fr.koka.sanctuary.expansion.ExpansionCommands.register();
|
||||
fr.koka.sanctuary.worldgen.heritage.HeritageCommands19.register();
|
||||
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("main_island"), MainIslandDensity.CODEC);
|
||||
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v13"), SanctuaryChunkGenerator.CODEC);
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v13"), SanctuaryBiomeSource.CODEC);
|
||||
@@ -60,6 +61,17 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v17"), SanctuaryChunkGenerator.RUINS17_CODEC);
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v17"), SanctuaryBiomeSource.RUINS17_CODEC);
|
||||
fr.koka.sanctuary.worldgen.city.LostCityStructures.register();
|
||||
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v19"), SanctuaryChunkGenerator.HERITAGE19_CODEC);
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v19"), SanctuaryBiomeSource.HERITAGE19_CODEC);
|
||||
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v20"), SanctuaryChunkGenerator.ARCHITECTURE20_CODEC);
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v20"), SanctuaryBiomeSource.ARCHITECTURE20_CODEC);
|
||||
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v21"), SanctuaryChunkGenerator.NETWORK21_CODEC);
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v21"), SanctuaryBiomeSource.NETWORK21_CODEC);
|
||||
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v22"), SanctuaryChunkGenerator.SELECTIVE22_CODEC);
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v22"), SanctuaryBiomeSource.SELECTIVE22_CODEC);
|
||||
fr.koka.sanctuary.worldgen.transit.TransitStructures21.register();
|
||||
fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.register();
|
||||
fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.register();
|
||||
fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.register();
|
||||
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("population_island"), PopulationIslandDensity.CODEC);
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("population_island"), PopulationIslandBiomeSource.CODEC);
|
||||
|
||||
+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) {
|
||||
if (generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22) {
|
||||
contents.addChild(CycleButton.builder((Boolean enabled) -> enabled ? CommonComponents.OPTION_ON : CommonComponents.OPTION_OFF, experimental)
|
||||
.withValues(true, false).create(0, 0, 280, 20, Component.translatable("sanctuary.world_options.experimental"),
|
||||
(button, value) -> experimental = value));
|
||||
contents.addChild(new MultiLineTextWidget(Component.translatable("sanctuary.world_options.experimental_hint"), font)
|
||||
contents.addChild(new MultiLineTextWidget(Component.translatable(generator.rootGeneration() == 22 ? "sanctuary.world_options.heritage22_hint" : (generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22) ? "sanctuary.world_options.heritage_hint" : "sanctuary.world_options.experimental_hint"), font)
|
||||
.setMaxWidth(310).setCentered(true));
|
||||
}
|
||||
layout.addToContents(contents);
|
||||
|
||||
@@ -122,6 +122,9 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
|
||||
ChunkAccess chunk, StructureTemplateManager templates, ResourceKey<Level> dimension) {
|
||||
fr.koka.sanctuary.worldgen.city.LostCityStructures.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||
fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||
fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||
fr.koka.sanctuary.worldgen.transit.TransitStructures21.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||
fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||
ExpansionStructures.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||
}
|
||||
@Override public void spawnOriginalMobs(WorldGenRegion region) {
|
||||
|
||||
@@ -41,7 +41,7 @@ public record ExpansionIsland(String id, String parent, String direction, String
|
||||
if (!((spatialGeneration(generation) ? DIAMETERS : LEGACY_DIAMETERS).contains(diameter) || (id.equals("sanctuary") && (diameter == 724 || (generation == 13 || spatialGeneration(generation)) && diameter == 1024)))) throw new IllegalArgumentException("Unsupported nominal diameter");
|
||||
if (Math.abs((long) centerX) > 100_000 || Math.abs((long) centerZ) > 100_000 || (centerX & 15) != 0 || (centerZ & 15) != 0) throw new IllegalArgumentException("Invalid island centre");
|
||||
if (!List.of("reserved", "ready").contains(status) || (generation != GENERATION && !spatialGeneration(generation) && !(generation == 13 && id.equals("sanctuary")))) throw new IllegalArgumentException("Unsupported island state or generation");
|
||||
if (climate.equals("lost_city") && (!spatialGeneration(generation) || id.equals("sanctuary"))) throw new IllegalArgumentException("Lost City requires a generation 14, 15, 16 or 17 expansion");
|
||||
if (climate.equals("lost_city") && (!spatialGeneration(generation) || id.equals("sanctuary"))) throw new IllegalArgumentException("Lost City requires a generation 14, 15, 16, 17, 19, 20, 21 or 22 expansion");
|
||||
if (reuseToken == null || !reuseToken.isEmpty() && (!spatialGeneration(generation) || id.equals("sanctuary") || !reuseToken.matches("[0-9a-f]{64}")))
|
||||
throw new IllegalArgumentException("Invalid void reuse certificate token");
|
||||
}
|
||||
@@ -53,7 +53,7 @@ public record ExpansionIsland(String id, String parent, String direction, String
|
||||
return new ExpansionIsland("sanctuary", "sanctuary", "north", "temperate", diameter, 0, 0, seed, "natural", "ready", generation);
|
||||
}
|
||||
|
||||
public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16 || generation == 17; }
|
||||
public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22; }
|
||||
public boolean origin() { return id.equals("sanctuary"); }
|
||||
public double referenceDiameter() { return origin() && diameter == 724 ? Math.sqrt(2) * 512 : 512; }
|
||||
public double horizontalScale() { return origin() ? 1.0 : diameter / referenceDiameter(); }
|
||||
|
||||
+4
-2
@@ -25,7 +25,9 @@ public abstract class UnifiedDecorationProtectionMixin {
|
||||
var destination = generator.islandForChunk(pos.getX() >> 4, pos.getZ() >> 4);
|
||||
if (owner == null || destination == null || !owner.id().equals(destination.id())
|
||||
|| !fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
|
||||
&& !fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.isPlacing() && generator.protectsCityDecoration(pos))
|
||||
&& !fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.isPlacing()
|
||||
&& !fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.isPlacing()
|
||||
&& !fr.koka.sanctuary.worldgen.transit.TransitStructures21.isPlacing() && generator.protectsCityDecoration(pos))
|
||||
callback.setReturnValue(false);
|
||||
}
|
||||
|
||||
@@ -40,7 +42,7 @@ public abstract class UnifiedDecorationProtectionMixin {
|
||||
if (state.is(Blocks.SUGAR_CANE)) return;
|
||||
// Generation16 reserves its architectural volume before decoration. Only its own
|
||||
// native pieces may replace water there; published generations retain the old rule.
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17)
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22)
|
||||
&& fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
|
||||
&& generator.protectsCityDecoration(pos)) return;
|
||||
if (generator.protectsDecoration(pos) && !region.getBlockState(pos).equals(state)) {
|
||||
|
||||
@@ -42,6 +42,22 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
|
||||
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
|
||||
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 17)));
|
||||
|
||||
public static final MapCodec<SanctuaryBiomeSource> HERITAGE19_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
|
||||
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
|
||||
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 19)));
|
||||
|
||||
public static final MapCodec<SanctuaryBiomeSource> ARCHITECTURE20_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
|
||||
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
|
||||
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 20)));
|
||||
|
||||
public static final MapCodec<SanctuaryBiomeSource> NETWORK21_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
|
||||
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
|
||||
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 21)));
|
||||
|
||||
public static final MapCodec<SanctuaryBiomeSource> SELECTIVE22_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
|
||||
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
|
||||
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 22)));
|
||||
|
||||
private final List<Holder<Biome>> allBiomes;
|
||||
private final PopulationIslandBiomeSource population;
|
||||
private final boolean cities;
|
||||
@@ -75,7 +91,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
|
||||
cityPlans = java.util.Map.copyOf(plans);
|
||||
}
|
||||
|
||||
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; }
|
||||
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 22 ? SELECTIVE22_CODEC : generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; }
|
||||
|
||||
@Override public BiomeResolver createResolver(Climate.Sampler sampler) {
|
||||
BiomeResolver woodlands = population.createResolver(sampler);
|
||||
|
||||
+191
-16
@@ -45,9 +45,47 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
|
||||
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 17, experimental)));
|
||||
|
||||
public static final MapCodec<SanctuaryChunkGenerator> HERITAGE19_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
|
||||
SanctuaryBiomeSource.HERITAGE19_CODEC.fieldOf("biome_source").forGetter(SanctuaryChunkGenerator::source),
|
||||
DIAMETER_CODEC.optionalFieldOf("initial_diameter", 724).forGetter(SanctuaryChunkGenerator::initialDiameter),
|
||||
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
|
||||
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 19, experimental)));
|
||||
|
||||
public static final MapCodec<SanctuaryChunkGenerator> ARCHITECTURE20_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
|
||||
SanctuaryBiomeSource.ARCHITECTURE20_CODEC.fieldOf("biome_source").forGetter(SanctuaryChunkGenerator::source),
|
||||
DIAMETER_CODEC.optionalFieldOf("initial_diameter", 724).forGetter(SanctuaryChunkGenerator::initialDiameter),
|
||||
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
|
||||
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 20, experimental)));
|
||||
|
||||
public static final MapCodec<SanctuaryChunkGenerator> NETWORK21_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
|
||||
SanctuaryBiomeSource.NETWORK21_CODEC.fieldOf("biome_source").forGetter(SanctuaryChunkGenerator::source),
|
||||
DIAMETER_CODEC.optionalFieldOf("initial_diameter", 724).forGetter(SanctuaryChunkGenerator::initialDiameter),
|
||||
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
|
||||
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 21, experimental)));
|
||||
|
||||
public static final MapCodec<SanctuaryChunkGenerator> SELECTIVE22_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
|
||||
SanctuaryBiomeSource.SELECTIVE22_CODEC.fieldOf("biome_source").forGetter(SanctuaryChunkGenerator::source),
|
||||
DIAMETER_CODEC.optionalFieldOf("initial_diameter", 724).forGetter(SanctuaryChunkGenerator::initialDiameter),
|
||||
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
|
||||
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 22, experimental)));
|
||||
|
||||
private final boolean experimentalStructures;
|
||||
private volatile fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17 = fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.EMPTY;
|
||||
private boolean ruinsPrepared;
|
||||
private volatile fr.koka.sanctuary.worldgen.heritage.HeritagePlan19 heritagePlan19 = fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.EMPTY;
|
||||
private boolean heritagePrepared;
|
||||
private final java.util.Map<ExpansionIsland, fr.koka.sanctuary.worldgen.city.LostCityPlanner21.Result> cityPlacements21 = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
private volatile fr.koka.sanctuary.worldgen.transit.TransitPlan21 transitPlan21 = fr.koka.sanctuary.worldgen.transit.TransitPlan21.EMPTY;
|
||||
private boolean transitPrepared;
|
||||
private volatile fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan undergroundHall22 = new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
|
||||
private volatile fr.koka.sanctuary.worldgen.city.LostCityProtection16 undergroundHallProtection22 = fr.koka.sanctuary.worldgen.city.LostCityProtection16.EMPTY;
|
||||
private boolean undergroundHallPrepared22;
|
||||
public fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan undergroundHall22() { return undergroundHall22; }
|
||||
public fr.koka.sanctuary.worldgen.transit.TransitPlan21 transitPlan21() { return transitPlan21; }
|
||||
public fr.koka.sanctuary.worldgen.city.LostCityPlanner21.Result cityPlacement21(ExpansionIsland island) {
|
||||
lostCityPlan(island);
|
||||
return cityPlacements21.get(cityIdentity(island));
|
||||
}
|
||||
private volatile Context originContext;
|
||||
private final int generation;
|
||||
private net.minecraft.world.level.LevelHeightAccessor heightAccessor;
|
||||
@@ -83,12 +121,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
}
|
||||
private SanctuaryChunkGenerator(SanctuaryBiomeSource source, int initialDiameter, int generation, boolean experimental) {
|
||||
super(source, initialDiameter, true);
|
||||
this.experimentalStructures = generation == 17 && experimental;
|
||||
this.experimentalStructures = (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) && experimental;
|
||||
this.generation = generation;
|
||||
if (generation != source.generation()) throw new IllegalArgumentException("Sanctuary generator and biome source versions must agree");
|
||||
}
|
||||
@Override public SanctuaryBiomeSource source() { return (SanctuaryBiomeSource) super.source(); }
|
||||
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; }
|
||||
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 22 ? SELECTIVE22_CODEC : generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; }
|
||||
public SanctuaryChunkGenerator withInitialDiameter(int diameter) {
|
||||
return new SanctuaryChunkGenerator(source().copy(), diameter, generation, experimentalStructures);
|
||||
}
|
||||
@@ -97,8 +135,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
return new SanctuaryChunkGenerator(source().copy(), initialDiameter(), generation, enabled);
|
||||
}
|
||||
public fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17() { return ruinsPlan17; }
|
||||
public fr.koka.sanctuary.worldgen.heritage.HeritagePlan19 heritagePlan19() { return heritagePlan19; }
|
||||
public boolean intersectsExperimentalStructures(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||
return generation == 17 && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ);
|
||||
return (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22) && heritagePlan19.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||
|| (generation == 21 || generation == 22) && transitPlan21.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||
|| generation == 22 && undergroundHallProtection22.intersects(minX, minY, minZ, maxX, maxY, maxZ);
|
||||
}
|
||||
public IslandCapacity capacity() {
|
||||
return switch (initialDiameter()) {
|
||||
@@ -133,12 +175,18 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
// Publish one root context before any generation workers use it.
|
||||
context(islands.getFirst());
|
||||
prepareCities(islands);
|
||||
if (generation == 22) { prepareTransit(islands); prepareUndergroundHall22(islands); }
|
||||
prepareRuins(islands);
|
||||
prepareHeritage(islands);
|
||||
prepareTransit(islands);
|
||||
}
|
||||
@Override public void setIslands(List<ExpansionIsland> islands) {
|
||||
// Complete and publish city geometry before workers can observe the new owner list.
|
||||
prepareCities(islands);
|
||||
if (generation == 22) { prepareTransit(islands); prepareUndergroundHall22(islands); }
|
||||
prepareRuins(islands);
|
||||
prepareHeritage(islands);
|
||||
prepareTransit(islands);
|
||||
super.setIslands(islands);
|
||||
}
|
||||
private void prepareCities(List<ExpansionIsland> islands) {
|
||||
@@ -149,7 +197,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
source().setCityPlans(java.util.Map.of());
|
||||
return;
|
||||
}
|
||||
if ((generation == 16 || generation == 17) && !cityStructuresEnabled) {
|
||||
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) && !cityStructuresEnabled) {
|
||||
cityProtection16 = fr.koka.sanctuary.worldgen.city.LostCityProtection16.EMPTY;
|
||||
activeCityPlans = java.util.Map.of();
|
||||
source().setCityPlans(java.util.Map.of());
|
||||
@@ -165,7 +213,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
}
|
||||
cityProtection15 = protection.build();
|
||||
}
|
||||
if (generation == 16 || generation == 17) {
|
||||
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) {
|
||||
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
|
||||
for (var island : islands) {
|
||||
protection.add(published.get(island.id()), undergroundPlan16(island));
|
||||
@@ -180,11 +228,11 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
source().setCityPlans(published);
|
||||
}
|
||||
private void prepareRuins(List<ExpansionIsland> islands) {
|
||||
if (generation != 17 || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return;
|
||||
if ((generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22) || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return;
|
||||
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
||||
var terrain = context(root);
|
||||
var sampled = new fr.koka.sanctuary.worldgen.ruins.RuinsTerrain17(root, terrain.generator(), terrain.random(),
|
||||
heightAccessor, pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()));
|
||||
heightAccessor, pos -> protectsCore22(pos) || protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()));
|
||||
long start = System.nanoTime();
|
||||
var plan = fr.koka.sanctuary.worldgen.ruins.RuinsPlanner17.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
|
||||
ruinsPlan17 = plan;
|
||||
@@ -192,6 +240,89 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
SanctuaryMod.LOGGER.info("Experimental ruins17 seed {}: {} networks, {} probes, {} noise columns in {} ms",
|
||||
root.seed(), plan.networks().size(), plan.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000);
|
||||
}
|
||||
private void prepareHeritage(List<ExpansionIsland> islands) {
|
||||
if ((generation != 19 && generation != 20 && generation != 21 && generation != 22) || !experimentalStructures || !cityStructuresEnabled || heritagePrepared) return;
|
||||
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
||||
var terrain = context(root);
|
||||
if (generation == 21 || generation == 22) {
|
||||
var sampled = new fr.koka.sanctuary.worldgen.heritage.HeritageTerrain21(root, terrain.generator(), terrain.random(),
|
||||
heightAccessor, pos -> protectsCore22(pos) || protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()));
|
||||
long start = System.nanoTime();
|
||||
sampled.discoverLandmarks(lostCityPlan(root), ruinsPlan17);
|
||||
var plan = generation == 22
|
||||
? fr.koka.sanctuary.worldgen.heritage.HeritagePlanner22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
|
||||
: fr.koka.sanctuary.worldgen.heritage.HeritagePlanner21.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
|
||||
heritagePlan19 = plan;
|
||||
heritagePrepared = true;
|
||||
SanctuaryMod.LOGGER.info("Heritage" + generation + " seed {}: {} sites, {} routes, {} segments, {} probes, {} native columns in {} ms; {}",
|
||||
root.seed(), plan.sites().size(), plan.links().size(), plan.segments().size(), plan.samples(), sampled.columnsUsed(),
|
||||
(System.nanoTime() - start) / 1_000_000, plan.diagnostics());
|
||||
return;
|
||||
}
|
||||
var sampled = new fr.koka.sanctuary.worldgen.heritage.HeritageTerrain19(root, terrain.generator(), terrain.random(),
|
||||
heightAccessor, pos -> protectsCore22(pos) || protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()));
|
||||
long start = System.nanoTime();
|
||||
sampled.discoverLandmarks(lostCityPlan(root), ruinsPlan17);
|
||||
var plan = fr.koka.sanctuary.worldgen.heritage.HeritagePlanner19.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
|
||||
heritagePlan19 = plan;
|
||||
heritagePrepared = true;
|
||||
SanctuaryMod.LOGGER.info("Heritage19 seed {}: {} sites, {} routes, {} segments, {} probes, {} native columns in {} ms; {}",
|
||||
root.seed(), plan.sites().size(), plan.links().size(), plan.segments().size(), plan.samples(), sampled.columnsUsed(),
|
||||
(System.nanoTime() - start) / 1_000_000, plan.diagnostics());
|
||||
}
|
||||
private void prepareTransit(List<ExpansionIsland> islands) {
|
||||
if ((generation != 21 && generation != 22) || generation == 21 && !experimentalStructures || !cityStructuresEnabled || transitPrepared) return;
|
||||
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
||||
var terrain = context(root);
|
||||
var sampled = new fr.koka.sanctuary.worldgen.transit.TransitTerrain21(root, terrain.generator(), terrain.random(),
|
||||
heightAccessor, pos -> protectsCore22(pos) || protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()) || heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ()));
|
||||
long start = System.nanoTime();
|
||||
transitPlan21 = generation == 22
|
||||
? fr.koka.sanctuary.worldgen.transit.TransitPlanner22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
|
||||
: fr.koka.sanctuary.worldgen.transit.TransitPlanner21.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
|
||||
int directColumns = 0;
|
||||
if (generation == 22 && transitPlan21.isEmpty()) {
|
||||
directColumns = sampled.columnsUsed();
|
||||
sampled = new fr.koka.sanctuary.worldgen.transit.TransitTerrain21(root, terrain.generator(), terrain.random(),
|
||||
heightAccessor, pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()));
|
||||
transitPlan21 = fr.koka.sanctuary.worldgen.transit.TransitRoute22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
|
||||
}
|
||||
transitPrepared = true;
|
||||
SanctuaryMod.LOGGER.info("Transit" + generation + " seed {}: {} modules, {} probes, {} columns in {} ms; {}", root.seed(),
|
||||
transitPlan21.modules().size(), transitPlan21.samples(), directColumns + sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000, transitPlan21.diagnostic());
|
||||
}
|
||||
private boolean protectsCore22(net.minecraft.core.BlockPos pos) {
|
||||
return generation == 22 && (transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| undergroundHallProtection22.contains(pos.getX(), pos.getY(), pos.getZ()));
|
||||
}
|
||||
private void prepareUndergroundHall22(List<ExpansionIsland> islands) {
|
||||
if (generation != 22 || !cityStructuresEnabled || undergroundHallPrepared22) return;
|
||||
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
||||
if (undergroundPlan16(root).networks().stream().flatMap(n -> n.modules().stream())
|
||||
.anyMatch(m -> m.kind() == fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Kind.EXPANSION_HALL)) {
|
||||
undergroundHallPrepared22 = true; return;
|
||||
}
|
||||
var terrain = context(root);
|
||||
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain21(root, terrain.generator(), terrain.random(), heightAccessor,
|
||||
pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()) || protectsCore22(pos));
|
||||
long start = System.nanoTime();
|
||||
undergroundHall22 = fr.koka.sanctuary.worldgen.city.LostCityUnderground22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(),
|
||||
new fr.koka.sanctuary.worldgen.city.LostCityUnderground22.Terrain() {
|
||||
@Override public int surfaceY(int x, int z) { var ground = sampled.ground(x, z); return ground.dry() ? ground.top() : -1; }
|
||||
@Override public boolean owns(int x, int z) { return sampled.owns(x, z); }
|
||||
@Override public boolean solid(int x, int y, int z) { return sampled.solid(x, y, z); }
|
||||
@Override public boolean protectedAt(int x, int y, int z) { return sampled.protectedAt(x, y, z); }
|
||||
@Override public boolean exteriorAir(int x, int y, int z, int dx, int dz) { return sampled.exteriorAir(x, y, z, dx, dz); }
|
||||
});
|
||||
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
|
||||
protection.add(fr.koka.sanctuary.worldgen.city.LostCityPlan.EMPTY, undergroundHall22);
|
||||
undergroundHallProtection22 = protection.build(); undergroundHallPrepared22 = true;
|
||||
SanctuaryMod.LOGGER.info("Independent hall22 seed {}: {} networks, {} probes, {} columns in {} ms", root.seed(),
|
||||
undergroundHall22.networks().size(), undergroundHall22.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000);
|
||||
}
|
||||
private static ExpansionIsland cityIdentity(ExpansionIsland island) {
|
||||
// State and receipt do not change the terrain geometry used by a cached plan.
|
||||
var ready = island.status().equals("ready") ? island : island.ready();
|
||||
@@ -202,6 +333,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
var key = cityIdentity(island);
|
||||
if (source().islands().stream().noneMatch(saved -> cityIdentity(saved).equals(key))) {
|
||||
cityPlans.remove(key);
|
||||
cityPlacements21.remove(key);
|
||||
undergroundPlans.remove(key);
|
||||
undergroundPlans16.remove(key);
|
||||
citySupports.remove(key);
|
||||
@@ -217,7 +349,41 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
return cityPlans.computeIfAbsent(cityIdentity(island), ignored -> {
|
||||
long start = System.nanoTime();
|
||||
var terrain = context(island);
|
||||
if (generation == 16 || generation == 17) {
|
||||
if (generation == 21 || generation == 22) {
|
||||
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain21(island, terrain.generator(), terrain.random(),
|
||||
heightAccessor, pos -> island.origin() && protectsDecoration(pos));
|
||||
var placement = generation == 22
|
||||
? fr.koka.sanctuary.worldgen.city.LostCityPlanner22.plan(island.seed(), island.centerX(), island.centerZ(), island.diameter(), island.origin(), sampled)
|
||||
: fr.koka.sanctuary.worldgen.city.LostCityPlanner21.plan(island.seed(), island.centerX(), island.centerZ(), island.diameter(), island.origin(), sampled);
|
||||
var surface = placement.surface();
|
||||
cityPlacements21.put(cityIdentity(island), placement);
|
||||
citySupports16.put(cityIdentity(island), placement.supports());
|
||||
if (placement.reservation() != null) cityReservations16.put(cityIdentity(island), placement.reservation());
|
||||
var supports = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
|
||||
for (var support : placement.supports()) supports.addViaductSupport(support);
|
||||
if (placement.reservation() != null) supports.addReservation(placement.reservation());
|
||||
var extraDistricts = surface.districts().stream()
|
||||
.filter(d -> !placement.core().surface().districts().contains(d)).toList();
|
||||
supports.add(new fr.koka.sanctuary.worldgen.city.LostCityPlan(extraDistricts),
|
||||
new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0));
|
||||
var supportMask = supports.build();
|
||||
var underground = fr.koka.sanctuary.worldgen.city.LostCityUnderground16.plan(island.seed(), placement.core().surface(),
|
||||
new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Terrain() {
|
||||
@Override public boolean solid(int x, int y, int z) { return sampled.solid(x, y, z); }
|
||||
@Override public boolean owns(int x, int z) { return sampled.owns(x, z); }
|
||||
@Override public boolean exteriorAir(int x, int y, int z, int dx, int dz) { return sampled.exteriorAir(x, y, z, dx, dz); }
|
||||
@Override public boolean protectedAt(int x, int y, int z) {
|
||||
return supportMask.contains(x, y, z) || sampled.protectedAt(x, y, z);
|
||||
}
|
||||
});
|
||||
undergroundPlans16.put(cityIdentity(island), underground);
|
||||
SanctuaryMod.LOGGER.info("Lost City" + generation + " {} seed {}: {} city districts, {} underground networks, {} noise columns in {} ms",
|
||||
island.id(), island.seed(), surface.districts().size(), underground.networks().size(), sampled.columnsUsed(),
|
||||
(System.nanoTime() - start) / 1_000_000);
|
||||
SanctuaryMod.LOGGER.info("City" + generation + " extensions: {}", placement.diagnostics());
|
||||
return surface;
|
||||
}
|
||||
if (generation == 16 || generation == 17 || generation == 19 || generation == 20) {
|
||||
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain16(island, terrain.generator(), terrain.random(),
|
||||
heightAccessor, pos -> island.origin() && protectsDecoration(pos));
|
||||
var placement = LostCityPlanner16.plan(island.seed(), island.centerX(), island.centerZ(),
|
||||
@@ -288,8 +454,11 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
});
|
||||
}
|
||||
public boolean protectsCityDecoration(net.minecraft.core.BlockPos pos) {
|
||||
if (generation == 16 || generation == 17) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| generation == 17 && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ());
|
||||
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22) && heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| (generation == 21 || generation == 22) && transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| generation == 22 && undergroundHallProtection22.contains(pos.getX(), pos.getY(), pos.getZ());
|
||||
if (generation == 15) return cityProtection15.contains(pos.getX(), pos.getY(), pos.getZ());
|
||||
if (generation != 14) return false;
|
||||
var island = islandAt(pos.getX(), pos.getZ());
|
||||
@@ -311,20 +480,20 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
return citySupports.getOrDefault(cityIdentity(island), List.of());
|
||||
}
|
||||
public boolean intersectsCityConstruction16(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||
return (generation == 16 || generation == 17) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
|
||||
return (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
|
||||
}
|
||||
public fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan undergroundPlan16(ExpansionIsland island) {
|
||||
if (generation != 16 && generation != 17) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
|
||||
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
|
||||
lostCityPlan(island);
|
||||
return undergroundPlans16.getOrDefault(cityIdentity(island), new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0));
|
||||
}
|
||||
public List<LostCityPlanner16.Support> citySupports16(ExpansionIsland island) {
|
||||
if (generation != 16 && generation != 17) return List.of();
|
||||
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22) return List.of();
|
||||
lostCityPlan(island);
|
||||
return citySupports16.getOrDefault(cityIdentity(island), List.of());
|
||||
}
|
||||
public LostCityPlanner16.Fallback cityReservation16(ExpansionIsland island) {
|
||||
if (generation != 16 && generation != 17) return null;
|
||||
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22) return null;
|
||||
lostCityPlan(island);
|
||||
return cityReservations16.get(cityIdentity(island));
|
||||
}
|
||||
@@ -334,10 +503,16 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
@Override public com.mojang.datafixers.util.Pair<net.minecraft.core.BlockPos, net.minecraft.core.Holder<net.minecraft.world.level.levelgen.structure.Structure>> findNearestMapStructure(
|
||||
ServerLevel level, net.minecraft.core.HolderSet<net.minecraft.world.level.levelgen.structure.Structure> structures,
|
||||
net.minecraft.core.BlockPos origin, int radius, boolean skipReferenced) {
|
||||
if (generation == 22 && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.KEY))
|
||||
return fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.findNearest(level, structures, origin, radius, skipReferenced);
|
||||
if (structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.city.LostCityStructures.KEY))
|
||||
return fr.koka.sanctuary.worldgen.city.LostCityStructures.findNearest(level, structures, origin, radius, skipReferenced);
|
||||
if (generation == 17 && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.KEY))
|
||||
if ((generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.KEY))
|
||||
return fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.findNearest(level, structures, origin, radius, skipReferenced);
|
||||
if ((generation == 19 || generation == 20 || generation == 21 || generation == 22) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.KEY))
|
||||
return fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.findNearest(level, structures, origin, radius, skipReferenced);
|
||||
if ((generation == 21 || generation == 22) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.transit.TransitStructures21.KEY))
|
||||
return fr.koka.sanctuary.worldgen.transit.TransitStructures21.findNearest(level, structures, origin, radius, skipReferenced);
|
||||
return super.findNearestMapStructure(level, structures, origin, radius, skipReferenced);
|
||||
}
|
||||
@Override protected Context context(ExpansionIsland island) {
|
||||
@@ -365,7 +540,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
@Override protected void afterTerrain(ExpansionIsland island, ChunkAccess chunk) {
|
||||
// Root water/lava are already applied by the Population hook on the named delegate.
|
||||
if (!island.origin()) super.afterTerrain(island, chunk);
|
||||
if ((generation == 16 || generation == 17) && cityStructuresEnabled) {
|
||||
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) && cityStructuresEnabled) {
|
||||
var reserved = cityReservations16.get(cityIdentity(island));
|
||||
if (reserved != null) {
|
||||
var mutable = new net.minecraft.core.BlockPos.MutableBlockPos();
|
||||
|
||||
+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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+767
@@ -0,0 +1,767 @@
|
||||
package fr.koka.sanctuary.worldgen.city;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Kind;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Parcel;
|
||||
import java.util.ArrayList;
|
||||
import fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
|
||||
import java.util.List;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BedPart;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.block.state.properties.DoorHingeSide;
|
||||
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
|
||||
import net.minecraft.world.level.block.state.properties.SlabType;
|
||||
import net.minecraft.world.level.storage.loot.BuiltInLootTables;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
|
||||
/** Complete generation-21 buildings. Published renderers deliberately never call this renderer.
|
||||
* A north-facing plan is rotated once; all positions remain relative to the saved parcel. */
|
||||
public final class LostCityInteriors21 {
|
||||
private static final BlockState AIR = Blocks.AIR.defaultBlockState();
|
||||
private static final BlockState STONE = Blocks.STONE.defaultBlockState();
|
||||
private static final BlockState SMOOTH = Blocks.SMOOTH_STONE.defaultBlockState();
|
||||
private static final BlockState WOOD = Blocks.OAK_PLANKS.defaultBlockState();
|
||||
private static final BlockState TILE = Blocks.POLISHED_ANDESITE.defaultBlockState();
|
||||
private static final int STOREY = 5;
|
||||
|
||||
private LostCityInteriors21() {}
|
||||
|
||||
/** Reachable standing positions, in the same rotated local coordinates as BlockWriter. */
|
||||
public record Room(String name, int x, int y, int z) {}
|
||||
|
||||
/** Local, already rotated enclosure including the floor, full ceiling, four walls and door. */
|
||||
public record Bathroom(String name, int minX, int minY, int minZ, int maxX, int maxY, int maxZ,
|
||||
int doorX, int doorY, int doorZ) {}
|
||||
|
||||
private record Area(int left, int front, int right, int back) {}
|
||||
|
||||
/** Adjacent storeys always change plan; the saved parcel seed chooses which plan comes first. */
|
||||
public static int layout(Parcel parcel, int floor) {
|
||||
validate(parcel);
|
||||
if (floor < 0 || floor >= parcel.floors()) throw new IllegalArgumentException("Invalid interior floor");
|
||||
return (int) ((parcel.seed() ^ (parcel.seed() >>> 32)) & 1L) ^ (floor & 1);
|
||||
}
|
||||
|
||||
private static Area rear(Parcel p, int floor, boolean east) {
|
||||
boolean wide = layout(p, floor) == 1;
|
||||
return east ? new Area(wide ? 10 : 11, wide ? 10 : 11, 14, 14)
|
||||
: new Area(2, wide ? 10 : 11, wide ? 6 : 5, 14);
|
||||
}
|
||||
|
||||
private static int frontWall(Parcel p, int floor) { return layout(p, floor) == 0 ? 7 : 8; }
|
||||
|
||||
private static String frontUse(Parcel p, int floor) {
|
||||
int variant = layout(p, floor);
|
||||
return switch (p.kind()) {
|
||||
case APARTMENT -> variant == 0 ? "kitchen_dining" : "studio_bedroom";
|
||||
case HOTEL -> floor == 0 ? "reception" : "guest_suite";
|
||||
case LIBRARY -> variant == 0 ? "book_stacks" : "periodicals_reading";
|
||||
case RESTAURANT -> variant == 0 ? "dining_room" : "kitchen";
|
||||
case BARRACKS -> variant == 0 ? "dormitory" : "duty_office";
|
||||
case HALL -> floor == 0 ? "ticket_office" : "administration";
|
||||
default -> throw new IllegalArgumentException("Not an interior kind");
|
||||
};
|
||||
}
|
||||
|
||||
private static String rearUse(Parcel p, int floor, boolean east) {
|
||||
int variant = layout(p, floor);
|
||||
return switch (p.kind()) {
|
||||
case APARTMENT -> east ? "bathroom" : variant == 0 ? "bedroom" : "kitchen_dining";
|
||||
case HOTEL -> floor == 0 ? east ? "laundry" : "lounge"
|
||||
: variant == 0 ? east ? "guest_room" : "bedroom" : east ? "bathroom" : "guest_lounge";
|
||||
case LIBRARY -> variant == 0 ? east ? "archive" : "reading_room" : east ? "bathroom" : "catalogue_office";
|
||||
case RESTAURANT -> variant == 0 ? east ? "kitchen" : "washrooms" : east ? "pantry" : "dining_room";
|
||||
case BARRACKS -> east ? "equipment_store" : variant == 0 ? "washrooms" : "officer_bedroom";
|
||||
case HALL -> variant == 0 ? east ? "bathroom" : "meeting_room" : east ? "meeting_room" : "archive";
|
||||
default -> throw new IllegalArgumentException("Not an interior kind");
|
||||
};
|
||||
}
|
||||
|
||||
public static void render(Parcel parcel, LostCityRenderer.BlockWriter output) {
|
||||
validate(parcel);
|
||||
var connected = ConnectedBlocks20.buffer(output);
|
||||
new Building(parcel, connected).render();
|
||||
connected.flush();
|
||||
}
|
||||
|
||||
/** These describe actual rooms and services, not decorative labels placed in the world. */
|
||||
public static List<Room> rooms(Parcel parcel) {
|
||||
validate(parcel);
|
||||
List<Room> result = new ArrayList<>();
|
||||
add(result, parcel, "street_entrance", 8, 1, 0);
|
||||
add(result, parcel, "vestibule", 8, 1, 3);
|
||||
for (int floor = 0; floor < parcel.floors(); floor++) {
|
||||
int y = floor * STOREY + 1;
|
||||
String prefix = "floor_" + floor + "/";
|
||||
add(result, parcel, prefix + "corridor", 8, y, 8);
|
||||
add(result, parcel, prefix + "stair_landing", 4, y, 8);
|
||||
add(result, parcel, prefix + frontUse(parcel, floor), 11, y, 6);
|
||||
if (parcel.kind() == Kind.HALL && floor < 2) {
|
||||
if (floor == 0) {
|
||||
add(result, parcel, prefix + "auditorium", 8, y, 11);
|
||||
add(result, parcel, prefix + "stage", 9, y + 1, 13);
|
||||
} else add(result, parcel, prefix + "gallery", 8, y, 10);
|
||||
} else {
|
||||
Area west = rear(parcel, floor, false), east = rear(parcel, floor, true);
|
||||
add(result, parcel, prefix + rearUse(parcel, floor, false), west.right(), y, 11);
|
||||
add(result, parcel, prefix + rearUse(parcel, floor, true), east.left(), y, 11);
|
||||
}
|
||||
}
|
||||
for (var bath : bathroomPlans(parcel)) {
|
||||
int targetZ = bath.southDoor() ? bath.z1() - 1 : bath.z0() + 1;
|
||||
add(result, parcel, bath.name(), bath.doorX(), bath.floor() * STOREY + 1, targetZ);
|
||||
}
|
||||
add(result, parcel, "roof", 8, parcel.floors() * STOREY + 1, 8);
|
||||
switch (parcel.kind()) {
|
||||
case HOTEL -> add(result, parcel, "roof_laundry_pavilion", 10, parcel.floors() * STOREY + 1, 5);
|
||||
case APARTMENT -> add(result, parcel, "roof_residents_terrace", 8, parcel.floors() * STOREY + 1, 10);
|
||||
case LIBRARY -> add(result, parcel, "roof_reading_terrace", 11, parcel.floors() * STOREY + 1, 10);
|
||||
case RESTAURANT -> add(result, parcel, "roof_dining_terrace", 11, parcel.floors() * STOREY + 1, 8);
|
||||
case BARRACKS -> add(result, parcel, "roof_service_stair", 4, parcel.floors() * STOREY + 1, 8);
|
||||
default -> { }
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private record BathPlan(String name, int floor, int x0, int z0, int x1, int z1, int doorX, boolean southDoor) {}
|
||||
|
||||
private static List<BathPlan> bathroomPlans(Parcel p) {
|
||||
List<BathPlan> result = new ArrayList<>();
|
||||
for (int floor = 0; floor < p.floors(); floor++) {
|
||||
if (p.kind() == Kind.HOTEL && floor > 0)
|
||||
result.add(new BathPlan("floor_" + floor + "/suite_bathroom", floor, 10, 1, 15, 4, 12, true));
|
||||
if (p.kind() == Kind.HALL && floor < 2) continue;
|
||||
for (boolean east : new boolean[]{false, true}) {
|
||||
String use = rearUse(p, floor, east);
|
||||
if (!use.equals("bathroom") && !use.equals("washrooms")) continue;
|
||||
Area room = rear(p, floor, east);
|
||||
result.add(new BathPlan("floor_" + floor + "/" + (east ? "east" : "west") + "_bathroom_interior",
|
||||
floor, room.left() - 1, 12, room.right() + 1, 15, east ? 12 : 4, false));
|
||||
}
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
public static List<Bathroom> bathrooms(Parcel p) {
|
||||
validate(p);
|
||||
List<Bathroom> result = new ArrayList<>();
|
||||
for (var b : bathroomPlans(p)) {
|
||||
int x0 = rx(b.x0(), b.z0(), p.facing()), z0 = rz(b.x0(), b.z0(), p.facing());
|
||||
int x1 = rx(b.x1(), b.z1(), p.facing()), z1 = rz(b.x1(), b.z1(), p.facing());
|
||||
int doorZ = b.southDoor() ? b.z1() : b.z0();
|
||||
result.add(new Bathroom(b.name(), Math.min(x0, x1), b.floor() * STOREY, Math.min(z0, z1),
|
||||
Math.max(x0, x1), b.floor() * STOREY + 4, Math.max(z0, z1), rx(b.doorX(), doorZ, p.facing()),
|
||||
b.floor() * STOREY + 1, rz(b.doorX(), doorZ, p.facing())));
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private static void validate(Parcel p) {
|
||||
if (!p.kind().building() || p.width() != 17 || p.depth() != 17 || p.floors() < 2)
|
||||
throw new IllegalArgumentException("Generation-21 interiors require a 17x17 building with 2..5 floors");
|
||||
}
|
||||
|
||||
private static void add(List<Room> result, Parcel p, String name, int x, int y, int z) {
|
||||
result.add(new Room(name, rx(x, z, p.facing()), y, rz(x, z, p.facing())));
|
||||
}
|
||||
private static int rx(int x, int z, int facing) {
|
||||
return switch (facing) { case 0 -> x; case 1 -> 16 - z; case 2 -> 16 - x; default -> z; };
|
||||
}
|
||||
private static int rz(int x, int z, int facing) {
|
||||
return switch (facing) { case 0 -> z; case 1 -> x; case 2 -> 16 - z; default -> 16 - x; };
|
||||
}
|
||||
|
||||
private static final class Building {
|
||||
final Parcel p;
|
||||
final LostCityRenderer.BlockWriter output;
|
||||
final Rotation rotation;
|
||||
Building(Parcel p, LostCityRenderer.BlockWriter output) {
|
||||
this.p = p;
|
||||
this.output = output;
|
||||
rotation = switch (p.facing()) {
|
||||
case 0 -> Rotation.NONE; case 1 -> Rotation.CLOCKWISE_90;
|
||||
case 2 -> Rotation.CLOCKWISE_180; default -> Rotation.COUNTERCLOCKWISE_90;
|
||||
};
|
||||
}
|
||||
void set(int x, int y, int z, BlockState state) {
|
||||
output.set(rx(x, z, p.facing()), y, rz(x, z, p.facing()), state.rotate(rotation));
|
||||
}
|
||||
void set(int x, int y, int z, Block block) { set(x, y, z, block.defaultBlockState()); }
|
||||
void box(int x0, int y0, int z0, int x1, int y1, int z1, BlockState state) {
|
||||
for (int x = x0; x <= x1; x++) for (int z = z0; z <= z1; z++)
|
||||
for (int y = y0; y <= y1; y++) set(x, y, z, state);
|
||||
}
|
||||
BlockState facing(Block block, Direction direction) {
|
||||
return block.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, direction);
|
||||
}
|
||||
BlockState brick(int x, int y, int z) { return LostCityPalette.masonry(p.seed(), x, y, z); }
|
||||
BlockState weatheredBrick(int x, int y, int z) {
|
||||
// Weathering follows a few masonry joints, never private room partitions.
|
||||
int weather = roll(x, y, z, 12);
|
||||
return (weather == 0 ? Blocks.MOSSY_STONE_BRICKS : weather < 3
|
||||
? Blocks.CRACKED_STONE_BRICKS : Blocks.STONE_BRICKS).defaultBlockState();
|
||||
}
|
||||
int roll(int x, int y, int z, int bound) { return LostCityPalette.roll(p.seed(), x, y, z, bound); }
|
||||
|
||||
void render() {
|
||||
int roof = p.floors() * STOREY;
|
||||
box(0, -p.foundationDepth(), 0, 16, -1, 16, Blocks.STONE_BRICKS.defaultBlockState());
|
||||
box(0, 0, 0, 16, 0, 16, SMOOTH);
|
||||
box(0, 1, 0, 16, roof + 4, 16, AIR);
|
||||
shell(roof);
|
||||
architecturalEnvelope(roof);
|
||||
for (int floor = 0; floor < p.floors(); floor++) {
|
||||
partitions(floor);
|
||||
furnishings(floor);
|
||||
}
|
||||
stairs();
|
||||
entrance();
|
||||
roof(roof);
|
||||
// Only one deferred loot table per building; empty furniture is not a resource vault.
|
||||
int lootX = 6;
|
||||
int lootZ = 6;
|
||||
box(lootX, 2, lootZ, lootX, 3, lootZ, AIR);
|
||||
ResourceKey<LootTable> table = switch (p.kind()) {
|
||||
case LIBRARY, HALL -> BuiltInLootTables.VILLAGE_CARTOGRAPHER;
|
||||
case RESTAURANT -> BuiltInLootTables.VILLAGE_BUTCHER;
|
||||
case BARRACKS -> BuiltInLootTables.VILLAGE_FLETCHER;
|
||||
default -> BuiltInLootTables.VILLAGE_PLAINS_HOUSE;
|
||||
};
|
||||
output.chest(rx(lootX, lootZ, p.facing()), 1, rz(lootX, lootZ, p.facing()), table,
|
||||
p.seed() ^ 0x21c17a7eL ^ p.kind().ordinal());
|
||||
}
|
||||
|
||||
void shell(int roof) {
|
||||
for (int floor = 0; floor <= p.floors(); floor++) {
|
||||
int base = floor * STOREY;
|
||||
for (int x = 1; x <= 15; x++) for (int z = 1; z <= 15; z++)
|
||||
set(x, base, z, floor == p.floors() ? SMOOTH : floorMaterial(floor, x, z));
|
||||
if (floor == p.floors()) continue;
|
||||
// A pale ceiling and contrasting structural floor are separate layers.
|
||||
box(2, base + 4, 2, 14, base + 4, 14,
|
||||
Blocks.SMOOTH_STONE_SLAB.defaultBlockState().setValue(BlockStateProperties.SLAB_TYPE, SlabType.TOP));
|
||||
for (int y = 1; y <= 4; y++) for (int horizontal = 1; horizontal <= 15; horizontal++) {
|
||||
facade(horizontal, base + y, 1, horizontal, y, true);
|
||||
facade(horizontal, base + y, 15, horizontal, y, true);
|
||||
facade(1, base + y, horizontal, horizontal, y, false);
|
||||
facade(15, base + y, horizontal, horizontal, y, false);
|
||||
}
|
||||
// Floor bands remain intact even where windows have fallen out.
|
||||
for (int n = 1; n <= 15; n++) {
|
||||
set(n, base, 1, SMOOTH); set(n, base, 15, SMOOTH);
|
||||
set(1, base, n, SMOOTH); set(15, base, n, SMOOTH);
|
||||
}
|
||||
if (p.kind() == Kind.HOTEL || p.kind() == Kind.LIBRARY) {
|
||||
for (int x : new int[]{3, 13}) for (int y = 0; y <= 4; y++) set(x, base + y, 0, SMOOTH);
|
||||
}
|
||||
if (p.kind() == Kind.APARTMENT) for (int x = 11; x <= 14; x++)
|
||||
set(x, base + 1, 0, Blocks.SMOOTH_STONE_SLAB);
|
||||
if (p.kind() == Kind.RESTAURANT) box(10, base + 4, 0, 14, base + 4, 0,
|
||||
Blocks.OAK_SLAB.defaultBlockState());
|
||||
}
|
||||
if (p.kind() == Kind.HALL) {
|
||||
// The auditorium spans two storeys; its upper level is a guarded gallery.
|
||||
box(3, 4, 11, 13, 5, 14, AIR);
|
||||
for (int x = 3; x <= 13; x++) set(x, 6, 11, rail(true));
|
||||
for (int z = 12; z <= 14; z++) { set(3, 6, z, rail(false)); set(13, 6, z, rail(false)); }
|
||||
}
|
||||
}
|
||||
|
||||
BlockState floorMaterial(int floor, int x, int z) {
|
||||
if (x == 1 || z == 1 || x == 15 || z == 15) return SMOOTH;
|
||||
return switch (p.kind()) {
|
||||
case LIBRARY -> (x <= 5 || z >= 10 ? Blocks.OAK_PLANKS : Blocks.SMOOTH_STONE).defaultBlockState();
|
||||
case APARTMENT, HOTEL -> WOOD;
|
||||
case RESTAURANT -> ((x / 3 + z / 3) & 1) == 0 ? SMOOTH : TILE;
|
||||
case BARRACKS -> x <= 5 ? TILE : WOOD;
|
||||
default -> x == 7 || x == 9 ? TILE : SMOOTH;
|
||||
};
|
||||
}
|
||||
|
||||
BlockState pier() {
|
||||
return switch (p.kind()) {
|
||||
case APARTMENT, LIBRARY -> Blocks.STRIPPED_OAK_LOG.defaultBlockState();
|
||||
case HOTEL, RESTAURANT -> SMOOTH;
|
||||
case BARRACKS -> Blocks.STONE_BRICKS.defaultBlockState();
|
||||
default -> Blocks.CHISELED_STONE_BRICKS.defaultBlockState();
|
||||
};
|
||||
}
|
||||
|
||||
void facade(int x, int y, int z, int horizontal, int withinFloor, boolean eastWest) {
|
||||
boolean column = horizontal == 1 || horizontal == 6 || horizontal == 10 || horizontal == 15;
|
||||
boolean window = !column && (withinFloor == 2 || withinFloor == 3
|
||||
|| withinFloor == 1 && (p.kind() == Kind.LIBRARY || p.kind() == Kind.RESTAURANT || p.kind() == Kind.HALL));
|
||||
if (column) set(x, y, z, pier());
|
||||
else if (window) {
|
||||
if (roll(x, y / 5, z, 17) != 0) set(x, y, z,
|
||||
p.kind() == Kind.BARRACKS ? rail(eastWest) : pane(eastWest));
|
||||
} else set(x, y, z, withinFloor == 4 && horizontal % 5 == 0
|
||||
? weatheredBrick(x, y, z) : SMOOTH);
|
||||
}
|
||||
|
||||
/** Volume belongs to its use: projected bays, clipped corners and distinct roof profiles. */
|
||||
void architecturalEnvelope(int roof) {
|
||||
for (int floor = 0; floor < p.floors(); floor++) {
|
||||
int y = floor * STOREY;
|
||||
// Project the floor plates as a continuous, one-block cornice inside the parcel.
|
||||
for (int n = 1; n <= 15; n++) {
|
||||
set(n, y, 0, stairTrim(Direction.NORTH)); set(n, y, 16, stairTrim(Direction.SOUTH));
|
||||
set(0, y, n, stairTrim(Direction.WEST)); set(16, y, n, stairTrim(Direction.EAST));
|
||||
}
|
||||
switch (p.kind()) {
|
||||
case APARTMENT -> {
|
||||
// Bay windows increase the living room rather than drawing flat stripes on it.
|
||||
for (int z : new int[]{4, 5, 6, 11, 12, 13}) {
|
||||
set(16, y, z, Blocks.OAK_PLANKS);
|
||||
set(16, y + 1, z, Blocks.OAK_PLANKS);
|
||||
set(16, y + 2, z, Blocks.GLASS_PANE);
|
||||
set(16, y + 3, z, Blocks.GLASS_PANE);
|
||||
set(16, y + 4, z, Blocks.OAK_SLAB.defaultBlockState().setValue(BlockStateProperties.SLAB_TYPE, SlabType.TOP));
|
||||
}
|
||||
for (int z : new int[]{3, 7, 10, 14}) box(16, y + 1, z, 16, y + 4, z, pier());
|
||||
// Original wall remains the sill, with an open bay above it; privacy walls restore themselves later.
|
||||
for (int z : new int[]{4, 5, 6, 11, 12, 13}) box(15, y + 2, z, 15, y + 3, z, AIR);
|
||||
bevelFront(y);
|
||||
}
|
||||
case HOTEL -> {
|
||||
for (int x : new int[]{2, 5, 11, 14}) box(x, y + 1, 0, x, y + 4, 0, pier());
|
||||
for (int x : new int[]{3, 4, 12, 13}) set(x, y + 1, 0, Blocks.OAK_FENCE);
|
||||
box(2, y + 4, 0, 5, y + 4, 0, Blocks.OAK_PLANKS.defaultBlockState());
|
||||
box(11, y + 4, 0, 14, y + 4, 0, Blocks.OAK_PLANKS.defaultBlockState());
|
||||
}
|
||||
case LIBRARY -> {
|
||||
// Tall fins frame broad glazing and project beyond the reading rooms.
|
||||
for (int x : new int[]{2, 5, 11, 14}) box(x, y + 1, 0, x, y + 4, 0, Blocks.STRIPPED_OAK_LOG.defaultBlockState());
|
||||
for (int x : new int[]{3, 4, 12, 13}) {
|
||||
set(x, y + 4, 0, Blocks.OAK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
|
||||
}
|
||||
bevelFront(y);
|
||||
}
|
||||
case RESTAURANT -> {
|
||||
// Separate fascia and timber valance shelter the shop windows.
|
||||
for (int x = 10; x <= 14; x++) {
|
||||
set(x, y + 3, 0, Blocks.OAK_SLAB);
|
||||
set(x, y + 4, 0, Blocks.OAK_PLANKS);
|
||||
}
|
||||
for (int z : new int[]{3, 7, 11, 14}) set(16, y + 4, z, Blocks.ANDESITE);
|
||||
}
|
||||
case BARRACKS -> {
|
||||
for (int z : new int[]{2, 7, 12, 15}) box(0, y + 1, z, 0, y + 4, z, pier());
|
||||
for (int x : new int[]{2, 5, 11, 14}) box(x, y + 1, 0, x, y + 4, 0, Blocks.STONE_BRICKS.defaultBlockState());
|
||||
for (int x : new int[]{3, 4, 12, 13}) set(x, y + 1, 0, Blocks.CHISELED_STONE_BRICKS);
|
||||
}
|
||||
case HALL -> {
|
||||
for (int x : new int[]{2, 5, 11, 14}) box(x, y + 1, 0, x, y + 4, 0, pier());
|
||||
for (int x = 2; x <= 14; x++) set(x, y + 4, 0,
|
||||
Blocks.ANDESITE_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
|
||||
}
|
||||
default -> throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BlockState stairTrim(Direction outward) {
|
||||
Block block = Blocks.STONE_BRICK_STAIRS;
|
||||
return facing(block, outward.getOpposite()).setValue(BlockStateProperties.HALF,
|
||||
net.minecraft.world.level.block.state.properties.Half.TOP);
|
||||
}
|
||||
|
||||
void bevelFront(int y) {
|
||||
// Remove two rectangular corners and close them with actual stepped returns.
|
||||
for (int x : new int[]{1, 15}) {
|
||||
box(x, y, 1, x, y + 4, 1, AIR);
|
||||
int adjacent = x == 1 ? 2 : 14;
|
||||
box(adjacent, y + 1, 1, adjacent, y + 4, 1, pier());
|
||||
box(x, y + 1, 2, x, y + 4, 2, pier());
|
||||
}
|
||||
}
|
||||
|
||||
void partitions(int floor) {
|
||||
int y = floor * STOREY;
|
||||
int front = frontWall(p, floor);
|
||||
// The west stairwell, central corridor and vestibule are independent of furniture.
|
||||
box(5, y + 1, 2, 5, y + 4, 9, SMOOTH);
|
||||
door(5, y + 1, 8, Direction.WEST, false);
|
||||
box(10, y + 1, 2, 10, y + 4, front, SMOOTH);
|
||||
box(10, y + 1, front, 15, y + 4, front, SMOOTH);
|
||||
door(10, y + 1, 6, Direction.EAST, false);
|
||||
if (floor == 0) {
|
||||
box(6, 1, 4, 9, 4, 4, SMOOTH);
|
||||
door(8, 1, 4, Direction.SOUTH, false);
|
||||
}
|
||||
if (p.kind() == Kind.HALL && floor < 2) {
|
||||
if (floor == 0) {
|
||||
box(2, 1, 10, 14, 4, 10, SMOOTH);
|
||||
door(7, 1, 10, Direction.SOUTH, true);
|
||||
door(8, 1, 10, Direction.SOUTH, false);
|
||||
}
|
||||
} else {
|
||||
Area west = rear(p, floor, false), east = rear(p, floor, true);
|
||||
box(1, y + 1, west.front() - 1, west.right() + 1, y + 4, west.front() - 1, SMOOTH);
|
||||
box(west.right() + 1, y + 1, west.front() - 1, west.right() + 1, y + 4, 15, SMOOTH);
|
||||
box(east.left() - 1, y + 1, east.front() - 1, 15, y + 4, east.front() - 1, SMOOTH);
|
||||
box(east.left() - 1, y + 1, east.front() - 1, east.left() - 1, y + 4, 15, SMOOTH);
|
||||
door(west.right() + 1, y + 1, 11, Direction.WEST, false);
|
||||
door(east.left() - 1, y + 1, 11, Direction.EAST, false);
|
||||
}
|
||||
// A hanging lantern touches a full ceiling block, never the recessed underside of a top slab.
|
||||
set(8, y + 4, 8, SMOOTH);
|
||||
set(8, y + 3, 8, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
|
||||
// Weathering stays on facades and roofs: it must not replace a privacy partition.
|
||||
}
|
||||
|
||||
void furnishings(int floor) {
|
||||
int y = floor * STOREY;
|
||||
int variant = layout(p, floor);
|
||||
switch (p.kind()) {
|
||||
case APARTMENT -> {
|
||||
if (variant == 0) { kitchenFront(y); bedroom(floor, false); }
|
||||
else { frontBedroom(floor); kitchenRear(floor, false); }
|
||||
}
|
||||
case HOTEL -> {
|
||||
if (floor == 0) { reception(y); lounge(floor, false); laundry(floor); }
|
||||
else {
|
||||
anchoredBed(14, y + 1, frontWall(p, floor) - 2, Direction.SOUTH);
|
||||
set(11, y + 1, 5, Blocks.BARREL); set(11, y + 2, 5, Blocks.FLOWER_POT);
|
||||
if (variant == 0) { bedroom(floor, false); bedroom(floor, true); }
|
||||
else lounge(floor, false);
|
||||
}
|
||||
}
|
||||
case LIBRARY -> {
|
||||
if (variant == 0) {
|
||||
box(14, y + 1, 2, 14, y + 3, 6, Blocks.BOOKSHELF.defaultBlockState());
|
||||
box(12, y + 1, 2, 12, y + 2, 3, Blocks.BOOKSHELF.defaultBlockState());
|
||||
desk(12, y, 5, Direction.SOUTH);
|
||||
readingRoom(floor, false); archive(floor, true);
|
||||
} else {
|
||||
readingFront(y); readingRoom(floor, false);
|
||||
set(2, y + 1, 14, Blocks.CARTOGRAPHY_TABLE);
|
||||
}
|
||||
}
|
||||
case RESTAURANT -> {
|
||||
if (variant == 0) {
|
||||
table(13, y, 3); table(13, y, 5);
|
||||
set(11, y + 1, 2, Blocks.POTTED_DEAD_BUSH);
|
||||
kitchenRear(floor, true);
|
||||
} else {
|
||||
kitchenFront(y); diningRear(floor, false); pantry(floor, true);
|
||||
}
|
||||
}
|
||||
case BARRACKS -> {
|
||||
if (variant == 0) {
|
||||
anchoredBed(12, y + 1, 3, Direction.NORTH); anchoredBed(14, y + 1, 3, Direction.NORTH);
|
||||
set(11, y + 1, 2, Blocks.BARREL); set(11, y + 2, 2, Blocks.BARREL);
|
||||
seat(13, y + 1, 6, Direction.NORTH);
|
||||
} else { reception(y); bedroom(floor, false); }
|
||||
box(12, y + 1, 14, 14, y + 2, 14, Blocks.BARREL.defaultBlockState());
|
||||
set(14, y + 1, 11, Blocks.FLETCHING_TABLE);
|
||||
set(12, y + 1, 11, Blocks.GRINDSTONE.defaultBlockState()
|
||||
.setValue(BlockStateProperties.ATTACH_FACE,
|
||||
net.minecraft.world.level.block.state.properties.AttachFace.FLOOR));
|
||||
}
|
||||
case HALL -> {
|
||||
if (floor == 0 || floor > 1 && variant == 0) reception(y); else readingFront(y);
|
||||
if (floor == 0) auditorium();
|
||||
else if (floor >= 2) {
|
||||
if (variant == 0) readingRoom(floor, false);
|
||||
else { archive(floor, false); readingRoom(floor, true); }
|
||||
}
|
||||
}
|
||||
default -> throw new IllegalArgumentException("Not an interior kind");
|
||||
}
|
||||
for (var bath : bathroomPlans(p)) if (bath.floor() == floor) bathroom(bath);
|
||||
}
|
||||
|
||||
void kitchenFront(int y) {
|
||||
kitchenLine(y, 11, 2, Direction.SOUTH);
|
||||
table(13, y, 5);
|
||||
}
|
||||
|
||||
void kitchenRear(int floor, boolean east) {
|
||||
int y = floor * STOREY;
|
||||
Area room = rear(p, floor, east);
|
||||
kitchenLine(y, room.left(), 14, Direction.NORTH);
|
||||
if (east) {
|
||||
set(14, y + 1, 11, SMOOTH); set(14, y + 2, 11, Blocks.FLOWER_POT);
|
||||
set(12, y + 1, 11, Blocks.CRAFTING_TABLE);
|
||||
} else table(room.left() + 2, y, 11);
|
||||
}
|
||||
|
||||
void kitchenLine(int y, int left, int z, Direction face) {
|
||||
set(left, y + 1, z, facing(Blocks.FURNACE, face));
|
||||
set(left + 1, y + 1, z, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 2));
|
||||
set(left + 2, y + 1, z, facing(Blocks.SMOKER, face));
|
||||
set(left + 3, y + 1, z, Blocks.BARREL);
|
||||
box(left, y + 3, z, left + 3, y + 3, z, SMOOTH);
|
||||
set(left + 2, y + 2, z, Blocks.IRON_TRAPDOOR);
|
||||
for (int x = left; x <= left + 3; x++) set(x, y + 3, z, Blocks.SMOOTH_STONE);
|
||||
for (int x = left; x <= left + 3; x++) set(x, y, z, (x & 1) == 0 ? SMOOTH : TILE);
|
||||
}
|
||||
|
||||
void bedroom(int floor, boolean east) {
|
||||
int y = floor * STOREY;
|
||||
Area room = rear(p, floor, east);
|
||||
anchoredBed(east ? room.right() - 1 : room.left() + 1, y + 1, 13, east ? Direction.EAST : Direction.WEST);
|
||||
set(east ? room.right() : room.left(), y + 1, 11, Blocks.BARREL);
|
||||
desk(east ? room.left() + 1 : room.right() - 1, y, 11, Direction.SOUTH);
|
||||
set(east ? room.left() : room.right(), y + 1, 14, Blocks.BOOKSHELF);
|
||||
}
|
||||
|
||||
void frontBedroom(int floor) {
|
||||
int y = floor * STOREY;
|
||||
anchoredBed(14, y + 1, 3, Direction.NORTH);
|
||||
set(11, y + 1, 2, Blocks.BARREL); set(11, y + 2, 2, Blocks.FLOWER_POT);
|
||||
desk(12, y, 5, Direction.SOUTH);
|
||||
}
|
||||
|
||||
void bathroom(BathPlan bath) {
|
||||
int y = bath.floor() * STOREY;
|
||||
for (int x = bath.x0(); x <= bath.x1(); x++) for (int z = bath.z0(); z <= bath.z1(); z++) {
|
||||
set(x, y, z, ((x + z) & 1) == 0 ? SMOOTH : TILE);
|
||||
set(x, y + 4, z, SMOOTH);
|
||||
boolean wall = x == bath.x0() || x == bath.x1() || z == bath.z0() || z == bath.z1();
|
||||
for (int up = 1; up <= 3; up++) set(x, y + up, z, wall ? SMOOTH : AIR);
|
||||
}
|
||||
int doorZ = bath.southDoor() ? bath.z1() : bath.z0();
|
||||
door(bath.doorX(), y + 1, doorZ, bath.southDoor() ? Direction.NORTH : Direction.SOUTH, false);
|
||||
int fixtureZ = bath.southDoor() ? bath.z0() + 1 : bath.z1() - 1;
|
||||
int toiletX = bath.southDoor() ? bath.x1() - 2 : bath.x0() + 1;
|
||||
Direction view = bath.southDoor() ? Direction.SOUTH : Direction.EAST;
|
||||
set(toiletX, y + 1, fixtureZ, seating(Blocks.POLISHED_ANDESITE_STAIRS, view));
|
||||
set(toiletX, y + 2, fixtureZ, Blocks.IRON_TRAPDOOR);
|
||||
set(bath.x1() - 1, y + 1, fixtureZ, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 2));
|
||||
}
|
||||
|
||||
void reception(int y) {
|
||||
box(12, y + 1, 5, 14, y + 1, 5, Blocks.OAK_PLANKS.defaultBlockState());
|
||||
set(13, y + 2, 5, Blocks.FLOWER_POT);
|
||||
set(14, y + 1, 2, Blocks.BOOKSHELF); set(14, y + 2, 2, Blocks.BARREL);
|
||||
desk(12, y, 3, Direction.SOUTH);
|
||||
set(11, y + 1, 2, Blocks.POTTED_DEAD_BUSH);
|
||||
}
|
||||
|
||||
void laundry(int floor) {
|
||||
int y = floor * STOREY;
|
||||
for (int z = 11; z <= 12; z++) set(14, y + 1, z, Blocks.WATER_CAULDRON.defaultBlockState()
|
||||
.setValue(BlockStateProperties.LEVEL_CAULDRON, z == 11 ? 1 : 2));
|
||||
box(12, y + 1, 14, 14, y + 1, 14, Blocks.BARREL.defaultBlockState());
|
||||
// The rack stands on the storage counter; recessed rear glazing cannot carry it.
|
||||
for (int x : new int[]{12, 14}) set(x, y + 2, 14, Blocks.OAK_PLANKS);
|
||||
box(12, y + 3, 14, 14, y + 3, 14, Blocks.OAK_SLAB.defaultBlockState());
|
||||
set(12, y + 1, 11, Blocks.LOOM);
|
||||
}
|
||||
|
||||
void lounge(int floor, boolean east) {
|
||||
int y = floor * STOREY;
|
||||
Area room = rear(p, floor, east);
|
||||
box(room.left(), y + 1, 14, room.left() + 2, y + 1, 14, seating(Blocks.OAK_STAIRS, Direction.NORTH));
|
||||
desk(room.left() + 1, y, 11, Direction.SOUTH);
|
||||
set(room.left(), y + 1, 12, Blocks.BOOKSHELF);
|
||||
}
|
||||
|
||||
void readingRoom(int floor, boolean east) {
|
||||
int y = floor * STOREY;
|
||||
Area room = rear(p, floor, east);
|
||||
desk(east ? room.left() + 1 : room.right() - 1, y, 11, Direction.SOUTH);
|
||||
box(room.left(), y + 1, 14, room.right(), y + 2, 14, Blocks.BOOKSHELF.defaultBlockState());
|
||||
set(east ? room.right() : room.left(), y + 1, 11, facing(Blocks.LECTERN, Direction.NORTH));
|
||||
}
|
||||
|
||||
void readingFront(int y) {
|
||||
desk(12, y, 3, Direction.SOUTH);
|
||||
box(14, y + 1, 2, 14, y + 2, 5, Blocks.BOOKSHELF.defaultBlockState());
|
||||
set(11, y + 1, 2, facing(Blocks.LECTERN, Direction.SOUTH));
|
||||
}
|
||||
|
||||
void archive(int floor, boolean east) {
|
||||
int y = floor * STOREY;
|
||||
Area room = rear(p, floor, east);
|
||||
box(room.left(), y + 1, 14, room.right(), y + 3, 14, Blocks.BOOKSHELF.defaultBlockState());
|
||||
box(east ? room.right() : room.left(), y + 1, 11, east ? room.right() : room.left(), y + 2, 12, Blocks.BARREL.defaultBlockState());
|
||||
set(room.left() + 1, y + 1, 12, facing(Blocks.LECTERN, Direction.NORTH));
|
||||
}
|
||||
|
||||
void pantry(int floor, boolean east) {
|
||||
int y = floor * STOREY;
|
||||
Area room = rear(p, floor, east);
|
||||
box(room.left(), y + 1, 14, room.right(), y + 2, 14, Blocks.BARREL.defaultBlockState());
|
||||
set(room.right(), y + 1, 11, Blocks.CRAFTING_TABLE);
|
||||
set(room.right(), y + 2, 11, Blocks.FLOWER_POT);
|
||||
}
|
||||
|
||||
void diningRear(int floor, boolean east) {
|
||||
Area room = rear(p, floor, east);
|
||||
int y = floor * STOREY;
|
||||
table(room.left() + 2, y, 12);
|
||||
box(room.left(), y + 1, 14, room.right(), y + 1, 14, seating(Blocks.OAK_STAIRS, Direction.NORTH));
|
||||
}
|
||||
|
||||
void auditorium() {
|
||||
for (int x : new int[]{3, 4, 5, 11, 12, 13}) seat(x, 1, 11, Direction.SOUTH);
|
||||
box(3, 1, 13, 13, 1, 14, WOOD);
|
||||
set(8, 1, 12, facing(Blocks.OAK_STAIRS, Direction.SOUTH));
|
||||
set(8, 2, 14, facing(Blocks.LECTERN, Direction.NORTH));
|
||||
for (int x : new int[]{3, 13}) {
|
||||
box(x, 2, 14, x, 3, 14, WOOD);
|
||||
set(x, 4, 14, Blocks.STONE_BRICK_SLAB);
|
||||
}
|
||||
}
|
||||
|
||||
void stairs() {
|
||||
for (int floor = 0; floor < p.floors(); floor++) {
|
||||
int base = floor * STOREY;
|
||||
for (int step = 1; step <= STOREY; step++) {
|
||||
int z = step + 2;
|
||||
box(2, base + step, z, 3, base + step + 3, z, AIR);
|
||||
for (int x = 2; x <= 3; x++) {
|
||||
// A one-block inclined spine joins successive treads physically, without
|
||||
// filling the headroom above the preceding storey's matching flight.
|
||||
set(x, base + step - 1, z, Blocks.STONE_BRICKS);
|
||||
set(x, base + step, z, facing(Blocks.STONE_BRICK_STAIRS, Direction.SOUTH));
|
||||
}
|
||||
}
|
||||
box(2, base + STOREY, 8, 4, base + STOREY, 9, TILE);
|
||||
box(2, base + STOREY + 1, 8, 4, base + STOREY + 3, 9, AIR);
|
||||
}
|
||||
// This full-height side aisle joins every lower stair approach to its landing door.
|
||||
for (int floor = 0; floor <= p.floors(); floor++) {
|
||||
int y = floor * STOREY;
|
||||
box(4, y + 1, 2, 4, y + 3, 9, AIR);
|
||||
}
|
||||
}
|
||||
|
||||
void entrance() {
|
||||
for (int x : new int[]{6, 9}) box(x, 1, 0, x, 3, 0, SMOOTH);
|
||||
box(6, 4, 0, 9, 4, 1, Blocks.STONE_BRICK_SLAB.defaultBlockState());
|
||||
box(7, 0, 0, 8, 0, 3, TILE);
|
||||
// Native SOUTH/RIGHT opens against the west jamb; SOUTH/LEFT against the east jamb.
|
||||
door(7, 1, 1, Direction.SOUTH, true);
|
||||
door(8, 1, 1, Direction.SOUTH, false);
|
||||
set(4, 1, 0, Blocks.POTTED_DEAD_BUSH);
|
||||
set(12, 1, 16, Blocks.COMPOSTER);
|
||||
}
|
||||
|
||||
void roof(int y) {
|
||||
// Clipped parapets and a heavy coping keep each accessible roof legible from below.
|
||||
for (int n = 2; n <= 14; n++) {
|
||||
set(n, y + 1, 1, Blocks.STONE_BRICKS); set(n, y + 1, 15, Blocks.STONE_BRICKS);
|
||||
set(1, y + 1, n, Blocks.STONE_BRICKS); set(15, y + 1, n, Blocks.STONE_BRICKS);
|
||||
set(n, y + 2, 1, Blocks.STONE_BRICK_SLAB); set(n, y + 2, 15, Blocks.STONE_BRICK_SLAB);
|
||||
set(1, y + 2, n, Blocks.STONE_BRICK_SLAB); set(15, y + 2, n, Blocks.STONE_BRICK_SLAB);
|
||||
}
|
||||
switch (p.kind()) {
|
||||
case APARTMENT -> {
|
||||
// Two small roof gardens and a timber portico leave the stair landing and central crossing free.
|
||||
roofPlanter(10, y, 4); roofPlanter(10, y, 12);
|
||||
for (int x : new int[]{6, 13}) box(x, y + 1, 14, x, y + 3, 14, Blocks.STRIPPED_OAK_LOG.defaultBlockState());
|
||||
box(6, y + 4, 14, 13, y + 4, 14, Blocks.OAK_SLAB.defaultBlockState());
|
||||
desk(8, y, 12, Direction.NORTH);
|
||||
}
|
||||
case HOTEL -> {
|
||||
roofRoom(y, 9, 3, 14, 7, Blocks.SMOOTH_STONE);
|
||||
door(9, y + 1, 5, Direction.EAST, false);
|
||||
set(12, y + 1, 4, Blocks.BARREL); set(13, y + 1, 4, Blocks.LOOM);
|
||||
box(11, y + 1, 12, 13, y + 1, 12, seating(Blocks.OAK_STAIRS, Direction.NORTH));
|
||||
}
|
||||
case LIBRARY -> {
|
||||
// One transparent cap above a hollow timber curb, open all the way to the stacks.
|
||||
// No opaque former roof plate or second glass floor remains under this canopy.
|
||||
for (int x = 10; x <= 13; x++) for (int z = 3; z <= 7; z++) {
|
||||
boolean edge = x == 10 || x == 13 || z == 3 || z == 7;
|
||||
set(x, y + 1, z, edge ? Blocks.STRIPPED_OAK_LOG.defaultBlockState() : AIR);
|
||||
set(x, y + 2, z, edge ? Blocks.OAK_SLAB : Blocks.GLASS);
|
||||
}
|
||||
box(11, y - 1, 4, 12, y + 1, 6, AIR);
|
||||
desk(11, y, 12, Direction.NORTH); set(14, y + 1, 12, Blocks.LECTERN);
|
||||
}
|
||||
case RESTAURANT -> {
|
||||
// A real dining terrace and rear kitchen extraction with full supporting columns.
|
||||
table(11, y, 6); table(11, y, 11);
|
||||
for (int z : new int[]{3, 14}) box(14, y + 1, z, 14, y + 3, z, Blocks.STRIPPED_OAK_LOG.defaultBlockState());
|
||||
box(14, y + 4, 3, 14, y + 4, 14, Blocks.OAK_PLANKS.defaultBlockState());
|
||||
box(6, y + 1, 14, 7, y + 3, 14, Blocks.STONE_BRICKS.defaultBlockState());
|
||||
set(6, y + 4, 14, Blocks.IRON_BARS); set(7, y + 4, 14, Blocks.IRON_BARS);
|
||||
}
|
||||
case BARRACKS -> {
|
||||
// Enclosed stair head gives the barracks a service tower, without hiding its usable exit.
|
||||
// The whole last flight sits inside the stair head: a front wall at z=6
|
||||
// would cross the headroom over its penultimate step.
|
||||
roofRoom(y, 1, 2, 5, 10, Blocks.SMOOTH_STONE);
|
||||
door(5, y + 1, 8, Direction.EAST, false);
|
||||
box(10, y + 1, 12, 13, y + 1, 14, Blocks.POLISHED_ANDESITE.defaultBlockState());
|
||||
set(11, y + 2, 13, Blocks.IRON_TRAPDOOR);
|
||||
}
|
||||
case HALL -> {
|
||||
for (int step = 0; step < 3; step++) {
|
||||
int edge = 2 + step * 2;
|
||||
box(edge, y + 1 + step, 1, 16 - edge, y + 1 + step, 2, Blocks.STONE_BRICKS.defaultBlockState());
|
||||
}
|
||||
for (int x : new int[]{6, 10}) set(x, y + 4, 2, Blocks.CHISELED_STONE_BRICKS);
|
||||
desk(11, y, 12, Direction.NORTH);
|
||||
}
|
||||
default -> throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
void roofRoom(int y, int x0, int z0, int x1, int z1, Block wall) {
|
||||
for (int x = x0; x <= x1; x++) for (int z = z0; z <= z1; z++) {
|
||||
boolean edge = x == x0 || x == x1 || z == z0 || z == z1;
|
||||
for (int up = 1; up <= 3; up++) set(x, y + up, z, edge ? wall.defaultBlockState() : AIR);
|
||||
// A bottom slab touches the full-height walls immediately below it.
|
||||
// A top slab here would float half a block above the pavilion.
|
||||
set(x, y + 4, z, Blocks.STONE_BRICK_SLAB.defaultBlockState());
|
||||
}
|
||||
}
|
||||
void roofPlanter(int x, int y, int z) {
|
||||
for (int dx = 0; dx <= 3; dx++) set(x + dx, y + 1, z, Blocks.STONE_BRICKS);
|
||||
set(x + 1, y + 1, z, Blocks.DIRT); set(x + 2, y + 1, z, Blocks.DIRT);
|
||||
set(x + 1, y + 2, z, Blocks.OXEYE_DAISY); set(x + 2, y + 2, z, Blocks.CORNFLOWER);
|
||||
}
|
||||
|
||||
void door(int x, int y, int z, Direction facing, boolean rightHinge) {
|
||||
BlockState state = facing(Blocks.OAK_DOOR, facing)
|
||||
.setValue(BlockStateProperties.DOOR_HINGE, rightHinge ? DoorHingeSide.RIGHT : DoorHingeSide.LEFT)
|
||||
.setValue(BlockStateProperties.OPEN, false)
|
||||
.setValue(BlockStateProperties.POWERED, false);
|
||||
set(x, y, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.LOWER));
|
||||
set(x, y + 1, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.UPPER));
|
||||
}
|
||||
void anchoredBed(int x, int y, int z, Direction facing) {
|
||||
BlockState state = facing(Blocks.BED.white(), facing);
|
||||
set(x, y, z, state.setValue(BlockStateProperties.BED_PART, BedPart.FOOT));
|
||||
set(x + facing.getStepX(), y, z + facing.getStepZ(), state.setValue(BlockStateProperties.BED_PART, BedPart.HEAD));
|
||||
int wallX = x + facing.getStepX() * 2, wallZ = z + facing.getStepZ() * 2;
|
||||
Direction alongWall = facing.getClockWise();
|
||||
for (int width = -1; width <= 1; width++) for (int up = 0; up <= 3; up++)
|
||||
set(wallX + alongWall.getStepX() * width, y + up, wallZ + alongWall.getStepZ() * width, SMOOTH);
|
||||
}
|
||||
void table(int x, int y, int z) {
|
||||
set(x, y + 1, z, Blocks.OAK_FENCE); set(x, y + 2, z, Blocks.OAK_PRESSURE_PLATE);
|
||||
seat(x - 1, y + 1, z, Direction.EAST);
|
||||
seat(x + 1, y + 1, z, Direction.WEST);
|
||||
}
|
||||
void desk(int x, int y, int z, Direction approachSide) {
|
||||
set(x, y + 1, z, Blocks.OAK_FENCE); set(x, y + 2, z, Blocks.OAK_PRESSURE_PLATE);
|
||||
seat(x + approachSide.getStepX(), y + 1, z + approachSide.getStepZ(), approachSide.getOpposite());
|
||||
}
|
||||
void seat(int x, int y, int z, Direction lookingToward) {
|
||||
set(x, y, z, seating(Blocks.OAK_STAIRS, lookingToward));
|
||||
}
|
||||
BlockState seating(Block stair, Direction lookingToward) {
|
||||
// Stair FACING points uphill, into its full-height back, opposite a seated player's view.
|
||||
return facing(stair, lookingToward.getOpposite());
|
||||
}
|
||||
BlockState rail(boolean eastWest) { return connected(Blocks.IRON_BARS, eastWest); }
|
||||
BlockState pane(boolean eastWest) { return connected(Blocks.GLASS_PANE, eastWest); }
|
||||
BlockState connected(Block block, boolean eastWest) {
|
||||
return block.defaultBlockState().setValue(BlockStateProperties.EAST, eastWest)
|
||||
.setValue(BlockStateProperties.WEST, eastWest).setValue(BlockStateProperties.NORTH, !eastWest)
|
||||
.setValue(BlockStateProperties.SOUTH, !eastWest);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package fr.koka.sanctuary.worldgen.city;
|
||||
|
||||
import java.util.Arrays;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
|
||||
/** Generation20 surface parcel. Its distinct native type preserves all published city renderers. */
|
||||
public final class LostCityPiece20 extends StructurePiece {
|
||||
private static final int SCHEMA = 1;
|
||||
private final LostCityPlan.Parcel parcel;
|
||||
|
||||
public LostCityPiece20(LostCityPlan.Parcel parcel) {
|
||||
super(LostCityStructures.PIECE20, 0, bounds(parcel));
|
||||
this.parcel = parcel;
|
||||
setOrientation(null);
|
||||
}
|
||||
|
||||
public LostCityPiece20(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||
super(LostCityStructures.PIECE20, tag);
|
||||
if (tag.getInt("SanctuarySchema").orElseThrow() != SCHEMA)
|
||||
throw new IllegalArgumentException("Unsupported Lost City piece schema");
|
||||
parcel = new LostCityPlan.Parcel(LostCityPlan.Kind.valueOf(tag.getString("Kind").orElseThrow()),
|
||||
tag.getInt("MinX").orElseThrow(), tag.getInt("MinZ").orElseThrow(),
|
||||
tag.getInt("Width").orElseThrow(), tag.getInt("Depth").orElseThrow(),
|
||||
tag.getInt("FloorY").orElseThrow(), tag.getInt("Floors").orElseThrow(),
|
||||
tag.getInt("Facing").orElseThrow(), tag.getLong("ParcelSeed").orElseThrow(),
|
||||
tag.getInt("FoundationDepth").orElseThrow(), tag.getIntArray("SurfaceHeights").orElseThrow());
|
||||
if (!getBoundingBox().equals(bounds(parcel)))
|
||||
throw new IllegalArgumentException("Lost City parcel disagrees with its saved native bounding box");
|
||||
setOrientation(null);
|
||||
}
|
||||
|
||||
public LostCityPlan.Parcel parcel() { return parcel; }
|
||||
|
||||
@Override public BlockPos getLocatorPosition() {
|
||||
int x = parcel.width() / 2, z = parcel.depth() / 2;
|
||||
return new BlockPos(parcel.minX() + x, parcel.surfaceY(x, z) + 1, parcel.minZ() + z);
|
||||
}
|
||||
|
||||
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||
tag.putInt("SanctuarySchema", SCHEMA);
|
||||
tag.putString("Kind", parcel.kind().name());
|
||||
tag.putInt("MinX", parcel.minX()); tag.putInt("MinZ", parcel.minZ());
|
||||
tag.putInt("Width", parcel.width()); tag.putInt("Depth", parcel.depth());
|
||||
tag.putInt("FloorY", parcel.floorY()); tag.putInt("Floors", parcel.floors());
|
||||
tag.putInt("Facing", parcel.facing()); tag.putLong("ParcelSeed", parcel.seed());
|
||||
tag.putInt("FoundationDepth", parcel.foundationDepth());
|
||||
tag.putIntArray("SurfaceHeights", parcel.surfaceHeights());
|
||||
}
|
||||
|
||||
@Override public void postProcess(WorldGenLevel level, StructureManager structures, ChunkGenerator generator,
|
||||
RandomSource random, BoundingBox chunkClip, ChunkPos chunkPos, BlockPos pivot) {
|
||||
LostCityStructures.placing(() -> render(parcel, new LostCityRenderer.BlockWriter() {
|
||||
@Override public void set(int x, int y, int z, BlockState block) {
|
||||
BlockPos pos = world(x, y, z);
|
||||
if (getBoundingBox().isInside(pos) && chunkClip.isInside(pos))
|
||||
placeBlock(level, block, pos.getX(), pos.getY(), pos.getZ(), chunkClip);
|
||||
}
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
|
||||
BlockPos pos = world(x, y, z);
|
||||
if (getBoundingBox().isInside(pos) && chunkClip.isInside(pos))
|
||||
createChest(level, chunkClip, RandomSource.create(seed), pos, loot, null);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private static void render(LostCityPlan.Parcel parcel, LostCityRenderer.BlockWriter writer) {
|
||||
LostCitySurface20.render(parcel, writer);
|
||||
}
|
||||
|
||||
private BlockPos world(int x, int y, int z) {
|
||||
return new BlockPos(parcel.minX() + x, parcel.floorY() + y, parcel.minZ() + z);
|
||||
}
|
||||
|
||||
private static BoundingBox bounds(LostCityPlan.Parcel parcel) {
|
||||
int low = Math.min(parcel.floorY(), Arrays.stream(parcel.surfaceHeights()).min().orElse(parcel.floorY()))
|
||||
- parcel.foundationDepth();
|
||||
int high = Math.max(parcel.maxY(), Arrays.stream(parcel.surfaceHeights()).max().orElse(parcel.floorY()) + 5);
|
||||
return new BoundingBox(parcel.minX(), low, parcel.minZ(), parcel.maxX(), high, parcel.maxZ());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package fr.koka.sanctuary.worldgen.city;
|
||||
|
||||
import java.util.Arrays;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
|
||||
/** Generation21 surface parcel. Its distinct native type preserves all published city renderers. */
|
||||
public final class LostCityPiece21 extends StructurePiece {
|
||||
private static final int SCHEMA = 1;
|
||||
private final LostCityPlan.Parcel parcel;
|
||||
|
||||
public LostCityPiece21(LostCityPlan.Parcel parcel) {
|
||||
super(LostCityStructures.PIECE21, 0, bounds(parcel));
|
||||
this.parcel = parcel;
|
||||
setOrientation(null);
|
||||
}
|
||||
|
||||
public LostCityPiece21(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||
super(LostCityStructures.PIECE21, tag);
|
||||
if (tag.getInt("SanctuarySchema").orElseThrow() != SCHEMA)
|
||||
throw new IllegalArgumentException("Unsupported Lost City piece schema");
|
||||
parcel = new LostCityPlan.Parcel(LostCityPlan.Kind.valueOf(tag.getString("Kind").orElseThrow()),
|
||||
tag.getInt("MinX").orElseThrow(), tag.getInt("MinZ").orElseThrow(),
|
||||
tag.getInt("Width").orElseThrow(), tag.getInt("Depth").orElseThrow(),
|
||||
tag.getInt("FloorY").orElseThrow(), tag.getInt("Floors").orElseThrow(),
|
||||
tag.getInt("Facing").orElseThrow(), tag.getLong("ParcelSeed").orElseThrow(),
|
||||
tag.getInt("FoundationDepth").orElseThrow(), tag.getIntArray("SurfaceHeights").orElseThrow());
|
||||
if (!getBoundingBox().equals(bounds(parcel)))
|
||||
throw new IllegalArgumentException("Lost City parcel disagrees with its saved native bounding box");
|
||||
setOrientation(null);
|
||||
}
|
||||
|
||||
public LostCityPlan.Parcel parcel() { return parcel; }
|
||||
|
||||
@Override public BlockPos getLocatorPosition() {
|
||||
int x = parcel.width() / 2, z = parcel.depth() / 2;
|
||||
return new BlockPos(parcel.minX() + x, parcel.surfaceY(x, z) + 1, parcel.minZ() + z);
|
||||
}
|
||||
|
||||
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||
tag.putInt("SanctuarySchema", SCHEMA);
|
||||
tag.putString("Kind", parcel.kind().name());
|
||||
tag.putInt("MinX", parcel.minX()); tag.putInt("MinZ", parcel.minZ());
|
||||
tag.putInt("Width", parcel.width()); tag.putInt("Depth", parcel.depth());
|
||||
tag.putInt("FloorY", parcel.floorY()); tag.putInt("Floors", parcel.floors());
|
||||
tag.putInt("Facing", parcel.facing()); tag.putLong("ParcelSeed", parcel.seed());
|
||||
tag.putInt("FoundationDepth", parcel.foundationDepth());
|
||||
tag.putIntArray("SurfaceHeights", parcel.surfaceHeights());
|
||||
}
|
||||
|
||||
@Override public void postProcess(WorldGenLevel level, StructureManager structures, ChunkGenerator generator,
|
||||
RandomSource random, BoundingBox chunkClip, ChunkPos chunkPos, BlockPos pivot) {
|
||||
LostCityStructures.placing(() -> render(parcel, new LostCityRenderer.BlockWriter() {
|
||||
@Override public void set(int x, int y, int z, BlockState block) {
|
||||
BlockPos pos = world(x, y, z);
|
||||
if (getBoundingBox().isInside(pos) && chunkClip.isInside(pos))
|
||||
placeBlock(level, block, pos.getX(), pos.getY(), pos.getZ(), chunkClip);
|
||||
}
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
|
||||
BlockPos pos = world(x, y, z);
|
||||
if (getBoundingBox().isInside(pos) && chunkClip.isInside(pos))
|
||||
createChest(level, chunkClip, RandomSource.create(seed), pos, loot, null);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private static void render(LostCityPlan.Parcel parcel, LostCityRenderer.BlockWriter writer) {
|
||||
LostCitySurface21.render(parcel, writer);
|
||||
}
|
||||
|
||||
private BlockPos world(int x, int y, int z) {
|
||||
return new BlockPos(parcel.minX() + x, parcel.floorY() + y, parcel.minZ() + z);
|
||||
}
|
||||
|
||||
private static BoundingBox bounds(LostCityPlan.Parcel parcel) {
|
||||
int low = Math.min(parcel.floorY(), Arrays.stream(parcel.surfaceHeights()).min().orElse(parcel.floorY()))
|
||||
- parcel.foundationDepth();
|
||||
int high = Math.max(parcel.maxY(), Arrays.stream(parcel.surfaceHeights()).max().orElse(parcel.floorY()) + 5);
|
||||
return new BoundingBox(parcel.minX(), low, parcel.minZ(), parcel.maxX(), high, parcel.maxZ());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
package fr.koka.sanctuary.worldgen.city;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityPlan.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Preserve the certified compact16 core. At most two modest natural outlying blocks are admitted
|
||||
* together with complete three-wide approaches; a failed route publishes neither its road nor its block. */
|
||||
public final class LostCityPlanner21 {
|
||||
public static final int MAX_EXTRA_COLUMNS = 4_000, MAX_EXTRA_CHECKS = 220_000;
|
||||
public static final int MAX_COLUMNS = LostCityPlanner16.MAX_COLUMNS + MAX_EXTRA_COLUMNS;
|
||||
public static final int MAX_VOLUME_CHECKS = LostCityPlanner16.MAX_VOLUME_CHECKS + MAX_EXTRA_CHECKS;
|
||||
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
|
||||
private static final int INVALID = Integer.MIN_VALUE;
|
||||
private LostCityPlanner21() {}
|
||||
public record Point(int x, int floorY, int z) {}
|
||||
public record Connection(int districtIndex, List<Point> path) { public Connection { path = List.copyOf(path); } }
|
||||
public record Result(LostCityPlanner16.Result core, LostCityPlan surface,
|
||||
List<LostCityPlanner16.Support> supports, int columns, int volumeChecks,
|
||||
List<Connection> connections, Map<String, Integer> diagnostics) {
|
||||
public Result {
|
||||
supports = List.copyOf(supports); connections = List.copyOf(connections); diagnostics = Map.copyOf(diagnostics);
|
||||
if (columns > MAX_COLUMNS || volumeChecks > MAX_VOLUME_CHECKS || connections.size() > 2)
|
||||
throw new IllegalArgumentException("Generation21 city exceeded its bounded extension");
|
||||
}
|
||||
public LostCityPlanner16.Fallback reservation() { return core.reservation(); }
|
||||
}
|
||||
public static Result plan(long seed, int cx, int cz, int diameter, boolean origin, LostCityPlanner16.Terrain terrain) {
|
||||
var core = LostCityPlanner16.plan(seed, cx, cz, diameter, origin, terrain);
|
||||
var districts = new ArrayList<>(core.surface().districts()); var supports = new ArrayList<>(core.supports());
|
||||
var links = new ArrayList<Connection>(); var probe = new Probe(terrain);
|
||||
if (origin && !districts.isEmpty()) {
|
||||
var center = districts.getFirst(); int mx = (center.minX() + center.maxX()) / 2, mz = (center.minZ() + center.maxZ()) / 2;
|
||||
int first = (int) Long.remainderUnsigned(mix(seed ^ 0x21c175L), 4);
|
||||
try {
|
||||
for (int attempt = 0; attempt < 8 && links.size() < 2; attempt++) {
|
||||
int d = (first + attempt) & 3;
|
||||
int radius = Math.max(center.maxX() - center.minX(), center.maxZ() - center.minZ()) / 2 + (attempt < 4 ? 42 : 58);
|
||||
int x = mx + DX[d] * radius, z = mz + DZ[d] * radius;
|
||||
probe.reject("cluster-attempt");
|
||||
// The old one-lot natural solver already certifies foundations, ramps and short supported bridges.
|
||||
var mini = LostCityPlanner16.plan(mix(seed ^ attempt * 3191L ^ 0x2100L), x, z, 96, false, probe);
|
||||
if (mini.surface().isEmpty()) { probe.reject("no-natural-block"); continue; }
|
||||
var district = mini.surface().districts().getFirst();
|
||||
if (overlap(district, districts, 14)) { probe.reject("insufficient-natural-gap"); continue; }
|
||||
var all = new ArrayList<>(districts); all.add(district);
|
||||
var path = connect(seed ^ attempt, center, district, all, probe);
|
||||
if (path == null) { probe.reject("no-complete-approach"); continue; }
|
||||
var parcels = new ArrayList<>(district.parcels()); parcels.addAll(path.roads());
|
||||
District joined;
|
||||
try { joined = new District(district.startChunkX(), district.startChunkZ(), district.seed(), parcels); }
|
||||
catch (IllegalArgumentException tooWide) { probe.reject("native-reference-radius"); continue; }
|
||||
links.add(new Connection(districts.size(), path.path())); districts.add(joined); supports.addAll(mini.supports());
|
||||
probe.reject("connected-cluster");
|
||||
}
|
||||
} catch (Exhausted exhausted) { probe.reject("extension-budget"); }
|
||||
}
|
||||
return new Result(core, new LostCityPlan(districts), supports, core.columns() + probe.columns.size(),
|
||||
core.volumeChecks() + probe.checks, links, probe.reasons);
|
||||
}
|
||||
private record Entry(int x, int y, int z, int facing) {}
|
||||
private record XZ(int x, int z) { XZ move(int d, int n) { return new XZ(x + DX[d] * n, z + DZ[d] * n); } }
|
||||
private record Pair(Entry a, Entry b) {}
|
||||
private record Road(int x, int z, int floor, int foundation) {}
|
||||
private record Footprint(int x, int z, int width, int depth) {}
|
||||
private record Route(List<Point> path, List<Parcel> roads) {}
|
||||
private static Route connect(long seed, District core, District extra, List<District> all, Probe probe) {
|
||||
var pairs = new ArrayList<Pair>();
|
||||
for (var a : entries(core, all)) for (var b : entries(extra, all)) pairs.add(new Pair(a, b));
|
||||
pairs.sort(Comparator.comparingInt(p -> Math.abs(p.a.x - p.b.x) + Math.abs(p.a.z - p.b.z)));
|
||||
for (var pair : pairs.stream().limit(6).toList()) {
|
||||
var a = pair.a; var b = pair.b; var outA = new XZ(a.x, a.z).move(a.facing, 3); var outB = new XZ(b.x, b.z).move(b.facing, 3);
|
||||
for (int variant = 0; variant < 4; variant++) {
|
||||
var points = new ArrayList<XZ>(); points.add(new XZ(a.x, a.z)); line(points, outA);
|
||||
if (variant == 0) line(points, new XZ(outB.x, outA.z));
|
||||
else if (variant == 1) line(points, new XZ(outA.x, outB.z));
|
||||
else if (variant == 2) { int mid = (outA.x + outB.x) / 2; line(points, new XZ(mid, outA.z)); line(points, new XZ(mid, outB.z)); }
|
||||
else { int mid = (outA.z + outB.z) / 2; line(points, new XZ(outA.x, mid)); line(points, new XZ(outB.x, mid)); }
|
||||
line(points, outB); line(points, new XZ(b.x, b.z));
|
||||
if (points.size() > 192 || new java.util.HashSet<>(points).size() != points.size()) continue;
|
||||
var route = profile(seed, points, a, b, all, probe); if (route != null) return route;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private static List<Entry> entries(District district, List<District> all) {
|
||||
var result = new ArrayList<Entry>();
|
||||
for (var p : district.parcels()) if (p.kind() == Kind.STREET && p.width() >= 3 && p.depth() >= 3) {
|
||||
for (int d = 0; d < 4; d++) {
|
||||
int lx = d == 1 ? p.width() - 1 : d == 3 ? 0 : p.width() / 2;
|
||||
int lz = d == 2 ? p.depth() - 1 : d == 0 ? 0 : p.depth() / 2;
|
||||
int x = p.minX() + lx, z = p.minZ() + lz, floor = p.surfaceY(lx, lz); boolean valid = true;
|
||||
for (int side = -1; side <= 1; side++) {
|
||||
int px = x + DZ[d] * side, pz = z + DX[d] * side;
|
||||
if (existingFloor(px, pz, all) != floor || existingFloor(px + DX[d], pz + DZ[d], all) != INVALID) { valid = false; break; }
|
||||
}
|
||||
if (valid) result.add(new Entry(x, floor, z, d));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private static Route profile(long seed, List<XZ> path, Entry a, Entry b, List<District> all, Probe probe) {
|
||||
int n = path.size(); int[] low = new int[n], high = new int[n], floor = new int[n];
|
||||
// Partition the complete footprint before choosing elevations. Every partition boundary
|
||||
// will be a level landing, so a renderer never needs a neighboring parcel to form a stair.
|
||||
var footprintCells = new LinkedHashMap<Long, XZ>();
|
||||
for (int i = 0; i < n; i++) for (var cell : strip(path, i))
|
||||
if (existingFloor(cell.x, cell.z, all) == INVALID) footprintCells.putIfAbsent(key(cell.x, cell.z), cell);
|
||||
var footprints = footprints(footprintCells);
|
||||
var owner = new HashMap<Long, Integer>();
|
||||
for (int i = 0; i < footprints.size(); i++) {
|
||||
var f = footprints.get(i);
|
||||
for (int z = f.z; z < f.z + f.depth; z++) for (int x = f.x; x < f.x + f.width; x++) owner.put(key(x, z), i);
|
||||
}
|
||||
int[] grade = new int[n];
|
||||
for (int i = 1; i < n; i++) {
|
||||
grade[i] = 1;
|
||||
for (var aCell : strip(path, i - 1)) for (var bCell : strip(path, i)) {
|
||||
int distance = Math.abs(aCell.x - bCell.x) + Math.abs(aCell.z - bCell.z);
|
||||
if (distance == 0 || distance == 1 && !java.util.Objects.equals(
|
||||
owner.get(key(aCell.x, aCell.z)), owner.get(key(bCell.x, bCell.z)))) grade[i] = 0;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < n; i++) {
|
||||
low[i] = 176; high[i] = 350;
|
||||
for (var p : strip(path, i)) {
|
||||
int old = existingFloor(p.x, p.z, all);
|
||||
if (old == -1) return null;
|
||||
if (old != INVALID) { low[i] = Math.max(low[i], old); high[i] = Math.min(high[i], old); }
|
||||
else {
|
||||
var g = probe.ground(p.x, p.z);
|
||||
if (!g.dry() || g.protectedTerrain() || g.solidDepth() < 3) return null;
|
||||
low[i] = Math.max(low[i], g.top() - 2); high[i] = Math.min(high[i], g.top() + 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
low[0] = high[0] = a.y; low[n - 1] = high[n - 1] = b.y;
|
||||
for (int i = 1; i < n; i++) { low[i] = Math.max(low[i], low[i - 1] - grade[i]); high[i] = Math.min(high[i], high[i - 1] + grade[i]); }
|
||||
for (int i = n - 2; i >= 0; i--) { low[i] = Math.max(low[i], low[i + 1] - grade[i + 1]); high[i] = Math.min(high[i], high[i + 1] + grade[i + 1]); }
|
||||
var cells = new LinkedHashMap<Long, Road>(); var result = new ArrayList<Point>();
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (low[i] > high[i]) return null;
|
||||
int min = i == 0 ? low[i] : Math.max(low[i], floor[i - 1] - grade[i]), max = i == 0 ? high[i] : Math.min(high[i], floor[i - 1] + grade[i]);
|
||||
if (min > max) return null;
|
||||
var p = path.get(i); floor[i] = Math.clamp(probe.ground(p.x, p.z).top(), min, max); result.add(new Point(p.x, floor[i], p.z));
|
||||
for (var c : strip(path, i)) {
|
||||
int old = existingFloor(c.x, c.z, all); if (old != INVALID) { if (old != floor[i]) return null; continue; }
|
||||
var g = probe.ground(c.x, c.z); int foundation = Math.max(1, floor[i] - g.top() + 1), bottom = floor[i] - foundation;
|
||||
if (!probe.solid(c.x, bottom, c.z) || !probe.solid(c.x, bottom - 1, c.z)) return null;
|
||||
for (int y = bottom - 1; y <= floor[i] + 8; y++) if (probe.protectedAt(c.x, y, c.z)) return null;
|
||||
var previous = cells.putIfAbsent(key(c.x, c.z), new Road(c.x, c.z, floor[i], foundation));
|
||||
if (previous != null && previous.floor != floor[i]) return null;
|
||||
}
|
||||
}
|
||||
// Bends may put nonconsecutive centerline strips next to each other. Their shared
|
||||
// partition edge obeys the same level-landing contract, including existing town roads.
|
||||
for (var cell : cells.values()) for (int d = 0; d < 4; d++) {
|
||||
int nx = cell.x + DX[d], nz = cell.z + DZ[d];
|
||||
var neighbor = cells.get(key(nx, nz));
|
||||
if (neighbor != null && !owner.get(key(cell.x, cell.z)).equals(owner.get(key(nx, nz)))
|
||||
&& neighbor.floor != cell.floor) return null;
|
||||
int old = existingFloor(nx, nz, all);
|
||||
if (old >= 0 && old != cell.floor) return null;
|
||||
}
|
||||
var parcels = new ArrayList<Parcel>();
|
||||
for (var f : footprints) {
|
||||
int foundation = 1;
|
||||
for (int z = f.z; z < f.z + f.depth; z++) for (int x = f.x; x < f.x + f.width; x++)
|
||||
foundation = Math.max(foundation, cells.get(key(x, z)).foundation);
|
||||
int[] heights = new int[f.width * f.depth];
|
||||
for (int z = f.z; z < f.z + f.depth; z++) for (int x = f.x; x < f.x + f.width; x++) {
|
||||
var cell = cells.get(key(x, z)); int bottom = cell.floor - foundation;
|
||||
// A rectangular parcel uses one foundation depth. Certify its deeper columns
|
||||
// explicitly rather than extending another column's rock certificate sideways.
|
||||
if (!probe.solid(x, bottom, z) || !probe.solid(x, bottom - 1, z)) return null;
|
||||
for (int y = bottom - 1; y < cell.floor - cell.foundation - 1; y++)
|
||||
if (probe.protectedAt(x, y, z)) return null;
|
||||
heights[(z - f.z) * f.width + x - f.x] = cell.floor;
|
||||
}
|
||||
parcels.add(new Parcel(Kind.STREET, f.x, f.z, f.width, f.depth, heights[0], 0, 0,
|
||||
mix(seed ^ key(f.x, f.z)), foundation, heights));
|
||||
}
|
||||
return new Route(List.copyOf(result), List.copyOf(parcels));
|
||||
}
|
||||
/** Disjoint, completely occupied rectangles, each contained in a single native chunk. */
|
||||
private static List<Footprint> footprints(Map<Long, XZ> cells) {
|
||||
var remaining = new java.util.HashSet<>(cells.keySet()); var result = new ArrayList<Footprint>();
|
||||
for (var at : cells.values().stream().sorted(Comparator.comparingInt(XZ::z).thenComparingInt(XZ::x)).toList()) {
|
||||
if (!remaining.contains(key(at.x, at.z))) continue;
|
||||
int width = 1, depth = 1;
|
||||
while ((at.x + width >> 4) == (at.x >> 4) && remaining.contains(key(at.x + width, at.z))) width++;
|
||||
while ((at.z + depth >> 4) == (at.z >> 4)) {
|
||||
boolean complete = true;
|
||||
for (int x = at.x; x < at.x + width; x++) complete &= remaining.contains(key(x, at.z + depth));
|
||||
if (!complete) break;
|
||||
depth++;
|
||||
}
|
||||
for (int z = at.z; z < at.z + depth; z++) for (int x = at.x; x < at.x + width; x++) remaining.remove(key(x, z));
|
||||
result.add(new Footprint(at.x, at.z, width, depth));
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
private static List<XZ> strip(List<XZ> points, int i) {
|
||||
var p = points.get(i); var result = new java.util.LinkedHashSet<XZ>(); result.add(p);
|
||||
for (int next : new int[]{i - 1, i + 1}) if (next >= 0 && next < points.size()) {
|
||||
var q = points.get(next); int dx = Integer.signum(q.x - p.x), dz = Integer.signum(q.z - p.z);
|
||||
result.add(new XZ(p.x + dz, p.z + dx)); result.add(new XZ(p.x - dz, p.z - dx));
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
private static void line(List<XZ> points, XZ goal) {
|
||||
var p = points.getLast(); while (p.x != goal.x) { p = new XZ(p.x + Integer.signum(goal.x - p.x), p.z); points.add(p); }
|
||||
while (p.z != goal.z) { p = new XZ(p.x, p.z + Integer.signum(goal.z - p.z)); points.add(p); }
|
||||
}
|
||||
private static int existingFloor(int x, int z, List<District> all) {
|
||||
for (var d : all) for (var p : d.parcels()) if (x >= p.minX() && x <= p.maxX() && z >= p.minZ() && z <= p.maxZ())
|
||||
return p.kind() == Kind.STREET ? p.surfaceY(x - p.minX(), z - p.minZ()) : -1;
|
||||
return INVALID;
|
||||
}
|
||||
private static boolean overlap(District candidate, List<District> all, int gap) {
|
||||
for (var d : all) if (candidate.minX() <= d.maxX() + gap && candidate.maxX() >= d.minX() - gap
|
||||
&& candidate.minZ() <= d.maxZ() + gap && candidate.maxZ() >= d.minZ() - gap) return true;
|
||||
return false;
|
||||
}
|
||||
private static final class Exhausted extends RuntimeException {}
|
||||
private static final class Probe implements LostCityPlanner16.Terrain {
|
||||
final LostCityPlanner16.Terrain source; final Map<Long, LostCityPlanner.Ground> columns = new HashMap<>();
|
||||
final Map<String, Integer> reasons = new LinkedHashMap<>(); int checks;
|
||||
Probe(LostCityPlanner16.Terrain source) { this.source = source; }
|
||||
public boolean owns(int x, int z) { return source.owns(x, z); }
|
||||
public LostCityPlanner.Ground ground(int x, int z) {
|
||||
if (!owns(x, z)) return new LostCityPlanner.Ground(0, 0, false, true);
|
||||
long key = key(x, z); var old = columns.get(key); if (old != null) return old;
|
||||
if (columns.size() >= MAX_EXTRA_COLUMNS) throw new Exhausted();
|
||||
var value = source.ground(x, z); columns.put(key, value); return value;
|
||||
}
|
||||
public boolean solid(int x, int y, int z) { count(); ground(x, z); return owns(x, z) && source.solid(x, y, z); }
|
||||
public boolean protectedAt(int x, int y, int z) { count(); ground(x, z); return !owns(x, z) || source.protectedAt(x, y, z); }
|
||||
void count() { if (checks >= MAX_EXTRA_CHECKS) throw new Exhausted(); checks++; }
|
||||
void reject(String reason) { reasons.merge(reason, 1, Integer::sum); }
|
||||
}
|
||||
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
|
||||
private static long mix(long v) { v = (v ^ v >>> 30) * 0xbf58476d1ce4e5b9L; v = (v ^ v >>> 27) * 0x94d049bb133111ebL; return v ^ v >>> 31; }
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package fr.koka.sanctuary.worldgen.city;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityPlan.District;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Kind;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Parcel;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Generation22 only: earlier planners propose geometry, but cannot authorize artificial terrain.
|
||||
* Proposals and their forced reservations are read-only; only fully recertified natural towns
|
||||
* reach the returned plan. The published generation21 solver and all renderers stay unchanged. */
|
||||
public final class LostCityPlanner22 {
|
||||
public static final int MAX_COLUMNS = LostCityPlanner21.MAX_COLUMNS;
|
||||
public static final int MAX_VOLUME_CHECKS = LostCityPlanner21.MAX_VOLUME_CHECKS;
|
||||
public static final int MAX_ATTEMPTS = 3, MAX_CUT = 2, MAX_FILL = 2;
|
||||
private static final LostCityPlanner.Ground INVALID = new LostCityPlanner.Ground(0, 0, false, true);
|
||||
private LostCityPlanner22() {}
|
||||
|
||||
public static LostCityPlanner21.Result plan(long seed, int cx, int cz, int diameter, boolean origin,
|
||||
LostCityPlanner16.Terrain terrain) {
|
||||
if (diameter < 64 || diameter > 1024 || origin && diameter < 512)
|
||||
throw new IllegalArgumentException("Unsupported natural city island size");
|
||||
var probe = new Probe(terrain);
|
||||
for (int attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
||||
probe.reject("proposal-attempt");
|
||||
long proposalSeed = attempt == 0 ? seed : mix(seed ^ attempt * 0x22C175L);
|
||||
try {
|
||||
var proposal = LostCityPlanner21.plan(proposalSeed, cx, cz, diameter, origin, probe);
|
||||
if (proposal.reservation() != null) { probe.reject("forced-reservation-rejected"); continue; }
|
||||
if (proposal.core().surface().isEmpty()) { probe.reject("no-natural-core"); continue; }
|
||||
if (!proposal.core().supports().isEmpty()) { probe.reject("core-pier-rejected"); continue; }
|
||||
boolean valid = true;
|
||||
for (var district : proposal.core().surface().districts())
|
||||
if (!validate(district, probe)) { valid = false; break; }
|
||||
if (!valid) continue;
|
||||
|
||||
var districts = new ArrayList<>(proposal.core().surface().districts());
|
||||
var connections = new ArrayList<LostCityPlanner21.Connection>();
|
||||
for (var connection : proposal.connections()) {
|
||||
try {
|
||||
var district = proposal.surface().districts().get(connection.districtIndex());
|
||||
if (proposal.supports().stream().anyMatch(s -> s.startChunkX() == district.startChunkX()
|
||||
&& s.startChunkZ() == district.startChunkZ())) {
|
||||
probe.reject("satellite-pier-rejected"); continue;
|
||||
}
|
||||
if (!validate(district, probe)) continue;
|
||||
var retained = new ArrayList<>(districts); retained.add(district);
|
||||
if (!connected(connection.path(), retained)) {
|
||||
probe.reject("satellite-depends-on-rejected-road"); continue;
|
||||
}
|
||||
connections.add(new LostCityPlanner21.Connection(districts.size(), connection.path()));
|
||||
districts.add(district);
|
||||
} catch (Exhausted exhausted) { probe.reject("satellite-budget"); break; }
|
||||
}
|
||||
probe.reject("natural-core-accepted");
|
||||
return result(proposal.core(), new LostCityPlan(districts), connections, probe);
|
||||
} catch (Exhausted exhausted) {
|
||||
probe.reject("natural-search-budget"); break;
|
||||
} catch (IllegalArgumentException rejectedReservation) {
|
||||
// The immutable16 proposal solver requires an owned fallback envelope. In22 an
|
||||
// island with no such envelope is simply unsuitable; unrelated errors still fail.
|
||||
String message = rejectedReservation.getMessage();
|
||||
if (!"Starter terrain does not own a central 49x49 city envelope".equals(message)
|
||||
&& !"Starter city reservation crosses another island's ownership".equals(message)) throw rejectedReservation;
|
||||
probe.reject("unowned-reservation-rejected");
|
||||
}
|
||||
}
|
||||
return result(new LostCityPlanner16.Result(LostCityPlan.EMPTY, List.of(), 0, 0),
|
||||
LostCityPlan.EMPTY, List.of(), probe);
|
||||
}
|
||||
|
||||
private static LostCityPlanner21.Result result(LostCityPlanner16.Result core, LostCityPlan surface,
|
||||
List<LostCityPlanner21.Connection> connections, Probe probe) {
|
||||
return new LostCityPlanner21.Result(core, surface, List.of(), probe.columns.size(), probe.checks,
|
||||
connections, probe.diagnostics);
|
||||
}
|
||||
|
||||
/** Validate actual emitted footprint and bearing columns, not just a few terrain samples.
|
||||
* Eight-block pool/garage basements remain possible only when entirely inside native rock. */
|
||||
private static boolean validate(District district, Probe probe) {
|
||||
for (Parcel parcel : district.parcels()) {
|
||||
if (parcel.kind() == Kind.BRIDGE) { probe.reject("suspended-bridge-rejected"); return false; }
|
||||
for (int z = parcel.minZ(); z <= parcel.maxZ(); z++) for (int x = parcel.minX(); x <= parcel.maxX(); x++) {
|
||||
var ground = probe.ground(x, z);
|
||||
if (!ground.dry() || ground.protectedTerrain() || ground.solidDepth() < 4) {
|
||||
probe.reject("not-dry-natural-ground"); return false;
|
||||
}
|
||||
int floor = parcel.surfaceY(x - parcel.minX(), z - parcel.minZ());
|
||||
if (ground.top() - floor > MAX_CUT || floor - ground.top() > MAX_FILL) {
|
||||
probe.reject("excess-cut-or-fill"); return false;
|
||||
}
|
||||
int bottom = floor - parcel.foundationDepth();
|
||||
// Two unpainted rock layers must carry every column. The entire native part of
|
||||
// each footing stays solid: no borrowed corner certificate across a cavity.
|
||||
for (int y = bottom - 2; y <= Math.min(floor - 1, ground.top()); y++) {
|
||||
if (!probe.solid(x, y, z)) { probe.reject("incomplete-natural-bearing"); return false; }
|
||||
}
|
||||
for (int y = bottom - 2; y <= parcel.maxY(); y++) {
|
||||
if (probe.protectedAt(x, y, z)) { probe.reject("protected-construction-volume"); return false; }
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean connected(List<LostCityPlanner21.Point> path, List<District> retained) {
|
||||
for (var point : path) {
|
||||
boolean found = false;
|
||||
for (var district : retained) for (var parcel : district.parcels()) {
|
||||
if (parcel.kind() == Kind.STREET && point.x() >= parcel.minX() && point.x() <= parcel.maxX()
|
||||
&& point.z() >= parcel.minZ() && point.z() <= parcel.maxZ()
|
||||
&& parcel.surfaceY(point.x() - parcel.minX(), point.z() - parcel.minZ()) == point.floorY()) found = true;
|
||||
}
|
||||
if (!found) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static final class Probe implements LostCityPlanner16.Terrain {
|
||||
final LostCityPlanner16.Terrain terrain;
|
||||
final Map<Long, LostCityPlanner.Ground> columns = new HashMap<>();
|
||||
final Map<String, Integer> diagnostics = new LinkedHashMap<>();
|
||||
int checks;
|
||||
Probe(LostCityPlanner16.Terrain terrain) { this.terrain = java.util.Objects.requireNonNull(terrain); }
|
||||
@Override public boolean owns(int x, int z) { return terrain.owns(x, z); }
|
||||
@Override public LostCityPlanner.Ground ground(int x, int z) {
|
||||
if (!owns(x, z)) return INVALID;
|
||||
long key = (long) x << 32 ^ z & 0xffffffffL;
|
||||
var cached = columns.get(key); if (cached != null) return cached;
|
||||
if (columns.size() >= MAX_COLUMNS) throw new Exhausted();
|
||||
var result = java.util.Objects.requireNonNull(terrain.ground(x, z)); columns.put(key, result); return result;
|
||||
}
|
||||
@Override public boolean solid(int x, int y, int z) {
|
||||
count(); return ground(x, z) != INVALID && terrain.solid(x, y, z);
|
||||
}
|
||||
@Override public boolean protectedAt(int x, int y, int z) {
|
||||
count(); return ground(x, z) == INVALID || terrain.protectedAt(x, y, z);
|
||||
}
|
||||
void count() { if (checks >= MAX_VOLUME_CHECKS) throw new Exhausted(); checks++; }
|
||||
void reject(String reason) { diagnostics.merge(reason, 1, Integer::sum); }
|
||||
}
|
||||
private static long mix(long v) { v = (v ^ v >>> 30) * 0xbf58476d1ce4e5b9L; v = (v ^ v >>> 27) * 0x94d049bb133111ebL; return v ^ v >>> 31; }
|
||||
private static final class Exhausted extends RuntimeException { private static final long serialVersionUID = 1L; }
|
||||
}
|
||||
@@ -29,17 +29,18 @@ public final class LostCityStructure extends Structure {
|
||||
throw new IllegalStateException("Lost City district exceeds native structure reference range");
|
||||
}
|
||||
return Optional.of(new GenerationStub(LostCityStructures.locator(district), pieces -> {
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17) && generator.cityReservation16(island) != null)
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22) && generator.cityReservation16(island) != null
|
||||
&& ((generator.rootGeneration() != 21 && generator.rootGeneration() != 22) || generator.cityPlacement21(island).core().surface().districts().contains(district)))
|
||||
pieces.addPiece(new LostCityFoundationPiece16(generator.cityReservation16(island), district.seed()));
|
||||
if (generator.rootGeneration() == 15) for (var support : generator.citySupports(island)) {
|
||||
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
|
||||
pieces.addPiece(new LostCitySupportPiece15(support));
|
||||
}
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17)) for (var support : generator.citySupports16(island)) {
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22)) for (var support : generator.citySupports16(island)) {
|
||||
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
|
||||
pieces.addPiece(new LostCitySupportPiece16(support));
|
||||
}
|
||||
district.parcels().forEach(parcel -> pieces.addPiece((generator.rootGeneration() == 16 || generator.rootGeneration() == 17) ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
|
||||
district.parcels().forEach(parcel -> pieces.addPiece((generator.rootGeneration() == 21 || generator.rootGeneration() == 22) ? new LostCityPiece21(parcel) : generator.rootGeneration() == 20 ? new LostCityPiece20(parcel) : (generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22) ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
|
||||
? new LostCityPiece15(parcel) : new LostCityPiece(parcel)));
|
||||
if (generator.rootGeneration() == 15) for (var network : generator.undergroundPlan(island).networks()) {
|
||||
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
|
||||
@@ -49,13 +50,13 @@ public final class LostCityStructure extends Structure {
|
||||
network.modules().stream().filter(m -> m.kind() == LostCityUnderground15.Kind.ACCESS)
|
||||
.forEach(m -> pieces.addPiece(new LostCityUndergroundPiece15(m)));
|
||||
}
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17)) for (var network : generator.undergroundPlan16(island).networks()) {
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22)) for (var network : generator.undergroundPlan16(island).networks()) {
|
||||
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
|
||||
// Surface comes first, rooms/corridors next, shafts last so every entrance remains open.
|
||||
network.modules().stream().filter(m -> m.kind() != LostCityUnderground16.Kind.ACCESS)
|
||||
.forEach(m -> pieces.addPiece(new LostCityUndergroundPiece16(m)));
|
||||
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
|
||||
network.modules().stream().filter(m -> m.kind() == LostCityUnderground16.Kind.ACCESS)
|
||||
.forEach(m -> pieces.addPiece(new LostCityUndergroundPiece16(m)));
|
||||
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -40,8 +40,14 @@ public final class LostCityStructures {
|
||||
BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_underground_v15"), LostCityUndergroundPiece15::new);
|
||||
public static final StructurePieceType SUPPORT15 = Registry.register(
|
||||
BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_support_v15"), LostCitySupportPiece15::new);
|
||||
public static final StructurePieceType PIECE21 = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
|
||||
SanctuaryMod.id("lost_city_parcel_v21"), LostCityPiece21::new);
|
||||
public static final StructurePieceType PIECE20 = Registry.register(
|
||||
BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_parcel_v20"), LostCityPiece20::new);
|
||||
public static final StructurePieceType PIECE16 = Registry.register(
|
||||
BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_parcel_v16"), LostCityPiece16::new);
|
||||
public static final StructurePieceType UNDERGROUND20 = Registry.register(
|
||||
BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_underground_v20"), LostCityUndergroundPiece20::new);
|
||||
public static final StructurePieceType UNDERGROUND16 = Registry.register(
|
||||
BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_underground_v16"), LostCityUndergroundPiece16::new);
|
||||
public static final StructurePieceType SUPPORT16 = Registry.register(
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
package fr.koka.sanctuary.worldgen.city;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.block.state.properties.Half;
|
||||
import net.minecraft.world.level.block.state.properties.SlabType;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
|
||||
/** New surface architecture on the saved generation16 footprints. Published renderers remain immutable. */
|
||||
public final class LostCitySurface20 {
|
||||
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
|
||||
private LostCitySurface20() {}
|
||||
|
||||
public static void render(LostCityPlan.Parcel parcel, LostCityRenderer.BlockWriter output) {
|
||||
if (parcel.kind().building()) { LostCityInteriors20.render(parcel, output); return; }
|
||||
var connected = ConnectedBlocks20.buffer(output);
|
||||
if (parcel.kind() == LostCityPlan.Kind.STREET || parcel.kind() == LostCityPlan.Kind.BRIDGE) {
|
||||
road(parcel, connected);
|
||||
} else {
|
||||
new Exterior(parcel, connected).render();
|
||||
}
|
||||
connected.flush();
|
||||
}
|
||||
|
||||
private static void road(LostCityPlan.Parcel p, LostCityRenderer.BlockWriter output) {
|
||||
boolean alongX = p.width() > p.depth(), bridge = p.kind() == LostCityPlan.Kind.BRIDGE;
|
||||
for (int z = 0; z < p.depth(); z++) for (int x = 0; x < p.width(); x++) {
|
||||
int y = p.surfaceY(x, z) - p.floorY();
|
||||
int across = alongX ? z : x, along = alongX ? x : z, breadth = alongX ? p.depth() : p.width();
|
||||
boolean edge = across == 0 || across == breadth - 1;
|
||||
// A legible curb and patterned shoulders, not per-block random pavement.
|
||||
Block paving = edge ? Blocks.POLISHED_TUFF : across == 1 || across == breadth - 2
|
||||
? along % 4 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS
|
||||
: Blocks.POLISHED_ANDESITE;
|
||||
if (!bridge) for (int down = 1; down <= p.foundationDepth(); down++)
|
||||
output.set(x, y - down, z, Blocks.STONE_BRICKS.defaultBlockState());
|
||||
output.set(x, y, z, paving.defaultBlockState());
|
||||
for (int up = 1; up <= 8; up++) output.set(x, y + up, z, Blocks.AIR.defaultBlockState());
|
||||
if (bridge && edge) {
|
||||
// Posts grow from the existing deck only; the certified natural bearings are untouched.
|
||||
output.set(x, y + 1, z, (along % 5 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.IRON_BARS).defaultBlockState());
|
||||
if (along % 5 == 0) output.set(x, y + 2, z, Blocks.TUFF_BRICK_SLAB.defaultBlockState());
|
||||
continue;
|
||||
}
|
||||
for (var direction : HORIZONTAL) {
|
||||
int nx = x + direction.getStepX(), nz = z + direction.getStepZ();
|
||||
if (nx >= 0 && nx < p.width() && nz >= 0 && nz < p.depth()
|
||||
&& p.surfaceY(nx, nz) == p.surfaceY(x, z) + 1) {
|
||||
output.set(x, y + 1, z, Blocks.TUFF_BRICK_STAIRS.defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, direction));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Exterior implements LostCityRenderer.BlockWriter {
|
||||
final LostCityPlan.Parcel p;
|
||||
final LostCityRenderer.BlockWriter output;
|
||||
final Rotation turn;
|
||||
Exterior(LostCityPlan.Parcel p, LostCityRenderer.BlockWriter output) {
|
||||
this.p = p; this.output = output;
|
||||
turn = switch (p.facing()) { case 1 -> Rotation.CLOCKWISE_90; case 2 -> Rotation.CLOCKWISE_180;
|
||||
case 3 -> Rotation.COUNTERCLOCKWISE_90; default -> Rotation.NONE; };
|
||||
}
|
||||
int x(int x, int z) { return switch (p.facing()) { case 1 -> p.width() - 1 - z; case 2 -> p.width() - 1 - x; case 3 -> z; default -> x; }; }
|
||||
int z(int x, int z) { return switch (p.facing()) { case 1 -> x; case 2 -> p.depth() - 1 - z; case 3 -> p.depth() - 1 - x; default -> z; }; }
|
||||
@Override public void set(int x, int y, int z, BlockState state) { output.set(x(x, z), y, z(x, z), state.rotate(turn)); }
|
||||
void set(int x, int y, int z, Block block) { set(x, y, z, block.defaultBlockState()); }
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
|
||||
output.chest(x(x, z), y, z(x, z), table, seed ^ 0x20c17a7eL);
|
||||
}
|
||||
void box(int x0, int y0, int z0, int x1, int y1, int z1, Block block) {
|
||||
for (int x = x0; x <= x1; x++) for (int z = z0; z <= z1; z++) for (int y = y0; y <= y1; y++) set(x, y, z, block);
|
||||
}
|
||||
void render() {
|
||||
if (p.width() != 17 || p.depth() != 17) throw new IllegalArgumentException("Generation20 amenities require the certified 17x17 footprint");
|
||||
// Keep the proven pool containment, excavation steps and garage ramps. All new ornament is
|
||||
// composed before connection resolution, so clipping cannot split a rail or window design.
|
||||
var north = new LostCityPlan.Parcel(p.kind(), p.minX(), p.minZ(), 17, 17, p.floorY(), p.floors(), 0,
|
||||
p.seed(), p.foundationDepth(), p.surfaceHeights());
|
||||
LostCitySurface16.render(north, new LostCityRenderer.BlockWriter() {
|
||||
@Override public void set(int x, int y, int z, BlockState state) {
|
||||
if (state.is(Blocks.STONE) && y < 0) state = Blocks.STONE_BRICKS.defaultBlockState();
|
||||
if (state.is(Blocks.DARK_OAK_PLANKS)) state = Blocks.OAK_PLANKS.defaultBlockState();
|
||||
if (state.is(Blocks.DARK_OAK_FENCE)) state = Blocks.OAK_FENCE.defaultBlockState();
|
||||
if (state.is(Blocks.DARK_OAK_STAIRS)) state = Blocks.OAK_STAIRS.defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, state.getValue(BlockStateProperties.HORIZONTAL_FACING));
|
||||
Exterior.this.set(x, y, z, state);
|
||||
}
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { Exterior.this.chest(x, y, z, table, seed); }
|
||||
});
|
||||
// The north threshold is a level promenade, shared by every admitted parcel.
|
||||
for (int x = 0; x <= 16; x++) set(x, 0, 0, x % 4 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS);
|
||||
switch (p.kind()) {
|
||||
case PARK -> park();
|
||||
case PARK_GARDEN -> garden();
|
||||
case PARK_PLAYGROUND -> playground();
|
||||
case PARK_MEMORIAL -> memorial();
|
||||
case PLAZA -> plaza();
|
||||
case MARKET -> market();
|
||||
case PARKING -> garage();
|
||||
case POOL -> { if (p.foundationDepth() >= 8) pool(); else park(); }
|
||||
case EXCAVATION -> { if (p.foundationDepth() >= 8) excavation(); else park(); }
|
||||
default -> throw new IllegalArgumentException("Not an exterior amenity");
|
||||
}
|
||||
}
|
||||
void paths() {
|
||||
for (int n = 1; n <= 16; n++) for (int across = 7; across <= 9; across++) {
|
||||
set(across, 0, n, across == 8 ? Blocks.POLISHED_TUFF : Blocks.TUFF_BRICKS);
|
||||
set(n, 0, across, across == 8 ? Blocks.POLISHED_TUFF : Blocks.TUFF_BRICKS);
|
||||
}
|
||||
}
|
||||
void bench(int x, int y, int z, int length, Direction view) {
|
||||
for (int dx = 0; dx < length; dx++) set(x + dx, y, z, Blocks.OAK_STAIRS.defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, view.getOpposite()));
|
||||
set(x - 1, y, z, Blocks.TUFF_BRICK_SLAB); set(x + length, y, z, Blocks.TUFF_BRICK_SLAB);
|
||||
}
|
||||
void lamp(int x, int z) {
|
||||
set(x, 1, z, Blocks.CHISELED_TUFF_BRICKS); box(x, 2, z, x, 3, z, Blocks.BIRCH_FENCE);
|
||||
set(x, 4, z, Blocks.BIRCH_PLANKS); set(x, 5, z, Blocks.LANTERN);
|
||||
}
|
||||
void park() {
|
||||
paths(); bench(10, 1, 3, 3, Direction.SOUTH); bench(10, 1, 11, 3, Direction.NORTH);
|
||||
lamp(5, 11); lamp(11, 5);
|
||||
// A birch screen beside the existing oak trees changes the park's canopy and rhythm.
|
||||
box(3, 1, 12, 3, 4, 12, Blocks.BIRCH_LOG);
|
||||
for (int x = 2; x <= 4; x++) for (int z = 11; z <= 13; z++)
|
||||
set(x, 5, z, Blocks.BIRCH_LEAVES.defaultBlockState().setValue(BlockStateProperties.PERSISTENT, true));
|
||||
for (int z = 2; z <= 5; z++) set(6, 0, z, Blocks.POLISHED_GRANITE);
|
||||
}
|
||||
void garden() {
|
||||
paths();
|
||||
// Four parterres have low carved kerbs, soil beds and a shared central promenade.
|
||||
for (int x0 : new int[]{2, 10}) for (int z0 : new int[]{2, 10}) {
|
||||
for (int n = 0; n < 5; n++) {
|
||||
set(x0 + n, 0, z0, Blocks.POLISHED_DIORITE); set(x0 + n, 0, z0 + 4, Blocks.POLISHED_DIORITE);
|
||||
set(x0, 0, z0 + n, Blocks.POLISHED_DIORITE); set(x0 + 4, 0, z0 + n, Blocks.POLISHED_DIORITE);
|
||||
}
|
||||
set(x0, 1, z0, Blocks.CHISELED_TUFF); set(x0 + 4, 1, z0 + 4, Blocks.CHISELED_TUFF);
|
||||
}
|
||||
bench(7, 1, 15, 3, Direction.NORTH);
|
||||
// Pergola along the west planting bed, not across a circulation route.
|
||||
for (int z : new int[]{2, 6}) box(1, 1, z, 1, 3, z, Blocks.STRIPPED_OAK_LOG);
|
||||
box(1, 4, 2, 1, 4, 6, Blocks.BIRCH_SLAB); lamp(15, 8);
|
||||
}
|
||||
void playground() {
|
||||
paths();
|
||||
for (int x = 1; x <= 7; x++) { set(x, 0, 1, Blocks.BIRCH_PLANKS); set(x, 0, 7, Blocks.BIRCH_PLANKS); }
|
||||
for (int z = 1; z <= 7; z++) { set(1, 0, z, Blocks.BIRCH_PLANKS); set(7, 0, z, Blocks.BIRCH_PLANKS); }
|
||||
// Braced climbing platform: low guards leave the three front steps open.
|
||||
for (int x : new int[]{11, 13}) set(x, 4, 13, Blocks.BIRCH_FENCE);
|
||||
set(12, 4, 13, Blocks.BIRCH_FENCE);
|
||||
set(11, 4, 11, Blocks.BIRCH_FENCE); set(13, 4, 11, Blocks.BIRCH_FENCE);
|
||||
bench(3, 1, 13, 3, Direction.NORTH); lamp(15, 3);
|
||||
}
|
||||
void plaza() {
|
||||
for (int x = 1; x <= 15; x++) for (int z = 1; z <= 15; z++) {
|
||||
int d = Math.max(Math.abs(x - 8), Math.abs(z - 8));
|
||||
set(x, 0, z, d == 3 || d == 6 ? Blocks.POLISHED_GRANITE : d == 4 || d == 7 ? Blocks.TUFF_BRICKS : Blocks.POLISHED_DIORITE);
|
||||
}
|
||||
// Dry civic fountain: bevelled bowl, carved pedestal, projecting cap.
|
||||
for (int x = 6; x <= 10; x++) for (int z = 6; z <= 10; z++) {
|
||||
if (x != 6 && x != 10 && z != 6 && z != 10) continue;
|
||||
boolean corner = (x == 6 || x == 10) && (z == 6 || z == 10);
|
||||
set(x, 1, z, corner ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICK_SLAB);
|
||||
}
|
||||
set(8, 1, 8, Blocks.CHISELED_TUFF); set(8, 2, 8, Blocks.CHISELED_TUFF_BRICKS);
|
||||
set(8, 3, 8, Blocks.POLISHED_TUFF_SLAB); lamp(2, 12); lamp(14, 4);
|
||||
}
|
||||
void memorial() {
|
||||
// The memorial keeps its open central approach and gains stepped stone shoulders.
|
||||
for (int x = 1; x <= 15; x++) for (int z = 1; z <= 15; z++)
|
||||
set(x, 0, z, x == 5 || x == 11 || z == 5 || z == 11 ? Blocks.TUFF_BRICKS : Blocks.POLISHED_ANDESITE);
|
||||
for (int x = 7; x <= 9; x++) {
|
||||
set(x, 1, 7, Blocks.TUFF_BRICK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
|
||||
set(x, 1, 9, Blocks.TUFF_BRICK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
|
||||
}
|
||||
set(8, 2, 8, Blocks.CHISELED_TUFF); set(8, 3, 8, Blocks.CHISELED_TUFF_BRICKS);
|
||||
set(8, 4, 8, Blocks.CHISELED_STONE_BRICKS); set(8, 5, 8, Blocks.POLISHED_DIORITE_SLAB);
|
||||
lamp(3, 8); lamp(13, 8);
|
||||
}
|
||||
void market() {
|
||||
for (int x = 1; x <= 15; x++) for (int z = 1; z <= 15; z++)
|
||||
set(x, 0, z, x >= 7 && x <= 9 || z >= 7 && z <= 9 ? Blocks.TUFF_BRICKS : Blocks.POLISHED_GRANITE);
|
||||
for (int x0 : new int[]{2, 11}) for (int z0 : new int[]{2, 11}) {
|
||||
for (int x = x0; x <= x0 + 3; x++) for (int z = z0; z <= z0 + 2; z++)
|
||||
set(x, 4, z, (x - x0 & 1) == 0 ? Blocks.BIRCH_SLAB : Blocks.OAK_SLAB);
|
||||
for (int x : new int[]{x0, x0 + 3}) {
|
||||
set(x, 1, z0, Blocks.CHISELED_TUFF_BRICKS);
|
||||
box(x, 2, z0, x, 3, z0, Blocks.STRIPPED_OAK_LOG);
|
||||
}
|
||||
set(x0 + 1, 5, z0 + 1, Blocks.BIRCH_SLAB);
|
||||
}
|
||||
lamp(1, 8); lamp(15, 8);
|
||||
}
|
||||
void pool() {
|
||||
for (int x = 3; x <= 13; x++) for (int z = 4; z <= 13; z++) {
|
||||
if (x == 3 || x == 13 || z == 4 || z == 13) set(x, 0, z, Blocks.POLISHED_DIORITE);
|
||||
}
|
||||
// Changing/showers canopy stands beside the deck; it never enters the sealed basin.
|
||||
for (int x : new int[]{10, 15}) box(x, 1, 1, x, 3, 1, Blocks.STRIPPED_BIRCH_LOG);
|
||||
box(10, 4, 1, 15, 4, 3, Blocks.BIRCH_SLAB);
|
||||
for (int x = 11; x <= 14; x++) set(x, 1, 1, Blocks.WATER_CAULDRON.defaultBlockState()
|
||||
.setValue(BlockStateProperties.LEVEL_CAULDRON, 2));
|
||||
set(15, 4, 2, Blocks.BIRCH_PLANKS); set(15, 5, 2, Blocks.LANTERN);
|
||||
for (int z : new int[]{5, 9}) set(15, 1, z, Blocks.OAK_STAIRS.defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST));
|
||||
box(2, 0, 14, 14, 0, 15, Blocks.TUFF_BRICKS);
|
||||
}
|
||||
void excavation() {
|
||||
// Reinforced temporary footings and timber trestles stay out of the central descent.
|
||||
for (int x : new int[]{4, 12}) {
|
||||
box(x, -4, 11, x, -1, 11, Blocks.CHISELED_STONE_BRICKS);
|
||||
set(x, 0, 11, Blocks.OAK_PLANKS);
|
||||
}
|
||||
box(3, 3, 13, 13, 3, 13, Blocks.STRIPPED_OAK_LOG);
|
||||
for (int x : new int[]{3, 13}) box(x, -4, 13, x, 2, 13, Blocks.STONE_BRICKS);
|
||||
set(3, 4, 13, Blocks.LANTERN);
|
||||
set(1, 1, 11, Blocks.GRINDSTONE); set(1, 1, 10, Blocks.CRAFTING_TABLE);
|
||||
}
|
||||
void garage() {
|
||||
int base = p.foundationDepth() >= 8 ? -5 : 0;
|
||||
for (int x : new int[]{3, 13}) for (int z : new int[]{3, 13}) {
|
||||
box(x, base + 1, z, x, base + 4, z, Blocks.POLISHED_ANDESITE);
|
||||
set(x, base + 1, z, Blocks.CHISELED_TUFF_BRICKS);
|
||||
}
|
||||
for (int z = 6; z <= 15; z++) for (int x = 1; x <= 15; x++)
|
||||
if (x <= 5 || x >= 11) set(x, base, z, z % 4 == 0 ? Blocks.POLISHED_DIORITE : Blocks.POLISHED_ANDESITE);
|
||||
// A ribbed structural fascia and ventilation blades make this an open modern garage.
|
||||
for (int x = 0; x <= 16; x++) {
|
||||
set(x, base + 5, 0, x % 4 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS);
|
||||
if (x < 6 || x > 10) set(x, base + 3, 0, Blocks.IRON_TRAPDOOR.defaultBlockState()
|
||||
.setValue(BlockStateProperties.OPEN, true).setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
|
||||
}
|
||||
for (int x : new int[]{5, 11}) {
|
||||
set(x, base + 4, 9, Blocks.SMOOTH_STONE);
|
||||
set(x, base + 3, 9, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
package fr.koka.sanctuary.worldgen.city;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.block.state.properties.Half;
|
||||
import net.minecraft.world.level.block.state.properties.SlabType;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
|
||||
/** New surface architecture on the saved generation16 footprints. Published renderers remain immutable. */
|
||||
public final class LostCitySurface21 {
|
||||
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
|
||||
private LostCitySurface21() {}
|
||||
|
||||
public static void render(LostCityPlan.Parcel parcel, LostCityRenderer.BlockWriter output) {
|
||||
if (parcel.kind().building()) { LostCityInteriors21.render(parcel, output); return; }
|
||||
var connected = ConnectedBlocks20.buffer(output);
|
||||
if (parcel.kind() == LostCityPlan.Kind.STREET || parcel.kind() == LostCityPlan.Kind.BRIDGE) {
|
||||
road(parcel, connected);
|
||||
} else {
|
||||
new Exterior(parcel, connected).render();
|
||||
}
|
||||
connected.flush();
|
||||
}
|
||||
|
||||
private static void road(LostCityPlan.Parcel p, LostCityRenderer.BlockWriter output) {
|
||||
boolean alongX = p.width() > p.depth(), bridge = p.kind() == LostCityPlan.Kind.BRIDGE;
|
||||
for (int z = 0; z < p.depth(); z++) for (int x = 0; x < p.width(); x++) {
|
||||
int y = p.surfaceY(x, z) - p.floorY();
|
||||
int across = alongX ? z : x, along = alongX ? x : z, breadth = alongX ? p.depth() : p.width();
|
||||
boolean edge = across == 0 || across == breadth - 1;
|
||||
// A legible curb and patterned shoulders, not per-block random pavement.
|
||||
Block paving = edge ? Blocks.POLISHED_TUFF : across == 1 || across == breadth - 2
|
||||
? along % 4 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS
|
||||
: Blocks.POLISHED_ANDESITE;
|
||||
if (!bridge) for (int down = 1; down <= p.foundationDepth(); down++)
|
||||
output.set(x, y - down, z, Blocks.STONE_BRICKS.defaultBlockState());
|
||||
output.set(x, y, z, paving.defaultBlockState());
|
||||
for (int up = 1; up <= 8; up++) output.set(x, y + up, z, Blocks.AIR.defaultBlockState());
|
||||
if (bridge && edge) {
|
||||
// Posts grow from the existing deck only; the certified natural bearings are untouched.
|
||||
output.set(x, y + 1, z, (along % 5 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.IRON_BARS).defaultBlockState());
|
||||
if (along % 5 == 0) output.set(x, y + 2, z, Blocks.TUFF_BRICK_SLAB.defaultBlockState());
|
||||
continue;
|
||||
}
|
||||
for (var direction : HORIZONTAL) {
|
||||
int nx = x + direction.getStepX(), nz = z + direction.getStepZ();
|
||||
if (nx >= 0 && nx < p.width() && nz >= 0 && nz < p.depth()
|
||||
&& p.surfaceY(nx, nz) == p.surfaceY(x, z) + 1) {
|
||||
output.set(x, y + 1, z, Blocks.TUFF_BRICK_STAIRS.defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, direction));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Exterior implements LostCityRenderer.BlockWriter {
|
||||
final LostCityPlan.Parcel p;
|
||||
final LostCityRenderer.BlockWriter output;
|
||||
final Rotation turn;
|
||||
Exterior(LostCityPlan.Parcel p, LostCityRenderer.BlockWriter output) {
|
||||
this.p = p; this.output = output;
|
||||
turn = switch (p.facing()) { case 1 -> Rotation.CLOCKWISE_90; case 2 -> Rotation.CLOCKWISE_180;
|
||||
case 3 -> Rotation.COUNTERCLOCKWISE_90; default -> Rotation.NONE; };
|
||||
}
|
||||
int x(int x, int z) { return switch (p.facing()) { case 1 -> p.width() - 1 - z; case 2 -> p.width() - 1 - x; case 3 -> z; default -> x; }; }
|
||||
int z(int x, int z) { return switch (p.facing()) { case 1 -> x; case 2 -> p.depth() - 1 - z; case 3 -> p.depth() - 1 - x; default -> z; }; }
|
||||
@Override public void set(int x, int y, int z, BlockState state) {
|
||||
output.set(x(x, z), y, z(x, z), palette(y, state).rotate(turn));
|
||||
}
|
||||
BlockState palette(int y, BlockState state) {
|
||||
Block block = null;
|
||||
if (state.is(Blocks.STONE) && y < 0) block = Blocks.STONE_BRICKS;
|
||||
else if (state.is(Blocks.DARK_OAK_PLANKS)) block = Blocks.OAK_PLANKS;
|
||||
else if (state.is(Blocks.DARK_OAK_FENCE)) block = Blocks.OAK_FENCE;
|
||||
else if (state.is(Blocks.DARK_OAK_STAIRS)) block = Blocks.OAK_STAIRS;
|
||||
else if (state.is(Blocks.DARK_OAK_SLAB)) block = Blocks.OAK_SLAB;
|
||||
else if (state.is(Blocks.CALCITE)) block = Blocks.SMOOTH_STONE;
|
||||
else if (y > 0) {
|
||||
if (state.is(Blocks.TUFF_BRICKS)) block = Blocks.STONE_BRICKS;
|
||||
else if (state.is(Blocks.CHISELED_TUFF_BRICKS) || state.is(Blocks.CHISELED_TUFF)) block = Blocks.CHISELED_STONE_BRICKS;
|
||||
else if (state.is(Blocks.TUFF_BRICK_SLAB)) block = Blocks.STONE_BRICK_SLAB;
|
||||
else if (state.is(Blocks.POLISHED_TUFF_SLAB)) block = Blocks.SMOOTH_STONE_SLAB;
|
||||
else if (state.is(Blocks.TUFF_BRICK_STAIRS)) block = Blocks.STONE_BRICK_STAIRS;
|
||||
else if (state.is(Blocks.POLISHED_TUFF)) block = Blocks.POLISHED_ANDESITE;
|
||||
}
|
||||
if (block == null) return state;
|
||||
BlockState result = block.defaultBlockState();
|
||||
for (var property : state.getProperties()) if (result.hasProperty(property)) result = copy(result, state, property);
|
||||
return result;
|
||||
}
|
||||
<T extends Comparable<T>> BlockState copy(BlockState into, BlockState from,
|
||||
net.minecraft.world.level.block.state.properties.Property<T> property) {
|
||||
return into.setValue(property, from.getValue(property));
|
||||
}
|
||||
void set(int x, int y, int z, Block block) { set(x, y, z, block.defaultBlockState()); }
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
|
||||
output.chest(x(x, z), y, z(x, z), table, seed ^ 0x21c17a7eL);
|
||||
}
|
||||
void box(int x0, int y0, int z0, int x1, int y1, int z1, Block block) {
|
||||
for (int x = x0; x <= x1; x++) for (int z = z0; z <= z1; z++) for (int y = y0; y <= y1; y++) set(x, y, z, block);
|
||||
}
|
||||
void render() {
|
||||
if (p.width() != 17 || p.depth() != 17) throw new IllegalArgumentException("Generation21 amenities require the certified 17x17 footprint");
|
||||
// Keep the proven pool containment, excavation steps and garage ramps. All new ornament is
|
||||
// composed before connection resolution, so clipping cannot split a rail or window design.
|
||||
var north = new LostCityPlan.Parcel(p.kind(), p.minX(), p.minZ(), 17, 17, p.floorY(), p.floors(), 0,
|
||||
p.seed(), p.foundationDepth(), p.surfaceHeights());
|
||||
LostCitySurface16.render(north, new LostCityRenderer.BlockWriter() {
|
||||
@Override public void set(int x, int y, int z, BlockState state) {
|
||||
Exterior.this.set(x, y, z, state);
|
||||
}
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { Exterior.this.chest(x, y, z, table, seed); }
|
||||
});
|
||||
// The north threshold is a level promenade, shared by every admitted parcel.
|
||||
for (int x = 0; x <= 16; x++) set(x, 0, 0, x % 4 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS);
|
||||
switch (p.kind()) {
|
||||
case PARK -> park();
|
||||
case PARK_GARDEN -> garden();
|
||||
case PARK_PLAYGROUND -> playground();
|
||||
case PARK_MEMORIAL -> memorial();
|
||||
case PLAZA -> plaza();
|
||||
case MARKET -> market();
|
||||
case PARKING -> garage();
|
||||
case POOL -> { if (p.foundationDepth() >= 8) pool(); else park(); }
|
||||
case EXCAVATION -> { if (p.foundationDepth() >= 8) excavation(); else park(); }
|
||||
default -> throw new IllegalArgumentException("Not an exterior amenity");
|
||||
}
|
||||
}
|
||||
void paths() {
|
||||
for (int n = 1; n <= 16; n++) for (int across = 7; across <= 9; across++) {
|
||||
set(across, 0, n, across == 8 ? Blocks.POLISHED_TUFF : Blocks.TUFF_BRICKS);
|
||||
set(n, 0, across, across == 8 ? Blocks.POLISHED_TUFF : Blocks.TUFF_BRICKS);
|
||||
}
|
||||
}
|
||||
void bench(int x, int y, int z, int length, Direction view) {
|
||||
for (int dx = 0; dx < length; dx++) set(x + dx, y, z, Blocks.OAK_STAIRS.defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, view.getOpposite()));
|
||||
set(x - 1, y, z, Blocks.TUFF_BRICK_SLAB); set(x + length, y, z, Blocks.TUFF_BRICK_SLAB);
|
||||
}
|
||||
void lamp(int x, int z) {
|
||||
set(x, 1, z, Blocks.CHISELED_TUFF_BRICKS); box(x, 2, z, x, 3, z, Blocks.OAK_FENCE);
|
||||
set(x, 4, z, Blocks.OAK_PLANKS); set(x, 5, z, Blocks.LANTERN);
|
||||
}
|
||||
void park() {
|
||||
paths(); bench(10, 1, 3, 3, Direction.SOUTH); bench(10, 1, 11, 3, Direction.NORTH);
|
||||
lamp(5, 11); lamp(11, 5);
|
||||
// A small oak screen joins the existing trees without introducing another wood palette.
|
||||
box(3, 1, 12, 3, 4, 12, Blocks.OAK_LOG);
|
||||
for (int x = 2; x <= 4; x++) for (int z = 11; z <= 13; z++)
|
||||
set(x, 5, z, Blocks.OAK_LEAVES.defaultBlockState().setValue(BlockStateProperties.PERSISTENT, true));
|
||||
for (int z = 2; z <= 5; z++) set(6, 0, z, Blocks.POLISHED_ANDESITE);
|
||||
}
|
||||
void garden() {
|
||||
paths();
|
||||
// Four parterres have low carved kerbs, soil beds and a shared central promenade.
|
||||
for (int x0 : new int[]{2, 10}) for (int z0 : new int[]{2, 10}) {
|
||||
for (int n = 0; n < 5; n++) {
|
||||
set(x0 + n, 0, z0, Blocks.SMOOTH_STONE); set(x0 + n, 0, z0 + 4, Blocks.SMOOTH_STONE);
|
||||
set(x0, 0, z0 + n, Blocks.SMOOTH_STONE); set(x0 + 4, 0, z0 + n, Blocks.SMOOTH_STONE);
|
||||
}
|
||||
set(x0, 1, z0, Blocks.CHISELED_TUFF); set(x0 + 4, 1, z0 + 4, Blocks.CHISELED_TUFF);
|
||||
}
|
||||
bench(7, 1, 15, 3, Direction.NORTH);
|
||||
// Pergola along the west planting bed, not across a circulation route.
|
||||
for (int z : new int[]{2, 6}) box(1, 1, z, 1, 3, z, Blocks.STRIPPED_OAK_LOG);
|
||||
box(1, 4, 2, 1, 4, 6, Blocks.OAK_SLAB); lamp(15, 8);
|
||||
}
|
||||
void playground() {
|
||||
paths();
|
||||
for (int x = 1; x <= 7; x++) { set(x, 0, 1, Blocks.OAK_PLANKS); set(x, 0, 7, Blocks.OAK_PLANKS); }
|
||||
for (int z = 1; z <= 7; z++) { set(1, 0, z, Blocks.OAK_PLANKS); set(7, 0, z, Blocks.OAK_PLANKS); }
|
||||
// The hanging seat's upper half meets the chain above; a bottom slab would leave a gap.
|
||||
set(4, 2, 4, Blocks.OAK_SLAB.defaultBlockState().setValue(BlockStateProperties.SLAB_TYPE, SlabType.TOP));
|
||||
// The last ascending step used to touch its neighbors only diagonally, over empty air.
|
||||
set(12, 1, 10, Blocks.OAK_PLANKS);
|
||||
// Braced climbing platform: low guards leave the three front steps open.
|
||||
for (int x : new int[]{11, 13}) set(x, 4, 13, Blocks.OAK_FENCE);
|
||||
set(12, 4, 13, Blocks.OAK_FENCE);
|
||||
set(11, 4, 11, Blocks.OAK_FENCE); set(13, 4, 11, Blocks.OAK_FENCE);
|
||||
bench(3, 1, 13, 3, Direction.NORTH); lamp(15, 3);
|
||||
}
|
||||
void plaza() {
|
||||
for (int x = 1; x <= 15; x++) for (int z = 1; z <= 15; z++) {
|
||||
int d = Math.max(Math.abs(x - 8), Math.abs(z - 8));
|
||||
set(x, 0, z, d == 3 || d == 6 ? Blocks.POLISHED_ANDESITE : d == 4 || d == 7 ? Blocks.TUFF_BRICKS : Blocks.SMOOTH_STONE);
|
||||
}
|
||||
// Dry civic fountain: bevelled bowl, carved pedestal, projecting cap.
|
||||
for (int x = 6; x <= 10; x++) for (int z = 6; z <= 10; z++) {
|
||||
if (x != 6 && x != 10 && z != 6 && z != 10) continue;
|
||||
boolean corner = (x == 6 || x == 10) && (z == 6 || z == 10);
|
||||
set(x, 1, z, corner ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICK_SLAB);
|
||||
}
|
||||
set(8, 1, 8, Blocks.CHISELED_TUFF); set(8, 2, 8, Blocks.CHISELED_TUFF_BRICKS);
|
||||
set(8, 3, 8, Blocks.POLISHED_TUFF_SLAB); lamp(2, 12); lamp(14, 4);
|
||||
}
|
||||
void memorial() {
|
||||
// The memorial keeps its open central approach and gains stepped stone shoulders.
|
||||
for (int x = 1; x <= 15; x++) for (int z = 1; z <= 15; z++)
|
||||
set(x, 0, z, x == 5 || x == 11 || z == 5 || z == 11 ? Blocks.TUFF_BRICKS : Blocks.POLISHED_ANDESITE);
|
||||
for (int x = 7; x <= 9; x++) {
|
||||
set(x, 1, 7, Blocks.TUFF_BRICK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
|
||||
set(x, 1, 9, Blocks.TUFF_BRICK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
|
||||
}
|
||||
set(8, 2, 8, Blocks.CHISELED_TUFF); set(8, 3, 8, Blocks.CHISELED_TUFF_BRICKS);
|
||||
set(8, 4, 8, Blocks.CHISELED_STONE_BRICKS); set(8, 5, 8, Blocks.SMOOTH_STONE_SLAB);
|
||||
lamp(3, 8); lamp(13, 8);
|
||||
}
|
||||
void market() {
|
||||
for (int x = 1; x <= 15; x++) for (int z = 1; z <= 15; z++)
|
||||
set(x, 0, z, x >= 7 && x <= 9 || z >= 7 && z <= 9 ? Blocks.TUFF_BRICKS : Blocks.POLISHED_ANDESITE);
|
||||
for (int x0 : new int[]{2, 11}) for (int z0 : new int[]{2, 11}) {
|
||||
for (int x = x0; x <= x0 + 3; x++) for (int z = z0; z <= z0 + 2; z++)
|
||||
set(x, 4, z, Blocks.OAK_SLAB);
|
||||
for (int x : new int[]{x0, x0 + 3}) {
|
||||
set(x, 1, z0, Blocks.CHISELED_TUFF_BRICKS);
|
||||
box(x, 2, z0, x, 3, z0, Blocks.STRIPPED_OAK_LOG);
|
||||
}
|
||||
// Only one roof layer: the former raised bottom slab was separated by half a block.
|
||||
// Rear posts and a full-width beam make the canopy's load path visible from the stall.
|
||||
for (int x : new int[]{x0, x0 + 3}) box(x, 2, z0 + 2, x, 3, z0 + 2, Blocks.STRIPPED_OAK_LOG);
|
||||
box(x0, 3, z0, x0 + 3, 3, z0, Blocks.OAK_PLANKS);
|
||||
box(x0, 3, z0 + 2, x0 + 3, 3, z0 + 2, Blocks.OAK_PLANKS);
|
||||
}
|
||||
// The shared chest replaces this stall's counter: a pot cannot sit on its recessed lid.
|
||||
set(13, 2, 13, Blocks.AIR); set(12, 2, 13, Blocks.FLOWER_POT);
|
||||
lamp(1, 8); lamp(15, 8);
|
||||
}
|
||||
void pool() {
|
||||
// Each submerged tread bears on a masonry riser down to the basin floor;
|
||||
// water between diagonal stairs is not a structural connection.
|
||||
for (int step = 1; step <= 3; step++) for (int x = 7; x <= 9; x++)
|
||||
for (int y = -3; y < -step; y++) set(x, y, step + 4, Blocks.SMOOTH_STONE);
|
||||
for (int x = 3; x <= 13; x++) for (int z = 4; z <= 13; z++) {
|
||||
if (x == 3 || x == 13 || z == 4 || z == 13) set(x, 0, z, Blocks.SMOOTH_STONE);
|
||||
}
|
||||
// Changing/showers canopy stands beside the deck; it never enters the sealed basin.
|
||||
for (int x : new int[]{10, 15}) box(x, 1, 1, x, 3, 1, Blocks.STRIPPED_OAK_LOG);
|
||||
for (int x : new int[]{10, 15}) box(x, 1, 3, x, 3, 3, Blocks.STRIPPED_OAK_LOG);
|
||||
box(10, 3, 1, 15, 3, 1, Blocks.OAK_PLANKS);
|
||||
box(10, 3, 3, 15, 3, 3, Blocks.OAK_PLANKS);
|
||||
box(10, 4, 1, 15, 4, 3, Blocks.OAK_SLAB);
|
||||
for (int x = 11; x <= 14; x++) set(x, 1, 1, Blocks.WATER_CAULDRON.defaultBlockState()
|
||||
.setValue(BlockStateProperties.LEVEL_CAULDRON, 2));
|
||||
set(15, 4, 2, Blocks.OAK_PLANKS); set(15, 5, 2, Blocks.LANTERN);
|
||||
for (int z : new int[]{5, 9}) set(15, 1, z, Blocks.OAK_STAIRS.defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST));
|
||||
box(2, 0, 14, 14, 0, 15, Blocks.TUFF_BRICKS);
|
||||
}
|
||||
void excavation() {
|
||||
// Each descending stone step rests on a small riser down to the certified pit floor.
|
||||
for (int step = 1; step <= 5; step++) for (int x = 7; x <= 9; x++)
|
||||
for (int y = -4; y < -step; y++) set(x, y, step + 2, Blocks.STONE_BRICKS);
|
||||
// Reinforced temporary footings and timber trestles stay out of the central descent.
|
||||
for (int x : new int[]{4, 12}) {
|
||||
box(x, -4, 11, x, -1, 11, Blocks.CHISELED_STONE_BRICKS);
|
||||
set(x, 0, 11, Blocks.OAK_PLANKS);
|
||||
}
|
||||
box(3, 3, 13, 13, 3, 13, Blocks.STRIPPED_OAK_LOG);
|
||||
for (int x : new int[]{3, 13}) box(x, -4, 13, x, 2, 13, Blocks.STONE_BRICKS);
|
||||
// Replace the single beam with an open construction shelter, one canopy on grounded posts.
|
||||
// All four trestles start on the excavated floor, not on the missing former ground plane.
|
||||
for (int x : new int[]{3, 13}) box(x, -4, 10, x, 2, 10, Blocks.STONE_BRICKS);
|
||||
box(3, 3, 10, 13, 3, 10, Blocks.STRIPPED_OAK_LOG);
|
||||
for (int x : new int[]{3, 8, 13}) box(x, 3, 10, x, 3, 13, Blocks.OAK_PLANKS);
|
||||
box(3, 4, 10, 13, 4, 13, Blocks.OAK_SLAB);
|
||||
set(3, 4, 13, Blocks.OAK_PLANKS);
|
||||
set(3, 5, 13, Blocks.LANTERN);
|
||||
set(1, 1, 11, Blocks.GRINDSTONE); set(1, 1, 10, Blocks.CRAFTING_TABLE);
|
||||
}
|
||||
void garage() {
|
||||
int base = p.foundationDepth() >= 8 ? -5 : 0;
|
||||
// The ramp's five-wide masonry wedge joins every tread to the garage floor.
|
||||
if (base < 0) for (int z = 0; z <= 4; z++) for (int x = 6; x <= 10; x++)
|
||||
for (int y = base + 1; y < -z; y++) set(x, y, z, Blocks.STONE_BRICKS);
|
||||
for (int x : new int[]{3, 13}) for (int z : new int[]{3, 13}) {
|
||||
box(x, base + 1, z, x, base + 4, z, Blocks.POLISHED_ANDESITE);
|
||||
set(x, base + 1, z, Blocks.CHISELED_TUFF_BRICKS);
|
||||
}
|
||||
for (int z = 6; z <= 15; z++) for (int x = 1; x <= 15; x++)
|
||||
if (x <= 5 || x >= 11) set(x, base, z, z % 4 == 0 ? Blocks.SMOOTH_STONE : Blocks.POLISHED_ANDESITE);
|
||||
// A ribbed structural fascia and ventilation blades make this an open modern garage.
|
||||
for (int x = 0; x <= 16; x++) {
|
||||
// The underground ramp cuts through this former ceiling strip: never paint it back.
|
||||
if (base == 0 || x < 6 || x > 10)
|
||||
set(x, base + 5, 0, x % 4 == 0 ? Blocks.CHISELED_STONE_BRICKS : Blocks.SMOOTH_STONE);
|
||||
if (x < 6 || x > 10) set(x, base + 3, 0, Blocks.IRON_TRAPDOOR.defaultBlockState()
|
||||
.setValue(BlockStateProperties.OPEN, true).setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
|
||||
}
|
||||
for (int x : new int[]{5, 11}) {
|
||||
set(x, base + 4, 9, Blocks.SMOOTH_STONE);
|
||||
set(x, base + 3, 9, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package fr.koka.sanctuary.worldgen.city;
|
||||
|
||||
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||
import fr.koka.sanctuary.expansion.ExpansionTerrain;
|
||||
import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
|
||||
/** Generation21 bounded noise-column cache shared by surface and underground planning, never loaded chunks. */
|
||||
public final class LostCityTerrain21 implements LostCityUnderground16.Terrain, LostCityPlanner16.Terrain {
|
||||
public static final int MAX_COLUMNS = 24_096;
|
||||
private final ExpansionIsland island;
|
||||
private final NoiseBasedChunkGenerator generator;
|
||||
private final RandomState random;
|
||||
private final LevelHeightAccessor height;
|
||||
private final Predicate<BlockPos> protectedVolume;
|
||||
private final Map<Long, NoiseColumn> columns = new HashMap<>();
|
||||
private final BlockPos fluidCandidate;
|
||||
|
||||
public LostCityTerrain21(ExpansionIsland island, NoiseBasedChunkGenerator generator, RandomState random,
|
||||
LevelHeightAccessor height, Predicate<BlockPos> protectedVolume) {
|
||||
this.island = island; this.generator = generator; this.random = random;
|
||||
this.height = height; this.protectedVolume = protectedVolume;
|
||||
fluidCandidate = ExpansionTerrain.pocketCandidate(island);
|
||||
}
|
||||
|
||||
private NoiseColumn column(int x, int z) {
|
||||
if (!owns(x, z)) return null;
|
||||
long key = ((long) x << 32) ^ (z & 0xffffffffL);
|
||||
var cached = columns.get(key);
|
||||
if (cached != null) return cached;
|
||||
if (columns.size() >= MAX_COLUMNS) return null;
|
||||
var result = generator.getBaseColumn(x, z, height, random);
|
||||
columns.put(key, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public LostCityPlanner.Ground ground(int x, int z) {
|
||||
var column = column(x, z);
|
||||
if (column == null) return new LostCityPlanner.Ground(0, 0, false, true);
|
||||
int top = 383;
|
||||
while (top > 0 && column.getBlock(top).isAir()) top--;
|
||||
int bottom = top;
|
||||
while (bottom > 0 && top - bottom < 24 && column.getBlock(bottom).isSolid()
|
||||
&& column.getBlock(bottom).getFluidState().isEmpty()) bottom--;
|
||||
boolean dry = column.getBlock(top).isSolid() && column.getBlock(top).getFluidState().isEmpty();
|
||||
boolean water = island.origin() && top >= 168 && dry && top - bottom >= 6
|
||||
&& PopulationHydrologyRuntime.protects(generator, random, new BlockPos(x, top, z), 1, 30, 12);
|
||||
return new LostCityPlanner.Ground(top, top - bottom, dry, water);
|
||||
}
|
||||
|
||||
@Override public boolean solid(int x, int y, int z) {
|
||||
if (y < 0 || y >= 384) return false;
|
||||
var column = column(x, z);
|
||||
return column != null && column.getBlock(y).isSolid() && column.getBlock(y).getFluidState().isEmpty();
|
||||
}
|
||||
@Override public boolean protectedAt(int x, int y, int z) {
|
||||
if (y < 0 || y >= 384) return true;
|
||||
var column = column(x, z);
|
||||
if (column == null || !column.getBlock(y).getFluidState().isEmpty()) return true;
|
||||
if (!island.origin() && island.relief().equals("volcano")
|
||||
&& Math.abs(x - fluidCandidate.getX()) <= 2 && Math.abs(z - fluidCandidate.getZ()) <= 2) return true;
|
||||
return protectedVolume.test(new BlockPos(x, y, z));
|
||||
}
|
||||
@Override public boolean exteriorAir(int x, int y, int z, int dx, int dz) {
|
||||
if (Math.abs(dx) + Math.abs(dz) != 1 || y < 1 || y >= 380 || !owns(x, z)) return false;
|
||||
for (int distance = 0; distance <= 32; distance++) {
|
||||
int px = x + dx * distance, pz = z + dz * distance;
|
||||
if (!owns(px, pz)) return false; // Never infer exterior air in another present or future owner.
|
||||
var sample = column(px, pz);
|
||||
if (sample == null) return false;
|
||||
for (int dy = -3; dy < 3; dy++) if (!sample.getBlock(y + dy).isAir()
|
||||
|| protectedVolume.test(new BlockPos(px, y + dy, pz))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@Override public boolean owns(int x, int z) { return island.contains(x, z); }
|
||||
public int columnsUsed() { return columns.size(); }
|
||||
}
|
||||
+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;
|
||||
}
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
package fr.koka.sanctuary.worldgen.city;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityUnderground16.*;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Module;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** An independent public undercroft. No surface district, fake parcel or city reservation is needed.
|
||||
* Only complete hall/access pairs are published; optional utility wings cannot invalidate them. */
|
||||
public final class LostCityUnderground22 {
|
||||
public static final int MAX_COLUMNS = 6_000, MAX_SAMPLES = LostCityUnderground16.MAX_SAMPLES;
|
||||
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
|
||||
private LostCityUnderground22() {}
|
||||
public interface Terrain extends LostCityUnderground16.Terrain { int surfaceY(int x, int z); }
|
||||
|
||||
public static Plan plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
|
||||
if (!List.of(512, 724, 1024).contains(diameter)) throw new IllegalArgumentException("Unsupported undercroft island size");
|
||||
var probe = new Probe(terrain, cx, cz, diameter);
|
||||
List<Module> accepted = null;
|
||||
long acceptedSeed = seed;
|
||||
try {
|
||||
search: for (int attempt = 0; attempt < 40; attempt++) {
|
||||
double angle = (mix(seed ^ 0x22A11L) >>> 11) * 0x1.0p-53 * Math.PI * 2 + attempt * 2.399963229728653;
|
||||
double radius = attempt == 0 ? 0 : diameter * (attempt < 9 ? .09 : attempt < 21 ? .18 : .28);
|
||||
int x = cx + (int) Math.round(Math.cos(angle) * radius), z = cz + (int) Math.round(Math.sin(angle) * radius);
|
||||
int top = accessHeight(x, z, probe);
|
||||
if (top < 0) continue;
|
||||
for (int drop : new int[]{22, 32, 46, 62, 86, 110}) {
|
||||
int floor = top - drop;
|
||||
if (floor < 24) continue;
|
||||
long networkSeed = mix(seed ^ ((long) x << 32) ^ z ^ drop);
|
||||
for (int turn = 0; turn < 4; turn++) {
|
||||
int direction = (int) Math.floorMod(networkSeed + turn, 4);
|
||||
var access = new Module(Kind.ACCESS, x - 3, floor - 1, z - 3, 7, drop + 4, 7,
|
||||
1 << direction, networkSeed, List.of());
|
||||
var hall = attached(access, Kind.EXPANSION_HALL, direction, mix(networkSeed + 1));
|
||||
// Cheap hall witnesses come before repeatedly scanning a tall access shaft.
|
||||
if (!coarse(hall, probe)) continue;
|
||||
var modules = new ArrayList<Module>();
|
||||
access = certify(access, probe);
|
||||
if (access == null) continue;
|
||||
modules.add(access);
|
||||
if (!attach(modules, 0, hall, direction, probe)) continue;
|
||||
accepted = modules; acceptedSeed = networkSeed;
|
||||
break search;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (BudgetExceeded exhausted) { probe.reject("required-search-budget"); }
|
||||
if (accepted != null) {
|
||||
// A sewer and a pool are additional destinations, never conditions for the public hall.
|
||||
int forward = Integer.numberOfTrailingZeros(accepted.getFirst().openings());
|
||||
try {
|
||||
for (int side : new int[]{(forward + 1) & 3, (forward + 3) & 3})
|
||||
if (attach(accepted, 1, attached(accepted.get(1), Kind.SEWER, side, mix(acceptedSeed + 2)), side, probe)) break;
|
||||
if (probe.samples < MAX_SAMPLES - 70_000 && probe.columns.size() < MAX_COLUMNS - 1_900)
|
||||
attach(accepted, 1, attached(accepted.get(1), Kind.POOLROOM, forward, mix(acceptedSeed + 3)), forward, probe);
|
||||
} catch (BudgetExceeded exhausted) { probe.reject("optional-wing-budget"); }
|
||||
}
|
||||
var networks = accepted == null ? List.<Network>of() : List.of(new Network(
|
||||
accepted.getFirst().centerX() >> 4, accepted.getFirst().centerZ() >> 4, acceptedSeed, accepted));
|
||||
System.getLogger(LostCityUnderground22.class.getName()).log(System.Logger.Level.INFO,
|
||||
"Independent undercroft22 seed=" + seed + " networks=" + networks.size() + " columns=" + probe.columns.size()
|
||||
+ " samples=" + probe.samples + " kinds=" + (accepted == null ? List.of() : accepted.stream().map(Module::kind).toList())
|
||||
+ " admission=" + probe.rejections);
|
||||
return new Plan(networks, probe.samples);
|
||||
}
|
||||
|
||||
/** The whole visible seven-square hatch deck stays within two blocks of existing dry ground. */
|
||||
private static int accessHeight(int cx, int cz, Probe p) {
|
||||
int low = 384, high = -1;
|
||||
for (int z = cz - 3; z <= cz + 3; z++) for (int x = cx - 3; x <= cx + 3; x++) {
|
||||
int top = p.surface(x, z);
|
||||
if (top < 64 || top > 365 || !p.solid(x, top, z) || p.protectedAt(x, top, z)) return p.rejectHeight("dry-access");
|
||||
low = Math.min(low, top); high = Math.max(high, top);
|
||||
if (high - low > 2) return p.rejectHeight("access-relief");
|
||||
}
|
||||
return high;
|
||||
}
|
||||
|
||||
private static Module attached(Module parent, Kind kind, int direction, long seed) {
|
||||
int width = kind == Kind.POOLROOM ? 57 : kind == Kind.EXPANSION_HALL ? 25 : 9;
|
||||
int depth = kind == Kind.POOLROOM ? 31 : width;
|
||||
int height = kind == Kind.POOLROOM ? 13 : kind == Kind.EXPANSION_HALL ? 10 : 7;
|
||||
int offset = (DX[direction] == 0 ? parent.depth() / 2 + depth / 2 : parent.width() / 2 + width / 2) + 8;
|
||||
int x = parent.centerX() + DX[direction] * offset, z = parent.centerZ() + DZ[direction] * offset;
|
||||
return new Module(kind, x - width / 2, parent.floorY() - (kind == Kind.POOLROOM ? 5 : 1),
|
||||
z - depth / 2, width, height, depth, 1 << ((direction + 2) & 3), seed, List.of());
|
||||
}
|
||||
private static boolean attach(List<Module> modules, int parentIndex, Module room, int direction, Probe p) {
|
||||
var parent = modules.get(parentIndex);
|
||||
int x = DX[direction] == 0 ? parent.centerX() - 2 : direction == 1 ? parent.maxX() + 1 : room.maxX() + 1;
|
||||
int z = DZ[direction] == 0 ? parent.centerZ() - 2 : direction == 2 ? parent.maxZ() + 1 : room.maxZ() + 1;
|
||||
var passage = new Module(Kind.PASSAGE, x, parent.floorY() - 1, z, DX[direction] == 0 ? 5 : 7, 6,
|
||||
DZ[direction] == 0 ? 5 : 7, (1 << direction) | (1 << ((direction + 2) & 3)), mix(room.seed()), List.of());
|
||||
for (var old : modules) if (overlaps(room, old) || overlaps(passage, old)) { p.reject("wing-collision"); return false; }
|
||||
if (!coarse(room, p) || !coarse(passage, p)) return false;
|
||||
room = certify(room, p); if (room == null) return false;
|
||||
passage = certify(passage, p); if (passage == null) return false;
|
||||
if (LostCityUnderground16.supportConflict(room, passage)) return false;
|
||||
for (var old : modules) if (LostCityUnderground16.supportConflict(room, old)
|
||||
|| LostCityUnderground16.supportConflict(passage, old)) return false;
|
||||
modules.set(parentIndex, new Module(parent.kind(), parent.minX(), parent.minY(), parent.minZ(), parent.width(),
|
||||
parent.height(), parent.depth(), parent.openings() | 1 << direction, parent.seed(), parent.supports()));
|
||||
modules.add(room); modules.add(passage);
|
||||
return true;
|
||||
}
|
||||
private static boolean overlaps(Module a, Module b) {
|
||||
return a.minX() <= b.maxX() && a.maxX() >= b.minX() && a.minZ() <= b.maxZ() && a.maxZ() >= b.minZ()
|
||||
&& a.boundsMinY() <= b.boundsMaxY() && a.boundsMaxY() >= b.boundsMinY();
|
||||
}
|
||||
private static boolean coarse(Module m, Probe p) {
|
||||
int solid = 0;
|
||||
for (int x : new int[]{m.minX(), m.centerX(), m.maxX()}) for (int z : new int[]{m.minZ(), m.centerZ(), m.maxZ()}) {
|
||||
if (!p.owns(x, z)) { p.reject("outside-island"); return false; }
|
||||
for (int y : new int[]{m.minY(), m.floorY() + 2, m.maxY()}) {
|
||||
if (p.protectedAt(x, y, z)) { p.reject("coarse-protected"); return false; }
|
||||
if (p.solid(x, y, z)) solid++;
|
||||
}
|
||||
}
|
||||
if (solid < 5) { p.reject("coarse-rock"); return false; }
|
||||
return true;
|
||||
}
|
||||
private static Module certify(Module m, Probe p) {
|
||||
var supports = new ArrayList<Support>();
|
||||
int stride = m.kind() == Kind.EXPANSION_HALL || m.kind() == Kind.POOLROOM ? 8 : 3;
|
||||
for (int z : lattice(m.minZ(), m.maxZ(), stride)) for (int x : lattice(m.minX(), m.maxX(), stride)) {
|
||||
var support = support(m, x, z, p);
|
||||
if (support == null) { p.reject("no-short-anchor"); return null; }
|
||||
supports.add(support);
|
||||
}
|
||||
int solid = 0, count = 0;
|
||||
for (int z = m.minZ(); z <= m.maxZ(); z++) for (int x = m.minX(); x <= m.maxX(); x++) {
|
||||
if (!p.owns(x, z)) { p.reject("outside-island"); return null; }
|
||||
for (int y = m.minY(); y <= m.maxY(); y++) {
|
||||
if (p.protectedAt(x, y, z)) { p.reject("protected-volume"); return null; }
|
||||
if (p.solid(x, y, z)) solid++; count++;
|
||||
}
|
||||
}
|
||||
// The constructed shell can cross a native cavity; it must still be embedded in real terrain.
|
||||
if (solid * 100L < count * (m.kind() == Kind.ACCESS ? 50 : 20)) { p.reject("insufficient-rock"); return null; }
|
||||
return new Module(m.kind(), m.minX(), m.minY(), m.minZ(), m.width(), m.height(), m.depth(),
|
||||
m.openings(), m.seed(), supports);
|
||||
}
|
||||
private static Support support(Module m, int x, int z, Probe p) {
|
||||
// Save even direct bearings: the existing protection builder then retains their untouched rock.
|
||||
for (int distance = 1; distance <= 8; distance++) {
|
||||
int y = m.minY() - distance;
|
||||
if (y < 2 || p.protectedAt(x, y, z)) break;
|
||||
if (p.solid(x, y, z) && p.solid(x, y - 1, z) && !p.protectedAt(x, y - 1, z))
|
||||
return new Support(Type.PILLAR, x, y, z, m.minY() - 1);
|
||||
}
|
||||
for (int distance = 1; distance <= 8; distance++) {
|
||||
int y = m.maxY() + distance;
|
||||
if (y > 381 || p.protectedAt(x, y, z)) break;
|
||||
if (p.solid(x, y, z) && p.solid(x, y + 1, z) && !p.protectedAt(x, y + 1, z))
|
||||
return new Support(Type.CHAIN, x, m.maxY() + 1, z, y);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private static List<Integer> lattice(int min, int max, int stride) {
|
||||
var result = new ArrayList<Integer>(); for (int n = min; n <= max; n += stride) result.add(n);
|
||||
if (result.getLast() != max) result.add(max); return result;
|
||||
}
|
||||
private static final class Probe {
|
||||
final Terrain terrain; final int cx, cz; final double radius;
|
||||
final Map<Long, Integer> columns = new HashMap<>(); final Map<String, Integer> rejections = new LinkedHashMap<>(); int samples;
|
||||
Probe(Terrain terrain, int cx, int cz, int diameter) { this.terrain = terrain; this.cx = cx; this.cz = cz; radius = diameter / 2.0 - 4; }
|
||||
void count() { if (samples >= MAX_SAMPLES) throw new BudgetExceeded(); samples++; }
|
||||
boolean owns(int x, int z) { return Math.hypot((double) x - cx, (double) z - cz) <= radius && terrain.owns(x, z); }
|
||||
int surface(int x, int z) {
|
||||
if (!owns(x, z)) return -1;
|
||||
long key = ((long) x << 32) ^ (z & 0xffffffffL); var value = columns.get(key); if (value != null) return value;
|
||||
if (columns.size() >= MAX_COLUMNS) throw new BudgetExceeded();
|
||||
count(); int y = terrain.surfaceY(x, z); columns.put(key, y); return y;
|
||||
}
|
||||
boolean solid(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) < 0) return false; count(); return terrain.solid(x, y, z); }
|
||||
boolean protectedAt(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) < 0) return true; count(); return terrain.protectedAt(x, y, z); }
|
||||
void reject(String reason) { rejections.merge(reason, 1, Integer::sum); }
|
||||
int rejectHeight(String reason) { reject(reason); return -1; }
|
||||
}
|
||||
private static final class BudgetExceeded extends RuntimeException { private static final long serialVersionUID = 1L; }
|
||||
private static long mix(long x) { x = (x ^ x >>> 30) * 0xbf58476d1ce4e5b9L; x = (x ^ x >>> 27) * 0x94d049bb133111ebL; return x ^ x >>> 31; }
|
||||
}
|
||||
+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());
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package fr.koka.sanctuary.worldgen.city;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.util.Optional;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureType;
|
||||
|
||||
/** An independent native start keeps a certified underground hall available without a surface city. */
|
||||
public final class LostCityUndergroundStructure22 extends Structure {
|
||||
public static final MapCodec<LostCityUndergroundStructure22> CODEC = simpleCodec(LostCityUndergroundStructure22::new);
|
||||
|
||||
public LostCityUndergroundStructure22(StructureSettings settings) { super(settings); }
|
||||
|
||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| generator.rootGeneration() != 22) return Optional.empty();
|
||||
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
|
||||
if (island == null || !island.origin() || island.generation() != 22) return Optional.empty();
|
||||
var networks = generator.undergroundHall22().networks().stream()
|
||||
.filter(n -> n.startChunkX() == context.chunkPos().x() && n.startChunkZ() == context.chunkPos().z()).toList();
|
||||
if (networks.isEmpty()) return Optional.empty();
|
||||
for (var network : networks) for (var module : network.modules()) {
|
||||
var bounds = LostCityUndergroundPiece20.bounds(module);
|
||||
if (Math.abs((long) Math.floorDiv(bounds.minX(), 16) - network.startChunkX()) > 7
|
||||
|| Math.abs((long) Math.floorDiv(bounds.maxX(), 16) - network.startChunkX()) > 7
|
||||
|| Math.abs((long) Math.floorDiv(bounds.minZ(), 16) - network.startChunkZ()) > 7
|
||||
|| Math.abs((long) Math.floorDiv(bounds.maxZ(), 16) - network.startChunkZ()) > 7)
|
||||
throw new IllegalStateException("Underground hall exceeds native structure reference range");
|
||||
}
|
||||
return Optional.of(new GenerationStub(LostCityUndergroundStructures22.locator(networks.getFirst()), pieces -> {
|
||||
// Shafts are last so connected rooms cannot refill their surface hatches.
|
||||
networks.forEach(n -> n.modules().stream().filter(m -> m.kind() != LostCityUnderground16.Kind.ACCESS)
|
||||
.forEach(m -> pieces.addPiece(new LostCityUndergroundPiece20(m))));
|
||||
networks.forEach(n -> n.modules().stream().filter(m -> m.kind() == LostCityUnderground16.Kind.ACCESS)
|
||||
.forEach(m -> pieces.addPiece(new LostCityUndergroundPiece20(m))));
|
||||
}));
|
||||
}
|
||||
|
||||
@Override public StructureType<?> type() { return LostCityUndergroundStructures22.TYPE; }
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
package fr.koka.sanctuary.worldgen.city;
|
||||
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureType;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
|
||||
/** Native lifecycle for generation22's standalone hall; its saved modules reuse the generation20 pieces. */
|
||||
public final class LostCityUndergroundStructures22 {
|
||||
public static final ResourceKey<Structure> KEY = ResourceKey.create(Registries.STRUCTURE,
|
||||
SanctuaryMod.id("underground_hall22"));
|
||||
public static final StructureType<LostCityUndergroundStructure22> TYPE = Registry.register(
|
||||
BuiltInRegistries.STRUCTURE_TYPE, SanctuaryMod.id("underground_hall_v22"), () -> LostCityUndergroundStructure22.CODEC);
|
||||
|
||||
private LostCityUndergroundStructures22() {}
|
||||
public static void register() { /* Initialize the codec before reading the dynamic structure registry. */ }
|
||||
|
||||
public static void generate(ExpansionChunkGenerator generator, RegistryAccess registries,
|
||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || generator.rootGeneration() != 22) return;
|
||||
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
||||
if (island == null || !island.origin() || island.generation() != 22) return;
|
||||
if (sanctuary.undergroundHall22().networks().stream().noneMatch(n -> n.startChunkX() == chunk.getPos().x()
|
||||
&& n.startChunkZ() == chunk.getPos().z())) return;
|
||||
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||
Structure structure = holder.value();
|
||||
StructureStart previous = manager.getStartForStructure(structure, chunk);
|
||||
if (previous != null && previous.isValid()) return;
|
||||
var random = generator.randomState(island);
|
||||
var sampler = random.createClimateSampler(SamplerContext.builder().enableCaches().build());
|
||||
var start = structure.generate(holder, level, registries, generator, generator.getBiomeSource(), sampler,
|
||||
random, templates, island.seed(), chunk.getPos(), previous == null ? 0 : previous.getReferences(),
|
||||
chunk, structure.biomes()::contains);
|
||||
if (start.isValid()) manager.setStartForStructure(structure, start, chunk);
|
||||
}
|
||||
|
||||
/** Locate returns the persisted surface hatch, loading only the native structure start when necessary. */
|
||||
public static Pair<BlockPos, Holder<Structure>> findNearest(ServerLevel level, HolderSet<Structure> structures,
|
||||
BlockPos origin, int radius, boolean skipReferenced) {
|
||||
if (!Level.OVERWORLD.equals(level.dimension())
|
||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| generator.rootGeneration() != 22 || !level.structureManager().shouldGenerateStructures()) return null;
|
||||
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||
if (!structures.contains(holder)) return null;
|
||||
var candidates = new ArrayList<LostCityUnderground16.Network>();
|
||||
int originChunkX = origin.getX() >> 4, originChunkZ = origin.getZ() >> 4;
|
||||
for (var network : generator.undergroundHall22().networks()) {
|
||||
var island = generator.islandForChunk(network.startChunkX(), network.startChunkZ());
|
||||
if (island == null || !island.origin() || island.generation() != 22 || !island.status().equals("ready")) continue;
|
||||
if (Math.abs((long) network.startChunkX() - originChunkX) <= radius
|
||||
&& Math.abs((long) network.startChunkZ() - originChunkZ) <= radius) candidates.add(network);
|
||||
}
|
||||
candidates.sort(Comparator.comparingDouble(n -> origin.distSqr(locator(n))));
|
||||
var visited = new HashSet<Long>();
|
||||
for (var network : candidates) {
|
||||
long key = ((long) network.startChunkX() << 32) ^ (network.startChunkZ() & 0xffffffffL);
|
||||
if (!visited.add(key)) continue;
|
||||
var chunk = level.getChunk(network.startChunkX(), network.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
StructureStart start = level.structureManager().getStartForStructure(holder.value(), chunk);
|
||||
if (start == null || !start.isValid() || skipReferenced && !start.canBeReferenced()) continue;
|
||||
var entrance = start.getPieces().stream()
|
||||
.filter(p -> p instanceof LostCityUndergroundPiece20 piece && piece.module().kind() == LostCityUnderground16.Kind.ACCESS)
|
||||
.map(p -> locator(((LostCityUndergroundPiece20) p).module()))
|
||||
.min(Comparator.comparingDouble(origin::distSqr));
|
||||
if (entrance.isEmpty()) continue;
|
||||
if (skipReferenced) level.structureManager().addReference(start);
|
||||
return Pair.of(entrance.get(), holder);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static BlockPos locator(LostCityUnderground16.Network network) {
|
||||
return locator(network.modules().stream().filter(m -> m.kind() == LostCityUnderground16.Kind.ACCESS)
|
||||
.findFirst().orElseThrow(() -> new IllegalArgumentException("Underground hall has no surface access")));
|
||||
}
|
||||
|
||||
private static BlockPos locator(LostCityUnderground16.Module access) {
|
||||
int exit = Integer.numberOfTrailingZeros(access.openings() == 0 ? 1 : access.openings());
|
||||
int wall = (exit + 2) & 3;
|
||||
int x = wall == 1 ? access.width() - 2 : wall == 3 ? 1 : access.width() / 2;
|
||||
int z = wall == 2 ? access.depth() - 2 : wall == 0 ? 1 : access.depth() / 2;
|
||||
return new BlockPos(access.minX() + x, access.maxY() - 1, access.minZ() + z);
|
||||
}
|
||||
}
|
||||
+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; }
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package fr.koka.sanctuary.worldgen.heritage;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.util.Locale;
|
||||
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** Read-only operator catalogue. Inspecting a plan neither loads chunks nor activates an expansion. */
|
||||
public final class HeritageCommands19 {
|
||||
private HeritageCommands19() {}
|
||||
public static void register() {
|
||||
CommandRegistrationCallback.EVENT.register((dispatcher, registries, environment) ->
|
||||
dispatcher.register(Commands.literal("sanctuary")
|
||||
.requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS))
|
||||
.then(Commands.literal("structures").executes(context -> {
|
||||
var source = context.getSource();
|
||||
var level = source.getLevel();
|
||||
if (!Level.OVERWORLD.equals(level.dimension())
|
||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && generator.rootGeneration() != 22) || generator.rootGeneration() != 22 && !generator.experimentalStructures()) {
|
||||
source.sendFailure(Component.translatable("sanctuary.heritage.unavailable"));
|
||||
return 0;
|
||||
}
|
||||
var plan = generator.heritagePlan19();
|
||||
source.sendSuccess(() -> Component.translatable("sanctuary.heritage.header", plan.sites().size(), plan.links().size()), false);
|
||||
for (var site : plan.sites()) source.sendSuccess(() -> Component.translatable("sanctuary.heritage.entry",
|
||||
Component.translatable("sanctuary.heritage.kind." + site.kind().name().toLowerCase(Locale.ROOT)),
|
||||
site.entry().x(), site.entry().y(), site.entry().z()), false);
|
||||
if (generator.rootGeneration() == 21 || generator.rootGeneration() == 22) for (var module : generator.transitPlan21().modules()) {
|
||||
if (module.kind() != fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind.ACCESS) continue;
|
||||
var at = fr.koka.sanctuary.worldgen.transit.TransitPiece21.locator(module);
|
||||
source.sendSuccess(() -> Component.translatable("sanctuary.transit.entry", at.getX(), at.getY(), at.getZ()), false);
|
||||
}
|
||||
if (generator.rootGeneration() == 22) for (var network : generator.undergroundHall22().networks()) {
|
||||
var at = fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.locator(network);
|
||||
source.sendSuccess(() -> Component.translatable("sanctuary.underground_hall.entry", at.getX(), at.getY(), at.getZ()), false);
|
||||
}
|
||||
return plan.sites().size() + (generator.rootGeneration() == 22
|
||||
? (int) generator.transitPlan21().modules().stream().filter(m -> m.kind() == fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind.ACCESS).count()
|
||||
+ generator.undergroundHall22().networks().size() : 0);
|
||||
}))));
|
||||
}
|
||||
}
|
||||
+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 schema19 geometry. Reload never needs the planner or a distant route destination. */
|
||||
public final class HeritagePiece19 extends StructurePiece {
|
||||
private static final Set<String> SITE_KEYS = Set.of("Schema", "Type", "Id", "Kind", "MinX", "FloorY", "MinZ",
|
||||
"Width", "Depth", "Height", "Facing", "Seed", "FoundationDepth", "Supports", "Entry");
|
||||
private static final Set<String> ROUTE_KEYS = Set.of("Schema", "Type", "Id", "FromId", "ToId", "Kind",
|
||||
"StartChunkX", "StartChunkZ", "Cells", "Seed");
|
||||
private static final Set<String> SUPPORT_KEYS = Set.of("X", "MinY", "Z", "MaxY");
|
||||
private static final Set<String> ENTRY_KEYS = Set.of("X", "Y", "Z", "Facing");
|
||||
private static final Set<String> CELL_KEYS = Set.of("X", "FloorY", "Z", "FoundationDepth", "Rail", "RailPresent");
|
||||
private final HeritagePlan19.Site site;
|
||||
private final HeritagePlan19.Segment segment;
|
||||
|
||||
public HeritagePiece19(HeritagePlan19.Site site) {
|
||||
super(HeritageStructures19.PIECE, 0, bounds(site));
|
||||
this.site = site; this.segment = null; setOrientation(null);
|
||||
}
|
||||
public HeritagePiece19(HeritagePlan19.Segment segment) {
|
||||
super(HeritageStructures19.PIECE, 0, bounds(segment));
|
||||
this.site = null; this.segment = segment; setOrientation(null);
|
||||
}
|
||||
public HeritagePiece19(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||
super(HeritageStructures19.PIECE, tag);
|
||||
var payload = tag.getCompound("SanctuaryHeritage").orElseThrow(() -> new IllegalArgumentException("Missing saved heritage geometry"));
|
||||
String type = string(payload, "Type");
|
||||
if (type.equals("SITE")) { site = decodeSite(payload); segment = null; }
|
||||
else if (type.equals("ROUTE")) { site = null; segment = decodeSegment(payload); }
|
||||
else throw new IllegalArgumentException("Unknown heritage piece type");
|
||||
if (!getBoundingBox().equals(isSite() ? bounds(site) : bounds(segment)))
|
||||
throw new IllegalArgumentException("Heritage geometry disagrees with its saved native bounds");
|
||||
setOrientation(null);
|
||||
}
|
||||
public boolean isSite() { return site != null; }
|
||||
public HeritagePlan19.Site site() { if (site == null) throw new IllegalStateException("This heritage piece is a route"); return site; }
|
||||
public HeritagePlan19.Segment segment() { if (segment == null) throw new IllegalStateException("This heritage piece is a site"); return segment; }
|
||||
@Override public BlockPos getLocatorPosition() {
|
||||
if (isSite()) return locator(site);
|
||||
var cell = segment.cells().getFirst(); return new BlockPos(cell.x(), cell.floorY() + 1, cell.z());
|
||||
}
|
||||
public static BlockPos locator(HeritagePlan19.Site site) { return new BlockPos(site.entry().x(), site.entry().y(), site.entry().z()); }
|
||||
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||
tag.put("SanctuaryHeritage", isSite() ? encodeSite(site) : encodeSegment(segment));
|
||||
}
|
||||
public static CompoundTag encodeSite(HeritagePlan19.Site site) {
|
||||
var tag = header("SITE", site.id(), site.kind().name(), site.seed());
|
||||
tag.putInt("MinX", site.minX()); tag.putInt("FloorY", site.floorY()); tag.putInt("MinZ", site.minZ());
|
||||
tag.putInt("Width", site.width()); tag.putInt("Depth", site.depth()); tag.putInt("Height", site.height());
|
||||
tag.putInt("Facing", site.facing()); tag.putInt("FoundationDepth", site.foundationDepth());
|
||||
var entry = new CompoundTag(); entry.putInt("X", site.entry().x()); entry.putInt("Y", site.entry().y());
|
||||
entry.putInt("Z", site.entry().z()); entry.putInt("Facing", site.entry().facing()); tag.put("Entry", entry);
|
||||
var supports = new ListTag();
|
||||
for (var support : site.supports()) {
|
||||
var value = new CompoundTag(); value.putInt("X", support.x()); value.putInt("MinY", support.minY());
|
||||
value.putInt("Z", support.z()); value.putInt("MaxY", support.maxY()); supports.add(value);
|
||||
}
|
||||
tag.put("Supports", supports); return tag;
|
||||
}
|
||||
public static CompoundTag encodeSegment(HeritagePlan19.Segment segment) {
|
||||
var tag = header("ROUTE", segment.id(), segment.kind().name(), segment.seed());
|
||||
tag.putString("FromId", segment.fromId()); tag.putString("ToId", segment.toId());
|
||||
tag.putInt("StartChunkX", segment.startChunkX()); tag.putInt("StartChunkZ", segment.startChunkZ());
|
||||
var cells = new ListTag();
|
||||
for (var cell : segment.cells()) {
|
||||
var value = new CompoundTag(); value.putInt("X", cell.x()); value.putInt("FloorY", cell.floorY());
|
||||
value.putInt("Z", cell.z()); value.putInt("FoundationDepth", cell.foundationDepth());
|
||||
value.putString("Rail", cell.rail().name()); value.putBoolean("RailPresent", cell.railPresent()); cells.add(value);
|
||||
}
|
||||
tag.put("Cells", cells); return tag;
|
||||
}
|
||||
public static HeritagePlan19.Site decodeSite(CompoundTag tag) {
|
||||
schema(tag, "SITE", SITE_KEYS);
|
||||
var entry = compound(tag, "Entry", ENTRY_KEYS);
|
||||
var saved = list(tag, "Supports", 9); var supports = new ArrayList<HeritagePlan19.Support>();
|
||||
for (var value : saved) {
|
||||
var support = element(value, SUPPORT_KEYS);
|
||||
supports.add(new HeritagePlan19.Support(coordinate(support, "X"), integer(support, "MinY"),
|
||||
coordinate(support, "Z"), integer(support, "MaxY")));
|
||||
}
|
||||
return new HeritagePlan19.Site(id(tag, "Id"), HeritagePlan19.Kind.valueOf(string(tag, "Kind")),
|
||||
coordinate(tag, "MinX"), integer(tag, "FloorY"), coordinate(tag, "MinZ"), integer(tag, "Width"),
|
||||
integer(tag, "Depth"), integer(tag, "Height"), integer(tag, "Facing"), seed(tag), integer(tag, "FoundationDepth"),
|
||||
supports, new HeritagePlan19.Entry(coordinate(entry, "X"), integer(entry, "Y"), coordinate(entry, "Z"), integer(entry, "Facing")));
|
||||
}
|
||||
public static HeritagePlan19.Segment decodeSegment(CompoundTag tag) {
|
||||
schema(tag, "ROUTE", ROUTE_KEYS);
|
||||
var saved = list(tag, "Cells", 256); var cells = new ArrayList<HeritagePlan19.Cell>();
|
||||
for (var value : saved) {
|
||||
var cell = element(value, CELL_KEYS);
|
||||
type(cell, "RailPresent", Tag.TAG_BYTE);
|
||||
byte present = cell.getByte("RailPresent").orElseThrow();
|
||||
if (present != 0 && present != 1) throw new IllegalArgumentException("Invalid saved rail presence");
|
||||
cells.add(new HeritagePlan19.Cell(coordinate(cell, "X"), integer(cell, "FloorY"), coordinate(cell, "Z"),
|
||||
integer(cell, "FoundationDepth"), HeritagePlan19.Rail.valueOf(string(cell, "Rail")), present == 1));
|
||||
}
|
||||
String from = id(tag, "FromId"), to = id(tag, "ToId");
|
||||
var kind = HeritagePlan19.RouteKind.valueOf(string(tag, "Kind"));
|
||||
if (from.equals(to) && kind != HeritagePlan19.RouteKind.PATH)
|
||||
throw new IllegalArgumentException("Only a local pedestrian landing may join a destination to itself");
|
||||
return new HeritagePlan19.Segment(id(tag, "Id"), from, to, kind,
|
||||
integer(tag, "StartChunkX"), integer(tag, "StartChunkZ"), cells, seed(tag));
|
||||
}
|
||||
private static CompoundTag header(String type, String id, String kind, long seed) {
|
||||
var value = new CompoundTag(); value.putInt("Schema", 19); value.putString("Type", type);
|
||||
value.putString("Id", id); value.putString("Kind", kind); value.putLong("Seed", seed); return value;
|
||||
}
|
||||
private static void schema(CompoundTag tag, String expected, Set<String> keys) {
|
||||
if (!tag.keySet().equals(keys) || integer(tag, "Schema") != 19 || !string(tag, "Type").equals(expected))
|
||||
throw new IllegalArgumentException("Unsupported or incomplete saved heritage schema");
|
||||
}
|
||||
private static CompoundTag compound(CompoundTag tag, String key, Set<String> keys) {
|
||||
type(tag, key, Tag.TAG_COMPOUND); return element(tag.get(key), keys);
|
||||
}
|
||||
private static CompoundTag element(Tag tag, Set<String> keys) {
|
||||
if (!(tag instanceof CompoundTag value) || !value.keySet().equals(keys))
|
||||
throw new IllegalArgumentException("Unsupported or incomplete saved heritage element");
|
||||
return value;
|
||||
}
|
||||
private static ListTag list(CompoundTag tag, String key, int maximum) {
|
||||
type(tag, key, Tag.TAG_LIST); var value = tag.getList(key).orElseThrow();
|
||||
if (value.size() > maximum) throw new IllegalArgumentException("Oversized saved heritage list"); return value;
|
||||
}
|
||||
private static long seed(CompoundTag tag) { type(tag, "Seed", Tag.TAG_LONG); return tag.getLong("Seed").orElseThrow(); }
|
||||
private static String id(CompoundTag tag, String key) {
|
||||
var value = string(tag, key);
|
||||
if (value.isBlank() || value.length() > 128) throw new IllegalArgumentException("Invalid saved heritage identifier"); return value;
|
||||
}
|
||||
private static String string(CompoundTag tag, String key) { type(tag, key, Tag.TAG_STRING); return tag.getString(key).orElseThrow(); }
|
||||
private static int coordinate(CompoundTag tag, String key) {
|
||||
int value = integer(tag, key);
|
||||
if (value < -30_000_000 || value > 30_000_000) throw new IllegalArgumentException("Heritage coordinate outside world bounds"); return value;
|
||||
}
|
||||
private static int integer(CompoundTag tag, String key) { type(tag, key, Tag.TAG_INT); return tag.getInt(key).orElseThrow(); }
|
||||
private static void type(CompoundTag tag, String key, byte id) {
|
||||
if (tag.get(key) == null || tag.get(key).getId() != id) throw new IllegalArgumentException("Invalid saved heritage field: " + key);
|
||||
}
|
||||
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
|
||||
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
|
||||
final int x0 = isSite() ? site.minX() : segment.minX();
|
||||
final int y0 = isSite() ? site.floorY() : segment.minY();
|
||||
final int z0 = isSite() ? site.minZ() : segment.minZ();
|
||||
var writer = new LostCityRenderer.BlockWriter() {
|
||||
private BlockPos world(int x, int y, int z) { return new BlockPos(Math.addExact(x0, x), Math.addExact(y0, y), Math.addExact(z0, z)); }
|
||||
@Override public void set(int x, int y, int z, BlockState block) {
|
||||
var position = world(x, y, z);
|
||||
if (getBoundingBox().isInside(position) && clip.isInside(position))
|
||||
placeBlock(level, block, position.getX(), position.getY(), position.getZ(), clip);
|
||||
}
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
|
||||
var position = world(x, y, z);
|
||||
if (getBoundingBox().isInside(position) && clip.isInside(position))
|
||||
createChest(level, clip, RandomSource.create(seed), position, loot, null);
|
||||
}
|
||||
};
|
||||
HeritageStructures19.placing(() -> {
|
||||
if (isSite()) HeritageRenderer19.render(site, writer);
|
||||
else HeritageRenderer19.renderSegment(segment, writer);
|
||||
});
|
||||
}
|
||||
public static BoundingBox bounds(HeritagePlan19.Site site) {
|
||||
return new BoundingBox(site.minX(), site.boundsMinY(), site.minZ(), site.maxX(), site.boundsMaxY(), site.maxZ());
|
||||
}
|
||||
public static BoundingBox bounds(HeritagePlan19.Segment segment) {
|
||||
return new BoundingBox(segment.minX(), segment.boundsMinY(), segment.minZ(), segment.maxX(), segment.boundsMaxY(), segment.maxZ());
|
||||
}
|
||||
}
|
||||
+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());
|
||||
}
|
||||
}
|
||||
+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 schema21 geometry. Reload never needs the planner or a distant route destination. */
|
||||
public final class HeritagePiece21 extends StructurePiece {
|
||||
private static final Set<String> SITE_KEYS = Set.of("Schema", "Type", "Id", "Kind", "MinX", "FloorY", "MinZ",
|
||||
"Width", "Depth", "Height", "Facing", "Seed", "FoundationDepth", "Supports", "Entry");
|
||||
private static final Set<String> ROUTE_KEYS = Set.of("Schema", "Type", "Id", "FromId", "ToId", "Kind",
|
||||
"StartChunkX", "StartChunkZ", "Cells", "Seed");
|
||||
private static final Set<String> SUPPORT_KEYS = Set.of("X", "MinY", "Z", "MaxY");
|
||||
private static final Set<String> ENTRY_KEYS = Set.of("X", "Y", "Z", "Facing");
|
||||
private static final Set<String> CELL_KEYS = Set.of("X", "FloorY", "Z", "FoundationDepth", "Rail", "RailPresent");
|
||||
private final HeritagePlan19.Site site;
|
||||
private final HeritagePlan19.Segment segment;
|
||||
|
||||
public HeritagePiece21(HeritagePlan19.Site site) {
|
||||
super(HeritageStructures19.PIECE21, 0, bounds(site));
|
||||
this.site = site; this.segment = null; setOrientation(null);
|
||||
}
|
||||
public HeritagePiece21(HeritagePlan19.Segment segment) {
|
||||
super(HeritageStructures19.PIECE21, 0, bounds(segment));
|
||||
this.site = null; this.segment = segment; setOrientation(null);
|
||||
}
|
||||
public HeritagePiece21(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||
super(HeritageStructures19.PIECE21, tag);
|
||||
var payload = tag.getCompound("SanctuaryHeritage").orElseThrow(() -> new IllegalArgumentException("Missing saved heritage geometry"));
|
||||
String type = string(payload, "Type");
|
||||
if (type.equals("SITE")) { site = decodeSite(payload); segment = null; }
|
||||
else if (type.equals("ROUTE")) { site = null; segment = decodeSegment(payload); }
|
||||
else throw new IllegalArgumentException("Unknown heritage piece type");
|
||||
if (!getBoundingBox().equals(isSite() ? bounds(site) : bounds(segment)))
|
||||
throw new IllegalArgumentException("Heritage geometry disagrees with its saved native bounds");
|
||||
setOrientation(null);
|
||||
}
|
||||
public boolean isSite() { return site != null; }
|
||||
public HeritagePlan19.Site site() { if (site == null) throw new IllegalStateException("This heritage piece is a route"); return site; }
|
||||
public HeritagePlan19.Segment segment() { if (segment == null) throw new IllegalStateException("This heritage piece is a site"); return segment; }
|
||||
@Override public BlockPos getLocatorPosition() {
|
||||
if (isSite()) return locator(site);
|
||||
var cell = segment.cells().getFirst(); return new BlockPos(cell.x(), cell.floorY() + 1, cell.z());
|
||||
}
|
||||
public static BlockPos locator(HeritagePlan19.Site site) { return new BlockPos(site.entry().x(), site.entry().y(), site.entry().z()); }
|
||||
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||
tag.put("SanctuaryHeritage", isSite() ? encodeSite(site) : encodeSegment(segment));
|
||||
}
|
||||
public static CompoundTag encodeSite(HeritagePlan19.Site site) {
|
||||
var tag = header("SITE", site.id(), site.kind().name(), site.seed());
|
||||
tag.putInt("MinX", site.minX()); tag.putInt("FloorY", site.floorY()); tag.putInt("MinZ", site.minZ());
|
||||
tag.putInt("Width", site.width()); tag.putInt("Depth", site.depth()); tag.putInt("Height", site.height());
|
||||
tag.putInt("Facing", site.facing()); tag.putInt("FoundationDepth", site.foundationDepth());
|
||||
var entry = new CompoundTag(); entry.putInt("X", site.entry().x()); entry.putInt("Y", site.entry().y());
|
||||
entry.putInt("Z", site.entry().z()); entry.putInt("Facing", site.entry().facing()); tag.put("Entry", entry);
|
||||
var supports = new ListTag();
|
||||
for (var support : site.supports()) {
|
||||
var value = new CompoundTag(); value.putInt("X", support.x()); value.putInt("MinY", support.minY());
|
||||
value.putInt("Z", support.z()); value.putInt("MaxY", support.maxY()); supports.add(value);
|
||||
}
|
||||
tag.put("Supports", supports); return tag;
|
||||
}
|
||||
public static CompoundTag encodeSegment(HeritagePlan19.Segment segment) {
|
||||
var tag = header("ROUTE", segment.id(), segment.kind().name(), segment.seed());
|
||||
tag.putString("FromId", segment.fromId()); tag.putString("ToId", segment.toId());
|
||||
tag.putInt("StartChunkX", segment.startChunkX()); tag.putInt("StartChunkZ", segment.startChunkZ());
|
||||
var cells = new ListTag();
|
||||
for (var cell : segment.cells()) {
|
||||
var value = new CompoundTag(); value.putInt("X", cell.x()); value.putInt("FloorY", cell.floorY());
|
||||
value.putInt("Z", cell.z()); value.putInt("FoundationDepth", cell.foundationDepth());
|
||||
value.putString("Rail", cell.rail().name()); value.putBoolean("RailPresent", cell.railPresent()); cells.add(value);
|
||||
}
|
||||
tag.put("Cells", cells); return tag;
|
||||
}
|
||||
public static HeritagePlan19.Site decodeSite(CompoundTag tag) {
|
||||
schema(tag, "SITE", SITE_KEYS);
|
||||
var entry = compound(tag, "Entry", ENTRY_KEYS);
|
||||
var saved = list(tag, "Supports", 9); var supports = new ArrayList<HeritagePlan19.Support>();
|
||||
for (var value : saved) {
|
||||
var support = element(value, SUPPORT_KEYS);
|
||||
supports.add(new HeritagePlan19.Support(coordinate(support, "X"), integer(support, "MinY"),
|
||||
coordinate(support, "Z"), integer(support, "MaxY")));
|
||||
}
|
||||
return new HeritagePlan19.Site(id(tag, "Id"), HeritagePlan19.Kind.valueOf(string(tag, "Kind")),
|
||||
coordinate(tag, "MinX"), integer(tag, "FloorY"), coordinate(tag, "MinZ"), integer(tag, "Width"),
|
||||
integer(tag, "Depth"), integer(tag, "Height"), integer(tag, "Facing"), seed(tag), integer(tag, "FoundationDepth"),
|
||||
supports, new HeritagePlan19.Entry(coordinate(entry, "X"), integer(entry, "Y"), coordinate(entry, "Z"), integer(entry, "Facing")));
|
||||
}
|
||||
public static HeritagePlan19.Segment decodeSegment(CompoundTag tag) {
|
||||
schema(tag, "ROUTE", ROUTE_KEYS);
|
||||
var saved = list(tag, "Cells", 256); var cells = new ArrayList<HeritagePlan19.Cell>();
|
||||
for (var value : saved) {
|
||||
var cell = element(value, CELL_KEYS);
|
||||
type(cell, "RailPresent", Tag.TAG_BYTE);
|
||||
byte present = cell.getByte("RailPresent").orElseThrow();
|
||||
if (present != 0 && present != 1) throw new IllegalArgumentException("Invalid saved rail presence");
|
||||
cells.add(new HeritagePlan19.Cell(coordinate(cell, "X"), integer(cell, "FloorY"), coordinate(cell, "Z"),
|
||||
integer(cell, "FoundationDepth"), HeritagePlan19.Rail.valueOf(string(cell, "Rail")), present == 1));
|
||||
}
|
||||
String from = id(tag, "FromId"), to = id(tag, "ToId");
|
||||
var kind = HeritagePlan19.RouteKind.valueOf(string(tag, "Kind"));
|
||||
if (from.equals(to) && kind != HeritagePlan19.RouteKind.PATH)
|
||||
throw new IllegalArgumentException("Only a local pedestrian landing may join a destination to itself");
|
||||
return new HeritagePlan19.Segment(id(tag, "Id"), from, to, kind,
|
||||
integer(tag, "StartChunkX"), integer(tag, "StartChunkZ"), cells, seed(tag));
|
||||
}
|
||||
private static CompoundTag header(String type, String id, String kind, long seed) {
|
||||
var value = new CompoundTag(); value.putInt("Schema", 21); value.putString("Type", type);
|
||||
value.putString("Id", id); value.putString("Kind", kind); value.putLong("Seed", seed); return value;
|
||||
}
|
||||
private static void schema(CompoundTag tag, String expected, Set<String> keys) {
|
||||
if (!tag.keySet().equals(keys) || integer(tag, "Schema") != 21 || !string(tag, "Type").equals(expected))
|
||||
throw new IllegalArgumentException("Unsupported or incomplete saved heritage schema");
|
||||
}
|
||||
private static CompoundTag compound(CompoundTag tag, String key, Set<String> keys) {
|
||||
type(tag, key, Tag.TAG_COMPOUND); return element(tag.get(key), keys);
|
||||
}
|
||||
private static CompoundTag element(Tag tag, Set<String> keys) {
|
||||
if (!(tag instanceof CompoundTag value) || !value.keySet().equals(keys))
|
||||
throw new IllegalArgumentException("Unsupported or incomplete saved heritage element");
|
||||
return value;
|
||||
}
|
||||
private static ListTag list(CompoundTag tag, String key, int maximum) {
|
||||
type(tag, key, Tag.TAG_LIST); var value = tag.getList(key).orElseThrow();
|
||||
if (value.size() > maximum) throw new IllegalArgumentException("Oversized saved heritage list"); return value;
|
||||
}
|
||||
private static long seed(CompoundTag tag) { type(tag, "Seed", Tag.TAG_LONG); return tag.getLong("Seed").orElseThrow(); }
|
||||
private static String id(CompoundTag tag, String key) {
|
||||
var value = string(tag, key);
|
||||
if (value.isBlank() || value.length() > 128) throw new IllegalArgumentException("Invalid saved heritage identifier"); return value;
|
||||
}
|
||||
private static String string(CompoundTag tag, String key) { type(tag, key, Tag.TAG_STRING); return tag.getString(key).orElseThrow(); }
|
||||
private static int coordinate(CompoundTag tag, String key) {
|
||||
int value = integer(tag, key);
|
||||
if (value < -30_000_000 || value > 30_000_000) throw new IllegalArgumentException("Heritage coordinate outside world bounds"); return value;
|
||||
}
|
||||
private static int integer(CompoundTag tag, String key) { type(tag, key, Tag.TAG_INT); return tag.getInt(key).orElseThrow(); }
|
||||
private static void type(CompoundTag tag, String key, byte id) {
|
||||
if (tag.get(key) == null || tag.get(key).getId() != id) throw new IllegalArgumentException("Invalid saved heritage field: " + key);
|
||||
}
|
||||
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
|
||||
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
|
||||
final int x0 = isSite() ? site.minX() : segment.minX();
|
||||
final int y0 = isSite() ? site.floorY() : segment.minY();
|
||||
final int z0 = isSite() ? site.minZ() : segment.minZ();
|
||||
var writer = new LostCityRenderer.BlockWriter() {
|
||||
private BlockPos world(int x, int y, int z) { return new BlockPos(Math.addExact(x0, x), Math.addExact(y0, y), Math.addExact(z0, z)); }
|
||||
@Override public void set(int x, int y, int z, BlockState block) {
|
||||
var position = world(x, y, z);
|
||||
if (getBoundingBox().isInside(position) && clip.isInside(position))
|
||||
placeBlock(level, block, position.getX(), position.getY(), position.getZ(), clip);
|
||||
}
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
|
||||
var position = world(x, y, z);
|
||||
if (getBoundingBox().isInside(position) && clip.isInside(position))
|
||||
createChest(level, clip, RandomSource.create(seed), position, loot, null);
|
||||
}
|
||||
};
|
||||
HeritageStructures19.placing(() -> {
|
||||
if (isSite()) HeritageRenderer21.render(site, writer);
|
||||
else HeritageRenderer21.renderSegment(segment, writer);
|
||||
});
|
||||
}
|
||||
public static BoundingBox bounds(HeritagePlan19.Site site) {
|
||||
return new BoundingBox(site.minX(), site.boundsMinY(), site.minZ(), site.maxX(), site.boundsMaxY(), site.maxZ());
|
||||
}
|
||||
public static BoundingBox bounds(HeritagePlan19.Segment segment) {
|
||||
return new BoundingBox(segment.minX(), segment.boundsMinY(), segment.minZ(), segment.maxX(), segment.boundsMaxY(), segment.maxZ());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package fr.koka.sanctuary.worldgen.heritage;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Immutable generation19 catalogue and real-destination routes. Pieces save geometry, not terrain queries. */
|
||||
public final class HeritagePlan19 {
|
||||
private final List<Site> sites;
|
||||
private final List<Landmark> landmarks;
|
||||
private final List<Link> links;
|
||||
private final List<Segment> segments;
|
||||
private final int samples, columns;
|
||||
private final Map<Kind, String> diagnostics;
|
||||
private final Map<Long, List<Range>> protectedColumns;
|
||||
private final Map<Long, List<Box>> protectedChunks;
|
||||
public static final int MAX_COLUMNS = 16_000, MAX_SAMPLES = 800_000;
|
||||
public static final HeritagePlan19 EMPTY = new HeritagePlan19(List.of(), List.of(), List.of(), List.of(), 0, 0, Map.of());
|
||||
public enum Kind { FREIGHT_STATION, POST_OFFICE, COMMUNAL_DEPOT, EXPEDITION_OFFICE, OBSERVATORY,
|
||||
RAIL_WORKSHOP, PUMP_STATION, SUBSTATION, GREENHOUSE, VOID_DOCK, RADIO_RELAY, PHOTO_CLUB,
|
||||
IMPOSSIBLE_SERVICE, CONSTRUCTION_REFUGE }
|
||||
public enum RouteKind { FREIGHT, PATH }
|
||||
public enum LandmarkKind { CITY, MINE, UTILITY }
|
||||
/** NONE is a side column or path; a missing rail retains its intended shape with railPresent=false. */
|
||||
public enum Rail { NONE, NORTH_SOUTH, EAST_WEST, ASCENDING_EAST, ASCENDING_WEST, ASCENDING_NORTH,
|
||||
ASCENDING_SOUTH, SOUTH_EAST, SOUTH_WEST, NORTH_WEST, NORTH_EAST }
|
||||
|
||||
/** Coordinates of the player's feet, immediately outside the facade. Facing0=N,1=E,2=S,3=W. */
|
||||
public record Entry(int x, int y, int z, int facing) {
|
||||
public Entry { if (!coordinate(x) || !coordinate(z) || y < 2 || y > 380 || facing < 0 || facing > 3) throw new IllegalArgumentException("Invalid heritage entry"); }
|
||||
}
|
||||
public record Support(int x, int minY, int z, int maxY) {
|
||||
public Support { if (!coordinate(x) || !coordinate(z) || minY < 2 || maxY < minY || maxY - minY >= 12 || maxY > 370) throw new IllegalArgumentException("Invalid heritage support"); }
|
||||
}
|
||||
public record Site(String id, Kind kind, int minX, int floorY, int minZ, int width, int depth, int height,
|
||||
int facing, long seed, int foundationDepth, List<Support> supports, Entry entry) {
|
||||
public Site {
|
||||
supports = List.copyOf(supports);
|
||||
if (!identifier(id) || !coordinate(minX) || !coordinate(minZ) || kind == null || width != 17 || depth != 17 || height < 7 || height > 24
|
||||
|| floorY < 32 || floorY + height > 380 || facing < 0 || facing > 3
|
||||
|| foundationDepth < 3 || foundationDepth > 8 || supports.size() > 9 || entry == null)
|
||||
throw new IllegalArgumentException("Invalid heritage site");
|
||||
int ex = facing == 1 ? minX + width : facing == 3 ? minX - 1 : minX + width / 2;
|
||||
int ez = facing == 2 ? minZ + depth : facing == 0 ? minZ - 1 : minZ + depth / 2;
|
||||
if (entry.x() != ex || entry.z() != ez || entry.y() != floorY + 1 || entry.facing() != facing)
|
||||
throw new IllegalArgumentException("Heritage entry does not meet its rotated facade");
|
||||
var positions = new HashSet<Long>();
|
||||
for (var support : supports) if (support.x() < minX || support.x() >= minX + width
|
||||
|| support.z() < minZ || support.z() >= minZ + depth || support.maxY() != floorY - foundationDepth - 1
|
||||
|| !positions.add(key(support.x(), support.z()))) throw new IllegalArgumentException("Invalid site footing");
|
||||
}
|
||||
public int maxX() { return minX + width - 1; }
|
||||
public int maxZ() { return minZ + depth - 1; }
|
||||
public int minY() { return floorY - foundationDepth; }
|
||||
public int maxY() { return floorY + height; }
|
||||
public int boundsMinY() { return supports.stream().mapToInt(Support::minY).min().orElse(minY()); }
|
||||
public int boundsMaxY() { return maxY(); }
|
||||
public int centerX() { return minX + width / 2; }
|
||||
public int centerZ() { return minZ + depth / 2; }
|
||||
public int startChunkX() { return centerX() >> 4; }
|
||||
public int startChunkZ() { return centerZ() >> 4; }
|
||||
}
|
||||
/** A pre-existing, read-only destination. Its coordinate is never granted a mask bypass. */
|
||||
public record Landmark(String id, LandmarkKind kind, Entry entry) {
|
||||
public Landmark { if (!identifier(id) || kind == null || entry == null) throw new IllegalArgumentException("Invalid heritage landmark"); }
|
||||
}
|
||||
/** Floor/block-support Y, not player feet. Endpoints may be read-only landmark coordinates. */
|
||||
public record Point(int x, int floorY, int z) {
|
||||
public Point { if (!coordinate(x) || !coordinate(z) || floorY < 2 || floorY > 370) throw new IllegalArgumentException("Invalid route point"); }
|
||||
}
|
||||
public record Link(String id, String fromId, String toId, RouteKind kind, List<Point> path) {
|
||||
public Link {
|
||||
path = List.copyOf(path);
|
||||
if (!identifier(id) || !identifier(fromId) || !identifier(toId) || fromId.equals(toId)
|
||||
|| kind == null || path.size() < 2 || path.size() > 1024) throw new IllegalArgumentException("Invalid heritage link");
|
||||
for (int i = 1; i < path.size(); i++) {
|
||||
var a = path.get(i - 1); var b = path.get(i);
|
||||
if (Math.abs(a.x() - b.x()) + Math.abs(a.z() - b.z()) != 1 || Math.abs(a.floorY() - b.floorY()) > 1)
|
||||
throw new IllegalArgumentException("Heritage route has a discontinuous centerline");
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Each written ground column belongs to exactly one Segment. Clear local y1..3 before rail placement. */
|
||||
public record Cell(int x, int floorY, int z, int foundationDepth, Rail rail, boolean railPresent) {
|
||||
public Cell {
|
||||
if (!coordinate(x) || !coordinate(z) || floorY < 14 || floorY > 350 || foundationDepth < 1 || foundationDepth > 12 || rail == null
|
||||
|| rail == Rail.NONE && railPresent) throw new IllegalArgumentException("Invalid heritage route cell");
|
||||
}
|
||||
public int minY() { return floorY - foundationDepth; }
|
||||
public int maxY() { return floorY + 3; }
|
||||
}
|
||||
/** Chunk-local route piece: stable ownership prevents neighboring links painting a shared column twice. */
|
||||
public record Segment(String id, String fromId, String toId, RouteKind kind, int startChunkX, int startChunkZ,
|
||||
List<Cell> cells, long seed) {
|
||||
public Segment {
|
||||
cells = List.copyOf(cells);
|
||||
if (!identifier(id) || !identifier(fromId) || !identifier(toId) || fromId.equals(toId) && kind != RouteKind.PATH || kind == null || cells.isEmpty() || cells.size() > 256)
|
||||
throw new IllegalArgumentException("Invalid heritage segment");
|
||||
var positions = new HashSet<Long>();
|
||||
for (var cell : cells) if ((cell.x() >> 4) != startChunkX || (cell.z() >> 4) != startChunkZ
|
||||
|| !positions.add(key(cell.x(), cell.z()))) throw new IllegalArgumentException("Route cells escaped their owning chunk");
|
||||
}
|
||||
public int minX() { return cells.stream().mapToInt(Cell::x).min().orElseThrow(); }
|
||||
public int maxX() { return cells.stream().mapToInt(Cell::x).max().orElseThrow(); }
|
||||
public int minZ() { return cells.stream().mapToInt(Cell::z).min().orElseThrow(); }
|
||||
public int maxZ() { return cells.stream().mapToInt(Cell::z).max().orElseThrow(); }
|
||||
public int minY() { return cells.stream().mapToInt(Cell::minY).min().orElseThrow(); }
|
||||
public int maxY() { return cells.stream().mapToInt(Cell::maxY).max().orElseThrow(); }
|
||||
public int boundsMinY() { return minY(); }
|
||||
public int boundsMaxY() { return maxY(); }
|
||||
}
|
||||
|
||||
public HeritagePlan19(List<Site> sites, List<Landmark> landmarks, List<Link> links, List<Segment> segments,
|
||||
int samples, int columns, Map<Kind, String> diagnostics) {
|
||||
sites = List.copyOf(sites); landmarks = List.copyOf(landmarks); links = List.copyOf(links); segments = List.copyOf(segments);
|
||||
diagnostics = Map.copyOf(diagnostics);
|
||||
if (sites.size() > 14 || landmarks.size() > 16 || links.size() > 24 || segments.size() > 512
|
||||
|| samples < 0 || samples > MAX_SAMPLES || columns < 0 || columns > MAX_COLUMNS)
|
||||
throw new IllegalArgumentException("Invalid heritage plan budget");
|
||||
var destinations = new HashSet<String>();
|
||||
for (var site : sites) if (!destinations.add(site.id())) throw new IllegalArgumentException("Duplicate heritage destination");
|
||||
for (var landmark : landmarks) if (!destinations.add(landmark.id())) throw new IllegalArgumentException("Duplicate heritage destination");
|
||||
for (var link : links) if (!destinations.contains(link.fromId()) || !destinations.contains(link.toId()))
|
||||
throw new IllegalArgumentException("A heritage route promises a nonexistent destination");
|
||||
var occupied = new HashSet<Long>();
|
||||
for (var segment : segments) {
|
||||
if (!destinations.contains(segment.fromId()) || !destinations.contains(segment.toId()))
|
||||
throw new IllegalArgumentException("A heritage segment promises a nonexistent destination");
|
||||
for (var cell : segment.cells()) if (!occupied.add(key(cell.x(), cell.z())))
|
||||
throw new IllegalArgumentException("Heritage segments write the same column");
|
||||
}
|
||||
this.sites = sites; this.landmarks = landmarks; this.links = links; this.segments = segments;
|
||||
this.samples = samples; this.columns = columns; this.diagnostics = diagnostics;
|
||||
var columnIndex = new HashMap<Long, List<Range>>(); var chunkIndex = new HashMap<Long, List<Box>>();
|
||||
for (var site : sites) {
|
||||
index(columnIndex, chunkIndex, new Box(site.minX(), site.minY() - 2, site.minZ(), site.maxX(), site.maxY(), site.maxZ()));
|
||||
for (var support : site.supports()) index(columnIndex, chunkIndex,
|
||||
new Box(support.x(), support.minY() - 2, support.z(), support.x(), support.maxY(), support.z()));
|
||||
}
|
||||
for (var segment : segments) for (var cell : segment.cells()) index(columnIndex, chunkIndex,
|
||||
new Box(cell.x(), cell.minY() - 1, cell.z(), cell.x(), cell.maxY(), cell.z()));
|
||||
var usedDestinations = new HashSet<String>();
|
||||
for (var link : links) { usedDestinations.add(link.fromId()); usedDestinations.add(link.toId()); }
|
||||
for (var landmark : landmarks) if (usedDestinations.contains(landmark.id())) {
|
||||
Entry e = landmark.entry();
|
||||
for (int side = -1; side <= 1; side++) {
|
||||
int x = e.x() + ((e.facing() & 1) == 0 ? side : 0);
|
||||
int z = e.z() + ((e.facing() & 1) == 1 ? side : 0);
|
||||
// Read-only dependencies: these cells never enter contains() or any native writer.
|
||||
index(columnIndex, chunkIndex, new Box(x, e.y() - 2, z, x, e.y() + 2, z));
|
||||
}
|
||||
}
|
||||
columnIndex.replaceAll((key, values) -> List.copyOf(values)); chunkIndex.replaceAll((key, values) -> List.copyOf(values));
|
||||
protectedColumns = Map.copyOf(columnIndex); protectedChunks = Map.copyOf(chunkIndex);
|
||||
}
|
||||
public List<Site> sites() { return sites; }
|
||||
public List<Landmark> landmarks() { return landmarks; }
|
||||
public List<Link> links() { return links; }
|
||||
public List<Segment> segments() { return segments; }
|
||||
public int samples() { return samples; }
|
||||
public int columns() { return columns; }
|
||||
public Map<Kind, String> diagnostics() { return diagnostics; }
|
||||
public boolean isEmpty() { return sites.isEmpty(); }
|
||||
public boolean contains(int x, int y, int z) {
|
||||
for (var site : sites) {
|
||||
if (x < site.minX() || x > site.maxX() || z < site.minZ() || z > site.maxZ()) continue;
|
||||
if (y >= site.minY() && y <= site.maxY()) return true;
|
||||
for (var support : site.supports()) if (x == support.x() && z == support.z() && y >= support.minY() && y <= support.maxY()) return true;
|
||||
}
|
||||
for (var segment : segments) if ((x >> 4) == segment.startChunkX() && (z >> 4) == segment.startChunkZ())
|
||||
for (var cell : segment.cells()) if (cell.x() == x && cell.z() == z && y >= cell.minY() && y <= cell.maxY()) return true;
|
||||
return false;
|
||||
}
|
||||
/** Protect the original rock below slabs as well as every written volume. */
|
||||
public boolean protects(int x, int y, int z) {
|
||||
for (var range : protectedColumns.getOrDefault(key(x, z), List.of())) if (y >= range.minY() && y <= range.maxY()) return true;
|
||||
return false;
|
||||
}
|
||||
public boolean intersectsProtection(int x0, int y0, int z0, int x1, int y1, int z1) {
|
||||
if (x0 > x1 || y0 > y1 || z0 > z1) throw new IllegalArgumentException("Reversed heritage query");
|
||||
for (int cx = x0 >> 4; cx <= x1 >> 4; cx++) for (int cz = z0 >> 4; cz <= z1 >> 4; cz++)
|
||||
for (var box : protectedChunks.getOrDefault(key(cx, cz), List.of()))
|
||||
if (x0 <= box.maxX() && x1 >= box.minX() && z0 <= box.maxZ() && z1 >= box.minZ()
|
||||
&& y0 <= box.maxY() && y1 >= box.minY()) return true;
|
||||
return false;
|
||||
}
|
||||
private record Range(int minY, int maxY) {}
|
||||
private record Box(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {}
|
||||
private static void index(Map<Long, List<Range>> columns, Map<Long, List<Box>> chunks, Box box) {
|
||||
for (int x = box.minX(); x <= box.maxX(); x++) for (int z = box.minZ(); z <= box.maxZ(); z++)
|
||||
columns.computeIfAbsent(key(x, z), ignored -> new ArrayList<>()).add(new Range(box.minY(), box.maxY()));
|
||||
for (int x = box.minX() >> 4; x <= box.maxX() >> 4; x++) for (int z = box.minZ() >> 4; z <= box.maxZ() >> 4; z++)
|
||||
chunks.computeIfAbsent(key(x, z), ignored -> new ArrayList<>()).add(box);
|
||||
}
|
||||
private static boolean coordinate(int value) { return Math.abs((long) value) <= 29_999_000; }
|
||||
private static boolean identifier(String value) { return value != null && !value.isBlank() && value.length() <= 128; }
|
||||
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
|
||||
}
|
||||
+545
@@ -0,0 +1,545 @@
|
||||
package fr.koka.sanctuary.worldgen.heritage;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/** Generation19 only. A finite catalogue is admitted before complete, repairable routes are published.
|
||||
* Everything samples native columns; no chunk, player object or future continent is requested. */
|
||||
public final class HeritagePlanner19 {
|
||||
public static final int MAX_COLUMNS = HeritagePlan19.MAX_COLUMNS, MAX_SAMPLES = HeritagePlan19.MAX_SAMPLES;
|
||||
public static final int MAX_SITE_CUT = 6, MAX_ROUTE_CUT = 3;
|
||||
private static final int SITE_COLUMNS = 11_000, SITE_SAMPLES = 540_000;
|
||||
private static final int INVALID = Integer.MIN_VALUE;
|
||||
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
|
||||
private HeritagePlanner19() {}
|
||||
public interface Terrain {
|
||||
boolean owns(int x, int z);
|
||||
int surfaceY(int x, int z);
|
||||
boolean solid(int x, int y, int z);
|
||||
boolean protectedAt(int x, int y, int z);
|
||||
default List<Landmark> landmarks() { return List.of(); }
|
||||
}
|
||||
|
||||
public static HeritagePlan19 plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
|
||||
if (!List.of(512, 724, 1024).contains(diameter)) throw new IllegalArgumentException("Heritage19 requires a starter size");
|
||||
var probe = new Probe(terrain, cx, cz, diameter);
|
||||
List<Landmark> landmarks = List.copyOf(terrain.landmarks()).stream().limit(16).toList();
|
||||
var sites = new ArrayList<Site>(); var cells = new LinkedHashMap<Long, OwnedCell>(); var diagnostics = new EnumMap<Kind, String>(Kind.class);
|
||||
for (var kind : Kind.values()) {
|
||||
int remaining = Kind.values().length - kind.ordinal() - 1;
|
||||
probe.columnLimit = SITE_COLUMNS - remaining * (SITE_COLUMNS / 14);
|
||||
probe.sampleLimit = SITE_SAMPLES - remaining * (SITE_SAMPLES / 14);
|
||||
SiteDraft admitted = null; int attempts = 0;
|
||||
var reasons = new java.util.TreeMap<String, Integer>(); probe.reasons = reasons;
|
||||
try {
|
||||
for (var candidate : candidates(seed, kind, cx, cz, diameter, sites, landmarks, probe)) {
|
||||
attempts++;
|
||||
admitted = site(seed, kind, candidate, sites, landmarks, cells, probe);
|
||||
if (admitted != null) break;
|
||||
}
|
||||
} catch (BudgetExceeded exhausted) { reasons.merge(exhausted.columns ? "column-budget" : "probe-budget", 1, Integer::sum); }
|
||||
if (admitted != null) {
|
||||
sites.add(admitted.site());
|
||||
Request landing = new Request(admitted.site().id() + "_landing", admitted.site().id(), admitted.site().id(), RouteKind.PATH);
|
||||
for (var cell : admitted.landings()) cells.put(key(cell.x(), cell.z()), new OwnedCell(landing, cell));
|
||||
}
|
||||
diagnostics.put(kind, (admitted == null ? "not admitted" : "admitted") + "; candidates=" + attempts + "; " + reasons);
|
||||
}
|
||||
probe.columnLimit = MAX_COLUMNS; probe.sampleLimit = MAX_SAMPLES;
|
||||
var links = new ArrayList<Link>();
|
||||
var routeResults = new LinkedHashMap<String, String>();
|
||||
var routeReasons = new java.util.TreeMap<String, Integer>(); probe.reasons = routeReasons;
|
||||
Map<String, Endpoint> endpoints = new LinkedHashMap<>();
|
||||
for (var site : sites) endpoints.put(site.id(), new Endpoint(site.id(), site.entry(), false));
|
||||
for (var landmark : landmarks) if (!endpoints.containsKey(landmark.id()))
|
||||
endpoints.put(landmark.id(), new Endpoint(landmark.id(), landmark.entry(), true));
|
||||
List<Request> requests = requests(sites, landmarks).stream()
|
||||
.filter(r -> endpoints.containsKey(r.from()) && endpoints.containsKey(r.to())).toList();
|
||||
requests = new ArrayList<>(requests);
|
||||
requests.sort(Comparator.comparingInt((Request r) -> r.kind() == RouteKind.FREIGHT ? 0
|
||||
: span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry()) >= diameter / 4 ? 1 : 2)
|
||||
.thenComparingInt(r -> span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry())));
|
||||
int adaptiveLong = 0;
|
||||
for (int requestIndex = 0; requestIndex < requests.size(); requestIndex++) {
|
||||
var request = requests.get(requestIndex);
|
||||
int remaining = requests.size() - requestIndex - 1;
|
||||
// Failed alignments must not starve every later destination. Unused allowances carry forward.
|
||||
boolean longAxis = span(endpoints.get(request.from()).entry(), endpoints.get(request.to()).entry()) >= diameter / 4;
|
||||
int allowance = longAxis && adaptiveLong++ < 2 ? 4_500 : 1_400;
|
||||
probe.columnLimit = Math.min(MAX_COLUMNS - remaining * 120, probe.columns.size() + allowance);
|
||||
probe.sampleLimit = Math.min(MAX_SAMPLES - remaining * 4_000, probe.samples + (longAxis ? 150_000 : 75_000));
|
||||
Endpoint from = endpoints.get(request.from()), to = endpoints.get(request.to());
|
||||
if (from == null || to == null) continue;
|
||||
int beforeRouteColumns = probe.columns.size();
|
||||
try {
|
||||
RouteDraft route = route(seed, request, from, to, sites, landmarks, cells, probe);
|
||||
if (route == null) { probe.reject("no-complete-route"); routeResults.put(request.id(), "refused; columns=" + (probe.columns.size() - beforeRouteColumns)); continue; }
|
||||
links.add(new Link(request.id(), request.from(), request.to(), request.kind(), route.path()));
|
||||
routeResults.put(request.id(), "admitted; span=" + span(from.entry(), to.entry()) + "; length=" + route.path().size() + "; columns=" + (probe.columns.size() - beforeRouteColumns));
|
||||
for (var cell : route.cells().values()) {
|
||||
long key = key(cell.x(), cell.z());
|
||||
OwnedCell old = cells.get(key);
|
||||
if (old == null) cells.put(key, new OwnedCell(request, cell));
|
||||
else if (cell.rail() != Rail.NONE && old.cell().rail() == Rail.NONE)
|
||||
cells.put(key, new OwnedCell(old.request(), cell));
|
||||
else if (cell.rail() != Rail.NONE && old.cell().rail() != Rail.NONE && cell.rail() != old.cell().rail()) {
|
||||
// A missing switch is intentional; never emit a rail with contradictory connections.
|
||||
var c = old.cell(); cells.put(key, new OwnedCell(old.request(),
|
||||
new Cell(c.x(), c.floorY(), c.z(), c.foundationDepth(), c.rail(), false)));
|
||||
}
|
||||
}
|
||||
} catch (BudgetExceeded exhausted) { probe.reject(exhausted.columns ? "route-column-budget" : "route-probe-budget"); routeResults.put(request.id(), "budget; columns=" + (probe.columns.size() - beforeRouteColumns)); }
|
||||
}
|
||||
var segments = segments(seed, cells);
|
||||
if (Boolean.getBoolean("sanctuary.test.heritage19") || Boolean.getBoolean("sanctuary.heritage.plannerDiagnostics"))
|
||||
System.getLogger(HeritagePlanner19.class.getName()).log(System.Logger.Level.INFO,
|
||||
"Heritage19 seed=" + seed + " diameter=" + diameter + " sites=" + sites.stream().map(Site::kind).toList()
|
||||
+ " links=" + links.size() + " segments=" + segments.size() + " columns=" + probe.columns.size()
|
||||
+ " samples=" + probe.samples + " catalogue=" + diagnostics + " routes=" + routeReasons + " routeResults=" + routeResults);
|
||||
return new HeritagePlan19(sites, landmarks, links, segments, probe.samples, probe.columns.size(), diagnostics);
|
||||
}
|
||||
|
||||
private static List<Candidate> candidates(long seed, Kind kind, int cx, int cz, int diameter,
|
||||
List<Site> sites, List<Landmark> landmarks, Probe probe) {
|
||||
double angle = unit(mix(seed ^ 0x19ACEL)) * Math.PI * 2 + kind.ordinal() * 2.399963229728653;
|
||||
double radius = diameter * (kind == Kind.VOID_DOCK ? .40 : kind == Kind.OBSERVATORY || kind == Kind.RADIO_RELAY ? .31 : .22);
|
||||
int x = cx + (int) Math.round(Math.cos(angle) * radius), z = cz + (int) Math.round(Math.sin(angle) * radius);
|
||||
Kind parent = switch (kind) {
|
||||
case POST_OFFICE, COMMUNAL_DEPOT, RAIL_WORKSHOP -> Kind.FREIGHT_STATION;
|
||||
case EXPEDITION_OFFICE, PHOTO_CLUB -> Kind.POST_OFFICE;
|
||||
case SUBSTATION, CONSTRUCTION_REFUGE -> Kind.RAIL_WORKSHOP;
|
||||
case GREENHOUSE, IMPOSSIBLE_SERVICE -> Kind.PUMP_STATION;
|
||||
default -> null;
|
||||
};
|
||||
Site neighbor = parent == null ? null : find(sites, parent);
|
||||
if (neighbor != null) {
|
||||
double local = unit(mix(seed ^ kind.ordinal() * 179L)) * Math.PI * 2;
|
||||
int distance = kind == Kind.RAIL_WORKSHOP ? 65 : 40;
|
||||
x = neighbor.centerX() + (int) Math.round(Math.cos(local) * distance);
|
||||
z = neighbor.centerZ() + (int) Math.round(Math.sin(local) * distance);
|
||||
}
|
||||
var candidates = new ArrayList<Candidate>();
|
||||
for (int i = 0; i < 16; i++) {
|
||||
double turn = angle + i * Math.PI / 3;
|
||||
int offset = i == 0 ? 0 : i < 5 ? 24 : i < 8 ? 48 : i < 12 ? 72 : 96;
|
||||
int px = x + (int) Math.round(Math.cos(turn) * offset), pz = z + (int) Math.round(Math.sin(turn) * offset);
|
||||
if (Math.hypot((double) px - cx, (double) pz - cz) > diameter / 2.0 - 18) continue;
|
||||
if (overlapsSites(px - 15, pz - 15, px + 15, pz + 15, sites)) continue;
|
||||
int low = 384, high = -1; boolean dry = true;
|
||||
for (int dz : new int[] {-8, 0, 8}) for (int dx : new int[] {-8, 0, 8}) {
|
||||
int top = probe.surface(px + dx, pz + dz);
|
||||
if (top < 48 || top > 330 || !probe.solid(px + dx, top, pz + dz)
|
||||
|| probe.protectedAt(px + dx, top, pz + dz)) { dry = false; break; }
|
||||
low = Math.min(low, top); high = Math.max(high, top);
|
||||
}
|
||||
if (!dry || high - low > 13) { probe.reject("coarse-terrain"); continue; }
|
||||
int facing = direction(px, pz, neighbor == null ? cx : neighbor.centerX(), neighbor == null ? cz : neighbor.centerZ());
|
||||
if (kind == Kind.VOID_DOCK) facing = (direction(px, pz, cx, cz) + 2) & 3;
|
||||
int score = (i >= 8 ? 1_000 : 0) + high - low + (kind == Kind.OBSERVATORY || kind == Kind.RADIO_RELAY ? (300 - high) / 4 : 0);
|
||||
candidates.add(new Candidate(px - 8, pz - 8, facing, score, mix(seed ^ kind.ordinal() * 463L ^ i)));
|
||||
}
|
||||
candidates.sort(Comparator.comparingInt(Candidate::score).thenComparingLong(Candidate::seed));
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private static SiteDraft site(long seed, Kind kind, Candidate candidate, List<Site> previous,
|
||||
List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
|
||||
int x = candidate.x(), z = candidate.z(), low = 384, high = -1;
|
||||
for (var landmark : landmarks) if (landmark.entry().x() >= x - 2 && landmark.entry().x() <= x + 18
|
||||
&& landmark.entry().z() >= z - 2 && landmark.entry().z() <= z + 18) { probe.reject("existing-entry"); return null; }
|
||||
var heights = new ArrayList<Integer>();
|
||||
for (int dz = 0; dz < 17; dz++) for (int dx = 0; dx < 17; dx++) {
|
||||
int top = probe.surface(x + dx, z + dz);
|
||||
if (top < 48 || top > 330 || !probe.solid(x + dx, top, z + dz)) { probe.reject("dry-footprint"); return null; }
|
||||
heights.add(top); low = Math.min(low, top); high = Math.max(high, top);
|
||||
}
|
||||
int minimum = Math.max(48, high - MAX_SITE_CUT), maximum = Math.min(330, low + 7);
|
||||
if (minimum > maximum) { probe.reject("site-slope"); return null; }
|
||||
heights.sort(Integer::compareTo);
|
||||
int floor = Math.clamp(heights.get(heights.size() / 2), minimum, maximum);
|
||||
int foundation = Math.max(3, floor - low + 1), height = height(kind);
|
||||
int base = floor - foundation;
|
||||
for (int dz = 0; dz < 17; dz++) for (int dx = 0; dx < 17; dx++)
|
||||
for (int y = base; y <= floor + height; y++) if (probe.protectedAt(x + dx, y, z + dz)) {
|
||||
probe.reject("site-protected-volume"); return null;
|
||||
}
|
||||
var supports = new ArrayList<Support>();
|
||||
for (int dz : new int[] {1, 8, 15}) for (int dx : new int[] {1, 8, 15}) {
|
||||
int bottom = base - 1, rock = bottom;
|
||||
while (rock >= Math.max(2, base - 13) && (!probe.solid(x + dx, rock, z + dz)
|
||||
|| !probe.solid(x + dx, rock - 1, z + dz))) rock--;
|
||||
if (rock < Math.max(2, base - 13)) { probe.reject("site-no-bearing"); return null; }
|
||||
for (int y = rock - 1; y <= bottom; y++) if (probe.protectedAt(x + dx, y, z + dz)) { probe.reject("support-protected"); return null; }
|
||||
if (rock < bottom) supports.add(new Support(x + dx, rock + 1, z + dz, bottom));
|
||||
}
|
||||
int ex = candidate.facing() == 1 ? x + 17 : candidate.facing() == 3 ? x - 1 : x + 8;
|
||||
int ez = candidate.facing() == 2 ? z + 17 : candidate.facing() == 0 ? z - 1 : z + 8;
|
||||
Entry entry = new Entry(ex, floor + 1, ez, candidate.facing());
|
||||
Site result = new Site(id(kind), kind, x, floor, z, 17, 17, height, candidate.facing(),
|
||||
mix(seed ^ candidate.seed()), foundation, supports, entry);
|
||||
var landings = landing(result.entry(), previous, probe);
|
||||
if (landings == null) { probe.reject("entry-landing"); return null; }
|
||||
for (var cell : landings) {
|
||||
OwnedCell old = existing.get(key(cell.x(), cell.z()));
|
||||
if (old != null && old.cell().floorY() != cell.floorY()) { probe.reject("landing-collision"); return null; }
|
||||
}
|
||||
return new SiteDraft(result, landings);
|
||||
}
|
||||
|
||||
private static int height(Kind kind) {
|
||||
return switch (kind) {
|
||||
case FREIGHT_STATION, POST_OFFICE, RAIL_WORKSHOP, SUBSTATION, GREENHOUSE, PHOTO_CLUB -> 10;
|
||||
case EXPEDITION_OFFICE, IMPOSSIBLE_SERVICE -> 12;
|
||||
case COMMUNAL_DEPOT, VOID_DOCK, CONSTRUCTION_REFUGE -> 14;
|
||||
case PUMP_STATION -> 18; case OBSERVATORY -> 20; case RADIO_RELAY -> 22;
|
||||
};
|
||||
}
|
||||
private static List<Request> requests(List<Site> sites, List<Landmark> landmarks) {
|
||||
var result = new ArrayList<Request>();
|
||||
request(result, Kind.FREIGHT_STATION, Kind.COMMUNAL_DEPOT, RouteKind.FREIGHT);
|
||||
request(result, Kind.FREIGHT_STATION, Kind.RAIL_WORKSHOP, RouteKind.FREIGHT);
|
||||
request(result, Kind.COMMUNAL_DEPOT, Kind.PUMP_STATION, RouteKind.FREIGHT);
|
||||
Site workshop = find(sites, Kind.RAIL_WORKSHOP);
|
||||
for (var landmark : landmarks) if (landmark.kind() == LandmarkKind.MINE && workshop != null)
|
||||
result.add(new Request("mine_freight", landmark.id(), workshop.id(), RouteKind.FREIGHT));
|
||||
request(result, Kind.FREIGHT_STATION, Kind.POST_OFFICE, RouteKind.PATH);
|
||||
request(result, Kind.POST_OFFICE, Kind.EXPEDITION_OFFICE, RouteKind.PATH);
|
||||
request(result, Kind.EXPEDITION_OFFICE, Kind.OBSERVATORY, RouteKind.PATH);
|
||||
request(result, Kind.COMMUNAL_DEPOT, Kind.VOID_DOCK, RouteKind.PATH);
|
||||
request(result, Kind.RAIL_WORKSHOP, Kind.SUBSTATION, RouteKind.PATH);
|
||||
request(result, Kind.SUBSTATION, Kind.PUMP_STATION, RouteKind.PATH);
|
||||
request(result, Kind.PUMP_STATION, Kind.GREENHOUSE, RouteKind.PATH);
|
||||
request(result, Kind.POST_OFFICE, Kind.PHOTO_CLUB, RouteKind.PATH);
|
||||
request(result, Kind.PUMP_STATION, Kind.IMPOSSIBLE_SERVICE, RouteKind.PATH);
|
||||
request(result, Kind.OBSERVATORY, Kind.RADIO_RELAY, RouteKind.PATH);
|
||||
request(result, Kind.RAIL_WORKSHOP, Kind.CONSTRUCTION_REFUGE, RouteKind.PATH);
|
||||
Site post = find(sites, Kind.POST_OFFICE);
|
||||
for (var landmark : landmarks) if (landmark.kind() == LandmarkKind.CITY && post != null) {
|
||||
result.add(new Request("city_post_" + result.size(), landmark.id(), post.id(), RouteKind.PATH)); break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private static void request(List<Request> requests, Kind from, Kind to, RouteKind kind) {
|
||||
requests.add(new Request(id(from) + "_" + id(to), id(from), id(to), kind));
|
||||
}
|
||||
|
||||
/** Always persisted, including for an isolated destination: five steps return to natural terrain. */
|
||||
private static List<Cell> landing(Entry entry, List<Site> sites, Probe probe) {
|
||||
int facing = entry.facing(), floor = entry.y() - 1;
|
||||
int endX = entry.x() + DX[facing] * 4, endZ = entry.z() + DZ[facing] * 4;
|
||||
int target = probe.surface(endX, endZ);
|
||||
if (target < 14 || target > 350 || Math.abs(target - floor) > 3) return null;
|
||||
var result = new ArrayList<Cell>();
|
||||
for (int step = 0; step < 5; step++) {
|
||||
// Keep the first two rows flat for the facade and the first rail turn.
|
||||
int y = floor + (int) Math.round((target - floor) * Math.max(0, step - 1) / 3.0);
|
||||
for (int cross = -1; cross <= 1; cross++) {
|
||||
int x = entry.x() + DX[facing] * step + DZ[facing] * cross;
|
||||
int z = entry.z() + DZ[facing] * step - DX[facing] * cross;
|
||||
if (overlapsSites(x, z, x, z, sites)) return null;
|
||||
Cell cell = routeCell(x, y, z, Rail.NONE, false, probe);
|
||||
if (cell == null) return null;
|
||||
result.add(cell);
|
||||
if (step == 4) {
|
||||
int beyond = probe.surface(x + DX[facing], z + DZ[facing]);
|
||||
if (Math.abs(beyond - y) > 1 || !probe.solid(x + DX[facing], beyond, z + DZ[facing])) return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private static RouteDraft route(long seed, Request request, Endpoint from, Endpoint to, List<Site> sites,
|
||||
List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
|
||||
Entry a = from.entry(), b = to.entry();
|
||||
XZ start = new XZ(a.x(), a.z()), end = new XZ(b.x(), b.z());
|
||||
XZ outA = start.move(a.facing(), 6), outB = end.move(b.facing(), 6);
|
||||
int middleX = (outA.x() + outB.x()) / 2, middleZ = (outA.z() + outB.z()) / 2;
|
||||
var alternatives = List.of(
|
||||
List.of(outA, new XZ(outB.x(), outA.z()), outB),
|
||||
List.of(outA, new XZ(outA.x(), outB.z()), outB),
|
||||
List.of(outA, new XZ(middleX, outA.z()), new XZ(middleX, outB.z()), outB),
|
||||
List.of(outA, new XZ(outA.x(), middleZ), new XZ(outB.x(), middleZ), outB),
|
||||
List.of(outA, new XZ(outA.x(), middleZ - 16), new XZ(outB.x(), middleZ - 16), outB),
|
||||
List.of(outA, new XZ(middleX + 16, outA.z()), new XZ(middleX + 16, outB.z()), outB));
|
||||
for (var waypoints : alternatives) {
|
||||
var points = new ArrayList<XZ>(); points.add(start);
|
||||
boolean okay = true;
|
||||
for (var waypoint : waypoints) if (!line(points, waypoint)) { okay = false; break; }
|
||||
if (!okay || !line(points, end) || points.size() > 1024 || new HashSet<>(points).size() != points.size()) continue;
|
||||
RouteDraft result = profile(seed, request, from, to, points, sites, landmarks, existing, probe);
|
||||
if (result != null) return result;
|
||||
}
|
||||
return adaptive(seed, request, from, to, start, end, outA, outB, sites, landmarks, existing, probe);
|
||||
}
|
||||
|
||||
/** Adaptive fallback. The coarse graph only proposes an alignment; it grants no writing permission. */
|
||||
private static RouteDraft adaptive(long seed, Request request, Endpoint from, Endpoint to, XZ start, XZ end,
|
||||
XZ outA, XZ outB, List<Site> sites, List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
|
||||
int stepSize = span(from.entry(), to.entry()) >= 160 ? 8 : 4;
|
||||
int certificateReserve = Math.min(2_200, distance(outA, outB) * 3 + 80);
|
||||
var excluded = new HashSet<XZ>();
|
||||
for (int attempt = 0; attempt < 3; attempt++) {
|
||||
var open = new java.util.PriorityQueue<SearchNode>(Comparator.comparingDouble(SearchNode::priority)
|
||||
.thenComparingDouble(SearchNode::cost).thenComparingInt(n -> n.position().x()).thenComparingInt(n -> n.position().z()));
|
||||
var best = new HashMap<XZ, Double>(); var parents = new HashMap<XZ, XZ>(); var closed = new HashSet<XZ>();
|
||||
var terrainHeights = new HashMap<XZ, Integer>();
|
||||
int begin = coarseRoute(outA, sites, terrainHeights, probe);
|
||||
if (begin == INVALID) return null;
|
||||
open.add(new SearchNode(outA, 0, distance(outA, outB))); best.put(outA, 0.0);
|
||||
List<XZ> found = null;
|
||||
int searchColumns = Math.max(probe.columns.size() + 150, probe.columnLimit - certificateReserve);
|
||||
while (!open.isEmpty() && closed.size() < 500 && probe.columns.size() < searchColumns) {
|
||||
var current = open.remove(); XZ p = current.position();
|
||||
if (!closed.add(p) || current.cost() > best.getOrDefault(p, Double.POSITIVE_INFINITY)) continue;
|
||||
if (distance(p, outB) <= stepSize * 2) {
|
||||
var reversed = new ArrayList<XZ>(); XZ ancestor = p;
|
||||
while (ancestor != null) { reversed.add(ancestor); ancestor = parents.get(ancestor); }
|
||||
java.util.Collections.reverse(reversed);
|
||||
for (int order = 0; order < 2 && found == null; order++) {
|
||||
var candidate = new ArrayList<XZ>(); candidate.add(start); boolean valid = true;
|
||||
for (XZ step : reversed) if (!line(candidate, step)) { valid = false; break; }
|
||||
XZ turn = order == 0 ? new XZ(outB.x(), p.z()) : new XZ(p.x(), outB.z());
|
||||
if (valid && line(candidate, turn) && line(candidate, outB) && line(candidate, end)
|
||||
&& new HashSet<>(candidate).size() == candidate.size()) found = candidate;
|
||||
}
|
||||
if (found != null) break;
|
||||
}
|
||||
int top = terrainHeights.get(p); XZ parent = parents.get(p);
|
||||
for (int d = 0; d < 4; d++) {
|
||||
if (parent == null && d == ((from.entry().facing() + 2) & 3)) continue;
|
||||
XZ next = p.move(d, stepSize);
|
||||
if (closed.contains(next) || excluded.contains(next)
|
||||
|| Math.min(distance(next, outA), distance(next, outB)) > distance(outA, outB) + 64) continue;
|
||||
int height = coarseRoute(next, sites, terrainHeights, probe);
|
||||
if (height == INVALID || Math.abs(height - top) > stepSize + 3) continue;
|
||||
double turnCost = parent == null || parent.x() == next.x() || parent.z() == next.z() ? 0 : 1.5;
|
||||
double cost = current.cost() + stepSize + Math.abs(height - top) * 1.1 + turnCost;
|
||||
if (cost >= best.getOrDefault(next, Double.POSITIVE_INFINITY)) continue;
|
||||
best.put(next, cost); parents.put(next, p);
|
||||
open.add(new SearchNode(next, cost, cost + distance(next, outB) * 1.6));
|
||||
}
|
||||
}
|
||||
if (found == null) { probe.reject("adaptive-no-alignment"); return null; }
|
||||
RouteDraft certified = profile(seed, request, from, to, found, sites, landmarks, existing, probe);
|
||||
if (certified != null) { probe.reject("adaptive-admitted"); return certified; }
|
||||
if (probe.routeBlock == null) return null;
|
||||
// A failed exact corridor removes only nearby coarse nodes on the next proposal.
|
||||
XZ bad = probe.routeBlock;
|
||||
int gx = outA.x() + (int) Math.round((bad.x() - outA.x()) / (double) stepSize) * stepSize;
|
||||
int gz = outA.z() + (int) Math.round((bad.z() - outA.z()) / (double) stepSize) * stepSize;
|
||||
excluded.add(new XZ(gx, gz));
|
||||
for (int d = 0; d < 4; d++) excluded.add(new XZ(gx, gz).move(d, stepSize));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private record SearchNode(XZ position, double cost, double priority) {}
|
||||
private static int distance(XZ a, XZ b) { return Math.abs(a.x() - b.x()) + Math.abs(a.z() - b.z()); }
|
||||
private static int coarseRoute(XZ p, List<Site> sites, Map<XZ, Integer> cache, Probe probe) {
|
||||
Integer old = cache.get(p); if (old != null) return old;
|
||||
if (overlapsSites(p.x() - 2, p.z() - 2, p.x() + 2, p.z() + 2, sites)) { cache.put(p, INVALID); return INVALID; }
|
||||
int top = probe.surface(p.x(), p.z());
|
||||
if (top < 14 || top > 350 || !probe.solid(p.x(), top, p.z()) || probe.protectedAt(p.x(), top, p.z())) {
|
||||
cache.put(p, INVALID); return INVALID;
|
||||
}
|
||||
for (int d = 0; d < 4; d++) {
|
||||
XZ side = p.move(d, 1); int sideTop = probe.surface(side.x(), side.z());
|
||||
if (sideTop < 14 || sideTop > 350 || Math.abs(sideTop - top) > 4 || probe.protectedAt(side.x(), sideTop, side.z())) {
|
||||
cache.put(p, INVALID); return INVALID;
|
||||
}
|
||||
}
|
||||
cache.put(p, top); return top;
|
||||
}
|
||||
|
||||
/** Finite orthogonal alignments, each certified at full block resolution before publication. */
|
||||
private static boolean line(List<XZ> points, XZ destination) {
|
||||
XZ p = points.getLast();
|
||||
if (p.x() != destination.x() && p.z() != destination.z()) return false;
|
||||
int dx = Integer.signum(destination.x() - p.x()), dz = Integer.signum(destination.z() - p.z());
|
||||
while (!p.equals(destination)) {
|
||||
p = new XZ(p.x() + dx, p.z() + dz); points.add(p);
|
||||
if (points.size() > 1024) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static RouteDraft profile(long seed, Request request, Endpoint from, Endpoint to, List<XZ> points,
|
||||
List<Site> sites, List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
|
||||
int size = points.size(); probe.routeBlock = null;
|
||||
int[] lo = new int[size], hi = new int[size], desired = new int[size], group = new int[size];
|
||||
boolean[] flat = new boolean[size - 1];
|
||||
for (int i = 1; i < size - 1; i++) if (turn(points, i)) { flat[i - 1] = true; flat[i] = true; }
|
||||
for (int i = 0; i < size; i++) {
|
||||
XZ point = points.get(i); probe.routeBlock = point; lo[i] = 14; hi[i] = 350;
|
||||
desired[i] = probe.surface(point.x(), point.z());
|
||||
boolean readOnly = i == 0 && from.readOnly() || i == size - 1 && to.readOnly();
|
||||
for (XZ column : strip(points, i)) {
|
||||
if (overlapsSites(column.x(), column.z(), column.x(), column.z(), sites)) { probe.reject("route-building"); return null; }
|
||||
int top = probe.surface(column.x(), column.z());
|
||||
if (top < 14 || top > 350 || !probe.solid(column.x(), top, column.z())) { probe.reject("route-dry-ground"); return null; }
|
||||
lo[i] = Math.max(lo[i], top - MAX_ROUTE_CUT); hi[i] = Math.min(hi[i], top + 4);
|
||||
OwnedCell old = existing.get(key(column.x(), column.z()));
|
||||
if (old != null) { lo[i] = Math.max(lo[i], old.cell().floorY()); hi[i] = Math.min(hi[i], old.cell().floorY()); }
|
||||
if (readOnly && top != (i == 0 ? from.entry().y() : to.entry().y()) - 1) return null;
|
||||
}
|
||||
if (i == 0 || i == size - 1) {
|
||||
int exact = (i == 0 ? from.entry().y() : to.entry().y()) - 1;
|
||||
lo[i] = Math.max(lo[i], exact); hi[i] = Math.min(hi[i], exact);
|
||||
}
|
||||
if (lo[i] > hi[i]) { probe.reject("route-grade-interval"); return null; }
|
||||
}
|
||||
// Curves and both adjacent rails share one elevation; a Minecraft curved rail cannot slope.
|
||||
int groups = 1;
|
||||
for (int i = 1; i < size; i++) { if (!flat[i - 1]) groups++; group[i] = groups - 1; }
|
||||
int[] gl = new int[groups], gh = new int[groups], gd = new int[groups], y = new int[groups];
|
||||
java.util.Arrays.fill(gl, 14); java.util.Arrays.fill(gh, 350);
|
||||
for (int i = 0; i < size; i++) { int g = group[i]; gl[g] = Math.max(gl[g], lo[i]); gh[g] = Math.min(gh[g], hi[i]); gd[g] = desired[i]; }
|
||||
for (int g = 1; g < groups; g++) { gl[g] = Math.max(gl[g], gl[g - 1] - 1); gh[g] = Math.min(gh[g], gh[g - 1] + 1); }
|
||||
for (int g = groups - 2; g >= 0; g--) { gl[g] = Math.max(gl[g], gl[g + 1] - 1); gh[g] = Math.min(gh[g], gh[g + 1] + 1); }
|
||||
for (int g = 0; g < groups; g++) {
|
||||
if (gl[g] > gh[g]) { probe.reject("route-grade"); return null; }
|
||||
int lower = g == 0 ? gl[g] : Math.max(gl[g], y[g - 1] - 1), upper = g == 0 ? gh[g] : Math.min(gh[g], y[g - 1] + 1);
|
||||
y[g] = Math.clamp(gd[g], lower, upper);
|
||||
}
|
||||
var path = new ArrayList<Point>(); var floors = new LinkedHashMap<Long, Integer>();
|
||||
for (int i = 0; i < size; i++) {
|
||||
XZ point = points.get(i); int floor = y[group[i]]; path.add(new Point(point.x(), floor, point.z()));
|
||||
if (i == 0 && from.readOnly() || i == size - 1 && to.readOnly()) continue;
|
||||
for (XZ column : strip(points, i)) {
|
||||
long key = key(column.x(), column.z()); Integer old = floors.putIfAbsent(key, floor);
|
||||
if (old != null && old != floor) { probe.reject("route-self-grade"); return null; }
|
||||
// Historical endpoints and their lateral landing are read-only, even at another nearby turn.
|
||||
for (var landmark : landmarks) if (landingColumn(landmark.entry(), column)) {
|
||||
boolean actualEnd = from.readOnly() && landmark.id().equals(from.id())
|
||||
|| to.readOnly() && landmark.id().equals(to.id());
|
||||
if (!actualEnd || floor != landmark.entry().y() - 1) { probe.reject("route-historical-entry"); return null; }
|
||||
floors.remove(key); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
var cells = new LinkedHashMap<Long, Cell>();
|
||||
for (var entry : floors.entrySet()) {
|
||||
int x = (int) (entry.getKey() >> 32), z = (int) (long) entry.getKey();
|
||||
probe.routeBlock = new XZ(x, z);
|
||||
Cell cell = routeCell(x, entry.getValue(), z, Rail.NONE, false, probe);
|
||||
if (cell == null) { probe.reject("route-column-certificate"); return null; }
|
||||
cells.put(entry.getKey(), cell);
|
||||
}
|
||||
if (request.kind() == RouteKind.FREIGHT) for (int i = 0; i < path.size(); i++) {
|
||||
Point point = path.get(i); long key = key(point.x(), point.z()); Cell cell = cells.get(key);
|
||||
if (cell == null) continue;
|
||||
Rail rail = rail(path, i);
|
||||
// A one-block valley cannot carry two ascending slopes on the same rail. Keep a repairable rail gap.
|
||||
boolean intact = rail != Rail.NONE && Math.floorMod(mix(seed ^ key ^ request.id().hashCode()), 19) != 0;
|
||||
if (rail == Rail.NONE) rail = points.get(Math.max(0, i - 1)).x() == points.get(Math.min(size - 1, i + 1)).x() ? Rail.NORTH_SOUTH : Rail.EAST_WEST;
|
||||
cells.put(key, new Cell(cell.x(), cell.floorY(), cell.z(), cell.foundationDepth(), rail, intact));
|
||||
}
|
||||
return new RouteDraft(List.copyOf(path), cells);
|
||||
}
|
||||
|
||||
private static boolean landingColumn(Entry entry, XZ column) {
|
||||
int dx = column.x() - entry.x(), dz = column.z() - entry.z();
|
||||
return DX[entry.facing()] * dx + DZ[entry.facing()] * dz == 0 && Math.abs(dx) + Math.abs(dz) <= 1;
|
||||
}
|
||||
private static List<XZ> strip(List<XZ> points, int i) {
|
||||
XZ p = points.get(i); var result = new ArrayList<XZ>();
|
||||
if (turn(points, i)) {
|
||||
for (int dx = -1; dx <= 1; dx++) for (int dz = -1; dz <= 1; dz++) result.add(new XZ(p.x() + dx, p.z() + dz));
|
||||
} else {
|
||||
XZ other = points.get(i == points.size() - 1 ? i - 1 : i + 1);
|
||||
int dx = Integer.signum(other.x() - p.x()), dz = Integer.signum(other.z() - p.z());
|
||||
for (int cross = -1; cross <= 1; cross++) result.add(new XZ(p.x() + dz * cross, p.z() - dx * cross));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private static boolean turn(List<XZ> points, int i) {
|
||||
if (i == 0 || i == points.size() - 1) return false;
|
||||
XZ a = points.get(i - 1), b = points.get(i + 1);
|
||||
return a.x() != b.x() && a.z() != b.z();
|
||||
}
|
||||
private static Rail rail(List<Point> path, int i) {
|
||||
Point p = path.get(i), a = path.get(Math.max(0, i - 1)), b = path.get(Math.min(path.size() - 1, i + 1));
|
||||
if (a.floorY() > p.floorY() && b.floorY() > p.floorY()) return Rail.NONE;
|
||||
Point high = a.floorY() > p.floorY() ? a : b.floorY() > p.floorY() ? b : null;
|
||||
if (high != null) return switch (direction(p.x(), p.z(), high.x(), high.z())) {
|
||||
case 0 -> Rail.ASCENDING_NORTH; case 1 -> Rail.ASCENDING_EAST; case 2 -> Rail.ASCENDING_SOUTH; default -> Rail.ASCENDING_WEST;
|
||||
};
|
||||
if (a.x() == b.x()) return Rail.NORTH_SOUTH;
|
||||
if (a.z() == b.z()) return Rail.EAST_WEST;
|
||||
boolean n = a.z() < p.z() || b.z() < p.z(), s = a.z() > p.z() || b.z() > p.z();
|
||||
boolean e = a.x() > p.x() || b.x() > p.x();
|
||||
return n ? e ? Rail.NORTH_EAST : Rail.NORTH_WEST : s && e ? Rail.SOUTH_EAST : Rail.SOUTH_WEST;
|
||||
}
|
||||
private static Cell routeCell(int x, int floor, int z, Rail rail, boolean present, Probe probe) {
|
||||
int top = probe.surface(x, z);
|
||||
if (floor < 14 || floor > 350 || top < floor - 4 || top > floor + MAX_ROUTE_CUT) return null;
|
||||
int foundation = Math.max(1, floor - top + 1), base = floor - foundation;
|
||||
if (!probe.solid(x, base - 1, z) || !probe.solid(x, base, z)) return null;
|
||||
for (int y = base - 1; y <= floor + 3; y++) if (probe.protectedAt(x, y, z)) return null;
|
||||
return new Cell(x, floor, z, foundation, rail, present);
|
||||
}
|
||||
private record XZ(int x, int z) { XZ move(int facing, int distance) { return new XZ(x + DX[facing] * distance, z + DZ[facing] * distance); } }
|
||||
private record SegmentKey(Request request, int cx, int cz) {}
|
||||
private static List<Segment> segments(long seed, Map<Long, OwnedCell> cells) {
|
||||
var groups = new LinkedHashMap<SegmentKey, List<Cell>>();
|
||||
for (var owned : cells.values()) {
|
||||
Cell c = owned.cell(); var key = new SegmentKey(owned.request(), c.x() >> 4, c.z() >> 4);
|
||||
groups.computeIfAbsent(key, ignored -> new ArrayList<>()).add(c);
|
||||
}
|
||||
var result = new ArrayList<Segment>();
|
||||
for (var entry : groups.entrySet()) {
|
||||
var k = entry.getKey(); var r = k.request();
|
||||
// IDs are stable, bounded and independent of native generation order.
|
||||
String id = "piece_" + r.id() + "_" + k.cx() + "_" + k.cz();
|
||||
result.add(new Segment(id, r.from(), r.to(), r.kind(), k.cx(), k.cz(), entry.getValue(), mix(seed ^ key(k.cx(), k.cz()) ^ r.id().hashCode())));
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
|
||||
private static int span(Entry a, Entry b) { return Math.max(Math.abs(a.x() - b.x()), Math.abs(a.z() - b.z())); }
|
||||
private static Site find(List<Site> sites, Kind kind) { return sites.stream().filter(site -> site.kind() == kind).findFirst().orElse(null); }
|
||||
private static String id(Kind kind) { return kind.name().toLowerCase(Locale.ROOT); }
|
||||
private static int direction(int x, int z, int tx, int tz) { return Math.abs(tx - x) >= Math.abs(tz - z) ? tx > x ? 1 : 3 : tz > z ? 2 : 0; }
|
||||
private static boolean overlapsSites(int x0, int z0, int x1, int z1, List<Site> sites) {
|
||||
for (var site : sites) if (x0 <= site.maxX() && x1 >= site.minX() && z0 <= site.maxZ() && z1 >= site.minZ()) return true;
|
||||
return false;
|
||||
}
|
||||
private record SiteDraft(Site site, List<Cell> landings) {}
|
||||
private record Candidate(int x, int z, int facing, int score, long seed) {}
|
||||
private record Endpoint(String id, Entry entry, boolean readOnly) {}
|
||||
private record Request(String id, String from, String to, RouteKind kind) {}
|
||||
private record OwnedCell(Request request, Cell cell) {}
|
||||
private record RouteDraft(List<Point> path, Map<Long, Cell> cells) {}
|
||||
private static final class BudgetExceeded extends RuntimeException {
|
||||
final boolean columns; BudgetExceeded(boolean columns) { this.columns = columns; }
|
||||
}
|
||||
private static final class Probe {
|
||||
final Terrain terrain; final int cx, cz; final double radius;
|
||||
final Map<Long, Integer> columns = new HashMap<>();
|
||||
XZ routeBlock; Map<String, Integer> reasons = new HashMap<>(); int samples, columnLimit, sampleLimit;
|
||||
Probe(Terrain terrain, int cx, int cz, int diameter) { this.terrain = java.util.Objects.requireNonNull(terrain); this.cx = cx; this.cz = cz; radius = diameter / 2.0 - 2; }
|
||||
int surface(int x, int z) {
|
||||
if (Math.hypot((double) x - cx, (double) z - cz) > radius || !terrain.owns(x, z)) return INVALID;
|
||||
long key = key(x, z); var cached = columns.get(key); if (cached != null) return cached;
|
||||
if (columns.size() >= columnLimit) throw new BudgetExceeded(true);
|
||||
count(); int top = terrain.surfaceY(x, z); columns.put(key, top); return top;
|
||||
}
|
||||
boolean solid(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) == INVALID) return false; count(); return terrain.solid(x, y, z); }
|
||||
boolean protectedAt(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) == INVALID) return true; count(); return terrain.protectedAt(x, y, z); }
|
||||
void count() { if (samples >= sampleLimit) throw new BudgetExceeded(false); samples++; }
|
||||
void reject(String reason) { reasons.merge(reason, 1, Integer::sum); }
|
||||
}
|
||||
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
|
||||
private static double unit(long value) { return (value >>> 11) * 0x1.0p-53; }
|
||||
private static long mix(long value) { value = (value ^ value >>> 30) * 0xbf58476d1ce4e5b9L; value = (value ^ value >>> 27) * 0x94d049bb133111ebL; return value ^ value >>> 31; }
|
||||
}
|
||||
+616
@@ -0,0 +1,616 @@
|
||||
package fr.koka.sanctuary.worldgen.heritage;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/** Generation21 only. A finite catalogue is admitted before complete, repairable routes are published.
|
||||
* Everything samples native columns; no chunk, player object or future continent is requested. */
|
||||
public final class HeritagePlanner21 {
|
||||
public static final int MAX_COLUMNS = HeritagePlan19.MAX_COLUMNS, MAX_SAMPLES = HeritagePlan19.MAX_SAMPLES;
|
||||
// Local17x17 sites may cut8 blocks, with the same at-most8-block slab and certified short piers.
|
||||
// Three-wide paths may cut4/fill5; grade stays one block, and water/old construction is never waived.
|
||||
public static final int MAX_SITE_CUT = 8, MAX_ROUTE_CUT = 4, MAX_ROUTE_FILL = 5;
|
||||
private static final int SITE_COLUMNS = 11_000, SITE_SAMPLES = 540_000;
|
||||
private static final int INVALID = Integer.MIN_VALUE;
|
||||
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
|
||||
private HeritagePlanner21() {}
|
||||
public interface Terrain extends HeritagePlanner19.Terrain {
|
||||
boolean owns(int x, int z);
|
||||
int surfaceY(int x, int z);
|
||||
boolean solid(int x, int y, int z);
|
||||
boolean protectedAt(int x, int y, int z);
|
||||
default List<Landmark> landmarks() { return List.of(); }
|
||||
}
|
||||
|
||||
public static HeritagePlan19 plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
|
||||
if (!List.of(512, 724, 1024).contains(diameter)) throw new IllegalArgumentException("Heritage21 requires a starter size");
|
||||
var probe = new Probe(terrain, cx, cz, diameter);
|
||||
List<Landmark> landmarks = List.copyOf(terrain.landmarks()).stream().limit(16).toList();
|
||||
var sites = new ArrayList<Site>(); var cells = new LinkedHashMap<Long, OwnedCell>(); var diagnostics = new EnumMap<Kind, String>(Kind.class);
|
||||
for (var kind : Kind.values()) {
|
||||
int remaining = Kind.values().length - kind.ordinal() - 1;
|
||||
probe.columnLimit = SITE_COLUMNS - remaining * (SITE_COLUMNS / 14);
|
||||
probe.sampleLimit = SITE_SAMPLES - remaining * (SITE_SAMPLES / 14);
|
||||
SiteDraft admitted = null; int attempts = 0;
|
||||
var reasons = new java.util.TreeMap<String, Integer>(); probe.reasons = reasons;
|
||||
try {
|
||||
for (var candidate : candidates(seed, kind, cx, cz, diameter, sites, landmarks, probe)) {
|
||||
attempts++;
|
||||
admitted = site(seed, kind, candidate, sites, landmarks, cells, probe);
|
||||
if (admitted != null) break;
|
||||
}
|
||||
} catch (BudgetExceeded exhausted) { reasons.merge(exhausted.columns ? "column-budget" : "probe-budget", 1, Integer::sum); }
|
||||
if (admitted != null) {
|
||||
sites.add(admitted.site());
|
||||
Request landing = new Request(admitted.site().id() + "_landing", admitted.site().id(), admitted.site().id(), RouteKind.PATH);
|
||||
for (var cell : admitted.landings()) cells.put(key(cell.x(), cell.z()), new OwnedCell(landing, cell));
|
||||
}
|
||||
diagnostics.put(kind, (admitted == null ? "not admitted" : "admitted") + "; candidates=" + attempts + "; " + reasons);
|
||||
}
|
||||
probe.columnLimit = MAX_COLUMNS; probe.sampleLimit = MAX_SAMPLES;
|
||||
var links = new ArrayList<Link>();
|
||||
var routeResults = new LinkedHashMap<String, String>();
|
||||
var routeReasons = new java.util.TreeMap<String, Integer>(); probe.reasons = routeReasons;
|
||||
Map<String, Endpoint> endpoints = new LinkedHashMap<>();
|
||||
for (var site : sites) endpoints.put(site.id(), new Endpoint(site.id(), site.entry(), false));
|
||||
for (var landmark : landmarks) if (!endpoints.containsKey(landmark.id()))
|
||||
endpoints.put(landmark.id(), new Endpoint(landmark.id(), landmark.entry(), true));
|
||||
var requests = new ArrayList<>(requests(sites, landmarks).stream()
|
||||
.filter(r -> endpoints.containsKey(r.from()) && endpoints.containsKey(r.to())).toList());
|
||||
var components = new Components(endpoints.keySet());
|
||||
// Multiple exterior entries into the certified connected city represent one already walkable destination.
|
||||
String firstCity = null;
|
||||
for (var landmark : landmarks) if (landmark.kind() == LandmarkKind.CITY) {
|
||||
if (firstCity == null) firstCity = landmark.id(); else components.join(firstCity, landmark.id());
|
||||
}
|
||||
Request longFreight = requests.stream().filter(r -> r.kind() == RouteKind.FREIGHT
|
||||
&& span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry()) >= diameter / 4)
|
||||
.max(Comparator.comparingInt(r -> span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry()))).orElse(null);
|
||||
var siteIds = new HashSet<String>(); sites.forEach(site -> siteIds.add(site.id()));
|
||||
var addressed = new HashSet<String>();
|
||||
var destinationAttempts = new HashMap<String, Integer>();
|
||||
int attempted = 0;
|
||||
while (!requests.isEmpty() && attempted < 36 && links.size() < 24) {
|
||||
// Give each isolated site a first useful route before spending its columns on a cycle.
|
||||
// Repeated city gates already belong to one walkable component; no redundant freight bypass is needed.
|
||||
requests.removeIf(r -> components.same(r.from(), r.to()));
|
||||
if (requests.isEmpty()) break;
|
||||
requests.sort(Comparator.comparingInt((Request r) -> isolatedSites(r, siteIds, addressed) > 0
|
||||
&& isolatedAttempts(r, siteIds, addressed, destinationAttempts) == 0 ? 0 : 1)
|
||||
.thenComparingInt(r -> span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry()))
|
||||
.thenComparingInt(r -> r.kind() == RouteKind.FREIGHT ? 0 : 1).thenComparing(Request::id));
|
||||
var request = attempted == 0 && longFreight != null ? longFreight : requests.getFirst(); requests.remove(request);
|
||||
attempted++;
|
||||
int distance = Math.abs(endpoints.get(request.from()).entry().x() - endpoints.get(request.to()).entry().x())
|
||||
+ Math.abs(endpoints.get(request.from()).entry().z() - endpoints.get(request.to()).entry().z());
|
||||
boolean longAxis = span(endpoints.get(request.from()).entry(), endpoints.get(request.to()).entry()) >= diameter / 4;
|
||||
int isolated = isolatedSites(request, siteIds, addressed);
|
||||
var waiting = new HashSet<String>();
|
||||
for (var next : requests) for (var id : List.of(next.from(), next.to()))
|
||||
if (siteIds.contains(id) && !addressed.contains(id) && !id.equals(request.from()) && !id.equals(request.to())) waiting.add(id);
|
||||
int reserve = Math.min(Math.min(waiting.size(), 8), 36 - attempted) * 100;
|
||||
int allowance = longAxis && attempted == 1 ? 3_800 : isolated > 0 ? Math.clamp(distance * 5 + 350, 900, 2_400) : 900;
|
||||
probe.columnLimit = Math.min(MAX_COLUMNS - reserve, probe.columns.size() + allowance);
|
||||
probe.sampleLimit = Math.min(MAX_SAMPLES - reserve * 20, probe.samples + (longAxis ? 150_000 : 80_000));
|
||||
for (var id : List.of(request.from(), request.to())) if (siteIds.contains(id) && !addressed.contains(id))
|
||||
destinationAttempts.merge(id, 1, Integer::sum);
|
||||
Endpoint from = endpoints.get(request.from()), to = endpoints.get(request.to());
|
||||
int beforeRouteColumns = probe.columns.size();
|
||||
var beforeReasons = new HashMap<>(routeReasons);
|
||||
try {
|
||||
RouteDraft route = route(seed, request, from, to, sites, landmarks, cells, probe);
|
||||
if (route == null) { probe.reject("no-complete-route"); routeResults.put(request.id(), "refused; columns=" + (probe.columns.size() - beforeRouteColumns) + rejectionDelta(beforeReasons, routeReasons, probe.routeBlock)); continue; }
|
||||
var groups = new HashSet<SegmentKey>();
|
||||
cells.values().forEach(c -> groups.add(new SegmentKey(c.request(), c.cell().x() >> 4, c.cell().z() >> 4)));
|
||||
route.cells().values().stream().filter(c -> !cells.containsKey(key(c.x(), c.z())))
|
||||
.forEach(c -> groups.add(new SegmentKey(request, c.x() >> 4, c.z() >> 4)));
|
||||
if (groups.size() > 512) { routeResults.put(request.id(), "refused; native-piece-budget"); continue; }
|
||||
links.add(new Link(request.id(), request.from(), request.to(), request.kind(), route.path()));
|
||||
components.join(request.from(), request.to());
|
||||
addressed.add(request.from()); addressed.add(request.to());
|
||||
routeResults.put(request.id(), "admitted; span=" + span(from.entry(), to.entry()) + "; length=" + route.path().size() + "; columns=" + (probe.columns.size() - beforeRouteColumns));
|
||||
for (var cell : route.cells().values()) {
|
||||
long key = key(cell.x(), cell.z());
|
||||
OwnedCell old = cells.get(key);
|
||||
if (old == null) cells.put(key, new OwnedCell(request, cell));
|
||||
else if (cell.rail() != Rail.NONE && old.cell().rail() == Rail.NONE)
|
||||
cells.put(key, new OwnedCell(old.request(), cell));
|
||||
else if (cell.rail() != Rail.NONE && old.cell().rail() != Rail.NONE && cell.rail() != old.cell().rail()) {
|
||||
// A missing switch is intentional; never emit a rail with contradictory connections.
|
||||
var c = old.cell(); cells.put(key, new OwnedCell(old.request(),
|
||||
new Cell(c.x(), c.floorY(), c.z(), c.foundationDepth(), c.rail(), false)));
|
||||
}
|
||||
}
|
||||
} catch (BudgetExceeded exhausted) { probe.reject(exhausted.columns ? "route-column-budget" : "route-probe-budget"); routeResults.put(request.id(), "budget; columns=" + (probe.columns.size() - beforeRouteColumns) + rejectionDelta(beforeReasons, routeReasons, probe.routeBlock)); }
|
||||
}
|
||||
for (var site : sites) {
|
||||
var incident = links.stream().filter(l -> l.fromId().equals(site.id()) || l.toId().equals(site.id())).map(Link::id).toList();
|
||||
var attempts = new LinkedHashMap<String, String>();
|
||||
routeResults.forEach((id, value) -> { if (id.contains(site.id())) attempts.put(id, value); });
|
||||
diagnostics.put(site.kind(), diagnostics.get(site.kind()) + "; connected=" + !incident.isEmpty()
|
||||
+ "; accepted-links=" + incident + "; route-attempts=" + attempts);
|
||||
}
|
||||
var segments = segments(seed, cells);
|
||||
if (Boolean.getBoolean("sanctuary.test.heritage21") || Boolean.getBoolean("sanctuary.heritage.plannerDiagnostics"))
|
||||
System.getLogger(HeritagePlanner21.class.getName()).log(System.Logger.Level.INFO,
|
||||
"Heritage21 seed=" + seed + " diameter=" + diameter + " sites=" + sites.stream().map(Site::kind).toList()
|
||||
+ " links=" + links.size() + " segments=" + segments.size() + " columns=" + probe.columns.size()
|
||||
+ " samples=" + probe.samples + " catalogue=" + diagnostics + " routes=" + routeReasons + " routeResults=" + routeResults);
|
||||
return new HeritagePlan19(sites, landmarks, links, segments, probe.samples, probe.columns.size(), diagnostics);
|
||||
}
|
||||
|
||||
private static List<Candidate> candidates(long seed, Kind kind, int cx, int cz, int diameter,
|
||||
List<Site> sites, List<Landmark> landmarks, Probe probe) {
|
||||
double angle = unit(mix(seed ^ 0x19ACEL)) * Math.PI * 2 + kind.ordinal() * 2.399963229728653;
|
||||
double radius = diameter * (kind == Kind.VOID_DOCK ? .40 : kind == Kind.OBSERVATORY || kind == Kind.RADIO_RELAY ? .31 : .22);
|
||||
int x = cx + (int) Math.round(Math.cos(angle) * radius), z = cz + (int) Math.round(Math.sin(angle) * radius);
|
||||
Kind parent = switch (kind) {
|
||||
case POST_OFFICE, COMMUNAL_DEPOT, RAIL_WORKSHOP -> Kind.FREIGHT_STATION;
|
||||
case EXPEDITION_OFFICE, PHOTO_CLUB -> Kind.POST_OFFICE;
|
||||
case SUBSTATION, CONSTRUCTION_REFUGE -> Kind.RAIL_WORKSHOP;
|
||||
case GREENHOUSE, IMPOSSIBLE_SERVICE -> Kind.PUMP_STATION;
|
||||
default -> null;
|
||||
};
|
||||
Site neighbor = parent == null ? null : find(sites, parent);
|
||||
if (neighbor != null) {
|
||||
double local = unit(mix(seed ^ kind.ordinal() * 179L)) * Math.PI * 2;
|
||||
int distance = kind == Kind.RAIL_WORKSHOP ? 65 : 40;
|
||||
x = neighbor.centerX() + (int) Math.round(Math.cos(local) * distance);
|
||||
z = neighbor.centerZ() + (int) Math.round(Math.sin(local) * distance);
|
||||
}
|
||||
var candidates = new ArrayList<Candidate>();
|
||||
for (int i = 0; i < 16; i++) {
|
||||
double turn = angle + i * Math.PI / 3;
|
||||
int offset = i == 0 ? 0 : i < 5 ? 24 : i < 8 ? 48 : i < 12 ? 72 : 96;
|
||||
int px = x + (int) Math.round(Math.cos(turn) * offset), pz = z + (int) Math.round(Math.sin(turn) * offset);
|
||||
if (Math.hypot((double) px - cx, (double) pz - cz) > diameter / 2.0 - 18) continue;
|
||||
if (overlapsSites(px - 15, pz - 15, px + 15, pz + 15, sites)) continue;
|
||||
int low = 384, high = -1; boolean dry = true;
|
||||
for (int dz : new int[] {-8, 0, 8}) for (int dx : new int[] {-8, 0, 8}) {
|
||||
int top = probe.surface(px + dx, pz + dz);
|
||||
if (top < 48 || top > 330 || !probe.solid(px + dx, top, pz + dz)
|
||||
|| probe.protectedAt(px + dx, top, pz + dz)) { dry = false; break; }
|
||||
low = Math.min(low, top); high = Math.max(high, top);
|
||||
}
|
||||
if (!dry || high - low > MAX_SITE_CUT + 7) { probe.reject("coarse-terrain"); continue; }
|
||||
int facing = direction(px, pz, neighbor == null ? cx : neighbor.centerX(), neighbor == null ? cz : neighbor.centerZ());
|
||||
if (kind == Kind.VOID_DOCK) facing = (direction(px, pz, cx, cz) + 2) & 3;
|
||||
int score = (i >= 8 ? 1_000 : 0) + high - low + (kind == Kind.OBSERVATORY || kind == Kind.RADIO_RELAY ? (300 - high) / 4 : 0);
|
||||
candidates.add(new Candidate(px - 8, pz - 8, facing, score, mix(seed ^ kind.ordinal() * 463L ^ i)));
|
||||
}
|
||||
candidates.sort(Comparator.comparingInt(Candidate::score).thenComparingLong(Candidate::seed));
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private static SiteDraft site(long seed, Kind kind, Candidate candidate, List<Site> previous,
|
||||
List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
|
||||
int x = candidate.x(), z = candidate.z(), low = 384, high = -1;
|
||||
for (var landmark : landmarks) if (landmark.entry().x() >= x - 2 && landmark.entry().x() <= x + 18
|
||||
&& landmark.entry().z() >= z - 2 && landmark.entry().z() <= z + 18) { probe.reject("existing-entry"); return null; }
|
||||
var heights = new ArrayList<Integer>();
|
||||
for (int dz = 0; dz < 17; dz++) for (int dx = 0; dx < 17; dx++) {
|
||||
int top = probe.surface(x + dx, z + dz);
|
||||
if (top < 48 || top > 330 || !probe.solid(x + dx, top, z + dz)) { probe.reject("dry-footprint"); return null; }
|
||||
heights.add(top); low = Math.min(low, top); high = Math.max(high, top);
|
||||
}
|
||||
int minimum = Math.max(48, high - MAX_SITE_CUT), maximum = Math.min(330, low + 7);
|
||||
if (minimum > maximum) { probe.reject("site-slope"); return null; }
|
||||
heights.sort(Integer::compareTo);
|
||||
int floor = Math.clamp(heights.get(heights.size() / 2), minimum, maximum);
|
||||
int foundation = Math.max(3, floor - low + 1), height = height(kind);
|
||||
int base = floor - foundation;
|
||||
for (int dz = 0; dz < 17; dz++) for (int dx = 0; dx < 17; dx++)
|
||||
for (int y = base; y <= floor + height; y++) if (probe.protectedAt(x + dx, y, z + dz)) {
|
||||
probe.reject("site-protected-volume"); return null;
|
||||
}
|
||||
var supports = new ArrayList<Support>();
|
||||
for (int dz : new int[] {1, 8, 15}) for (int dx : new int[] {1, 8, 15}) {
|
||||
int bottom = base - 1, rock = bottom;
|
||||
while (rock >= Math.max(2, base - 13) && (!probe.solid(x + dx, rock, z + dz)
|
||||
|| !probe.solid(x + dx, rock - 1, z + dz))) rock--;
|
||||
if (rock < Math.max(2, base - 13)) { probe.reject("site-no-bearing"); return null; }
|
||||
for (int y = rock - 1; y <= bottom; y++) if (probe.protectedAt(x + dx, y, z + dz)) { probe.reject("support-protected"); return null; }
|
||||
if (rock < bottom) supports.add(new Support(x + dx, rock + 1, z + dz, bottom));
|
||||
}
|
||||
// The square envelope and bearing grid have already been certified. A difficult front slope
|
||||
// should try another real facade before rejecting the entire building; no terrain is weakened.
|
||||
for (int turn : new int[]{0, 1, 3, 2}) {
|
||||
int facing = (candidate.facing() + turn) & 3;
|
||||
int ex = facing == 1 ? x + 17 : facing == 3 ? x - 1 : x + 8;
|
||||
int ez = facing == 2 ? z + 17 : facing == 0 ? z - 1 : z + 8;
|
||||
Entry entry = new Entry(ex, floor + 1, ez, facing);
|
||||
Site result = new Site(id(kind), kind, x, floor, z, 17, 17, height, facing,
|
||||
mix(seed ^ candidate.seed()), foundation, supports, entry);
|
||||
var landings = landing(result.entry(), previous, probe);
|
||||
if (landings == null) { probe.reject("entry-landing"); continue; }
|
||||
boolean collision = false;
|
||||
for (var cell : landings) {
|
||||
OwnedCell old = existing.get(key(cell.x(), cell.z()));
|
||||
if (old != null && old.cell().floorY() != cell.floorY()) { collision = true; break; }
|
||||
}
|
||||
if (collision) { probe.reject("landing-collision"); continue; }
|
||||
if (turn != 0) probe.reject("admitted-other-facade");
|
||||
return new SiteDraft(result, landings);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int height(Kind kind) {
|
||||
return switch (kind) {
|
||||
case FREIGHT_STATION, POST_OFFICE, RAIL_WORKSHOP, SUBSTATION, GREENHOUSE, PHOTO_CLUB -> 10;
|
||||
case EXPEDITION_OFFICE, IMPOSSIBLE_SERVICE -> 12;
|
||||
case COMMUNAL_DEPOT, VOID_DOCK, CONSTRUCTION_REFUGE -> 14;
|
||||
case PUMP_STATION -> 18; case OBSERVATORY -> 20; case RADIO_RELAY -> 22;
|
||||
};
|
||||
}
|
||||
private static List<Request> requests(List<Site> sites, List<Landmark> landmarks) {
|
||||
var result = new LinkedHashMap<String, Request>();
|
||||
var endpoints = new ArrayList<Endpoint>();
|
||||
sites.forEach(s -> endpoints.add(new Endpoint(s.id(), s.entry(), false)));
|
||||
landmarks.forEach(l -> endpoints.add(new Endpoint(l.id(), l.entry(), true)));
|
||||
var freight = new HashSet<String>();
|
||||
for (var kind : new Kind[] {Kind.FREIGHT_STATION, Kind.COMMUNAL_DEPOT, Kind.RAIL_WORKSHOP, Kind.PUMP_STATION}) {
|
||||
var site = find(sites, kind); if (site != null) freight.add(site.id());
|
||||
}
|
||||
landmarks.stream().filter(l -> l.kind() == LandmarkKind.MINE).forEach(l -> freight.add(l.id()));
|
||||
for (var a : endpoints) {
|
||||
// Four alternatives per entrance tolerate one protected cut without a quadratic terrain search.
|
||||
var nearest = endpoints.stream().filter(b -> !a.id().equals(b.id()))
|
||||
.sorted(Comparator.comparingInt((Endpoint b) -> span(a.entry(), b.entry())).thenComparing(Endpoint::id)).limit(4).toList();
|
||||
for (var b : nearest) addRequest(result, a, b, freight.contains(a.id()) && freight.contains(b.id()) ? RouteKind.FREIGHT : RouteKind.PATH);
|
||||
}
|
||||
for (var pair : List.of(new Kind[]{Kind.FREIGHT_STATION, Kind.COMMUNAL_DEPOT},
|
||||
new Kind[]{Kind.FREIGHT_STATION, Kind.RAIL_WORKSHOP}, new Kind[]{Kind.COMMUNAL_DEPOT, Kind.PUMP_STATION})) {
|
||||
var a = find(sites, pair[0]); var b = find(sites, pair[1]);
|
||||
if (a != null && b != null) addRequest(result, new Endpoint(a.id(), a.entry(), false), new Endpoint(b.id(), b.entry(), false), RouteKind.FREIGHT);
|
||||
}
|
||||
return List.copyOf(result.values());
|
||||
}
|
||||
private static void addRequest(Map<String, Request> result, Endpoint a, Endpoint b, RouteKind kind) {
|
||||
if (a.id().compareTo(b.id()) > 0) { var swap = a; a = b; b = swap; }
|
||||
String key = a.id() + "__" + b.id(); var old = result.get(key);
|
||||
if (old == null || kind == RouteKind.FREIGHT) result.put(key, new Request("graph_" + key, a.id(), b.id(), kind));
|
||||
}
|
||||
private static String rejectionDelta(Map<String, Integer> before, Map<String, Integer> after, XZ obstacle) {
|
||||
var changes = new java.util.TreeMap<String, Integer>();
|
||||
after.forEach((key, count) -> { int difference = count - before.getOrDefault(key, 0); if (difference > 0) changes.put(key, difference); });
|
||||
return "; reasons=" + changes + (obstacle == null ? "" : "; last-column=" + obstacle.x() + "," + obstacle.z());
|
||||
}
|
||||
private static int isolatedSites(Request request, Set<String> siteIds, Set<String> addressed) {
|
||||
return (siteIds.contains(request.from()) && !addressed.contains(request.from()) ? 1 : 0)
|
||||
+ (siteIds.contains(request.to()) && !addressed.contains(request.to()) ? 1 : 0);
|
||||
}
|
||||
private static int isolatedAttempts(Request request, Set<String> siteIds, Set<String> addressed, Map<String, Integer> attempts) {
|
||||
int result = Integer.MAX_VALUE;
|
||||
for (var id : List.of(request.from(), request.to())) if (siteIds.contains(id) && !addressed.contains(id))
|
||||
result = Math.min(result, attempts.getOrDefault(id, 0));
|
||||
return result == Integer.MAX_VALUE ? 0 : result;
|
||||
}
|
||||
private static final class Components {
|
||||
final Map<String, String> parents = new HashMap<>();
|
||||
Components(Set<String> ids) { for (var id : ids) parents.put(id, id); }
|
||||
String root(String id) { String parent = parents.get(id); while (!parent.equals(id)) { id = parent; parent = parents.get(id); } return parent; }
|
||||
boolean same(String a, String b) { return root(a).equals(root(b)); }
|
||||
void join(String a, String b) { parents.put(root(b), root(a)); }
|
||||
}
|
||||
|
||||
/** Always persisted, including for an isolated destination: five steps return to natural terrain. */
|
||||
private static List<Cell> landing(Entry entry, List<Site> sites, Probe probe) {
|
||||
int facing = entry.facing(), floor = entry.y() - 1;
|
||||
int endX = entry.x() + DX[facing] * 4, endZ = entry.z() + DZ[facing] * 4;
|
||||
int target = probe.surface(endX, endZ);
|
||||
if (target < 14 || target > 350 || Math.abs(target - floor) > 3) return null;
|
||||
var result = new ArrayList<Cell>();
|
||||
for (int step = 0; step < 5; step++) {
|
||||
// Keep the first two rows flat for the facade and the first rail turn.
|
||||
int y = floor + (int) Math.round((target - floor) * Math.max(0, step - 1) / 3.0);
|
||||
for (int cross = -1; cross <= 1; cross++) {
|
||||
int x = entry.x() + DX[facing] * step + DZ[facing] * cross;
|
||||
int z = entry.z() + DZ[facing] * step - DX[facing] * cross;
|
||||
if (overlapsSites(x, z, x, z, sites)) return null;
|
||||
Cell cell = routeCell(x, y, z, Rail.NONE, false, probe);
|
||||
if (cell == null) return null;
|
||||
result.add(cell);
|
||||
if (step == 4) {
|
||||
int beyond = probe.surface(x + DX[facing], z + DZ[facing]);
|
||||
if (Math.abs(beyond - y) > 1 || !probe.solid(x + DX[facing], beyond, z + DZ[facing])) return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private static RouteDraft route(long seed, Request request, Endpoint from, Endpoint to, List<Site> sites,
|
||||
List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
|
||||
Entry a = from.entry(), b = to.entry();
|
||||
XZ start = new XZ(a.x(), a.z()), end = new XZ(b.x(), b.z());
|
||||
XZ outA = start.move(a.facing(), 6), outB = end.move(b.facing(), 6);
|
||||
int middleX = (outA.x() + outB.x()) / 2, middleZ = (outA.z() + outB.z()) / 2;
|
||||
var alternatives = List.of(
|
||||
List.of(outA, new XZ(outB.x(), outA.z()), outB),
|
||||
List.of(outA, new XZ(outA.x(), outB.z()), outB),
|
||||
List.of(outA, new XZ(middleX, outA.z()), new XZ(middleX, outB.z()), outB),
|
||||
List.of(outA, new XZ(outA.x(), middleZ), new XZ(outB.x(), middleZ), outB),
|
||||
List.of(outA, new XZ(outA.x(), middleZ - 16), new XZ(outB.x(), middleZ - 16), outB),
|
||||
List.of(outA, new XZ(middleX + 16, outA.z()), new XZ(middleX + 16, outB.z()), outB));
|
||||
for (var waypoints : alternatives) {
|
||||
var points = new ArrayList<XZ>(); points.add(start);
|
||||
boolean okay = true;
|
||||
for (var waypoint : waypoints) if (!line(points, waypoint)) { okay = false; break; }
|
||||
if (!okay || !line(points, end) || points.size() > 1024 || new HashSet<>(points).size() != points.size()) continue;
|
||||
RouteDraft result = profile(seed, request, from, to, points, sites, landmarks, existing, probe);
|
||||
if (result != null) return result;
|
||||
}
|
||||
return adaptive(seed, request, from, to, start, end, outA, outB, sites, landmarks, existing, probe);
|
||||
}
|
||||
|
||||
/** Adaptive fallback. The coarse graph only proposes an alignment; it grants no writing permission. */
|
||||
private static RouteDraft adaptive(long seed, Request request, Endpoint from, Endpoint to, XZ start, XZ end,
|
||||
XZ outA, XZ outB, List<Site> sites, List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
|
||||
int stepSize = span(from.entry(), to.entry()) >= 160 ? 8 : 4;
|
||||
int certificateReserve = Math.min(2_200, distance(outA, outB) * 3 + 80);
|
||||
var excluded = new HashSet<XZ>();
|
||||
for (int attempt = 0; attempt < 3; attempt++) {
|
||||
var open = new java.util.PriorityQueue<SearchNode>(Comparator.comparingDouble(SearchNode::priority)
|
||||
.thenComparingDouble(SearchNode::cost).thenComparingInt(n -> n.position().x()).thenComparingInt(n -> n.position().z()));
|
||||
var best = new HashMap<XZ, Double>(); var parents = new HashMap<XZ, XZ>(); var closed = new HashSet<XZ>();
|
||||
var terrainHeights = new HashMap<XZ, Integer>();
|
||||
int begin = coarseRoute(outA, sites, terrainHeights, probe);
|
||||
if (begin == INVALID) return null;
|
||||
open.add(new SearchNode(outA, 0, distance(outA, outB))); best.put(outA, 0.0);
|
||||
List<XZ> found = null;
|
||||
int searchColumns = Math.max(probe.columns.size() + 150, probe.columnLimit - certificateReserve);
|
||||
while (!open.isEmpty() && closed.size() < 500 && probe.columns.size() < searchColumns) {
|
||||
var current = open.remove(); XZ p = current.position();
|
||||
if (!closed.add(p) || current.cost() > best.getOrDefault(p, Double.POSITIVE_INFINITY)) continue;
|
||||
if (distance(p, outB) <= stepSize * 2) {
|
||||
var reversed = new ArrayList<XZ>(); XZ ancestor = p;
|
||||
while (ancestor != null) { reversed.add(ancestor); ancestor = parents.get(ancestor); }
|
||||
java.util.Collections.reverse(reversed);
|
||||
for (int order = 0; order < 2 && found == null; order++) {
|
||||
var candidate = new ArrayList<XZ>(); candidate.add(start); boolean valid = true;
|
||||
for (XZ step : reversed) if (!line(candidate, step)) { valid = false; break; }
|
||||
XZ turn = order == 0 ? new XZ(outB.x(), p.z()) : new XZ(p.x(), outB.z());
|
||||
if (valid && line(candidate, turn) && line(candidate, outB) && line(candidate, end)
|
||||
&& new HashSet<>(candidate).size() == candidate.size()) found = candidate;
|
||||
}
|
||||
if (found != null) break;
|
||||
}
|
||||
int top = terrainHeights.get(p); XZ parent = parents.get(p);
|
||||
for (int d = 0; d < 4; d++) {
|
||||
if (parent == null && d == ((from.entry().facing() + 2) & 3)) continue;
|
||||
XZ next = p.move(d, stepSize);
|
||||
if (closed.contains(next) || excluded.contains(next)
|
||||
|| Math.min(distance(next, outA), distance(next, outB)) > distance(outA, outB) + 64) continue;
|
||||
int height = coarseRoute(next, sites, terrainHeights, probe);
|
||||
if (height == INVALID || Math.abs(height - top) > stepSize + 3) continue;
|
||||
double turnCost = parent == null || parent.x() == next.x() || parent.z() == next.z() ? 0 : 1.5;
|
||||
double cost = current.cost() + stepSize + Math.abs(height - top) * 1.1 + turnCost;
|
||||
if (cost >= best.getOrDefault(next, Double.POSITIVE_INFINITY)) continue;
|
||||
best.put(next, cost); parents.put(next, p);
|
||||
open.add(new SearchNode(next, cost, cost + distance(next, outB) * 1.6));
|
||||
}
|
||||
}
|
||||
if (found == null) { probe.reject("adaptive-no-alignment"); return null; }
|
||||
RouteDraft certified = profile(seed, request, from, to, found, sites, landmarks, existing, probe);
|
||||
if (certified != null) { probe.reject("adaptive-admitted"); return certified; }
|
||||
if (probe.routeBlock == null) return null;
|
||||
// A failed exact corridor removes only nearby coarse nodes on the next proposal.
|
||||
XZ bad = probe.routeBlock;
|
||||
int gx = outA.x() + (int) Math.round((bad.x() - outA.x()) / (double) stepSize) * stepSize;
|
||||
int gz = outA.z() + (int) Math.round((bad.z() - outA.z()) / (double) stepSize) * stepSize;
|
||||
excluded.add(new XZ(gx, gz));
|
||||
for (int d = 0; d < 4; d++) excluded.add(new XZ(gx, gz).move(d, stepSize));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private record SearchNode(XZ position, double cost, double priority) {}
|
||||
private static int distance(XZ a, XZ b) { return Math.abs(a.x() - b.x()) + Math.abs(a.z() - b.z()); }
|
||||
private static int coarseRoute(XZ p, List<Site> sites, Map<XZ, Integer> cache, Probe probe) {
|
||||
Integer old = cache.get(p); if (old != null) return old;
|
||||
if (overlapsSites(p.x() - 2, p.z() - 2, p.x() + 2, p.z() + 2, sites)) { cache.put(p, INVALID); return INVALID; }
|
||||
int top = probe.surface(p.x(), p.z());
|
||||
if (top < 14 || top > 350 || !probe.solid(p.x(), top, p.z()) || probe.protectedAt(p.x(), top, p.z())) {
|
||||
cache.put(p, INVALID); return INVALID;
|
||||
}
|
||||
for (int d = 0; d < 4; d++) {
|
||||
XZ side = p.move(d, 1); int sideTop = probe.surface(side.x(), side.z());
|
||||
if (sideTop < 14 || sideTop > 350 || Math.abs(sideTop - top) > MAX_ROUTE_CUT + 1 || probe.protectedAt(side.x(), sideTop, side.z())) {
|
||||
cache.put(p, INVALID); return INVALID;
|
||||
}
|
||||
}
|
||||
cache.put(p, top); return top;
|
||||
}
|
||||
|
||||
/** Finite orthogonal alignments, each certified at full block resolution before publication. */
|
||||
private static boolean line(List<XZ> points, XZ destination) {
|
||||
XZ p = points.getLast();
|
||||
if (p.x() != destination.x() && p.z() != destination.z()) return false;
|
||||
int dx = Integer.signum(destination.x() - p.x()), dz = Integer.signum(destination.z() - p.z());
|
||||
while (!p.equals(destination)) {
|
||||
p = new XZ(p.x() + dx, p.z() + dz); points.add(p);
|
||||
if (points.size() > 1024) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static RouteDraft profile(long seed, Request request, Endpoint from, Endpoint to, List<XZ> points,
|
||||
List<Site> sites, List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
|
||||
int size = points.size(); probe.routeBlock = null;
|
||||
int[] lo = new int[size], hi = new int[size], desired = new int[size], group = new int[size];
|
||||
boolean[] flat = new boolean[size - 1];
|
||||
for (int i = 1; i < size - 1; i++) if (turn(points, i)) { flat[i - 1] = true; flat[i] = true; }
|
||||
for (int i = 0; i < size; i++) {
|
||||
XZ point = points.get(i); probe.routeBlock = point; lo[i] = 14; hi[i] = 350;
|
||||
desired[i] = probe.surface(point.x(), point.z());
|
||||
boolean readOnly = i == 0 && from.readOnly() || i == size - 1 && to.readOnly();
|
||||
for (XZ column : strip(points, i)) {
|
||||
if (overlapsSites(column.x(), column.z(), column.x(), column.z(), sites)) { probe.reject("route-building"); return null; }
|
||||
int top = probe.surface(column.x(), column.z());
|
||||
if (top < 14 || top > 350 || !probe.solid(column.x(), top, column.z())) { probe.reject("route-dry-ground"); return null; }
|
||||
lo[i] = Math.max(lo[i], top - MAX_ROUTE_CUT); hi[i] = Math.min(hi[i], top + MAX_ROUTE_FILL);
|
||||
OwnedCell old = existing.get(key(column.x(), column.z()));
|
||||
if (old != null) { lo[i] = Math.max(lo[i], old.cell().floorY()); hi[i] = Math.min(hi[i], old.cell().floorY()); }
|
||||
if (readOnly && top != (i == 0 ? from.entry().y() : to.entry().y()) - 1) return null;
|
||||
}
|
||||
if (i == 0 || i == size - 1) {
|
||||
int exact = (i == 0 ? from.entry().y() : to.entry().y()) - 1;
|
||||
lo[i] = Math.max(lo[i], exact); hi[i] = Math.min(hi[i], exact);
|
||||
}
|
||||
if (lo[i] > hi[i]) { probe.reject("route-grade-interval"); return null; }
|
||||
}
|
||||
// Curves and both adjacent rails share one elevation; a Minecraft curved rail cannot slope.
|
||||
int groups = 1;
|
||||
for (int i = 1; i < size; i++) { if (!flat[i - 1]) groups++; group[i] = groups - 1; }
|
||||
int[] gl = new int[groups], gh = new int[groups], gd = new int[groups], y = new int[groups];
|
||||
java.util.Arrays.fill(gl, 14); java.util.Arrays.fill(gh, 350);
|
||||
for (int i = 0; i < size; i++) { int g = group[i]; gl[g] = Math.max(gl[g], lo[i]); gh[g] = Math.min(gh[g], hi[i]); gd[g] = desired[i]; }
|
||||
for (int g = 1; g < groups; g++) { gl[g] = Math.max(gl[g], gl[g - 1] - 1); gh[g] = Math.min(gh[g], gh[g - 1] + 1); }
|
||||
for (int g = groups - 2; g >= 0; g--) { gl[g] = Math.max(gl[g], gl[g + 1] - 1); gh[g] = Math.min(gh[g], gh[g + 1] + 1); }
|
||||
for (int g = 0; g < groups; g++) {
|
||||
if (gl[g] > gh[g]) { probe.reject("route-grade"); return null; }
|
||||
int lower = g == 0 ? gl[g] : Math.max(gl[g], y[g - 1] - 1), upper = g == 0 ? gh[g] : Math.min(gh[g], y[g - 1] + 1);
|
||||
y[g] = Math.clamp(gd[g], lower, upper);
|
||||
}
|
||||
var path = new ArrayList<Point>(); var floors = new LinkedHashMap<Long, Integer>();
|
||||
for (int i = 0; i < size; i++) {
|
||||
XZ point = points.get(i); int floor = y[group[i]]; path.add(new Point(point.x(), floor, point.z()));
|
||||
if (i == 0 && from.readOnly() || i == size - 1 && to.readOnly()) continue;
|
||||
for (XZ column : strip(points, i)) {
|
||||
long key = key(column.x(), column.z()); Integer old = floors.putIfAbsent(key, floor);
|
||||
if (old != null && old != floor) { probe.reject("route-self-grade"); return null; }
|
||||
// Historical endpoints and their lateral landing are read-only, even at another nearby turn.
|
||||
for (var landmark : landmarks) if (landingColumn(landmark.entry(), column)) {
|
||||
boolean actualEnd = from.readOnly() && landmark.id().equals(from.id())
|
||||
|| to.readOnly() && landmark.id().equals(to.id());
|
||||
if (!actualEnd || floor != landmark.entry().y() - 1) { probe.reject("route-historical-entry"); return null; }
|
||||
floors.remove(key); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
var cells = new LinkedHashMap<Long, Cell>();
|
||||
for (var entry : floors.entrySet()) {
|
||||
int x = (int) (entry.getKey() >> 32), z = (int) (long) entry.getKey();
|
||||
probe.routeBlock = new XZ(x, z);
|
||||
Cell cell = routeCell(x, entry.getValue(), z, Rail.NONE, false, probe);
|
||||
if (cell == null) { probe.reject("route-column-certificate"); return null; }
|
||||
cells.put(entry.getKey(), cell);
|
||||
}
|
||||
if (request.kind() == RouteKind.FREIGHT) for (int i = 0; i < path.size(); i++) {
|
||||
Point point = path.get(i); long key = key(point.x(), point.z()); Cell cell = cells.get(key);
|
||||
if (cell == null) continue;
|
||||
Rail rail = rail(path, i);
|
||||
// A one-block valley cannot carry two ascending slopes on the same rail. Keep a repairable rail gap.
|
||||
boolean intact = rail != Rail.NONE && Math.floorMod(mix(seed ^ key ^ request.id().hashCode()), 19) != 0;
|
||||
if (rail == Rail.NONE) rail = points.get(Math.max(0, i - 1)).x() == points.get(Math.min(size - 1, i + 1)).x() ? Rail.NORTH_SOUTH : Rail.EAST_WEST;
|
||||
cells.put(key, new Cell(cell.x(), cell.floorY(), cell.z(), cell.foundationDepth(), rail, intact));
|
||||
}
|
||||
return new RouteDraft(List.copyOf(path), cells);
|
||||
}
|
||||
|
||||
private static boolean landingColumn(Entry entry, XZ column) {
|
||||
int dx = column.x() - entry.x(), dz = column.z() - entry.z();
|
||||
return DX[entry.facing()] * dx + DZ[entry.facing()] * dz == 0 && Math.abs(dx) + Math.abs(dz) <= 1;
|
||||
}
|
||||
private static List<XZ> strip(List<XZ> points, int i) {
|
||||
XZ p = points.get(i); var result = new ArrayList<XZ>();
|
||||
if (turn(points, i)) {
|
||||
for (int dx = -1; dx <= 1; dx++) for (int dz = -1; dz <= 1; dz++) result.add(new XZ(p.x() + dx, p.z() + dz));
|
||||
} else {
|
||||
XZ other = points.get(i == points.size() - 1 ? i - 1 : i + 1);
|
||||
int dx = Integer.signum(other.x() - p.x()), dz = Integer.signum(other.z() - p.z());
|
||||
for (int cross = -1; cross <= 1; cross++) result.add(new XZ(p.x() + dz * cross, p.z() - dx * cross));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private static boolean turn(List<XZ> points, int i) {
|
||||
if (i == 0 || i == points.size() - 1) return false;
|
||||
XZ a = points.get(i - 1), b = points.get(i + 1);
|
||||
return a.x() != b.x() && a.z() != b.z();
|
||||
}
|
||||
private static Rail rail(List<Point> path, int i) {
|
||||
Point p = path.get(i), a = path.get(Math.max(0, i - 1)), b = path.get(Math.min(path.size() - 1, i + 1));
|
||||
if (a.floorY() > p.floorY() && b.floorY() > p.floorY()) return Rail.NONE;
|
||||
Point high = a.floorY() > p.floorY() ? a : b.floorY() > p.floorY() ? b : null;
|
||||
if (high != null) return switch (direction(p.x(), p.z(), high.x(), high.z())) {
|
||||
case 0 -> Rail.ASCENDING_NORTH; case 1 -> Rail.ASCENDING_EAST; case 2 -> Rail.ASCENDING_SOUTH; default -> Rail.ASCENDING_WEST;
|
||||
};
|
||||
if (a.x() == b.x()) return Rail.NORTH_SOUTH;
|
||||
if (a.z() == b.z()) return Rail.EAST_WEST;
|
||||
boolean n = a.z() < p.z() || b.z() < p.z(), s = a.z() > p.z() || b.z() > p.z();
|
||||
boolean e = a.x() > p.x() || b.x() > p.x();
|
||||
return n ? e ? Rail.NORTH_EAST : Rail.NORTH_WEST : s && e ? Rail.SOUTH_EAST : Rail.SOUTH_WEST;
|
||||
}
|
||||
private static Cell routeCell(int x, int floor, int z, Rail rail, boolean present, Probe probe) {
|
||||
int top = probe.surface(x, z);
|
||||
if (floor < 14 || floor > 350 || top < floor - MAX_ROUTE_FILL || top > floor + MAX_ROUTE_CUT) return null;
|
||||
int foundation = Math.max(1, floor - top + 1), base = floor - foundation;
|
||||
if (!probe.solid(x, base - 1, z) || !probe.solid(x, base, z)) return null;
|
||||
for (int y = base - 1; y <= floor + 3; y++) if (probe.protectedAt(x, y, z)) return null;
|
||||
return new Cell(x, floor, z, foundation, rail, present);
|
||||
}
|
||||
private record XZ(int x, int z) { XZ move(int facing, int distance) { return new XZ(x + DX[facing] * distance, z + DZ[facing] * distance); } }
|
||||
private record SegmentKey(Request request, int cx, int cz) {}
|
||||
private static List<Segment> segments(long seed, Map<Long, OwnedCell> cells) {
|
||||
var groups = new LinkedHashMap<SegmentKey, List<Cell>>();
|
||||
for (var owned : cells.values()) {
|
||||
Cell c = owned.cell(); var key = new SegmentKey(owned.request(), c.x() >> 4, c.z() >> 4);
|
||||
groups.computeIfAbsent(key, ignored -> new ArrayList<>()).add(c);
|
||||
}
|
||||
var result = new ArrayList<Segment>();
|
||||
for (var entry : groups.entrySet()) {
|
||||
var k = entry.getKey(); var r = k.request();
|
||||
// IDs are stable, bounded and independent of native generation order.
|
||||
String id = "piece_" + r.id() + "_" + k.cx() + "_" + k.cz();
|
||||
result.add(new Segment(id, r.from(), r.to(), r.kind(), k.cx(), k.cz(), entry.getValue(), mix(seed ^ key(k.cx(), k.cz()) ^ r.id().hashCode())));
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
|
||||
private static int span(Entry a, Entry b) { return Math.max(Math.abs(a.x() - b.x()), Math.abs(a.z() - b.z())); }
|
||||
private static Site find(List<Site> sites, Kind kind) { return sites.stream().filter(site -> site.kind() == kind).findFirst().orElse(null); }
|
||||
private static String id(Kind kind) { return kind.name().toLowerCase(Locale.ROOT); }
|
||||
private static int direction(int x, int z, int tx, int tz) { return Math.abs(tx - x) >= Math.abs(tz - z) ? tx > x ? 1 : 3 : tz > z ? 2 : 0; }
|
||||
private static boolean overlapsSites(int x0, int z0, int x1, int z1, List<Site> sites) {
|
||||
for (var site : sites) if (x0 <= site.maxX() && x1 >= site.minX() && z0 <= site.maxZ() && z1 >= site.minZ()) return true;
|
||||
return false;
|
||||
}
|
||||
private record SiteDraft(Site site, List<Cell> landings) {}
|
||||
private record Candidate(int x, int z, int facing, int score, long seed) {}
|
||||
private record Endpoint(String id, Entry entry, boolean readOnly) {}
|
||||
private record Request(String id, String from, String to, RouteKind kind) {}
|
||||
private record OwnedCell(Request request, Cell cell) {}
|
||||
private record RouteDraft(List<Point> path, Map<Long, Cell> cells) {}
|
||||
private static final class BudgetExceeded extends RuntimeException {
|
||||
final boolean columns; BudgetExceeded(boolean columns) { this.columns = columns; }
|
||||
}
|
||||
private static final class Probe {
|
||||
final Terrain terrain; final int cx, cz; final double radius;
|
||||
final Map<Long, Integer> columns = new HashMap<>();
|
||||
XZ routeBlock; Map<String, Integer> reasons = new HashMap<>(); int samples, columnLimit, sampleLimit;
|
||||
Probe(Terrain terrain, int cx, int cz, int diameter) { this.terrain = java.util.Objects.requireNonNull(terrain); this.cx = cx; this.cz = cz; radius = diameter / 2.0 - 2; }
|
||||
int surface(int x, int z) {
|
||||
if (Math.hypot((double) x - cx, (double) z - cz) > radius || !terrain.owns(x, z)) return INVALID;
|
||||
long key = key(x, z); var cached = columns.get(key); if (cached != null) return cached;
|
||||
if (columns.size() >= columnLimit) throw new BudgetExceeded(true);
|
||||
count(); int top = terrain.surfaceY(x, z); columns.put(key, top); return top;
|
||||
}
|
||||
boolean solid(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) == INVALID) return false; count(); return terrain.solid(x, y, z); }
|
||||
boolean protectedAt(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) == INVALID) return true; count(); return terrain.protectedAt(x, y, z); }
|
||||
void count() { if (samples >= sampleLimit) throw new BudgetExceeded(false); samples++; }
|
||||
void reject(String reason) { reasons.merge(reason, 1, Integer::sum); }
|
||||
}
|
||||
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
|
||||
private static double unit(long value) { return (value >>> 11) * 0x1.0p-53; }
|
||||
private static long mix(long value) { value = (value ^ value >>> 30) * 0xbf58476d1ce4e5b9L; value = (value ^ value >>> 27) * 0x94d049bb133111ebL; return value ^ value >>> 31; }
|
||||
}
|
||||
+612
@@ -0,0 +1,612 @@
|
||||
package fr.koka.sanctuary.worldgen.heritage;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.*;
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritagePlanner21.Terrain;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/** Generation22 only. A finite catalogue is admitted before complete, repairable routes are published.
|
||||
* Everything samples native columns; no chunk, player object or future continent is requested. */
|
||||
public final class HeritagePlanner22 {
|
||||
public static final int MAX_COLUMNS = HeritagePlan19.MAX_COLUMNS, MAX_SAMPLES = HeritagePlan19.MAX_SAMPLES;
|
||||
// Prefer an existing platform: at most two blocks of cut/fill and four of total relief.
|
||||
public static final int MAX_SITE_CUT = 2, MAX_SITE_FILL = 2, MAX_SITE_RELIEF = 4;
|
||||
public static final int MAX_ROUTE_CUT = 1, MAX_ROUTE_FILL = 1;
|
||||
private static final int SITE_COLUMNS = 11_000, SITE_SAMPLES = 540_000;
|
||||
private static final int INVALID = Integer.MIN_VALUE;
|
||||
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
|
||||
private HeritagePlanner22() {}
|
||||
|
||||
public static HeritagePlan19 plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
|
||||
if (!List.of(512, 724, 1024).contains(diameter)) throw new IllegalArgumentException("Heritage22 requires a starter size");
|
||||
var probe = new Probe(terrain, cx, cz, diameter);
|
||||
List<Landmark> landmarks = List.copyOf(terrain.landmarks()).stream().limit(16).toList();
|
||||
var sites = new ArrayList<Site>(); var cells = new LinkedHashMap<Long, OwnedCell>(); var diagnostics = new EnumMap<Kind, String>(Kind.class);
|
||||
for (var kind : Kind.values()) {
|
||||
int remaining = Kind.values().length - kind.ordinal() - 1;
|
||||
probe.columnLimit = SITE_COLUMNS - remaining * (SITE_COLUMNS / 14);
|
||||
probe.sampleLimit = SITE_SAMPLES - remaining * (SITE_SAMPLES / 14);
|
||||
SiteDraft admitted = null; int attempts = 0;
|
||||
var reasons = new java.util.TreeMap<String, Integer>(); probe.reasons = reasons;
|
||||
try {
|
||||
for (var candidate : candidates(seed, kind, cx, cz, diameter, sites, landmarks, probe)) {
|
||||
attempts++;
|
||||
admitted = site(seed, kind, candidate, sites, landmarks, cells, probe);
|
||||
if (admitted != null) break;
|
||||
}
|
||||
} catch (BudgetExceeded exhausted) { reasons.merge(exhausted.columns ? "column-budget" : "probe-budget", 1, Integer::sum); }
|
||||
if (admitted != null) {
|
||||
sites.add(admitted.site());
|
||||
Request landing = new Request(admitted.site().id() + "_landing", admitted.site().id(), admitted.site().id(), RouteKind.PATH);
|
||||
for (var cell : admitted.landings()) cells.put(key(cell.x(), cell.z()), new OwnedCell(landing, cell));
|
||||
}
|
||||
diagnostics.put(kind, (admitted == null ? "not admitted" : "admitted") + "; candidates=" + attempts + "; " + reasons);
|
||||
}
|
||||
probe.columnLimit = MAX_COLUMNS; probe.sampleLimit = MAX_SAMPLES;
|
||||
var links = new ArrayList<Link>();
|
||||
var routeResults = new LinkedHashMap<String, String>();
|
||||
var routeReasons = new java.util.TreeMap<String, Integer>(); probe.reasons = routeReasons;
|
||||
Map<String, Endpoint> endpoints = new LinkedHashMap<>();
|
||||
for (var site : sites) endpoints.put(site.id(), new Endpoint(site.id(), site.entry(), false));
|
||||
for (var landmark : landmarks) if (!endpoints.containsKey(landmark.id()))
|
||||
endpoints.put(landmark.id(), new Endpoint(landmark.id(), landmark.entry(), true));
|
||||
var requests = new ArrayList<>(requests(sites, landmarks).stream()
|
||||
.filter(r -> endpoints.containsKey(r.from()) && endpoints.containsKey(r.to())).toList());
|
||||
var components = new Components(endpoints.keySet());
|
||||
// Multiple exterior entries into the certified connected city represent one already walkable destination.
|
||||
String firstCity = null;
|
||||
for (var landmark : landmarks) if (landmark.kind() == LandmarkKind.CITY) {
|
||||
if (firstCity == null) firstCity = landmark.id(); else components.join(firstCity, landmark.id());
|
||||
}
|
||||
Request longFreight = requests.stream().filter(r -> r.kind() == RouteKind.FREIGHT
|
||||
&& span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry()) >= diameter / 4)
|
||||
.max(Comparator.comparingInt(r -> span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry()))).orElse(null);
|
||||
var siteIds = new HashSet<String>(); sites.forEach(site -> siteIds.add(site.id()));
|
||||
var addressed = new HashSet<String>();
|
||||
var destinationAttempts = new HashMap<String, Integer>();
|
||||
int attempted = 0;
|
||||
while (!requests.isEmpty() && attempted < 36 && links.size() < 24) {
|
||||
// Give each isolated site a first useful route before spending its columns on a cycle.
|
||||
// Repeated city gates already belong to one walkable component; no redundant freight bypass is needed.
|
||||
requests.removeIf(r -> components.same(r.from(), r.to()));
|
||||
if (requests.isEmpty()) break;
|
||||
requests.sort(Comparator.comparingInt((Request r) -> isolatedSites(r, siteIds, addressed) > 0
|
||||
&& isolatedAttempts(r, siteIds, addressed, destinationAttempts) == 0 ? 0 : 1)
|
||||
.thenComparingInt(r -> span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry()))
|
||||
.thenComparingInt(r -> r.kind() == RouteKind.FREIGHT ? 0 : 1).thenComparing(Request::id));
|
||||
var request = attempted == 0 && longFreight != null ? longFreight : requests.getFirst(); requests.remove(request);
|
||||
attempted++;
|
||||
int distance = Math.abs(endpoints.get(request.from()).entry().x() - endpoints.get(request.to()).entry().x())
|
||||
+ Math.abs(endpoints.get(request.from()).entry().z() - endpoints.get(request.to()).entry().z());
|
||||
boolean longAxis = span(endpoints.get(request.from()).entry(), endpoints.get(request.to()).entry()) >= diameter / 4;
|
||||
int isolated = isolatedSites(request, siteIds, addressed);
|
||||
var waiting = new HashSet<String>();
|
||||
for (var next : requests) for (var id : List.of(next.from(), next.to()))
|
||||
if (siteIds.contains(id) && !addressed.contains(id) && !id.equals(request.from()) && !id.equals(request.to())) waiting.add(id);
|
||||
int reserve = Math.min(Math.min(waiting.size(), 8), 36 - attempted) * 100;
|
||||
int allowance = longAxis && attempted == 1 ? 3_800 : isolated > 0 ? Math.clamp(distance * 5 + 350, 900, 2_400) : 900;
|
||||
probe.columnLimit = Math.min(MAX_COLUMNS - reserve, probe.columns.size() + allowance);
|
||||
probe.sampleLimit = Math.min(MAX_SAMPLES - reserve * 20, probe.samples + (longAxis ? 150_000 : 80_000));
|
||||
for (var id : List.of(request.from(), request.to())) if (siteIds.contains(id) && !addressed.contains(id))
|
||||
destinationAttempts.merge(id, 1, Integer::sum);
|
||||
Endpoint from = endpoints.get(request.from()), to = endpoints.get(request.to());
|
||||
int beforeRouteColumns = probe.columns.size();
|
||||
var beforeReasons = new HashMap<>(routeReasons);
|
||||
try {
|
||||
RouteDraft route = route(seed, request, from, to, sites, landmarks, cells, probe);
|
||||
if (route == null) { probe.reject("no-complete-route"); routeResults.put(request.id(), "refused; columns=" + (probe.columns.size() - beforeRouteColumns) + rejectionDelta(beforeReasons, routeReasons, probe.routeBlock)); continue; }
|
||||
var groups = new HashSet<SegmentKey>();
|
||||
cells.values().forEach(c -> groups.add(new SegmentKey(c.request(), c.cell().x() >> 4, c.cell().z() >> 4)));
|
||||
route.cells().values().stream().filter(c -> !cells.containsKey(key(c.x(), c.z())))
|
||||
.forEach(c -> groups.add(new SegmentKey(request, c.x() >> 4, c.z() >> 4)));
|
||||
if (groups.size() > 512) { routeResults.put(request.id(), "refused; native-piece-budget"); continue; }
|
||||
links.add(new Link(request.id(), request.from(), request.to(), request.kind(), route.path()));
|
||||
components.join(request.from(), request.to());
|
||||
addressed.add(request.from()); addressed.add(request.to());
|
||||
routeResults.put(request.id(), "admitted; span=" + span(from.entry(), to.entry()) + "; length=" + route.path().size() + "; columns=" + (probe.columns.size() - beforeRouteColumns));
|
||||
for (var cell : route.cells().values()) {
|
||||
long key = key(cell.x(), cell.z());
|
||||
OwnedCell old = cells.get(key);
|
||||
if (old == null) cells.put(key, new OwnedCell(request, cell));
|
||||
else if (cell.rail() != Rail.NONE && old.cell().rail() == Rail.NONE)
|
||||
cells.put(key, new OwnedCell(old.request(), cell));
|
||||
else if (cell.rail() != Rail.NONE && old.cell().rail() != Rail.NONE && cell.rail() != old.cell().rail()) {
|
||||
// A missing switch is intentional; never emit a rail with contradictory connections.
|
||||
var c = old.cell(); cells.put(key, new OwnedCell(old.request(),
|
||||
new Cell(c.x(), c.floorY(), c.z(), c.foundationDepth(), c.rail(), false)));
|
||||
}
|
||||
}
|
||||
} catch (BudgetExceeded exhausted) { probe.reject(exhausted.columns ? "route-column-budget" : "route-probe-budget"); routeResults.put(request.id(), "budget; columns=" + (probe.columns.size() - beforeRouteColumns) + rejectionDelta(beforeReasons, routeReasons, probe.routeBlock)); }
|
||||
}
|
||||
for (var site : sites) {
|
||||
var incident = links.stream().filter(l -> l.fromId().equals(site.id()) || l.toId().equals(site.id())).map(Link::id).toList();
|
||||
var attempts = new LinkedHashMap<String, String>();
|
||||
routeResults.forEach((id, value) -> { if (id.contains(site.id())) attempts.put(id, value); });
|
||||
diagnostics.put(site.kind(), diagnostics.get(site.kind()) + "; connected=" + !incident.isEmpty()
|
||||
+ "; accepted-links=" + incident + "; route-attempts=" + attempts);
|
||||
}
|
||||
var segments = segments(seed, cells);
|
||||
if (Boolean.getBoolean("sanctuary.test.heritage22") || Boolean.getBoolean("sanctuary.heritage.plannerDiagnostics"))
|
||||
System.getLogger(HeritagePlanner22.class.getName()).log(System.Logger.Level.INFO,
|
||||
"Heritage22 seed=" + seed + " diameter=" + diameter + " sites=" + sites.stream().map(Site::kind).toList()
|
||||
+ " links=" + links.size() + " segments=" + segments.size() + " columns=" + probe.columns.size()
|
||||
+ " samples=" + probe.samples + " catalogue=" + diagnostics + " routes=" + routeReasons + " routeResults=" + routeResults);
|
||||
return new HeritagePlan19(sites, landmarks, links, segments, probe.samples, probe.columns.size(), diagnostics);
|
||||
}
|
||||
|
||||
private static List<Candidate> candidates(long seed, Kind kind, int cx, int cz, int diameter,
|
||||
List<Site> sites, List<Landmark> landmarks, Probe probe) {
|
||||
double angle = unit(mix(seed ^ 0x19ACEL)) * Math.PI * 2 + kind.ordinal() * 2.399963229728653;
|
||||
double radius = diameter * (kind == Kind.VOID_DOCK ? .40 : kind == Kind.OBSERVATORY || kind == Kind.RADIO_RELAY ? .31 : .22);
|
||||
int x = cx + (int) Math.round(Math.cos(angle) * radius), z = cz + (int) Math.round(Math.sin(angle) * radius);
|
||||
Kind parent = switch (kind) {
|
||||
case POST_OFFICE, COMMUNAL_DEPOT, RAIL_WORKSHOP -> Kind.FREIGHT_STATION;
|
||||
case EXPEDITION_OFFICE, PHOTO_CLUB -> Kind.POST_OFFICE;
|
||||
case SUBSTATION, CONSTRUCTION_REFUGE -> Kind.RAIL_WORKSHOP;
|
||||
case GREENHOUSE, IMPOSSIBLE_SERVICE -> Kind.PUMP_STATION;
|
||||
default -> null;
|
||||
};
|
||||
Site neighbor = parent == null ? null : find(sites, parent);
|
||||
if (neighbor != null) {
|
||||
double local = unit(mix(seed ^ kind.ordinal() * 179L)) * Math.PI * 2;
|
||||
int distance = kind == Kind.RAIL_WORKSHOP ? 65 : 40;
|
||||
x = neighbor.centerX() + (int) Math.round(Math.cos(local) * distance);
|
||||
z = neighbor.centerZ() + (int) Math.round(Math.sin(local) * distance);
|
||||
}
|
||||
var candidates = new ArrayList<Candidate>();
|
||||
for (int i = 0; i < 24; i++) {
|
||||
double turn = angle + i * Math.PI / 3;
|
||||
int offset = i == 0 ? 0 : i < 5 ? 24 : i < 8 ? 48 : i < 12 ? 72 : i < 16 ? 96 : 128;
|
||||
int px = x + (int) Math.round(Math.cos(turn) * offset), pz = z + (int) Math.round(Math.sin(turn) * offset);
|
||||
if (Math.hypot((double) px - cx, (double) pz - cz) > diameter / 2.0 - 18) continue;
|
||||
if (overlapsSites(px - 15, pz - 15, px + 15, pz + 15, sites)) continue;
|
||||
int low = 384, high = -1; boolean dry = true;
|
||||
for (int dz : new int[] {-8, 0, 8}) for (int dx : new int[] {-8, 0, 8}) {
|
||||
int top = probe.surface(px + dx, pz + dz);
|
||||
if (top < 48 || top > 330 || !probe.solid(px + dx, top, pz + dz)
|
||||
|| probe.protectedAt(px + dx, top, pz + dz)) { dry = false; break; }
|
||||
low = Math.min(low, top); high = Math.max(high, top);
|
||||
}
|
||||
if (!dry || high - low > MAX_SITE_RELIEF) { probe.reject("coarse-terrain"); continue; }
|
||||
int facing = direction(px, pz, neighbor == null ? cx : neighbor.centerX(), neighbor == null ? cz : neighbor.centerZ());
|
||||
if (kind == Kind.VOID_DOCK) facing = (direction(px, pz, cx, cz) + 2) & 3;
|
||||
int score = (high - low) * 1_024 + i * 8 + (kind == Kind.OBSERVATORY || kind == Kind.RADIO_RELAY ? (300 - high) / 4 : 0);
|
||||
candidates.add(new Candidate(px - 8, pz - 8, facing, score, mix(seed ^ kind.ordinal() * 463L ^ i)));
|
||||
}
|
||||
candidates.sort(Comparator.comparingInt(Candidate::score).thenComparingLong(Candidate::seed));
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private static SiteDraft site(long seed, Kind kind, Candidate candidate, List<Site> previous,
|
||||
List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
|
||||
int x = candidate.x(), z = candidate.z(), low = 384, high = -1;
|
||||
for (var landmark : landmarks) if (landmark.entry().x() >= x - 2 && landmark.entry().x() <= x + 18
|
||||
&& landmark.entry().z() >= z - 2 && landmark.entry().z() <= z + 18) { probe.reject("existing-entry"); return null; }
|
||||
var heights = new ArrayList<Integer>();
|
||||
for (int dz = 0; dz < 17; dz++) for (int dx = 0; dx < 17; dx++) {
|
||||
int top = probe.surface(x + dx, z + dz);
|
||||
if (top < 48 || top > 330 || !probe.solid(x + dx, top, z + dz)) { probe.reject("dry-footprint"); return null; }
|
||||
heights.add(top); low = Math.min(low, top); high = Math.max(high, top);
|
||||
}
|
||||
int minimum = Math.max(48, high - MAX_SITE_CUT), maximum = Math.min(330, low + MAX_SITE_FILL);
|
||||
if (minimum > maximum || high - low > MAX_SITE_RELIEF) { probe.reject("site-slope"); return null; }
|
||||
heights.sort(Integer::compareTo);
|
||||
int floor = Math.clamp(heights.get(heights.size() / 2), minimum, maximum);
|
||||
int foundation = Math.max(3, floor - low + 1), height = height(kind);
|
||||
int base = floor - foundation;
|
||||
for (int dz = 0; dz < 17; dz++) for (int dx = 0; dx < 17; dx++)
|
||||
for (int y = base; y <= floor + height; y++) if (probe.protectedAt(x + dx, y, z + dz)) {
|
||||
probe.reject("site-protected-volume"); return null;
|
||||
}
|
||||
var supports = new ArrayList<Support>();
|
||||
// Every foundation column rests directly on intact rock. Do not catch a cliff
|
||||
// or a cavity with long piers just to retain this candidate.
|
||||
for (int dz = 0; dz < 17; dz++) for (int dx = 0; dx < 17; dx++) {
|
||||
if (!probe.solid(x + dx, base - 1, z + dz) || !probe.solid(x + dx, base - 2, z + dz)) {
|
||||
probe.reject("site-no-direct-bearing"); return null;
|
||||
}
|
||||
for (int y = base - 2; y < base; y++) if (probe.protectedAt(x + dx, y, z + dz)) {
|
||||
probe.reject("support-protected"); return null;
|
||||
}
|
||||
}
|
||||
// The square envelope and bearing grid have already been certified. A difficult front slope
|
||||
// should try another real facade before rejecting the entire building; no terrain is weakened.
|
||||
for (int turn : new int[]{0, 1, 3, 2}) {
|
||||
int facing = (candidate.facing() + turn) & 3;
|
||||
int ex = facing == 1 ? x + 17 : facing == 3 ? x - 1 : x + 8;
|
||||
int ez = facing == 2 ? z + 17 : facing == 0 ? z - 1 : z + 8;
|
||||
Entry entry = new Entry(ex, floor + 1, ez, facing);
|
||||
Site result = new Site(id(kind), kind, x, floor, z, 17, 17, height, facing,
|
||||
mix(seed ^ candidate.seed()), foundation, supports, entry);
|
||||
var landings = landing(result.entry(), previous, probe);
|
||||
if (landings == null) { probe.reject("entry-landing"); continue; }
|
||||
boolean collision = false;
|
||||
for (var cell : landings) {
|
||||
OwnedCell old = existing.get(key(cell.x(), cell.z()));
|
||||
if (old != null && old.cell().floorY() != cell.floorY()) { collision = true; break; }
|
||||
}
|
||||
if (collision) { probe.reject("landing-collision"); continue; }
|
||||
if (turn != 0) probe.reject("admitted-other-facade");
|
||||
return new SiteDraft(result, landings);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int height(Kind kind) {
|
||||
return switch (kind) {
|
||||
case FREIGHT_STATION, POST_OFFICE, RAIL_WORKSHOP, SUBSTATION, GREENHOUSE, PHOTO_CLUB -> 10;
|
||||
case EXPEDITION_OFFICE, IMPOSSIBLE_SERVICE -> 12;
|
||||
case COMMUNAL_DEPOT, VOID_DOCK, CONSTRUCTION_REFUGE -> 14;
|
||||
case PUMP_STATION -> 18; case OBSERVATORY -> 20; case RADIO_RELAY -> 22;
|
||||
};
|
||||
}
|
||||
private static List<Request> requests(List<Site> sites, List<Landmark> landmarks) {
|
||||
var result = new LinkedHashMap<String, Request>();
|
||||
var endpoints = new ArrayList<Endpoint>();
|
||||
sites.forEach(s -> endpoints.add(new Endpoint(s.id(), s.entry(), false)));
|
||||
landmarks.forEach(l -> endpoints.add(new Endpoint(l.id(), l.entry(), true)));
|
||||
var freight = new HashSet<String>();
|
||||
for (var kind : new Kind[] {Kind.FREIGHT_STATION, Kind.COMMUNAL_DEPOT, Kind.RAIL_WORKSHOP, Kind.PUMP_STATION}) {
|
||||
var site = find(sites, kind); if (site != null) freight.add(site.id());
|
||||
}
|
||||
landmarks.stream().filter(l -> l.kind() == LandmarkKind.MINE).forEach(l -> freight.add(l.id()));
|
||||
for (var a : endpoints) {
|
||||
// Four alternatives per entrance tolerate one protected cut without a quadratic terrain search.
|
||||
var nearest = endpoints.stream().filter(b -> !a.id().equals(b.id()))
|
||||
.sorted(Comparator.comparingInt((Endpoint b) -> span(a.entry(), b.entry())).thenComparing(Endpoint::id)).limit(4).toList();
|
||||
for (var b : nearest) addRequest(result, a, b, freight.contains(a.id()) && freight.contains(b.id()) ? RouteKind.FREIGHT : RouteKind.PATH);
|
||||
}
|
||||
for (var pair : List.of(new Kind[]{Kind.FREIGHT_STATION, Kind.COMMUNAL_DEPOT},
|
||||
new Kind[]{Kind.FREIGHT_STATION, Kind.RAIL_WORKSHOP}, new Kind[]{Kind.COMMUNAL_DEPOT, Kind.PUMP_STATION})) {
|
||||
var a = find(sites, pair[0]); var b = find(sites, pair[1]);
|
||||
if (a != null && b != null) addRequest(result, new Endpoint(a.id(), a.entry(), false), new Endpoint(b.id(), b.entry(), false), RouteKind.FREIGHT);
|
||||
}
|
||||
return List.copyOf(result.values());
|
||||
}
|
||||
private static void addRequest(Map<String, Request> result, Endpoint a, Endpoint b, RouteKind kind) {
|
||||
if (a.id().compareTo(b.id()) > 0) { var swap = a; a = b; b = swap; }
|
||||
String key = a.id() + "__" + b.id(); var old = result.get(key);
|
||||
if (old == null || kind == RouteKind.FREIGHT) result.put(key, new Request("graph_" + key, a.id(), b.id(), kind));
|
||||
}
|
||||
private static String rejectionDelta(Map<String, Integer> before, Map<String, Integer> after, XZ obstacle) {
|
||||
var changes = new java.util.TreeMap<String, Integer>();
|
||||
after.forEach((key, count) -> { int difference = count - before.getOrDefault(key, 0); if (difference > 0) changes.put(key, difference); });
|
||||
return "; reasons=" + changes + (obstacle == null ? "" : "; last-column=" + obstacle.x() + "," + obstacle.z());
|
||||
}
|
||||
private static int isolatedSites(Request request, Set<String> siteIds, Set<String> addressed) {
|
||||
return (siteIds.contains(request.from()) && !addressed.contains(request.from()) ? 1 : 0)
|
||||
+ (siteIds.contains(request.to()) && !addressed.contains(request.to()) ? 1 : 0);
|
||||
}
|
||||
private static int isolatedAttempts(Request request, Set<String> siteIds, Set<String> addressed, Map<String, Integer> attempts) {
|
||||
int result = Integer.MAX_VALUE;
|
||||
for (var id : List.of(request.from(), request.to())) if (siteIds.contains(id) && !addressed.contains(id))
|
||||
result = Math.min(result, attempts.getOrDefault(id, 0));
|
||||
return result == Integer.MAX_VALUE ? 0 : result;
|
||||
}
|
||||
private static final class Components {
|
||||
final Map<String, String> parents = new HashMap<>();
|
||||
Components(Set<String> ids) { for (var id : ids) parents.put(id, id); }
|
||||
String root(String id) { String parent = parents.get(id); while (!parent.equals(id)) { id = parent; parent = parents.get(id); } return parent; }
|
||||
boolean same(String a, String b) { return root(a).equals(root(b)); }
|
||||
void join(String a, String b) { parents.put(root(b), root(a)); }
|
||||
}
|
||||
|
||||
/** Always persisted, including for an isolated destination: five steps return to natural terrain. */
|
||||
private static List<Cell> landing(Entry entry, List<Site> sites, Probe probe) {
|
||||
int facing = entry.facing(), floor = entry.y() - 1;
|
||||
int endX = entry.x() + DX[facing] * 4, endZ = entry.z() + DZ[facing] * 4;
|
||||
int target = probe.surface(endX, endZ);
|
||||
if (target < 14 || target > 350 || Math.abs(target - floor) > 3) return null;
|
||||
var result = new ArrayList<Cell>();
|
||||
for (int step = 0; step < 5; step++) {
|
||||
// Keep the first two rows flat for the facade and the first rail turn.
|
||||
int y = floor + (int) Math.round((target - floor) * Math.max(0, step - 1) / 3.0);
|
||||
for (int cross = -1; cross <= 1; cross++) {
|
||||
int x = entry.x() + DX[facing] * step + DZ[facing] * cross;
|
||||
int z = entry.z() + DZ[facing] * step - DX[facing] * cross;
|
||||
if (overlapsSites(x, z, x, z, sites)) return null;
|
||||
Cell cell = routeCell(x, y, z, Rail.NONE, false, probe);
|
||||
if (cell == null) return null;
|
||||
result.add(cell);
|
||||
if (step == 4) {
|
||||
int beyond = probe.surface(x + DX[facing], z + DZ[facing]);
|
||||
if (Math.abs(beyond - y) > 1 || !probe.solid(x + DX[facing], beyond, z + DZ[facing])) return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private static RouteDraft route(long seed, Request request, Endpoint from, Endpoint to, List<Site> sites,
|
||||
List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
|
||||
Entry a = from.entry(), b = to.entry();
|
||||
XZ start = new XZ(a.x(), a.z()), end = new XZ(b.x(), b.z());
|
||||
XZ outA = start.move(a.facing(), 6), outB = end.move(b.facing(), 6);
|
||||
int middleX = (outA.x() + outB.x()) / 2, middleZ = (outA.z() + outB.z()) / 2;
|
||||
var alternatives = List.of(
|
||||
List.of(outA, new XZ(outB.x(), outA.z()), outB),
|
||||
List.of(outA, new XZ(outA.x(), outB.z()), outB),
|
||||
List.of(outA, new XZ(middleX, outA.z()), new XZ(middleX, outB.z()), outB),
|
||||
List.of(outA, new XZ(outA.x(), middleZ), new XZ(outB.x(), middleZ), outB),
|
||||
List.of(outA, new XZ(outA.x(), middleZ - 16), new XZ(outB.x(), middleZ - 16), outB),
|
||||
List.of(outA, new XZ(middleX + 16, outA.z()), new XZ(middleX + 16, outB.z()), outB));
|
||||
for (var waypoints : alternatives) {
|
||||
var points = new ArrayList<XZ>(); points.add(start);
|
||||
boolean okay = true;
|
||||
for (var waypoint : waypoints) if (!line(points, waypoint)) { okay = false; break; }
|
||||
if (!okay || !line(points, end) || points.size() > 1024 || new HashSet<>(points).size() != points.size()) continue;
|
||||
RouteDraft result = profile(seed, request, from, to, points, sites, landmarks, existing, probe);
|
||||
if (result != null) return result;
|
||||
}
|
||||
return adaptive(seed, request, from, to, start, end, outA, outB, sites, landmarks, existing, probe);
|
||||
}
|
||||
|
||||
/** Adaptive fallback. The coarse graph only proposes an alignment; it grants no writing permission. */
|
||||
private static RouteDraft adaptive(long seed, Request request, Endpoint from, Endpoint to, XZ start, XZ end,
|
||||
XZ outA, XZ outB, List<Site> sites, List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
|
||||
int stepSize = span(from.entry(), to.entry()) >= 160 ? 8 : 4;
|
||||
int certificateReserve = Math.min(2_200, distance(outA, outB) * 3 + 80);
|
||||
var excluded = new HashSet<XZ>();
|
||||
for (int attempt = 0; attempt < 3; attempt++) {
|
||||
var open = new java.util.PriorityQueue<SearchNode>(Comparator.comparingDouble(SearchNode::priority)
|
||||
.thenComparingDouble(SearchNode::cost).thenComparingInt(n -> n.position().x()).thenComparingInt(n -> n.position().z()));
|
||||
var best = new HashMap<XZ, Double>(); var parents = new HashMap<XZ, XZ>(); var closed = new HashSet<XZ>();
|
||||
var terrainHeights = new HashMap<XZ, Integer>();
|
||||
int begin = coarseRoute(outA, sites, terrainHeights, probe);
|
||||
if (begin == INVALID) return null;
|
||||
open.add(new SearchNode(outA, 0, distance(outA, outB))); best.put(outA, 0.0);
|
||||
List<XZ> found = null;
|
||||
int searchColumns = Math.max(probe.columns.size() + 150, probe.columnLimit - certificateReserve);
|
||||
while (!open.isEmpty() && closed.size() < 500 && probe.columns.size() < searchColumns) {
|
||||
var current = open.remove(); XZ p = current.position();
|
||||
if (!closed.add(p) || current.cost() > best.getOrDefault(p, Double.POSITIVE_INFINITY)) continue;
|
||||
if (distance(p, outB) <= stepSize * 2) {
|
||||
var reversed = new ArrayList<XZ>(); XZ ancestor = p;
|
||||
while (ancestor != null) { reversed.add(ancestor); ancestor = parents.get(ancestor); }
|
||||
java.util.Collections.reverse(reversed);
|
||||
for (int order = 0; order < 2 && found == null; order++) {
|
||||
var candidate = new ArrayList<XZ>(); candidate.add(start); boolean valid = true;
|
||||
for (XZ step : reversed) if (!line(candidate, step)) { valid = false; break; }
|
||||
XZ turn = order == 0 ? new XZ(outB.x(), p.z()) : new XZ(p.x(), outB.z());
|
||||
if (valid && line(candidate, turn) && line(candidate, outB) && line(candidate, end)
|
||||
&& new HashSet<>(candidate).size() == candidate.size()) found = candidate;
|
||||
}
|
||||
if (found != null) break;
|
||||
}
|
||||
int top = terrainHeights.get(p); XZ parent = parents.get(p);
|
||||
for (int d = 0; d < 4; d++) {
|
||||
if (parent == null && d == ((from.entry().facing() + 2) & 3)) continue;
|
||||
XZ next = p.move(d, stepSize);
|
||||
if (closed.contains(next) || excluded.contains(next)
|
||||
|| Math.min(distance(next, outA), distance(next, outB)) > distance(outA, outB) + 64) continue;
|
||||
int height = coarseRoute(next, sites, terrainHeights, probe);
|
||||
if (height == INVALID || Math.abs(height - top) > stepSize + 3) continue;
|
||||
double turnCost = parent == null || parent.x() == next.x() || parent.z() == next.z() ? 0 : 1.5;
|
||||
double cost = current.cost() + stepSize + Math.abs(height - top) * 1.1 + turnCost;
|
||||
if (cost >= best.getOrDefault(next, Double.POSITIVE_INFINITY)) continue;
|
||||
best.put(next, cost); parents.put(next, p);
|
||||
open.add(new SearchNode(next, cost, cost + distance(next, outB) * 1.6));
|
||||
}
|
||||
}
|
||||
if (found == null) { probe.reject("adaptive-no-alignment"); return null; }
|
||||
RouteDraft certified = profile(seed, request, from, to, found, sites, landmarks, existing, probe);
|
||||
if (certified != null) { probe.reject("adaptive-admitted"); return certified; }
|
||||
if (probe.routeBlock == null) return null;
|
||||
// A failed exact corridor removes only nearby coarse nodes on the next proposal.
|
||||
XZ bad = probe.routeBlock;
|
||||
int gx = outA.x() + (int) Math.round((bad.x() - outA.x()) / (double) stepSize) * stepSize;
|
||||
int gz = outA.z() + (int) Math.round((bad.z() - outA.z()) / (double) stepSize) * stepSize;
|
||||
excluded.add(new XZ(gx, gz));
|
||||
for (int d = 0; d < 4; d++) excluded.add(new XZ(gx, gz).move(d, stepSize));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private record SearchNode(XZ position, double cost, double priority) {}
|
||||
private static int distance(XZ a, XZ b) { return Math.abs(a.x() - b.x()) + Math.abs(a.z() - b.z()); }
|
||||
private static int coarseRoute(XZ p, List<Site> sites, Map<XZ, Integer> cache, Probe probe) {
|
||||
Integer old = cache.get(p); if (old != null) return old;
|
||||
if (overlapsSites(p.x() - 2, p.z() - 2, p.x() + 2, p.z() + 2, sites)) { cache.put(p, INVALID); return INVALID; }
|
||||
int top = probe.surface(p.x(), p.z());
|
||||
if (top < 14 || top > 350 || !probe.solid(p.x(), top, p.z()) || probe.protectedAt(p.x(), top, p.z())) {
|
||||
cache.put(p, INVALID); return INVALID;
|
||||
}
|
||||
for (int d = 0; d < 4; d++) {
|
||||
XZ side = p.move(d, 1); int sideTop = probe.surface(side.x(), side.z());
|
||||
if (sideTop < 14 || sideTop > 350 || Math.abs(sideTop - top) > MAX_ROUTE_CUT + 1 || probe.protectedAt(side.x(), sideTop, side.z())) {
|
||||
cache.put(p, INVALID); return INVALID;
|
||||
}
|
||||
}
|
||||
cache.put(p, top); return top;
|
||||
}
|
||||
|
||||
/** Finite orthogonal alignments, each certified at full block resolution before publication. */
|
||||
private static boolean line(List<XZ> points, XZ destination) {
|
||||
XZ p = points.getLast();
|
||||
if (p.x() != destination.x() && p.z() != destination.z()) return false;
|
||||
int dx = Integer.signum(destination.x() - p.x()), dz = Integer.signum(destination.z() - p.z());
|
||||
while (!p.equals(destination)) {
|
||||
p = new XZ(p.x() + dx, p.z() + dz); points.add(p);
|
||||
if (points.size() > 1024) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static RouteDraft profile(long seed, Request request, Endpoint from, Endpoint to, List<XZ> points,
|
||||
List<Site> sites, List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
|
||||
int size = points.size(); probe.routeBlock = null;
|
||||
int[] lo = new int[size], hi = new int[size], desired = new int[size], group = new int[size];
|
||||
boolean[] flat = new boolean[size - 1];
|
||||
for (int i = 1; i < size - 1; i++) if (turn(points, i)) { flat[i - 1] = true; flat[i] = true; }
|
||||
for (int i = 0; i < size; i++) {
|
||||
XZ point = points.get(i); probe.routeBlock = point; lo[i] = 14; hi[i] = 350;
|
||||
desired[i] = probe.surface(point.x(), point.z());
|
||||
boolean readOnly = i == 0 && from.readOnly() || i == size - 1 && to.readOnly();
|
||||
for (XZ column : strip(points, i)) {
|
||||
if (overlapsSites(column.x(), column.z(), column.x(), column.z(), sites)) { probe.reject("route-building"); return null; }
|
||||
int top = probe.surface(column.x(), column.z());
|
||||
if (top < 14 || top > 350 || !probe.solid(column.x(), top, column.z())) { probe.reject("route-dry-ground"); return null; }
|
||||
lo[i] = Math.max(lo[i], top - MAX_ROUTE_CUT); hi[i] = Math.min(hi[i], top + MAX_ROUTE_FILL);
|
||||
OwnedCell old = existing.get(key(column.x(), column.z()));
|
||||
if (old != null) { lo[i] = Math.max(lo[i], old.cell().floorY()); hi[i] = Math.min(hi[i], old.cell().floorY()); }
|
||||
if (readOnly && top != (i == 0 ? from.entry().y() : to.entry().y()) - 1) return null;
|
||||
}
|
||||
if (i == 0 || i == size - 1) {
|
||||
int exact = (i == 0 ? from.entry().y() : to.entry().y()) - 1;
|
||||
lo[i] = Math.max(lo[i], exact); hi[i] = Math.min(hi[i], exact);
|
||||
}
|
||||
if (lo[i] > hi[i]) { probe.reject("route-grade-interval"); return null; }
|
||||
}
|
||||
// Curves and both adjacent rails share one elevation; a Minecraft curved rail cannot slope.
|
||||
int groups = 1;
|
||||
for (int i = 1; i < size; i++) { if (!flat[i - 1]) groups++; group[i] = groups - 1; }
|
||||
int[] gl = new int[groups], gh = new int[groups], gd = new int[groups], y = new int[groups];
|
||||
java.util.Arrays.fill(gl, 14); java.util.Arrays.fill(gh, 350);
|
||||
for (int i = 0; i < size; i++) { int g = group[i]; gl[g] = Math.max(gl[g], lo[i]); gh[g] = Math.min(gh[g], hi[i]); gd[g] = desired[i]; }
|
||||
for (int g = 1; g < groups; g++) { gl[g] = Math.max(gl[g], gl[g - 1] - 1); gh[g] = Math.min(gh[g], gh[g - 1] + 1); }
|
||||
for (int g = groups - 2; g >= 0; g--) { gl[g] = Math.max(gl[g], gl[g + 1] - 1); gh[g] = Math.min(gh[g], gh[g + 1] + 1); }
|
||||
for (int g = 0; g < groups; g++) {
|
||||
if (gl[g] > gh[g]) { probe.reject("route-grade"); return null; }
|
||||
int lower = g == 0 ? gl[g] : Math.max(gl[g], y[g - 1] - 1), upper = g == 0 ? gh[g] : Math.min(gh[g], y[g - 1] + 1);
|
||||
y[g] = Math.clamp(gd[g], lower, upper);
|
||||
}
|
||||
var path = new ArrayList<Point>(); var floors = new LinkedHashMap<Long, Integer>();
|
||||
for (int i = 0; i < size; i++) {
|
||||
XZ point = points.get(i); int floor = y[group[i]]; path.add(new Point(point.x(), floor, point.z()));
|
||||
if (i == 0 && from.readOnly() || i == size - 1 && to.readOnly()) continue;
|
||||
for (XZ column : strip(points, i)) {
|
||||
long key = key(column.x(), column.z()); Integer old = floors.putIfAbsent(key, floor);
|
||||
if (old != null && old != floor) { probe.reject("route-self-grade"); return null; }
|
||||
// Historical endpoints and their lateral landing are read-only, even at another nearby turn.
|
||||
for (var landmark : landmarks) if (landingColumn(landmark.entry(), column)) {
|
||||
boolean actualEnd = from.readOnly() && landmark.id().equals(from.id())
|
||||
|| to.readOnly() && landmark.id().equals(to.id());
|
||||
if (!actualEnd || floor != landmark.entry().y() - 1) { probe.reject("route-historical-entry"); return null; }
|
||||
floors.remove(key); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
var cells = new LinkedHashMap<Long, Cell>();
|
||||
for (var entry : floors.entrySet()) {
|
||||
int x = (int) (entry.getKey() >> 32), z = (int) (long) entry.getKey();
|
||||
probe.routeBlock = new XZ(x, z);
|
||||
Cell cell = routeCell(x, entry.getValue(), z, Rail.NONE, false, probe);
|
||||
if (cell == null) { probe.reject("route-column-certificate"); return null; }
|
||||
cells.put(entry.getKey(), cell);
|
||||
}
|
||||
if (request.kind() == RouteKind.FREIGHT) for (int i = 0; i < path.size(); i++) {
|
||||
Point point = path.get(i); long key = key(point.x(), point.z()); Cell cell = cells.get(key);
|
||||
if (cell == null) continue;
|
||||
Rail rail = rail(path, i);
|
||||
// A one-block valley cannot carry two ascending slopes on the same rail. Keep a repairable rail gap.
|
||||
boolean intact = rail != Rail.NONE && Math.floorMod(mix(seed ^ key ^ request.id().hashCode()), 19) != 0;
|
||||
if (rail == Rail.NONE) rail = points.get(Math.max(0, i - 1)).x() == points.get(Math.min(size - 1, i + 1)).x() ? Rail.NORTH_SOUTH : Rail.EAST_WEST;
|
||||
cells.put(key, new Cell(cell.x(), cell.floorY(), cell.z(), cell.foundationDepth(), rail, intact));
|
||||
}
|
||||
return new RouteDraft(List.copyOf(path), cells);
|
||||
}
|
||||
|
||||
private static boolean landingColumn(Entry entry, XZ column) {
|
||||
int dx = column.x() - entry.x(), dz = column.z() - entry.z();
|
||||
return DX[entry.facing()] * dx + DZ[entry.facing()] * dz == 0 && Math.abs(dx) + Math.abs(dz) <= 1;
|
||||
}
|
||||
private static List<XZ> strip(List<XZ> points, int i) {
|
||||
XZ p = points.get(i); var result = new ArrayList<XZ>();
|
||||
if (turn(points, i)) {
|
||||
for (int dx = -1; dx <= 1; dx++) for (int dz = -1; dz <= 1; dz++) result.add(new XZ(p.x() + dx, p.z() + dz));
|
||||
} else {
|
||||
XZ other = points.get(i == points.size() - 1 ? i - 1 : i + 1);
|
||||
int dx = Integer.signum(other.x() - p.x()), dz = Integer.signum(other.z() - p.z());
|
||||
for (int cross = -1; cross <= 1; cross++) result.add(new XZ(p.x() + dz * cross, p.z() - dx * cross));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private static boolean turn(List<XZ> points, int i) {
|
||||
if (i == 0 || i == points.size() - 1) return false;
|
||||
XZ a = points.get(i - 1), b = points.get(i + 1);
|
||||
return a.x() != b.x() && a.z() != b.z();
|
||||
}
|
||||
private static Rail rail(List<Point> path, int i) {
|
||||
Point p = path.get(i), a = path.get(Math.max(0, i - 1)), b = path.get(Math.min(path.size() - 1, i + 1));
|
||||
if (a.floorY() > p.floorY() && b.floorY() > p.floorY()) return Rail.NONE;
|
||||
Point high = a.floorY() > p.floorY() ? a : b.floorY() > p.floorY() ? b : null;
|
||||
if (high != null) return switch (direction(p.x(), p.z(), high.x(), high.z())) {
|
||||
case 0 -> Rail.ASCENDING_NORTH; case 1 -> Rail.ASCENDING_EAST; case 2 -> Rail.ASCENDING_SOUTH; default -> Rail.ASCENDING_WEST;
|
||||
};
|
||||
if (a.x() == b.x()) return Rail.NORTH_SOUTH;
|
||||
if (a.z() == b.z()) return Rail.EAST_WEST;
|
||||
boolean n = a.z() < p.z() || b.z() < p.z(), s = a.z() > p.z() || b.z() > p.z();
|
||||
boolean e = a.x() > p.x() || b.x() > p.x();
|
||||
return n ? e ? Rail.NORTH_EAST : Rail.NORTH_WEST : s && e ? Rail.SOUTH_EAST : Rail.SOUTH_WEST;
|
||||
}
|
||||
private static Cell routeCell(int x, int floor, int z, Rail rail, boolean present, Probe probe) {
|
||||
int top = probe.surface(x, z);
|
||||
if (floor < 14 || floor > 350 || top < floor - MAX_ROUTE_FILL || top > floor + MAX_ROUTE_CUT) return null;
|
||||
int foundation = Math.max(1, floor - top + 1), base = floor - foundation;
|
||||
if (!probe.solid(x, base - 1, z) || !probe.solid(x, base, z)) return null;
|
||||
for (int y = base - 1; y <= floor + 3; y++) if (probe.protectedAt(x, y, z)) return null;
|
||||
return new Cell(x, floor, z, foundation, rail, present);
|
||||
}
|
||||
private record XZ(int x, int z) { XZ move(int facing, int distance) { return new XZ(x + DX[facing] * distance, z + DZ[facing] * distance); } }
|
||||
private record SegmentKey(Request request, int cx, int cz) {}
|
||||
private static List<Segment> segments(long seed, Map<Long, OwnedCell> cells) {
|
||||
var groups = new LinkedHashMap<SegmentKey, List<Cell>>();
|
||||
for (var owned : cells.values()) {
|
||||
Cell c = owned.cell(); var key = new SegmentKey(owned.request(), c.x() >> 4, c.z() >> 4);
|
||||
groups.computeIfAbsent(key, ignored -> new ArrayList<>()).add(c);
|
||||
}
|
||||
var result = new ArrayList<Segment>();
|
||||
for (var entry : groups.entrySet()) {
|
||||
var k = entry.getKey(); var r = k.request();
|
||||
// IDs are stable, bounded and independent of native generation order.
|
||||
String id = "piece_" + r.id() + "_" + k.cx() + "_" + k.cz();
|
||||
result.add(new Segment(id, r.from(), r.to(), r.kind(), k.cx(), k.cz(), entry.getValue(), mix(seed ^ key(k.cx(), k.cz()) ^ r.id().hashCode())));
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
|
||||
private static int span(Entry a, Entry b) { return Math.max(Math.abs(a.x() - b.x()), Math.abs(a.z() - b.z())); }
|
||||
private static Site find(List<Site> sites, Kind kind) { return sites.stream().filter(site -> site.kind() == kind).findFirst().orElse(null); }
|
||||
private static String id(Kind kind) { return kind.name().toLowerCase(Locale.ROOT); }
|
||||
private static int direction(int x, int z, int tx, int tz) { return Math.abs(tx - x) >= Math.abs(tz - z) ? tx > x ? 1 : 3 : tz > z ? 2 : 0; }
|
||||
private static boolean overlapsSites(int x0, int z0, int x1, int z1, List<Site> sites) {
|
||||
for (var site : sites) if (x0 <= site.maxX() && x1 >= site.minX() && z0 <= site.maxZ() && z1 >= site.minZ()) return true;
|
||||
return false;
|
||||
}
|
||||
private record SiteDraft(Site site, List<Cell> landings) {}
|
||||
private record Candidate(int x, int z, int facing, int score, long seed) {}
|
||||
private record Endpoint(String id, Entry entry, boolean readOnly) {}
|
||||
private record Request(String id, String from, String to, RouteKind kind) {}
|
||||
private record OwnedCell(Request request, Cell cell) {}
|
||||
private record RouteDraft(List<Point> path, Map<Long, Cell> cells) {}
|
||||
private static final class BudgetExceeded extends RuntimeException {
|
||||
final boolean columns; BudgetExceeded(boolean columns) { this.columns = columns; }
|
||||
}
|
||||
private static final class Probe {
|
||||
final Terrain terrain; final int cx, cz; final double radius;
|
||||
final Map<Long, Integer> columns = new HashMap<>();
|
||||
XZ routeBlock; Map<String, Integer> reasons = new HashMap<>(); int samples, columnLimit, sampleLimit;
|
||||
Probe(Terrain terrain, int cx, int cz, int diameter) { this.terrain = java.util.Objects.requireNonNull(terrain); this.cx = cx; this.cz = cz; radius = diameter / 2.0 - 2; }
|
||||
int surface(int x, int z) {
|
||||
if (Math.hypot((double) x - cx, (double) z - cz) > radius || !terrain.owns(x, z)) return INVALID;
|
||||
long key = key(x, z); var cached = columns.get(key); if (cached != null) return cached;
|
||||
if (columns.size() >= columnLimit) throw new BudgetExceeded(true);
|
||||
count(); int top = terrain.surfaceY(x, z); columns.put(key, top); return top;
|
||||
}
|
||||
boolean solid(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) == INVALID) return false; count(); return terrain.solid(x, y, z); }
|
||||
boolean protectedAt(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) == INVALID) return true; count(); return terrain.protectedAt(x, y, z); }
|
||||
void count() { if (samples >= sampleLimit) throw new BudgetExceeded(false); samples++; }
|
||||
void reject(String reason) { reasons.merge(reason, 1, Integer::sum); }
|
||||
}
|
||||
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
|
||||
private static double unit(long value) { return (value >>> 11) * 0x1.0p-53; }
|
||||
private static long mix(long value) { value = (value ^ value >>> 30) * 0xbf58476d1ce4e5b9L; value = (value ^ value >>> 27) * 0x94d049bb133111ebL; return value ^ value >>> 31; }
|
||||
}
|
||||
+361
@@ -0,0 +1,361 @@
|
||||
package fr.koka.sanctuary.worldgen.heritage;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityPalette;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.AttachFace;
|
||||
import net.minecraft.world.level.block.state.properties.BedPart;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.block.state.properties.DoorHingeSide;
|
||||
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
|
||||
import net.minecraft.world.level.block.state.properties.Half;
|
||||
import net.minecraft.world.level.block.state.properties.RailShape;
|
||||
|
||||
/** Fourteen original, physical generation19 places. The saved site controls all geometry and loot.
|
||||
* Site writes are local to minX/floorY/minZ; route writes are local to the segment's minimum bounds. */
|
||||
public final class HeritageRenderer19 {
|
||||
private HeritageRenderer19() {}
|
||||
public record Target(String name, int x, int y, int z) {}
|
||||
|
||||
/** These are already rotated local standing positions, not world coordinates. */
|
||||
public static List<Target> targets(HeritagePlan19.Site site) {
|
||||
var b = new Builder(site, null); b.layout(); return List.copyOf(b.targets);
|
||||
}
|
||||
public static void render(HeritagePlan19.Site site, LostCityRenderer.BlockWriter writer) {
|
||||
var b = new Builder(site, writer); b.ground(); b.layout();
|
||||
// Footings have absolute saved coordinates and must not undergo a second facade rotation.
|
||||
for (var support : site.supports()) for (int y = support.minY(); y <= support.maxY(); y++)
|
||||
writer.set(support.x() - site.minX(), y - site.floorY(), support.z() - site.minZ(),
|
||||
y == support.minY() ? Blocks.SMOOTH_STONE.defaultBlockState() : Blocks.STONE_BRICKS.defaultBlockState());
|
||||
}
|
||||
public static void renderSegment(HeritagePlan19.Segment segment, LostCityRenderer.BlockWriter writer) {
|
||||
int x0 = segment.minX(), y0 = segment.minY(), z0 = segment.minZ();
|
||||
for (var cell : segment.cells()) {
|
||||
int x = cell.x() - x0, z = cell.z() - z0, floor = cell.floorY() - y0;
|
||||
for (int y = cell.minY() - y0; y <= floor; y++) {
|
||||
Block material = Blocks.STONE;
|
||||
if (y == floor) material = segment.kind() == HeritagePlan19.RouteKind.FREIGHT
|
||||
? cell.rail() != HeritagePlan19.Rail.NONE && Math.floorMod(cell.x() + cell.z(), 3) == 0
|
||||
? Blocks.DARK_OAK_PLANKS : Blocks.GRAVEL
|
||||
: LostCityPalette.roll(segment.seed(), cell.x(), y, cell.z(), 4) == 0 ? Blocks.ANDESITE : Blocks.STONE;
|
||||
writer.set(x, y, z, material.defaultBlockState());
|
||||
}
|
||||
for (int y = floor + 1; y <= floor + 3; y++) writer.set(x, y, z, Blocks.AIR.defaultBlockState());
|
||||
if (cell.railPresent()) writer.set(x, floor + 1, z, Blocks.RAIL.defaultBlockState()
|
||||
.setValue(BlockStateProperties.RAIL_SHAPE, RailShape.valueOf(cell.rail().name())));
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Builder {
|
||||
final HeritagePlan19.Site s;
|
||||
final LostCityRenderer.BlockWriter out;
|
||||
final List<Target> targets = new ArrayList<>();
|
||||
final Rotation rotation;
|
||||
Builder(HeritagePlan19.Site s, LostCityRenderer.BlockWriter out) {
|
||||
this.s = s; this.out = out;
|
||||
rotation = switch (s.facing()) { case 1 -> Rotation.CLOCKWISE_90; case 2 -> Rotation.CLOCKWISE_180;
|
||||
case 3 -> Rotation.COUNTERCLOCKWISE_90; default -> Rotation.NONE; };
|
||||
}
|
||||
int rx(int x, int z) { return switch (s.facing()) { case 1 -> 16 - z; case 2 -> 16 - x; case 3 -> z; default -> x; }; }
|
||||
int rz(int x, int z) { return switch (s.facing()) { case 1 -> x; case 2 -> 16 - z; case 3 -> 16 - x; default -> z; }; }
|
||||
void set(int x, int y, int z, BlockState state) {
|
||||
if (x < 0 || x > 16 || z < 0 || z > 16 || y < -s.foundationDepth() || y > s.height())
|
||||
throw new IllegalArgumentException("Heritage " + s.kind() + " renderer escaped saved site: " + x + "/" + y + "/" + z);
|
||||
if (out != null) out.set(rx(x, z), y, rz(x, z), state.rotate(rotation));
|
||||
}
|
||||
void set(int x, int y, int z, Block b) { set(x, y, z, b.defaultBlockState()); }
|
||||
void box(int x0, int y0, int z0, int x1, int y1, int z1, Block b) {
|
||||
for (int y = y0; y <= y1; y++) for (int z = z0; z <= z1; z++) for (int x = x0; x <= x1; x++) set(x, y, z, b);
|
||||
}
|
||||
void target(String name, int x, int y, int z) { targets.add(new Target(name, rx(x, z), y, rz(x, z))); }
|
||||
void ground() {
|
||||
for (int y = -s.foundationDepth(); y <= s.height(); y++) for (int z = 0; z <= 16; z++) for (int x = 0; x <= 16; x++)
|
||||
set(x, y, z, y < 0 ? Blocks.STONE.defaultBlockState() : y == 0
|
||||
? LostCityPalette.pavement(s.seed(), x, z) : Blocks.AIR.defaultBlockState());
|
||||
}
|
||||
void layout() {
|
||||
target("main_entry", 8, 1, 0);
|
||||
switch (s.kind()) {
|
||||
case FREIGHT_STATION -> station();
|
||||
case POST_OFFICE -> post();
|
||||
case COMMUNAL_DEPOT -> depot();
|
||||
case EXPEDITION_OFFICE -> expedition();
|
||||
case OBSERVATORY -> observatory();
|
||||
case RAIL_WORKSHOP -> railway();
|
||||
case PUMP_STATION -> pumping();
|
||||
case SUBSTATION -> substation();
|
||||
case GREENHOUSE -> greenhouse();
|
||||
case VOID_DOCK -> dock();
|
||||
case RADIO_RELAY -> radio();
|
||||
case PHOTO_CLUB -> photo();
|
||||
case IMPOSSIBLE_SERVICE -> impossible();
|
||||
case CONSTRUCTION_REFUGE -> refuge();
|
||||
}
|
||||
}
|
||||
void shell(int x0, int floor, int z0, int x1, int ceiling, int z1) {
|
||||
for (int y = floor; y <= ceiling; y++) for (int z = z0; z <= z1; z++) for (int x = x0; x <= x1; x++) {
|
||||
boolean wall = x == x0 || x == x1 || z == z0 || z == z1;
|
||||
set(x, y, z, y == floor || y == ceiling ? Blocks.SMOOTH_STONE.defaultBlockState()
|
||||
: wall ? LostCityPalette.masonry(s.seed(), x, y, z) : Blocks.AIR.defaultBlockState());
|
||||
}
|
||||
}
|
||||
void gable(int x0, int z0, int x1, int z1, int y) {
|
||||
int rise = Math.min(3, (x1 - x0) / 2);
|
||||
for (int step = 0; step <= rise; step++)
|
||||
box(x0 + step, y + step, z0, x1 - step, y + step, z1, step == rise ? Blocks.STONE_BRICK_SLAB : Blocks.STONE_BRICKS);
|
||||
}
|
||||
void door(int x, int y, int z, Direction facing) { door(x, y, z, facing, false); }
|
||||
void door(int x, int y, int z, Direction facing, boolean right) {
|
||||
BlockState state = Blocks.DARK_OAK_DOOR.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing)
|
||||
.setValue(BlockStateProperties.DOOR_HINGE, right ? DoorHingeSide.RIGHT : DoorHingeSide.LEFT)
|
||||
.setValue(BlockStateProperties.OPEN, false).setValue(BlockStateProperties.POWERED, false);
|
||||
set(x, y, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.LOWER));
|
||||
set(x, y + 1, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.UPPER));
|
||||
}
|
||||
void doubleDoor(int x, int y, int z) { door(x, y, z, Direction.SOUTH, true); door(x + 1, y, z, Direction.SOUTH, false); }
|
||||
void northWindows(int x0, int x1, int z, int y) { box(x0, y, z, x1, y + 1, z, Blocks.GLASS_PANE); }
|
||||
void hang(int x, int ceiling, int z) { set(x, ceiling - 1, z, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true)); }
|
||||
void seat(int x, int y, int z, Direction view) {
|
||||
set(x, y, z, Blocks.DARK_OAK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, view.getOpposite()));
|
||||
}
|
||||
void lever(int x, int y, int z, Direction facing) {
|
||||
set(x, y, z, Blocks.LEVER.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR)
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, facing));
|
||||
}
|
||||
void rail(int x, int z, RailShape shape) { set(x, 1, z, Blocks.RAIL.defaultBlockState().setValue(BlockStateProperties.RAIL_SHAPE, shape)); }
|
||||
void loot(int x, int y, int z, int approachX, int approachZ) {
|
||||
target("loot_approach", approachX, y, approachZ);
|
||||
if (out != null) out.chest(rx(x, z), y, rz(x, z), ResourceKey.create(Registries.LOOT_TABLE,
|
||||
SanctuaryMod.id("chests/heritage19/" + s.kind().name().toLowerCase(Locale.ROOT))), s.seed() ^ 0x19CA7A10L);
|
||||
}
|
||||
void stairs(int x, int z, int base, int rise) {
|
||||
for (int step = 1; step <= rise; step++) for (int xx = x; xx <= x + 1; xx++) {
|
||||
box(xx, base + step + 1, z + step - 1, xx, base + step + 3, z + step - 1, Blocks.AIR);
|
||||
set(xx, base + step - 1, z + step - 1, Blocks.STONE_BRICKS);
|
||||
set(xx, base + step, z + step - 1, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
|
||||
}
|
||||
}
|
||||
void bed(int x, int y, int z, Direction facing) {
|
||||
BlockState state = Blocks.BED.white().defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing);
|
||||
set(x, y, z, state.setValue(BlockStateProperties.BED_PART, BedPart.FOOT));
|
||||
set(x + facing.getStepX(), y, z + facing.getStepZ(), state.setValue(BlockStateProperties.BED_PART, BedPart.HEAD));
|
||||
int wx = x + facing.getStepX() * 2, wz = z + facing.getStepZ() * 2;
|
||||
Direction along = facing.getClockWise();
|
||||
for (int side = -1; side <= 1; side++) for (int up = 0; up <= 2; up++)
|
||||
set(wx + along.getStepX() * side, y + up, wz + along.getStepZ() * side, Blocks.SMOOTH_STONE);
|
||||
}
|
||||
|
||||
void station() {
|
||||
shell(4, 0, 3, 12, 6, 11); gable(4, 3, 12, 11, 6);
|
||||
door(8, 1, 3, Direction.SOUTH); northWindows(5, 6, 3, 2); northWindows(10, 11, 3, 2);
|
||||
box(5, 1, 5, 6, 1, 5, Blocks.SMOOTH_STONE); set(5, 1, 8, Blocks.BARREL); seat(10, 1, 5, Direction.WEST);
|
||||
for (int z = 2; z <= 14; z++) rail(2, z, RailShape.NORTH_SOUTH);
|
||||
set(2, 1, 15, Blocks.DARK_OAK_FENCE);
|
||||
rail(8, 0, RailShape.NORTH_SOUTH); rail(8, 1, RailShape.NORTH_WEST);
|
||||
for (int x = 3; x <= 7; x++) rail(x, 1, RailShape.EAST_WEST);
|
||||
rail(2, 1, RailShape.SOUTH_EAST);
|
||||
box(0, 5, 5, 3, 5, 11, Blocks.DARK_OAK_PLANKS);
|
||||
for (int z : new int[] {5, 11}) box(1, 1, z, 1, 4, z, Blocks.STRIPPED_OAK_LOG);
|
||||
shell(9, 0, 11, 15, 4, 15); door(10, 1, 11, Direction.SOUTH); door(12, 1, 15, Direction.NORTH);
|
||||
set(10, 1, 13, Blocks.SMOOTH_STONE); lever(10, 2, 13, Direction.EAST);
|
||||
northWindows(13, 14, 15, 2); hang(8, 6, 7); hang(12, 4, 12);
|
||||
target("ticket_counter", 8, 1, 5); target("freight_platform", 3, 1, 9); target("signal_office", 12, 1, 12);
|
||||
target("secondary_entry", 12, 1, 16); loot(14, 1, 14, 13, 14);
|
||||
}
|
||||
void post() {
|
||||
shell(2, 0, 2, 14, 6, 14); door(8, 1, 2, Direction.SOUTH);
|
||||
northWindows(3, 6, 2, 2); northWindows(10, 13, 2, 2);
|
||||
box(3, 1, 7, 13, 5, 7, Blocks.SMOOTH_STONE); door(8, 1, 7, Direction.SOUTH);
|
||||
box(4, 1, 6, 6, 1, 6, Blocks.DARK_OAK_PLANKS);
|
||||
for (int z = 9; z <= 11; z++) { set(3, 1, z, Blocks.BARREL); set(3, 2, z, Blocks.BARREL); }
|
||||
set(6, 1, 10, Blocks.CRAFTING_TABLE); set(8, 1, 12, Blocks.BARREL);
|
||||
shell(10, 0, 10, 14, 6, 14); door(10, 1, 12, Direction.EAST); door(14, 1, 12, Direction.WEST);
|
||||
set(11, 1, 11, Blocks.CARTOGRAPHY_TABLE); hang(8, 6, 4); hang(7, 6, 10); hang(12, 6, 12);
|
||||
target("public_hall", 8, 1, 4); target("sorting_aisle", 7, 1, 10); target("factor_office", 12, 1, 12);
|
||||
target("secondary_entry", 15, 1, 12); loot(13, 1, 13, 12, 13);
|
||||
}
|
||||
void depot() {
|
||||
shell(1, 0, 2, 15, 9, 14); gable(1, 2, 15, 14, 9);
|
||||
box(7, 1, 2, 9, 3, 2, Blocks.AIR); door(4, 1, 2, Direction.SOUTH);
|
||||
for (int z : new int[] {5, 8}) {
|
||||
box(4, 1, z, 5, 1, z + 1, Blocks.DARK_OAK_PLANKS); set(4, 2, z, Blocks.BARREL);
|
||||
box(11, 1, z, 13, 1, z + 1, Blocks.DARK_OAK_PLANKS); set(13, 2, z + 1, Blocks.BARREL);
|
||||
}
|
||||
box(2, 5, 10, 14, 5, 13, Blocks.SMOOTH_STONE);
|
||||
for (int x = 5; x <= 14; x++) set(x, 6, 10, Blocks.IRON_BARS);
|
||||
shell(10, 5, 10, 14, 9, 14); door(10, 6, 12, Direction.EAST);
|
||||
set(11, 6, 11, Blocks.LECTERN.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
|
||||
stairs(2, 5, 0, 5); door(15, 1, 12, Direction.WEST); hang(8, 9, 6); hang(12, 9, 12);
|
||||
for (int z = 0; z <= 8; z++) rail(8, z, RailShape.NORTH_SOUTH);
|
||||
set(8, 1, 9, Blocks.DARK_OAK_FENCE);
|
||||
target("unloading_bay", 8, 1, 5); target("inventory_mezzanine", 5, 6, 12);
|
||||
target("secondary_entry", 16, 1, 12); loot(13, 6, 13, 12, 13);
|
||||
}
|
||||
void expedition() {
|
||||
shell(2, 0, 2, 12, 6, 12); door(8, 1, 2, Direction.SOUTH);
|
||||
northWindows(3, 5, 2, 2); northWindows(10, 11, 2, 2);
|
||||
set(4, 1, 4, Blocks.CARTOGRAPHY_TABLE); set(5, 1, 4, Blocks.LECTERN.defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH)); set(3, 1, 6, Blocks.BARREL);
|
||||
shell(2, 0, 8, 6, 6, 12); door(6, 1, 10, Direction.WEST); set(3, 1, 9, Blocks.BOOKSHELF);
|
||||
box(3, 0, 13, 14, 0, 15, Blocks.DARK_OAK_PLANKS);
|
||||
box(3, 1, 15, 14, 1, 15, Blocks.DARK_OAK_FENCE); door(10, 1, 12, Direction.NORTH);
|
||||
seat(6, 1, 13, Direction.SOUTH); hang(8, 6, 5); hang(4, 6, 10);
|
||||
target("expedition_table", 5, 1, 5); target("balcony", 10, 1, 14);
|
||||
target("secondary_entry", 10, 1, 13); loot(3, 1, 11, 4, 11);
|
||||
}
|
||||
boolean tower(int x, int z) { return x >= 3 && x <= 13 && z >= 3 && z <= 13 && Math.abs(x - 8) + Math.abs(z - 8) <= 8; }
|
||||
void observatory() {
|
||||
for (int y = 0; y <= 12; y++) for (int z = 3; z <= 13; z++) for (int x = 3; x <= 13; x++) if (tower(x, z)) {
|
||||
boolean wall = !tower(x - 1, z) || !tower(x + 1, z) || !tower(x, z - 1) || !tower(x, z + 1);
|
||||
set(x, y, z, y == 0 || y == 6 || y == 12 ? Blocks.SMOOTH_STONE.defaultBlockState()
|
||||
: wall ? LostCityPalette.masonry(s.seed(), x, y, z) : Blocks.AIR.defaultBlockState());
|
||||
if (y == 12 && wall) set(x, 13, z, Blocks.IRON_BARS);
|
||||
}
|
||||
door(8, 1, 3, Direction.SOUTH); door(8, 1, 13, Direction.NORTH);
|
||||
northWindows(7, 9, 3, 8); box(13, 8, 7, 13, 9, 9, Blocks.GLASS_PANE);
|
||||
shell(9, 0, 9, 12, 4, 12); door(9, 1, 10, Direction.EAST); set(10, 1, 11, Blocks.BARREL);
|
||||
set(10, 7, 5, Blocks.CARTOGRAPHY_TABLE); set(11, 7, 8, Blocks.SMOOTH_STONE); lever(11, 8, 8, Direction.WEST);
|
||||
stairs(4, 5, 0, 6); stairs(4, 5, 6, 6);
|
||||
box(4, 12, 11, 6, 12, 11, Blocks.SMOOTH_STONE); box(4, 13, 11, 6, 15, 11, Blocks.AIR);
|
||||
box(10, 13, 9, 10, 18, 9, Blocks.DARK_OAK_FENCE); box(9, 18, 9, 11, 18, 9, Blocks.IRON_BARS);
|
||||
hang(8, 6, 6); hang(10, 4, 11);
|
||||
target("instruments", 9, 7, 8); target("sky_platform", 8, 13, 8);
|
||||
target("secondary_entry", 8, 1, 14); loot(11, 1, 11, 11, 10);
|
||||
}
|
||||
void railway() {
|
||||
shell(1, 0, 2, 15, 6, 14); gable(1, 2, 15, 14, 6); doubleDoor(7, 1, 2);
|
||||
for (int z = 3; z <= 13; z++) for (int x : new int[] {5, 11}) set(x, 1, z, Blocks.RAIL);
|
||||
box(4, 1, 2, 6, 3, 2, Blocks.AIR); rail(8, 0, RailShape.NORTH_SOUTH); rail(8, 1, RailShape.NORTH_WEST);
|
||||
rail(7, 1, RailShape.EAST_WEST); rail(6, 1, RailShape.EAST_WEST); rail(5, 1, RailShape.SOUTH_EAST); rail(5, 2, RailShape.NORTH_SOUTH);
|
||||
box(7, -2, 5, 9, -2, 11, Blocks.SMOOTH_STONE); box(7, -1, 5, 9, 0, 11, Blocks.AIR);
|
||||
for (int step = 0; step <= 2; step++) set(8, -step, 4 + step, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
|
||||
for (int z = 6; z <= 11; z++) { set(6, 1, z, Blocks.IRON_BARS); set(10, 1, z, Blocks.IRON_BARS); }
|
||||
set(2, 1, 4, Blocks.CRAFTING_TABLE); set(3, 1, 4, Blocks.FURNACE.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
|
||||
set(2, 1, 6, Blocks.GRINDSTONE.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR));
|
||||
shell(1, 0, 10, 5, 5, 14); door(5, 1, 12, Direction.WEST); door(8, 1, 14, Direction.NORTH);
|
||||
set(5, 1, 9, Blocks.DARK_OAK_FENCE);
|
||||
set(2, 1, 11, Blocks.ANVIL); hang(8, 6, 8); hang(3, 5, 12);
|
||||
target("maintenance_pit", 8, -1, 9); target("workbench", 3, 1, 5);
|
||||
target("secondary_entry", 8, 1, 15); loot(2, 1, 13, 3, 13);
|
||||
}
|
||||
void pumping() {
|
||||
shell(2, 0, 3, 9, 6, 12); door(8, 1, 3, Direction.SOUTH);
|
||||
set(3, 1, 5, Blocks.DISPENSER.defaultBlockState().setValue(BlockStateProperties.FACING, Direction.EAST));
|
||||
set(4, 1, 5, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 3));
|
||||
set(3, 2, 5, Blocks.POLISHED_ANDESITE); box(3, 3, 5, 3, 5, 8, Blocks.IRON_BARS);
|
||||
for (int x : new int[] {10, 15}) for (int z : new int[] {5, 11}) box(x, 1, z, x, 7, z, Blocks.POLISHED_ANDESITE);
|
||||
shell(10, 8, 5, 15, 13, 11); box(11, 9, 6, 14, 11, 10, Blocks.WATER);
|
||||
box(10, 3, 6, 14, 3, 13, Blocks.SMOOTH_STONE); stairs(10, 3, 0, 3);
|
||||
shell(2, 0, 10, 6, 5, 15); door(5, 1, 10, Direction.SOUTH); door(5, 1, 15, Direction.NORTH);
|
||||
set(3, 1, 11, Blocks.BARREL); hang(7, 6, 7); hang(4, 5, 12);
|
||||
target("pump_room", 6, 1, 6); target("reservoir_walkway", 13, 4, 12);
|
||||
target("secondary_entry", 5, 1, 16); loot(3, 1, 14, 4, 14);
|
||||
}
|
||||
void substation() {
|
||||
for (int z = 2; z <= 14; z++) { set(1, 1, z, Blocks.IRON_BARS); set(15, 1, z, Blocks.IRON_BARS); }
|
||||
for (int x = 1; x <= 15; x++) if (x < 7 || x > 9) set(x, 1, 2, Blocks.IRON_BARS);
|
||||
for (int z : new int[] {5, 8}) {
|
||||
box(3, 1, z, 4, 2, z + 1, Blocks.POLISHED_ANDESITE); box(3, 3, z, 4, 3, z, Blocks.IRON_BARS);
|
||||
}
|
||||
shell(10, 0, 4, 15, 5, 10); door(10, 1, 6, Direction.EAST); northWindows(12, 14, 4, 2);
|
||||
set(12, 1, 5, Blocks.SMOOTH_STONE); lever(12, 2, 5, Direction.SOUTH); set(14, 1, 5, Blocks.CRAFTING_TABLE);
|
||||
shell(10, 0, 10, 15, 5, 15); door(12, 1, 10, Direction.SOUTH); door(15, 1, 13, Direction.WEST);
|
||||
box(6, -2, 10, 7, -2, 13, Blocks.SMOOTH_STONE); box(6, -1, 10, 7, 0, 13, Blocks.AIR);
|
||||
for (int step = 0; step <= 2; step++) set(6, -step, 9 + step, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
|
||||
hang(13, 5, 7); hang(12, 5, 13); target("distribution_yard", 8, 1, 6); target("cable_trench", 6, -1, 12);
|
||||
target("secondary_entry", 16, 1, 13); loot(14, 1, 14, 13, 14);
|
||||
}
|
||||
void greenhouse() {
|
||||
shell(1, 0, 2, 11, 6, 14);
|
||||
for (int y = 2; y <= 5; y++) for (int z = 3; z <= 13; z++) { set(1, y, z, Blocks.GLASS); set(11, y, z, Blocks.GLASS); }
|
||||
northWindows(2, 5, 2, 2); northWindows(7, 10, 2, 2); door(6, 1, 2, Direction.SOUTH);
|
||||
for (int x = 2; x <= 10; x++) for (int z = 3; z <= 13; z++) set(x, 6, z, x == 6 ? Blocks.SMOOTH_STONE : Blocks.GLASS);
|
||||
for (int x : new int[] {2, 3, 4, 8, 9, 10}) for (int z = 4; z <= 10; z++) {
|
||||
set(x, 0, z, Blocks.FARMLAND.defaultBlockState().setValue(BlockStateProperties.MOISTURE, 7));
|
||||
set(x, 1, z, Blocks.WHEAT.defaultBlockState().setValue(BlockStateProperties.AGE_7,
|
||||
2 + LostCityPalette.roll(s.seed(), x, 1, z, 6)));
|
||||
}
|
||||
set(3, 0, 6, Blocks.WATER); set(9, 0, 6, Blocks.WATER);
|
||||
set(3, 1, 6, Blocks.AIR); set(9, 1, 6, Blocks.AIR);
|
||||
shell(11, 0, 9, 15, 5, 15); door(11, 1, 12, Direction.EAST); door(13, 1, 15, Direction.NORTH);
|
||||
set(12, 1, 10, Blocks.CRAFTING_TABLE); set(13, 1, 10, Blocks.FLOWER_POT); set(9, 1, 12, Blocks.COMPOSTER);
|
||||
hang(6, 6, 5); hang(13, 5, 12); target("glasshouse_aisle", 6, 1, 7); target("potting_shed", 13, 1, 12);
|
||||
target("secondary_entry", 13, 1, 16); loot(14, 1, 14, 13, 14);
|
||||
}
|
||||
void dock() {
|
||||
box(0, 0, 0, 16, 0, 16, Blocks.DARK_OAK_PLANKS); box(0, 1, 16, 16, 1, 16, Blocks.DARK_OAK_FENCE);
|
||||
box(0, 1, 1, 0, 1, 15, Blocks.DARK_OAK_FENCE); box(16, 1, 1, 16, 1, 15, Blocks.DARK_OAK_FENCE);
|
||||
shell(2, 0, 3, 7, 5, 10); door(7, 1, 6, Direction.WEST); seat(3, 1, 5, Direction.EAST);
|
||||
shell(2, 0, 10, 7, 5, 15); door(5, 1, 10, Direction.SOUTH); door(7, 1, 13, Direction.WEST);
|
||||
box(12, 1, 5, 12, 11, 5, Blocks.STRIPPED_OAK_LOG); box(8, 11, 5, 14, 11, 5, Blocks.STRIPPED_OAK_LOG);
|
||||
box(14, 7, 5, 14, 10, 5, Blocks.IRON_CHAIN); hang(10, 11, 5); hang(5, 5, 6); hang(5, 5, 12);
|
||||
target("departure_deck", 8, 1, 14); target("waiting_room", 5, 1, 6); target("secondary_entry", 8, 1, 13);
|
||||
loot(3, 1, 14, 4, 14);
|
||||
}
|
||||
void radio() {
|
||||
shell(2, 0, 3, 10, 6, 12); door(8, 1, 3, Direction.SOUTH); northWindows(3, 5, 3, 2);
|
||||
bed(4, 1, 5, Direction.NORTH); set(8, 1, 5, Blocks.CARTOGRAPHY_TABLE);
|
||||
shell(2, 0, 8, 6, 6, 12); door(6, 1, 10, Direction.WEST); door(2, 1, 10, Direction.EAST);
|
||||
box(11, 5, 6, 15, 5, 10, Blocks.SMOOTH_STONE);
|
||||
for (int x : new int[] {11, 15}) for (int z : new int[] {6, 10}) box(x, 1, z, x, 4, z, Blocks.POLISHED_ANDESITE);
|
||||
box(13, 1, 8, 13, 18, 8, Blocks.POLISHED_ANDESITE); box(11, 17, 8, 15, 17, 8, Blocks.IRON_BARS);
|
||||
box(13, 19, 8, 13, 21, 8, Blocks.IRON_BARS); stairs(11, 1, 0, 5);
|
||||
hang(7, 6, 6); hang(4, 6, 10);
|
||||
target("caretaker_room", 7, 1, 6); target("antenna_platform", 12, 6, 9); target("secondary_entry", 1, 1, 10);
|
||||
loot(3, 1, 11, 4, 11);
|
||||
}
|
||||
void photo() {
|
||||
shell(2, 0, 2, 14, 6, 14); door(8, 1, 2, Direction.SOUTH); northWindows(3, 6, 2, 2); northWindows(10, 13, 2, 2);
|
||||
box(3, 1, 8, 13, 5, 8, Blocks.SMOOTH_STONE); door(4, 1, 8, Direction.SOUTH);
|
||||
box(8, 1, 9, 8, 5, 13, Blocks.SMOOTH_STONE); door(8, 1, 11, Direction.EAST);
|
||||
box(10, 1, 5, 12, 1, 5, Blocks.DARK_OAK_PLANKS); set(3, 1, 4, Blocks.BOOKSHELF);
|
||||
for (int z : new int[] {10, 12}) set(3, 1, z, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 3));
|
||||
box(11, 4, 10, 11, 5, 12, Blocks.IRON_CHAIN); set(12, 1, 10, Blocks.CRAFTING_TABLE);
|
||||
door(12, 1, 14, Direction.NORTH); hang(8, 6, 4); hang(5, 6, 11); hang(12, 6, 12);
|
||||
target("club_room", 8, 1, 4); target("darkroom", 5, 1, 11); target("drying_room", 10, 1, 11);
|
||||
target("secondary_entry", 12, 1, 15); loot(13, 1, 13, 12, 13);
|
||||
}
|
||||
void impossible() {
|
||||
shell(3, 0, 2, 11, 4, 7); door(8, 1, 2, Direction.SOUTH);
|
||||
for (int z = 3; z <= 5; z++) { set(4, 1, z, Blocks.BARREL); set(4, 2, z, Blocks.BARREL); }
|
||||
seat(10, 1, 4, Direction.WEST);
|
||||
shell(1, 0, 7, 15, 10, 15); door(8, 1, 7, Direction.SOUTH);
|
||||
for (int x = 2; x <= 14; x++) for (int z = 8; z <= 14; z++) set(x, 0, z,
|
||||
((x + z) & 1) == 0 ? Blocks.CALCITE : Blocks.POLISHED_ANDESITE);
|
||||
for (int x : new int[] {3, 6, 10, 13}) box(x, 1, 11, x, 9, 11, Blocks.CALCITE);
|
||||
shell(1, 0, 12, 5, 5, 15); door(5, 1, 13, Direction.WEST); door(3, 1, 15, Direction.NORTH);
|
||||
set(2, 2, 12, Blocks.GLASS); set(2, 1, 13, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 2));
|
||||
hang(8, 4, 4); hang(8, 10, 11); hang(3, 5, 13);
|
||||
target("lockers", 8, 1, 4); target("oversized_tiled_hall", 8, 1, 11);
|
||||
target("secondary_entry", 3, 1, 16); loot(2, 1, 14, 3, 14);
|
||||
}
|
||||
void refuge() {
|
||||
box(3, 4, 5, 14, 4, 15, Blocks.DARK_OAK_PLANKS);
|
||||
for (int x : new int[] {3, 14}) for (int z : new int[] {5, 15}) box(x, 1, z, x, 3, z, Blocks.STONE_BRICKS);
|
||||
shell(5, 4, 6, 13, 10, 14); gable(5, 6, 13, 14, 10); door(8, 5, 6, Direction.SOUTH);
|
||||
bed(6, 5, 8, Direction.NORTH); set(12, 5, 8, Blocks.CRAFTING_TABLE);
|
||||
shell(6, 0, 9, 11, 4, 14); door(6, 1, 11, Direction.EAST);
|
||||
// Carry the ladder through the floor so opening the hatch leaves a climbable lip.
|
||||
for (int y = 1; y <= 4; y++) set(10, y, 11, Blocks.LADDER.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST));
|
||||
set(10, 5, 11, Blocks.DARK_OAK_TRAPDOOR.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST)
|
||||
.setValue(BlockStateProperties.HALF, Half.BOTTOM));
|
||||
stairs(7, 1, 0, 4); box(2, 1, 3, 3, 11, 4, Blocks.STONE_BRICKS); box(2, 11, 3, 14, 11, 4, Blocks.STRIPPED_OAK_LOG);
|
||||
hang(10, 10, 8); hang(8, 4, 12);
|
||||
target("suspended_hut", 9, 5, 8); target("hidden_floor_hatch", 10, 5, 11); target("secondary_entry", 5, 1, 11);
|
||||
loot(7, 1, 13, 8, 13);
|
||||
}
|
||||
}
|
||||
}
|
||||
+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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+770
@@ -0,0 +1,770 @@
|
||||
package fr.koka.sanctuary.worldgen.heritage;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityPalette;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.AttachFace;
|
||||
import net.minecraft.world.level.block.state.properties.BedPart;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.block.state.properties.DoorHingeSide;
|
||||
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
|
||||
import net.minecraft.world.level.block.state.properties.Half;
|
||||
import net.minecraft.world.level.block.state.properties.RailShape;
|
||||
|
||||
/** Generation21 restrained architectural rendition of the immutable generation19 site geometry. The saved site controls all geometry and loot.
|
||||
* Site writes are local to minX/floorY/minZ; route writes are local to the segment's minimum bounds. */
|
||||
public final class HeritageRenderer21 {
|
||||
private HeritageRenderer21() {}
|
||||
public record Target(String name, int x, int y, int z) {}
|
||||
|
||||
/** These are already rotated local standing positions, not world coordinates. */
|
||||
public static List<Target> targets(HeritagePlan19.Site site) {
|
||||
var b = new Builder(site, null); b.layout(); return List.copyOf(b.targets);
|
||||
}
|
||||
public static void render(HeritagePlan19.Site site, LostCityRenderer.BlockWriter writer) {
|
||||
var buffer = ConnectedBlocks20.buffer(writer);
|
||||
var b = new Builder(site, buffer); b.ground(); b.layout();
|
||||
// Footings have absolute saved coordinates and must not undergo a second facade rotation.
|
||||
for (var support : site.supports()) for (int y = support.minY(); y <= support.maxY(); y++)
|
||||
buffer.set(support.x() - site.minX(), y - site.floorY(), support.z() - site.minZ(),
|
||||
y == support.minY() ? Blocks.CHISELED_TUFF_BRICKS.defaultBlockState() : Blocks.STONE_BRICKS.defaultBlockState());
|
||||
buffer.flush();
|
||||
}
|
||||
public static void renderSegment(HeritagePlan19.Segment segment, LostCityRenderer.BlockWriter writer) {
|
||||
var buffer = ConnectedBlocks20.buffer(writer);
|
||||
var occupied = new java.util.HashSet<Long>();
|
||||
for (var cell : segment.cells()) occupied.add(columnKey(cell.x(), cell.z()));
|
||||
int x0 = segment.minX(), y0 = segment.minY(), z0 = segment.minZ();
|
||||
for (var cell : segment.cells()) {
|
||||
int x = cell.x() - x0, z = cell.z() - z0, floor = cell.floorY() - y0;
|
||||
for (int y = cell.minY() - y0; y <= floor; y++) {
|
||||
Block material = Blocks.STONE_BRICKS;
|
||||
if (y == floor) {
|
||||
boolean edge = false;
|
||||
for (Direction d : new Direction[] {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}) {
|
||||
int nx = cell.x() + d.getStepX(), nz = cell.z() + d.getStepZ();
|
||||
// Unknown neighboring chunks are continuations, not artificial crosswise curbs.
|
||||
if ((nx >> 4) == segment.startChunkX() && (nz >> 4) == segment.startChunkZ()
|
||||
&& !occupied.contains(columnKey(nx, nz))) edge = true;
|
||||
}
|
||||
if (cell.rail() != HeritagePlan19.Rail.NONE) material = Math.floorMod(cell.x() + cell.z(), 3) == 0 ? Blocks.OAK_PLANKS : Blocks.GRAVEL;
|
||||
else if (edge) material = Math.floorMod(cell.x() + cell.z(), 8) == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS;
|
||||
else material = LostCityPalette.roll(segment.seed(), cell.x(), 0, cell.z(), 5) == 0 ? Blocks.TUFF : Blocks.POLISHED_TUFF;
|
||||
}
|
||||
buffer.set(x, y, z, material.defaultBlockState());
|
||||
}
|
||||
for (int y = floor + 1; y <= floor + 3; y++) buffer.set(x, y, z, Blocks.AIR.defaultBlockState());
|
||||
if (cell.railPresent()) buffer.set(x, floor + 1, z, Blocks.RAIL.defaultBlockState()
|
||||
.setValue(BlockStateProperties.RAIL_SHAPE, RailShape.valueOf(cell.rail().name())));
|
||||
}
|
||||
buffer.flush();
|
||||
}
|
||||
private static long columnKey(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
|
||||
|
||||
private static final class Builder {
|
||||
final HeritagePlan19.Site s;
|
||||
final LostCityRenderer.BlockWriter out;
|
||||
final List<Target> targets = new ArrayList<>();
|
||||
final Rotation rotation;
|
||||
Builder(HeritagePlan19.Site s, LostCityRenderer.BlockWriter out) {
|
||||
this.s = s; this.out = out;
|
||||
rotation = switch (s.facing()) { case 1 -> Rotation.CLOCKWISE_90; case 2 -> Rotation.CLOCKWISE_180;
|
||||
case 3 -> Rotation.COUNTERCLOCKWISE_90; default -> Rotation.NONE; };
|
||||
}
|
||||
int rx(int x, int z) { return switch (s.facing()) { case 1 -> 16 - z; case 2 -> 16 - x; case 3 -> z; default -> x; }; }
|
||||
int rz(int x, int z) { return switch (s.facing()) { case 1 -> x; case 2 -> 16 - z; case 3 -> 16 - x; default -> z; }; }
|
||||
void set(int x, int y, int z, BlockState state) {
|
||||
if (x < 0 || x > 16 || z < 0 || z > 16 || y < -s.foundationDepth() || y > s.height())
|
||||
throw new IllegalArgumentException("Heritage " + s.kind() + " renderer escaped saved site: " + x + "/" + y + "/" + z);
|
||||
if (out != null) out.set(rx(x, z), y, rz(x, z), palette(state, x, y, z).rotate(rotation));
|
||||
}
|
||||
void set(int x, int y, int z, Block b) { set(x, y, z, b.defaultBlockState()); }
|
||||
void box(int x0, int y0, int z0, int x1, int y1, int z1, Block b) {
|
||||
for (int y = y0; y <= y1; y++) for (int z = z0; z <= z1; z++) for (int x = x0; x <= x1; x++) set(x, y, z, b);
|
||||
}
|
||||
void target(String name, int x, int y, int z) { targets.add(new Target(name, rx(x, z), y, rz(x, z))); }
|
||||
void ground() {
|
||||
for (int y = -s.foundationDepth(); y <= s.height(); y++) for (int z = 0; z <= 16; z++) for (int x = 0; x <= 16; x++)
|
||||
set(x, y, z, y < 0 ? Blocks.STONE_BRICKS.defaultBlockState() : y == 0
|
||||
? (x == 0 || z == 0 || x == 16 || z == 16 ? Blocks.TUFF_BRICKS
|
||||
: x >= 7 && x <= 9 ? Blocks.POLISHED_TUFF
|
||||
: LostCityPalette.roll(s.seed(), x, 0, z, 7) == 0 ? Blocks.TUFF : Blocks.ANDESITE).defaultBlockState()
|
||||
: Blocks.AIR.defaultBlockState());
|
||||
}
|
||||
void layout() {
|
||||
target("main_entry", 8, 1, 0);
|
||||
switch (s.kind()) {
|
||||
case FREIGHT_STATION -> station();
|
||||
case POST_OFFICE -> post();
|
||||
case COMMUNAL_DEPOT -> depot();
|
||||
case EXPEDITION_OFFICE -> expedition();
|
||||
case OBSERVATORY -> observatory();
|
||||
case RAIL_WORKSHOP -> railway();
|
||||
case PUMP_STATION -> pumping();
|
||||
case SUBSTATION -> substation();
|
||||
case GREENHOUSE -> greenhouse();
|
||||
case VOID_DOCK -> dock();
|
||||
case RADIO_RELAY -> radio();
|
||||
case PHOTO_CLUB -> photo();
|
||||
case IMPOSSIBLE_SERVICE -> impossible();
|
||||
case CONSTRUCTION_REFUGE -> refuge();
|
||||
}
|
||||
architecture();
|
||||
roofs21();
|
||||
}
|
||||
void shell(int x0, int floor, int z0, int x1, int ceiling, int z1) {
|
||||
for (int y = floor; y <= ceiling; y++) for (int z = z0; z <= z1; z++) for (int x = x0; x <= x1; x++) {
|
||||
boolean wall = x == x0 || x == x1 || z == z0 || z == z1;
|
||||
set(x, y, z, y == floor ? floorMaterial().defaultBlockState() : y == ceiling ? Blocks.SMOOTH_STONE.defaultBlockState()
|
||||
: wall ? wallMaterial(y - floor, ceiling - floor).defaultBlockState() : Blocks.AIR.defaultBlockState());
|
||||
}
|
||||
}
|
||||
Block floorMaterial() {
|
||||
return switch (s.kind()) {
|
||||
case POST_OFFICE, EXPEDITION_OFFICE, RADIO_RELAY, CONSTRUCTION_REFUGE -> Blocks.OAK_PLANKS;
|
||||
case PHOTO_CLUB, GREENHOUSE -> Blocks.BIRCH_PLANKS;
|
||||
case IMPOSSIBLE_SERVICE -> Blocks.POLISHED_DIORITE;
|
||||
default -> Blocks.POLISHED_ANDESITE;
|
||||
};
|
||||
}
|
||||
Block wallMaterial(int y, int height) {
|
||||
if (y == 1) return Blocks.STONE_BRICKS;
|
||||
if (y == height - 1) return Blocks.ANDESITE;
|
||||
return s.kind() == HeritagePlan19.Kind.GREENHOUSE ? Blocks.BIRCH_PLANKS : Blocks.SMOOTH_STONE;
|
||||
}
|
||||
BlockState palette(BlockState state, int x, int y, int z) {
|
||||
Block original = state.getBlock(), replacement = original;
|
||||
boolean birch = s.kind() == HeritagePlan19.Kind.GREENHOUSE;
|
||||
if (original == Blocks.DARK_OAK_PLANKS || original == Blocks.OAK_PLANKS || original == Blocks.BIRCH_PLANKS)
|
||||
replacement = birch ? Blocks.BIRCH_PLANKS : Blocks.OAK_PLANKS;
|
||||
else if (original == Blocks.OAK_WOOD || original == Blocks.BIRCH_WOOD || original == Blocks.STRIPPED_OAK_WOOD)
|
||||
replacement = birch ? Blocks.BIRCH_WOOD : Blocks.OAK_WOOD;
|
||||
else if (original == Blocks.OAK_LOG || original == Blocks.BIRCH_LOG || original == Blocks.STRIPPED_OAK_LOG)
|
||||
replacement = birch ? Blocks.STRIPPED_BIRCH_LOG : Blocks.STRIPPED_OAK_LOG;
|
||||
else if (original == Blocks.DARK_OAK_SLAB || original == Blocks.OAK_SLAB || original == Blocks.BIRCH_SLAB)
|
||||
replacement = birch ? Blocks.BIRCH_SLAB : Blocks.OAK_SLAB;
|
||||
else if (original == Blocks.DARK_OAK_STAIRS || original == Blocks.OAK_STAIRS || original == Blocks.BIRCH_STAIRS)
|
||||
replacement = birch ? Blocks.BIRCH_STAIRS : Blocks.OAK_STAIRS;
|
||||
else if (original == Blocks.DARK_OAK_FENCE || original == Blocks.OAK_FENCE || original == Blocks.BIRCH_FENCE)
|
||||
replacement = birch ? Blocks.BIRCH_FENCE : Blocks.OAK_FENCE;
|
||||
else if (original == Blocks.DARK_OAK_DOOR || original == Blocks.OAK_DOOR || original == Blocks.BIRCH_DOOR)
|
||||
replacement = birch ? Blocks.BIRCH_DOOR : Blocks.OAK_DOOR;
|
||||
else if (original == Blocks.DARK_OAK_TRAPDOOR || original == Blocks.OAK_TRAPDOOR || original == Blocks.BIRCH_TRAPDOOR)
|
||||
replacement = birch ? Blocks.BIRCH_TRAPDOOR : Blocks.OAK_TRAPDOOR;
|
||||
else if (original == Blocks.GRANITE || original == Blocks.POLISHED_GRANITE || original == Blocks.DIORITE
|
||||
|| original == Blocks.POLISHED_DIORITE || original == Blocks.POLISHED_ANDESITE)
|
||||
replacement = original == Blocks.POLISHED_ANDESITE ? Blocks.ANDESITE : Blocks.SMOOTH_STONE;
|
||||
else if (original == Blocks.POLISHED_GRANITE_SLAB || original == Blocks.POLISHED_DIORITE_SLAB
|
||||
|| original == Blocks.POLISHED_ANDESITE_SLAB) replacement = Blocks.SMOOTH_STONE_SLAB;
|
||||
if (y > 0) {
|
||||
if (original == Blocks.TUFF || original == Blocks.POLISHED_TUFF) replacement = Blocks.SMOOTH_STONE;
|
||||
else if (original == Blocks.TUFF_BRICKS) replacement = Blocks.STONE_BRICKS;
|
||||
else if (original == Blocks.CHISELED_TUFF || original == Blocks.CHISELED_TUFF_BRICKS) replacement = Blocks.CHISELED_STONE_BRICKS;
|
||||
else if (original == Blocks.TUFF_BRICK_SLAB) replacement = Blocks.SMOOTH_STONE_SLAB;
|
||||
else if (original == Blocks.TUFF_BRICK_STAIRS) replacement = Blocks.STONE_BRICK_STAIRS;
|
||||
else if (original == Blocks.TUFF_BRICK_WALL) replacement = Blocks.STONE_BRICK_WALL;
|
||||
}
|
||||
if ((original == Blocks.STONE_BRICKS || original == Blocks.CRACKED_STONE_BRICKS
|
||||
|| original == Blocks.MOSSY_STONE_BRICKS) && y > 1 && y % 5 != 0)
|
||||
replacement = Blocks.SMOOTH_STONE;
|
||||
if (replacement == Blocks.ANDESITE && y > 0 && Math.floorMod(x + z, 4) == 0)
|
||||
replacement = Blocks.STONE;
|
||||
// Quiet, reproducible weathering follows construction joints, never random holes in access floors.
|
||||
if (replacement == Blocks.STONE_BRICKS && y > 0) {
|
||||
int weather = LostCityPalette.roll(s.seed(), x, y, z, 23);
|
||||
if (weather == 0) replacement = Blocks.CRACKED_STONE_BRICKS;
|
||||
else if (weather == 1 && y <= 3) replacement = Blocks.MOSSY_STONE_BRICKS;
|
||||
}
|
||||
if (replacement == original) return state;
|
||||
var mapped = replacement.defaultBlockState();
|
||||
for (var property : state.getProperties()) mapped = copyProperty(mapped, state, property);
|
||||
return mapped;
|
||||
}
|
||||
private static <T extends Comparable<T>> BlockState copyProperty(BlockState destination, BlockState source,
|
||||
net.minecraft.world.level.block.state.properties.Property<T> property) {
|
||||
return destination.hasProperty(property) ? destination.setValue(property, source.getValue(property)) : destination;
|
||||
}
|
||||
// These additions compose each use separately; all thresholds and maintenance passages stay in the saved footprint.
|
||||
void architecture() {
|
||||
switch (s.kind()) {
|
||||
case FREIGHT_STATION -> stationArchitecture();
|
||||
case POST_OFFICE -> postArchitecture();
|
||||
case COMMUNAL_DEPOT -> depotArchitecture();
|
||||
case EXPEDITION_OFFICE -> expeditionArchitecture();
|
||||
case OBSERVATORY -> observatoryArchitecture();
|
||||
case RAIL_WORKSHOP -> railwayArchitecture();
|
||||
case PUMP_STATION -> pumpingArchitecture();
|
||||
case SUBSTATION -> substationArchitecture();
|
||||
case GREENHOUSE -> greenhouseArchitecture();
|
||||
case VOID_DOCK -> dockArchitecture();
|
||||
case RADIO_RELAY -> radioArchitecture();
|
||||
case PHOTO_CLUB -> photoArchitecture();
|
||||
case IMPOSSIBLE_SERVICE -> impossibleArchitecture();
|
||||
case CONSTRUCTION_REFUGE -> refugeArchitecture();
|
||||
}
|
||||
}
|
||||
void slab(int x, int y, int z, Block block) { set(x, y, z, block.defaultBlockState().setValue(BlockStateProperties.SLAB_TYPE, net.minecraft.world.level.block.state.properties.SlabType.BOTTOM)); }
|
||||
void stair(int x, int y, int z, Block block, Direction direction, Half half) {
|
||||
set(x, y, z, block.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, direction).setValue(BlockStateProperties.HALF, half));
|
||||
}
|
||||
/** Cut only the four exterior corner cells; the two adjacent wall faces keep the interior sealed. */
|
||||
void bevel(int x0, int z0, int x1, int z1, int low, int high) {
|
||||
for (int x : new int[] {x0, x1}) for (int z : new int[] {z0, z1}) {
|
||||
box(x, low, z, x, high, z, Blocks.AIR);
|
||||
set(x, low - 1, z, Blocks.CHISELED_STONE_BRICKS);
|
||||
}
|
||||
}
|
||||
void rim(int x0, int z0, int x1, int z1, int y, Block material) {
|
||||
for (int x = x0; x <= x1; x++) { slab(x, y, z0, material); slab(x, y, z1, material); }
|
||||
for (int z = z0 + 1; z < z1; z++) { slab(x0, y, z, material); slab(x1, y, z, material); }
|
||||
}
|
||||
void screen(int x0, int z, int x1, int y) {
|
||||
for (int x = x0; x <= x1; x++) set(x, y, z, Blocks.BIRCH_TRAPDOOR.defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH).setValue(BlockStateProperties.OPEN, true));
|
||||
}
|
||||
void stationArchitecture() {
|
||||
bevel(4, 3, 12, 11, 1, 6);
|
||||
box(4, 7, 3, 12, 10, 11, Blocks.AIR);
|
||||
rim(3, 2, 13, 12, 6, Blocks.TUFF_BRICK_SLAB);
|
||||
for (int z = 5; z <= 9; z++) for (int x = 5; x <= 11; x++) {
|
||||
if (x == 5 || x == 11 || z == 5 || z == 9) box(x, 7, z, x, 8, z, Blocks.GLASS_PANE);
|
||||
slab(x, 9, z, Blocks.POLISHED_DIORITE_SLAB);
|
||||
}
|
||||
// Open the former ceiling beneath the sealed roof glazing. The middle beam
|
||||
// still supports the existing hanging lantern and joins the two hall walls.
|
||||
for (int z = 6; z <= 8; z++) for (int x = 6; x <= 10; x++) {
|
||||
if (z != 7) set(x, 6, z, Blocks.AIR);
|
||||
set(x, 9, z, Blocks.GLASS);
|
||||
}
|
||||
// The long cantilever reads as a train platform, distinct from the glazed passenger hall.
|
||||
box(0, 5, 4, 3, 5, 12, Blocks.OAK_PLANKS);
|
||||
for (int z : new int[] {4, 8, 12}) stair(0, 4, z, Blocks.TUFF_BRICK_STAIRS, Direction.EAST, Half.TOP);
|
||||
for (int z : new int[] {5, 11}) box(1, 1, z, 1, 4, z, Blocks.OAK_WOOD);
|
||||
for (int x : new int[] {4, 12}) box(x, 2, 4, x, 4, 4, Blocks.CHISELED_TUFF_BRICKS);
|
||||
screen(5, 4, 6, 3); box(5, 0, 4, 11, 0, 10, Blocks.POLISHED_DIORITE);
|
||||
box(7, 0, 3, 9, 0, 10, Blocks.BIRCH_PLANKS);
|
||||
rim(9, 11, 15, 15, 4, Blocks.TUFF_BRICK_SLAB);
|
||||
}
|
||||
void postArchitecture() {
|
||||
bevel(2, 2, 14, 14, 1, 6); rim(1, 1, 15, 15, 6, Blocks.POLISHED_GRANITE_SLAB);
|
||||
box(4, 7, 4, 12, 7, 12, Blocks.TUFF_BRICKS);
|
||||
rim(4, 4, 12, 12, 8, Blocks.TUFF_BRICK_SLAB);
|
||||
for (int x : new int[] {2, 7, 9, 14}) {
|
||||
box(x, 3, 1, x, 4, 1, Blocks.POLISHED_DIORITE);
|
||||
stair(x, 5, 1, Blocks.TUFF_BRICK_STAIRS, Direction.SOUTH, Half.TOP);
|
||||
}
|
||||
box(5, 4, 1, 11, 4, 1, Blocks.CHISELED_TUFF_BRICKS);
|
||||
box(4, 1, 6, 6, 1, 6, Blocks.BIRCH_PLANKS); screen(4, 6, 6, 3);
|
||||
for (int z = 9; z <= 12; z++) { set(3, 3, z, Blocks.OAK_TRAPDOOR.defaultBlockState().setValue(BlockStateProperties.HALF, Half.TOP)); }
|
||||
for (int x = 4; x <= 12; x++) set(x, 0, 5, x % 2 == 0 ? Blocks.POLISHED_GRANITE : Blocks.OAK_PLANKS);
|
||||
box(10, 0, 8, 13, 0, 9, Blocks.DIORITE);
|
||||
}
|
||||
void depotArchitecture() {
|
||||
bevel(1, 2, 15, 14, 1, 9); box(1, 10, 2, 15, 14, 14, Blocks.AIR);
|
||||
// Three industrial north-light roof bays, not another pitched house roof.
|
||||
for (int z : new int[] {3, 7, 11}) {
|
||||
box(2, 10, z, 14, 11, z, Blocks.GLASS_PANE);
|
||||
for (int x = 2; x <= 14; x++) {
|
||||
slab(x, 12, z, Blocks.TUFF_BRICK_SLAB);
|
||||
stair(x, 11, z + 1, Blocks.TUFF_BRICK_STAIRS, Direction.NORTH, Half.BOTTOM);
|
||||
stair(x, 10, z + 2, Blocks.TUFF_BRICK_STAIRS, Direction.NORTH, Half.BOTTOM);
|
||||
if (x > 2 && x < 14) {
|
||||
// Both transparent faces close the north-light well, while the
|
||||
// retained roof outside these bands keeps the warehouse sealed.
|
||||
set(x, 9, z, Blocks.GLASS);
|
||||
set(x, 12, z, Blocks.GLASS);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int z : new int[] {3, 7, 11}) for (int x : new int[] {1, 15}) box(x, 1, z, x, 9, z, Blocks.POLISHED_ANDESITE);
|
||||
for (int x : new int[] {6, 10}) { box(x, 1, 1, x, 3, 1, Blocks.CHISELED_TUFF); stair(x, 4, 1, Blocks.TUFF_BRICK_STAIRS, Direction.SOUTH, Half.TOP); }
|
||||
box(6, 4, 1, 10, 4, 1, Blocks.TUFF_BRICKS);
|
||||
for (int z : new int[] {5, 8}) {
|
||||
// Thin wooden covers sit directly on the barrel stack and the lower packing bench.
|
||||
set(4, 3, z, Blocks.OAK_TRAPDOOR.defaultBlockState().setValue(BlockStateProperties.HALF, Half.BOTTOM));
|
||||
set(12, 2, z, Blocks.OAK_TRAPDOOR.defaultBlockState().setValue(BlockStateProperties.HALF, Half.BOTTOM));
|
||||
}
|
||||
for (int z = 3; z <= 9; z++) { set(6, 0, z, Blocks.TUFF_BRICKS); set(10, 0, z, Blocks.TUFF_BRICKS); }
|
||||
box(2, 5, 11, 9, 5, 13, Blocks.BIRCH_PLANKS);
|
||||
}
|
||||
void expeditionArchitecture() {
|
||||
bevel(2, 2, 12, 12, 1, 6); rim(1, 1, 13, 13, 6, Blocks.TUFF_BRICK_SLAB);
|
||||
box(3, 7, 3, 6, 9, 7, Blocks.BIRCH_WOOD); rim(3, 3, 6, 7, 10, Blocks.TUFF_BRICK_SLAB);
|
||||
box(4, 8, 3, 5, 8, 3, Blocks.GLASS_PANE);
|
||||
// Sheltered map table, archive wing and broad shaded rear balcony.
|
||||
box(7, 4, 1, 9, 4, 1, Blocks.CHISELED_TUFF_BRICKS);
|
||||
for (int x : new int[] {2, 12}) box(x, 2, 3, x, 5, 3, Blocks.OAK_WOOD);
|
||||
screen(3, 4, 5, 3); box(3, 3, 9, 5, 3, 9, Blocks.OAK_PLANKS);
|
||||
for (int x = 3; x <= 14; x++) for (int z = 13; z <= 15; z++) set(x, 0, z, x % 3 == 0 ? Blocks.OAK_WOOD : Blocks.OAK_PLANKS);
|
||||
for (int x = 3; x <= 14; x++) slab(x, 5, 14, Blocks.BIRCH_SLAB);
|
||||
box(3, 1, 14, 3, 4, 14, Blocks.BIRCH_FENCE); box(14, 1, 14, 14, 4, 14, Blocks.BIRCH_FENCE);
|
||||
}
|
||||
void observatoryArchitecture() {
|
||||
// Eight faceted buttresses below a broken observatory dome with an open central viewing slot.
|
||||
for (int[] p : new int[][] {{3, 8}, {5, 4}, {11, 4}, {13, 8}, {11, 12}, {5, 12}}) {
|
||||
box(p[0], 1, p[1], p[0], 11, p[1], Blocks.DIORITE);
|
||||
set(p[0], 5, p[1], Blocks.CHISELED_TUFF_BRICKS);
|
||||
stair(p[0], 12, p[1], Blocks.TUFF_BRICK_STAIRS, p[0] < 8 ? Direction.EAST : Direction.WEST, Half.BOTTOM);
|
||||
}
|
||||
for (int z = 4; z <= 12; z++) for (int x = 4; x <= 12; x++) {
|
||||
int distance = Math.max(Math.abs(x - 8), Math.abs(z - 8));
|
||||
if (distance == 4) set(x, 15, z, Blocks.GLASS_PANE);
|
||||
if (distance == 3 && x != 8) slab(x, 17, z, Blocks.TUFF_BRICK_SLAB);
|
||||
}
|
||||
for (int x : new int[] {4, 12}) for (int z : new int[] {5, 11}) box(x, 13, z, x, 14, z, Blocks.CHISELED_TUFF_BRICKS);
|
||||
// Four short ribs visibly carry the inset crown; the central viewing slot stays open.
|
||||
for (int x : new int[] {4, 12}) for (int z : new int[] {5, 11}) {
|
||||
box(x, 15, z, x, 16, z, Blocks.CHISELED_TUFF_BRICKS);
|
||||
stair(x < 8 ? x + 1 : x - 1, 16, z, Blocks.TUFF_BRICK_STAIRS,
|
||||
x < 8 ? Direction.EAST : Direction.WEST, Half.BOTTOM);
|
||||
}
|
||||
box(7, 0, 5, 9, 0, 11, Blocks.BIRCH_PLANKS);
|
||||
box(7, 6, 5, 9, 6, 10, Blocks.POLISHED_DIORITE);
|
||||
}
|
||||
void railwayArchitecture() {
|
||||
bevel(1, 2, 15, 14, 1, 6); box(1, 7, 2, 15, 10, 14, Blocks.AIR);
|
||||
// Broad barrel-vault ribs sit above the two inspection tracks.
|
||||
for (int z = 2; z <= 14; z++) for (int x = 1; x <= 15; x++) {
|
||||
int rise = Math.min(3, Math.min(x - 1, 15 - x));
|
||||
if (z % 4 == 2) set(x, 6 + rise, z, Blocks.TUFF_BRICKS);
|
||||
else if (x >= 6 && x <= 10) set(x, 6 + rise, z, Blocks.GLASS);
|
||||
else slab(x, 6 + rise, z, Blocks.TUFF_BRICK_SLAB);
|
||||
}
|
||||
for (int z : new int[] {3, 7, 11}) for (int x : new int[] {1, 15}) box(x, 2, z, x, 5, z, Blocks.GRANITE);
|
||||
box(7, -2, 5, 9, -2, 11, Blocks.POLISHED_ANDESITE);
|
||||
for (int z = 6; z <= 11; z++) { set(7, -1, z, Blocks.CHISELED_TUFF_BRICKS); set(9, -1, z, Blocks.CHISELED_TUFF_BRICKS); }
|
||||
// Side ledges leave the one-block maintenance aisle and its stairs clear.
|
||||
screen(2, 3, 3, 3); box(2, 3, 11, 4, 3, 11, Blocks.OAK_PLANKS);
|
||||
}
|
||||
void pumpingArchitecture() {
|
||||
bevel(2, 3, 9, 12, 1, 6); rim(1, 2, 9, 13, 6, Blocks.TUFF_BRICK_SLAB);
|
||||
for (int x : new int[] {10, 15}) for (int z : new int[] {5, 11}) {
|
||||
box(x, 1, z, x, 7, z, Blocks.CHISELED_TUFF_BRICKS);
|
||||
set(x, 8, z, Blocks.POLISHED_ANDESITE);
|
||||
}
|
||||
rim(9, 4, 16, 12, 13, Blocks.TUFF_BRICK_SLAB);
|
||||
for (int x = 10; x <= 15; x++) for (int z = 5; z <= 11; z++) slab(x, 14, z, Blocks.POLISHED_ANDESITE_SLAB);
|
||||
box(3, 7, 5, 3, 12, 5, Blocks.TUFF_BRICK_WALL);
|
||||
box(3, 3, 6, 3, 5, 8, Blocks.IRON_BARS);
|
||||
box(3, 0, 4, 8, 0, 9, Blocks.ANDESITE); box(5, 0, 4, 7, 0, 10, Blocks.POLISHED_DIORITE);
|
||||
screen(3, 11, 4, 3);
|
||||
}
|
||||
void substationArchitecture() {
|
||||
bevel(10, 4, 15, 10, 1, 5); rim(9, 3, 16, 15, 5, Blocks.TUFF_BRICK_SLAB);
|
||||
for (int z : new int[] {2, 6, 10, 14}) for (int x : new int[] {1, 15}) {
|
||||
set(x, 0, z, Blocks.STONE_BRICKS); set(x, 1, z, Blocks.CHISELED_TUFF_BRICKS);
|
||||
}
|
||||
box(2, 4, 5, 5, 4, 5, Blocks.IRON_BARS); box(2, 4, 8, 5, 4, 8, Blocks.IRON_BARS);
|
||||
for (int x : new int[] {2, 5}) for (int z : new int[] {5, 8}) box(x, 1, z, x, 3, z, Blocks.TUFF_BRICK_WALL);
|
||||
box(11, 6, 5, 14, 6, 9, Blocks.POLISHED_TUFF);
|
||||
for (int z = 5; z <= 9; z += 2) screen(11, z, 14, 7);
|
||||
for (int z = 3; z <= 8; z++) set(7, 0, z, Blocks.GRANITE);
|
||||
box(6, -2, 10, 7, -2, 13, Blocks.CHISELED_TUFF_BRICKS);
|
||||
}
|
||||
void greenhouseArchitecture() {
|
||||
// Tall birch ribs and a steep glass ridge give the glasshouse an agricultural silhouette.
|
||||
for (int z = 2; z <= 14; z++) for (int x = 1; x <= 11; x++) {
|
||||
int rise = Math.min(3, Math.min(x - 1, 11 - x));
|
||||
if (z == 2 || z == 8 || z == 14 || x == 6) set(x, 6 + rise, z, Blocks.BIRCH_WOOD);
|
||||
else set(x, 6 + rise, z, Blocks.GLASS);
|
||||
}
|
||||
for (int z : new int[] {3, 7, 11}) { box(1, 1, z, 1, 5, z, Blocks.BIRCH_WOOD); box(11, 1, z, 11, 5, z, Blocks.BIRCH_WOOD); }
|
||||
rim(11, 9, 15, 15, 5, Blocks.TUFF_BRICK_SLAB);
|
||||
box(12, 3, 10, 14, 3, 10, Blocks.OAK_PLANKS); screen(12, 10, 14, 4);
|
||||
for (int z = 3; z <= 13; z++) set(6, 0, z, z % 3 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.POLISHED_TUFF);
|
||||
}
|
||||
void dockArchitecture() {
|
||||
for (int x = 1; x <= 15; x++) for (int z = 0; z <= 15; z++) set(x, 0, z, x % 4 == 0 ? Blocks.OAK_WOOD : Blocks.OAK_PLANKS);
|
||||
bevel(2, 3, 7, 10, 1, 5); rim(1, 2, 8, 15, 5, Blocks.BIRCH_SLAB);
|
||||
for (int z : new int[] {2, 8, 14}) {
|
||||
box(0, 1, z, 0, 2, z, Blocks.CHISELED_TUFF_BRICKS);
|
||||
box(16, 1, z, 16, 2, z, Blocks.CHISELED_TUFF_BRICKS);
|
||||
slab(0, 3, z, Blocks.TUFF_BRICK_SLAB); slab(16, 3, z, Blocks.TUFF_BRICK_SLAB);
|
||||
}
|
||||
// Brackets and a separated customs roof leave the departure platform open to the void.
|
||||
for (int x = 2; x <= 7; x++) for (int z = 3; z <= 15; z++) if (z % 4 == 0) slab(x, 6, z, Blocks.BIRCH_SLAB);
|
||||
stair(11, 9, 5, Blocks.OAK_STAIRS, Direction.EAST, Half.TOP);
|
||||
stair(13, 9, 5, Blocks.OAK_STAIRS, Direction.WEST, Half.TOP);
|
||||
screen(3, 4, 6, 3);
|
||||
}
|
||||
void radioArchitecture() {
|
||||
bevel(2, 3, 10, 12, 1, 6); rim(1, 2, 10, 13, 6, Blocks.TUFF_BRICK_SLAB);
|
||||
for (int x = 2; x <= 10; x++) for (int z = 3; z <= 12; z++) {
|
||||
int roofY = 7 + Math.min(2, Math.min(x - 2, 10 - x));
|
||||
slab(x, roofY, z, x <= 5 ? Blocks.BIRCH_SLAB : Blocks.TUFF_BRICK_SLAB);
|
||||
}
|
||||
box(9, 7, 4, 9, 11, 4, Blocks.CHISELED_TUFF_BRICKS);
|
||||
for (int y : new int[] {10, 14, 18}) { box(12, y, 8, 14, y, 8, Blocks.IRON_BARS); box(13, y, 7, 13, y, 9, Blocks.IRON_BARS); }
|
||||
box(3, 0, 8, 5, 0, 11, Blocks.BIRCH_PLANKS); screen(7, 4, 9, 3);
|
||||
}
|
||||
void photoArchitecture() {
|
||||
bevel(2, 2, 14, 14, 1, 6); rim(1, 1, 15, 15, 6, Blocks.POLISHED_DIORITE_SLAB);
|
||||
box(3, 7, 9, 13, 7, 13, Blocks.TUFF_BRICKS); rim(3, 9, 13, 13, 8, Blocks.TUFF_BRICK_SLAB);
|
||||
for (int x : new int[] {3, 6, 10, 13}) stair(x, 4, 1, Blocks.TUFF_BRICK_STAIRS, Direction.SOUTH, Half.TOP);
|
||||
box(3, 5, 1, 13, 5, 1, Blocks.CHISELED_TUFF_BRICKS);
|
||||
for (int x = 4; x <= 12; x++) set(x, 0, 4, x % 2 == 0 ? Blocks.POLISHED_GRANITE : Blocks.BIRCH_PLANKS);
|
||||
box(3, 0, 9, 7, 0, 13, Blocks.POLISHED_ANDESITE);
|
||||
screen(10, 5, 12, 2); box(3, 3, 10, 3, 3, 12, Blocks.OAK_PLANKS);
|
||||
}
|
||||
void impossibleArchitecture() {
|
||||
bevel(1, 7, 15, 15, 1, 10); rim(0, 6, 16, 16, 10, Blocks.TUFF_BRICK_SLAB);
|
||||
box(4, 11, 9, 12, 11, 13, Blocks.TUFF_BRICKS);
|
||||
for (int x : new int[] {4, 12}) for (int z : new int[] {9, 13}) set(x, 12, z, Blocks.CHISELED_TUFF);
|
||||
// A cramped civil facade gives way to an over-scaled, deliberately ceremonial service hall.
|
||||
for (int x : new int[] {3, 11}) box(x, 1, 1, x, 4, 1, Blocks.CHISELED_TUFF_BRICKS);
|
||||
box(3, 5, 1, 11, 5, 1, Blocks.TUFF_BRICKS);
|
||||
for (int x : new int[] {3, 6, 10, 13}) { set(x, 1, 11, Blocks.CHISELED_TUFF_BRICKS); set(x, 8, 11, Blocks.CHISELED_TUFF_BRICKS); }
|
||||
screen(4, 3, 4, 3); box(12, 3, 14, 14, 3, 14, Blocks.BIRCH_PLANKS);
|
||||
}
|
||||
void refugeArchitecture() {
|
||||
bevel(5, 6, 13, 14, 5, 10);
|
||||
box(5, 11, 6, 13, 14, 14, Blocks.AIR);
|
||||
for (int z = 6; z <= 14; z++) for (int x = 5; x <= 13; x++) {
|
||||
int rise = Math.min(3, (x - 5) / 2);
|
||||
slab(x, 11 + rise, z, (z & 1) == 0 ? Blocks.BIRCH_SLAB : Blocks.TUFF_BRICK_SLAB);
|
||||
}
|
||||
for (int x : new int[] {3, 14}) for (int z : new int[] {5, 15}) { box(x, 1, z, x, 3, z, Blocks.CHISELED_TUFF_BRICKS); set(x, 4, z, Blocks.OAK_WOOD); }
|
||||
for (int z = 6; z <= 14; z += 4) box(4, 1, z, 4, 7, z, Blocks.IRON_BARS);
|
||||
box(2, 1, 3, 3, 10, 4, Blocks.TUFF_BRICKS); box(2, 11, 3, 14, 11, 4, Blocks.OAK_WOOD);
|
||||
// Upper lockers and a birch sleeping corner avoid the saved hatch and its complete ladder shaft.
|
||||
screen(11, 7, 12, 7); box(7, 4, 7, 11, 4, 9, Blocks.BIRCH_PLANKS);
|
||||
}
|
||||
|
||||
/** Replacement roofs: one weather skin, connected rafters and retained lamp-bearing beams.
|
||||
* Clearing starts at the old roof, never at a floor, furnishing or standing threshold. */
|
||||
void roofs21() {
|
||||
switch (s.kind()) {
|
||||
case RAIL_WORKSHOP -> {
|
||||
vaultRoof(1, 2, 15, 14, 6, false, true);
|
||||
box(1, 6, 8, 15, 6, 8, Blocks.OAK_WOOD); // existing workshop lamp
|
||||
// Two missing panes expose an intact neighbouring rafter, not a floating roof fragment.
|
||||
box(7, 9, 4, 8, 9, 4, Blocks.AIR);
|
||||
}
|
||||
case GREENHOUSE -> {
|
||||
vaultRoof(1, 2, 11, 14, 6, true, true);
|
||||
box(1, 6, 5, 11, 6, 5, Blocks.BIRCH_WOOD); // existing aisle lamp
|
||||
}
|
||||
case COMMUNAL_DEPOT -> sawtoothRoof();
|
||||
case RADIO_RELAY -> {
|
||||
vaultRoof(2, 3, 10, 12, 6, true, false);
|
||||
box(2, 6, 6, 10, 6, 6, Blocks.OAK_WOOD);
|
||||
box(2, 6, 10, 10, 6, 10, Blocks.OAK_WOOD);
|
||||
box(9, 7, 4, 9, 11, 4, Blocks.STONE_BRICKS);
|
||||
}
|
||||
case CONSTRUCTION_REFUGE -> {
|
||||
shedRoof(5, 6, 13, 14, 10);
|
||||
box(5, 10, 8, 13, 10, 8, Blocks.OAK_WOOD); // lamp above the shared sleeping room
|
||||
}
|
||||
case POST_OFFICE -> box(5, 7, 5, 11, 7, 11, Blocks.AIR); // a parapet, not a second slab ceiling
|
||||
case PHOTO_CLUB -> box(4, 7, 10, 12, 7, 12, Blocks.AIR);
|
||||
default -> { } // Other roofs already have a single supported slab or a sealed working tank.
|
||||
}
|
||||
}
|
||||
void vaultRoof(int x0, int z0, int x1, int z1, int eave, boolean timber, boolean glazed) {
|
||||
box(x0, eave, z0, x1, s.height(), z1, Blocks.AIR);
|
||||
int mid = (z0 + z1) / 2;
|
||||
for (int z = z0; z <= z1; z++) for (int x = x0; x <= x1; x++) {
|
||||
int top = vaultY(x, x0, x1, eave);
|
||||
int adjoining = Math.min(vaultY(Math.max(x0, x - 1), x0, x1, eave),
|
||||
vaultY(Math.min(x1, x + 1), x0, x1, eave));
|
||||
boolean rib = z == z0 || z == mid || z == z1;
|
||||
Block skin = rib ? Blocks.OAK_WOOD : glazed && x > x0 + 2 && x < x1 - 2
|
||||
? Blocks.GLASS : timber ? Blocks.OAK_PLANKS : Blocks.SMOOTH_STONE;
|
||||
// This short riser joins neighbouring height steps by a face, not just a diagonal corner.
|
||||
box(x, Math.min(top, adjoining), z, x, top, z, skin);
|
||||
if (z == z0 || z == z1) box(x, eave, z, x, top, z,
|
||||
glazed && x > x0 + 1 && x < x1 - 1 ? Blocks.GLASS : Blocks.OAK_WOOD);
|
||||
}
|
||||
// Truss feet sit on the existing two long walls; no columns are introduced in an aisle.
|
||||
box(x0, eave, mid, x1, eave, mid, Blocks.OAK_WOOD);
|
||||
for (int x : new int[] {x0, x1}) box(x, eave, z0, x, eave, z1, Blocks.OAK_WOOD);
|
||||
}
|
||||
int vaultY(int x, int x0, int x1, int eave) {
|
||||
return eave + Math.min(3, Math.min(x - x0, x1 - x));
|
||||
}
|
||||
void shedRoof(int x0, int z0, int x1, int z1, int eave) {
|
||||
box(x0, eave, z0, x1, s.height(), z1, Blocks.AIR);
|
||||
for (int z = z0; z <= z1; z++) for (int x = x0; x <= x1; x++) {
|
||||
int top = eave + (x - x0) / 3;
|
||||
int previous = eave + Math.max(0, x - 1 - x0) / 3;
|
||||
box(x, previous, z, x, top, z,
|
||||
z == z0 || z == z1 || z == (z0 + z1) / 2 ? Blocks.OAK_WOOD : Blocks.OAK_PLANKS);
|
||||
if (z == z0 || z == z1 || x == x1) box(x, eave, z, x, top, z, Blocks.OAK_WOOD);
|
||||
}
|
||||
}
|
||||
void sawtoothRoof() {
|
||||
box(1, 9, 2, 15, s.height(), 14, Blocks.AIR);
|
||||
for (int z = 2; z <= 14; z++) for (int x = 1; x <= 15; x++) {
|
||||
int phase = Math.floorMod(z - 2, 4), top = z == 14 ? 9 : 12 - phase;
|
||||
boolean frame = x == 1 || x == 15;
|
||||
if (phase == 0 && z != 14) box(x, 9, z, x, top, z, frame ? Blocks.OAK_WOOD : Blocks.GLASS);
|
||||
else box(x, Math.max(9, top - 1), z, x, top, z,
|
||||
frame ? Blocks.OAK_WOOD : phase == 1 ? Blocks.GLASS : Blocks.SMOOTH_STONE);
|
||||
if (frame) box(x, 9, z, x, top, z, Blocks.OAK_WOOD);
|
||||
}
|
||||
box(1, 9, 6, 15, 9, 6, Blocks.OAK_WOOD); // carries the original hall lamp
|
||||
// Local, readable storm damage. The perimeter frame and remaining panes still share their bearings.
|
||||
box(12, 10, 11, 13, 11, 11, Blocks.AIR);
|
||||
}
|
||||
|
||||
void gable(int x0, int z0, int x1, int z1, int y) {
|
||||
int rise = Math.min(3, (x1 - x0) / 2);
|
||||
for (int step = 0; step <= rise; step++)
|
||||
box(x0 + step, y + step, z0, x1 - step, y + step, z1, step == rise ? Blocks.STONE_BRICK_SLAB : Blocks.STONE_BRICKS);
|
||||
}
|
||||
void door(int x, int y, int z, Direction facing) { door(x, y, z, facing, false); }
|
||||
void door(int x, int y, int z, Direction facing, boolean right) {
|
||||
BlockState state = Blocks.DARK_OAK_DOOR.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing)
|
||||
.setValue(BlockStateProperties.DOOR_HINGE, right ? DoorHingeSide.RIGHT : DoorHingeSide.LEFT)
|
||||
.setValue(BlockStateProperties.OPEN, false).setValue(BlockStateProperties.POWERED, false);
|
||||
set(x, y, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.LOWER));
|
||||
set(x, y + 1, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.UPPER));
|
||||
}
|
||||
void doubleDoor(int x, int y, int z) { door(x, y, z, Direction.SOUTH, true); door(x + 1, y, z, Direction.SOUTH, false); }
|
||||
void northWindows(int x0, int x1, int z, int y) { box(x0, y, z, x1, y + 1, z, Blocks.GLASS_PANE); }
|
||||
void hang(int x, int ceiling, int z) { set(x, ceiling - 1, z, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true)); }
|
||||
void seat(int x, int y, int z, Direction view) {
|
||||
set(x, y, z, Blocks.DARK_OAK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, view.getOpposite()));
|
||||
}
|
||||
void lever(int x, int y, int z, Direction facing) {
|
||||
set(x, y, z, Blocks.LEVER.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR)
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, facing));
|
||||
}
|
||||
void rail(int x, int z, RailShape shape) { set(x, 1, z, Blocks.RAIL.defaultBlockState().setValue(BlockStateProperties.RAIL_SHAPE, shape)); }
|
||||
void loot(int x, int y, int z, int approachX, int approachZ) {
|
||||
target("loot_approach", approachX, y, approachZ);
|
||||
if (out != null) out.chest(rx(x, z), y, rz(x, z), ResourceKey.create(Registries.LOOT_TABLE,
|
||||
SanctuaryMod.id("chests/heritage19/" + s.kind().name().toLowerCase(Locale.ROOT))), s.seed() ^ 0x19CA7A10L);
|
||||
}
|
||||
void stairs(int x, int z, int base, int rise) {
|
||||
for (int step = 1; step <= rise; step++) for (int xx = x; xx <= x + 1; xx++) {
|
||||
box(xx, base + step + 1, z + step - 1, xx, base + step + 3, z + step - 1, Blocks.AIR);
|
||||
set(xx, base + step - 1, z + step - 1, Blocks.STONE_BRICKS);
|
||||
set(xx, base + step, z + step - 1, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
|
||||
}
|
||||
}
|
||||
void bed(int x, int y, int z, Direction facing) {
|
||||
BlockState state = Blocks.BED.white().defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing);
|
||||
set(x, y, z, state.setValue(BlockStateProperties.BED_PART, BedPart.FOOT));
|
||||
set(x + facing.getStepX(), y, z + facing.getStepZ(), state.setValue(BlockStateProperties.BED_PART, BedPart.HEAD));
|
||||
int wx = x + facing.getStepX() * 2, wz = z + facing.getStepZ() * 2;
|
||||
Direction along = facing.getClockWise();
|
||||
for (int side = -1; side <= 1; side++) for (int up = 0; up <= 2; up++)
|
||||
set(wx + along.getStepX() * side, y + up, wz + along.getStepZ() * side, Blocks.SMOOTH_STONE);
|
||||
}
|
||||
|
||||
void station() {
|
||||
shell(4, 0, 3, 12, 6, 11); gable(4, 3, 12, 11, 6);
|
||||
door(8, 1, 3, Direction.SOUTH); northWindows(5, 6, 3, 2); northWindows(10, 11, 3, 2);
|
||||
box(5, 1, 5, 6, 1, 5, Blocks.SMOOTH_STONE); set(5, 1, 8, Blocks.BARREL); seat(10, 1, 5, Direction.WEST);
|
||||
for (int z = 2; z <= 14; z++) rail(2, z, RailShape.NORTH_SOUTH);
|
||||
set(2, 1, 15, Blocks.DARK_OAK_FENCE);
|
||||
rail(8, 0, RailShape.NORTH_SOUTH); rail(8, 1, RailShape.NORTH_WEST);
|
||||
for (int x = 3; x <= 7; x++) rail(x, 1, RailShape.EAST_WEST);
|
||||
rail(2, 1, RailShape.SOUTH_EAST);
|
||||
box(0, 5, 5, 3, 5, 11, Blocks.DARK_OAK_PLANKS);
|
||||
for (int z : new int[] {5, 11}) box(1, 1, z, 1, 4, z, Blocks.STRIPPED_OAK_LOG);
|
||||
shell(9, 0, 11, 15, 4, 15); door(10, 1, 11, Direction.SOUTH); door(12, 1, 15, Direction.NORTH);
|
||||
set(10, 1, 13, Blocks.SMOOTH_STONE); lever(10, 2, 13, Direction.EAST);
|
||||
northWindows(13, 14, 15, 2); hang(8, 6, 7); hang(12, 4, 12);
|
||||
target("ticket_counter", 8, 1, 5); target("freight_platform", 3, 1, 9); target("signal_office", 12, 1, 12);
|
||||
target("secondary_entry", 12, 1, 16); loot(14, 1, 14, 13, 14);
|
||||
}
|
||||
void post() {
|
||||
shell(2, 0, 2, 14, 6, 14); door(8, 1, 2, Direction.SOUTH);
|
||||
northWindows(3, 6, 2, 2); northWindows(10, 13, 2, 2);
|
||||
box(3, 1, 7, 13, 5, 7, Blocks.SMOOTH_STONE); door(8, 1, 7, Direction.SOUTH);
|
||||
box(4, 1, 6, 6, 1, 6, Blocks.DARK_OAK_PLANKS);
|
||||
for (int z = 9; z <= 11; z++) { set(3, 1, z, Blocks.BARREL); set(3, 2, z, Blocks.BARREL); }
|
||||
set(6, 1, 10, Blocks.CRAFTING_TABLE); set(8, 1, 12, Blocks.BARREL);
|
||||
shell(10, 0, 10, 14, 6, 14); door(10, 1, 12, Direction.EAST); door(14, 1, 12, Direction.WEST);
|
||||
set(11, 1, 11, Blocks.CARTOGRAPHY_TABLE); hang(8, 6, 4); hang(7, 6, 10); hang(12, 6, 12);
|
||||
target("public_hall", 8, 1, 4); target("sorting_aisle", 7, 1, 10); target("factor_office", 12, 1, 12);
|
||||
target("secondary_entry", 15, 1, 12); loot(13, 1, 13, 12, 13);
|
||||
}
|
||||
void depot() {
|
||||
shell(1, 0, 2, 15, 9, 14); gable(1, 2, 15, 14, 9);
|
||||
box(7, 1, 2, 9, 3, 2, Blocks.AIR); door(4, 1, 2, Direction.SOUTH);
|
||||
for (int z : new int[] {5, 8}) {
|
||||
box(4, 1, z, 5, 1, z + 1, Blocks.DARK_OAK_PLANKS); set(4, 2, z, Blocks.BARREL);
|
||||
box(11, 1, z, 13, 1, z + 1, Blocks.DARK_OAK_PLANKS); set(13, 2, z + 1, Blocks.BARREL);
|
||||
}
|
||||
box(2, 5, 10, 14, 5, 13, Blocks.SMOOTH_STONE);
|
||||
for (int x = 5; x <= 14; x++) set(x, 6, 10, Blocks.IRON_BARS);
|
||||
shell(10, 5, 10, 14, 9, 14); door(10, 6, 12, Direction.EAST);
|
||||
set(11, 6, 11, Blocks.LECTERN.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
|
||||
stairs(2, 5, 0, 5); door(15, 1, 12, Direction.WEST); hang(8, 9, 6); hang(12, 9, 12);
|
||||
for (int z = 0; z <= 8; z++) rail(8, z, RailShape.NORTH_SOUTH);
|
||||
set(8, 1, 9, Blocks.DARK_OAK_FENCE);
|
||||
target("unloading_bay", 8, 1, 5); target("inventory_mezzanine", 5, 6, 12);
|
||||
target("secondary_entry", 16, 1, 12); loot(13, 6, 13, 12, 13);
|
||||
}
|
||||
void expedition() {
|
||||
shell(2, 0, 2, 12, 6, 12); door(8, 1, 2, Direction.SOUTH);
|
||||
northWindows(3, 5, 2, 2); northWindows(10, 11, 2, 2);
|
||||
set(4, 1, 4, Blocks.CARTOGRAPHY_TABLE); set(5, 1, 4, Blocks.LECTERN.defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH)); set(3, 1, 6, Blocks.BARREL);
|
||||
shell(2, 0, 8, 6, 6, 12); door(6, 1, 10, Direction.WEST); set(3, 1, 9, Blocks.BOOKSHELF);
|
||||
box(3, 0, 13, 14, 0, 15, Blocks.DARK_OAK_PLANKS);
|
||||
box(3, 1, 15, 14, 1, 15, Blocks.DARK_OAK_FENCE); door(10, 1, 12, Direction.NORTH);
|
||||
seat(6, 1, 13, Direction.SOUTH); hang(8, 6, 5); hang(4, 6, 10);
|
||||
target("expedition_table", 5, 1, 5); target("balcony", 10, 1, 14);
|
||||
target("secondary_entry", 10, 1, 13); loot(3, 1, 11, 4, 11);
|
||||
}
|
||||
boolean tower(int x, int z) { return x >= 3 && x <= 13 && z >= 3 && z <= 13 && Math.abs(x - 8) + Math.abs(z - 8) <= 8; }
|
||||
void observatory() {
|
||||
for (int y = 0; y <= 12; y++) for (int z = 3; z <= 13; z++) for (int x = 3; x <= 13; x++) if (tower(x, z)) {
|
||||
boolean wall = !tower(x - 1, z) || !tower(x + 1, z) || !tower(x, z - 1) || !tower(x, z + 1);
|
||||
set(x, y, z, y == 0 || y == 6 || y == 12 ? Blocks.SMOOTH_STONE.defaultBlockState()
|
||||
: wall ? LostCityPalette.masonry(s.seed(), x, y, z) : Blocks.AIR.defaultBlockState());
|
||||
if (y == 12 && wall) set(x, 13, z, Blocks.IRON_BARS);
|
||||
}
|
||||
door(8, 1, 3, Direction.SOUTH); door(8, 1, 13, Direction.NORTH);
|
||||
northWindows(7, 9, 3, 8); box(13, 8, 7, 13, 9, 9, Blocks.GLASS_PANE);
|
||||
shell(9, 0, 9, 12, 4, 12); door(9, 1, 10, Direction.EAST); set(10, 1, 11, Blocks.BARREL);
|
||||
set(10, 7, 5, Blocks.CARTOGRAPHY_TABLE); set(11, 7, 8, Blocks.SMOOTH_STONE); lever(11, 8, 8, Direction.WEST);
|
||||
stairs(4, 5, 0, 6); stairs(4, 5, 6, 6);
|
||||
box(4, 12, 11, 6, 12, 11, Blocks.SMOOTH_STONE); box(4, 13, 11, 6, 15, 11, Blocks.AIR);
|
||||
box(10, 13, 9, 10, 18, 9, Blocks.DARK_OAK_FENCE); box(9, 18, 9, 11, 18, 9, Blocks.IRON_BARS);
|
||||
hang(8, 6, 6); hang(10, 4, 11);
|
||||
target("instruments", 9, 7, 8); target("sky_platform", 8, 13, 8);
|
||||
target("secondary_entry", 8, 1, 14); loot(11, 1, 11, 11, 10);
|
||||
}
|
||||
void railway() {
|
||||
shell(1, 0, 2, 15, 6, 14); gable(1, 2, 15, 14, 6); doubleDoor(7, 1, 2);
|
||||
for (int z = 3; z <= 13; z++) for (int x : new int[] {5, 11}) set(x, 1, z, Blocks.RAIL);
|
||||
box(4, 1, 2, 6, 3, 2, Blocks.AIR); rail(8, 0, RailShape.NORTH_SOUTH); rail(8, 1, RailShape.NORTH_WEST);
|
||||
rail(7, 1, RailShape.EAST_WEST); rail(6, 1, RailShape.EAST_WEST); rail(5, 1, RailShape.SOUTH_EAST); rail(5, 2, RailShape.NORTH_SOUTH);
|
||||
box(7, -2, 5, 9, -2, 11, Blocks.SMOOTH_STONE); box(7, -1, 5, 9, 0, 11, Blocks.AIR);
|
||||
for (int step = 0; step <= 2; step++) set(8, -step, 4 + step, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
|
||||
for (int z = 6; z <= 11; z++) { set(6, 1, z, Blocks.IRON_BARS); set(10, 1, z, Blocks.IRON_BARS); }
|
||||
set(2, 1, 4, Blocks.CRAFTING_TABLE); set(3, 1, 4, Blocks.FURNACE.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
|
||||
set(2, 1, 6, Blocks.GRINDSTONE.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR));
|
||||
shell(1, 0, 10, 5, 5, 14); door(5, 1, 12, Direction.WEST); door(8, 1, 14, Direction.NORTH);
|
||||
set(5, 1, 9, Blocks.DARK_OAK_FENCE);
|
||||
set(2, 1, 11, Blocks.ANVIL); hang(8, 6, 8); hang(3, 5, 12);
|
||||
target("maintenance_pit", 8, -1, 9); target("workbench", 3, 1, 5);
|
||||
target("secondary_entry", 8, 1, 15); loot(2, 1, 13, 3, 13);
|
||||
}
|
||||
void pumping() {
|
||||
shell(2, 0, 3, 9, 6, 12); door(8, 1, 3, Direction.SOUTH);
|
||||
set(3, 1, 5, Blocks.DISPENSER.defaultBlockState().setValue(BlockStateProperties.FACING, Direction.EAST));
|
||||
set(4, 1, 5, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 3));
|
||||
set(3, 2, 5, Blocks.POLISHED_ANDESITE); box(3, 3, 5, 3, 5, 8, Blocks.IRON_BARS);
|
||||
for (int x : new int[] {10, 15}) for (int z : new int[] {5, 11}) box(x, 1, z, x, 7, z, Blocks.POLISHED_ANDESITE);
|
||||
shell(10, 8, 5, 15, 13, 11); box(11, 9, 6, 14, 11, 10, Blocks.WATER);
|
||||
box(10, 3, 6, 14, 3, 13, Blocks.SMOOTH_STONE); stairs(10, 3, 0, 3);
|
||||
shell(2, 0, 10, 6, 5, 15); door(5, 1, 10, Direction.SOUTH); door(5, 1, 15, Direction.NORTH);
|
||||
set(3, 1, 11, Blocks.BARREL); hang(7, 6, 7); hang(4, 5, 12);
|
||||
target("pump_room", 6, 1, 6); target("reservoir_walkway", 13, 4, 12);
|
||||
target("secondary_entry", 5, 1, 16); loot(3, 1, 14, 4, 14);
|
||||
}
|
||||
void substation() {
|
||||
for (int z = 2; z <= 14; z++) { set(1, 1, z, Blocks.IRON_BARS); set(15, 1, z, Blocks.IRON_BARS); }
|
||||
for (int x = 1; x <= 15; x++) if (x < 7 || x > 9) set(x, 1, 2, Blocks.IRON_BARS);
|
||||
for (int z : new int[] {5, 8}) {
|
||||
box(3, 1, z, 4, 2, z + 1, Blocks.POLISHED_ANDESITE); box(3, 3, z, 4, 3, z, Blocks.IRON_BARS);
|
||||
}
|
||||
shell(10, 0, 4, 15, 5, 10); door(10, 1, 6, Direction.EAST); northWindows(12, 14, 4, 2);
|
||||
set(12, 1, 5, Blocks.SMOOTH_STONE); lever(12, 2, 5, Direction.SOUTH); set(14, 1, 5, Blocks.CRAFTING_TABLE);
|
||||
shell(10, 0, 10, 15, 5, 15); door(12, 1, 10, Direction.SOUTH); door(15, 1, 13, Direction.WEST);
|
||||
box(6, -2, 10, 7, -2, 13, Blocks.SMOOTH_STONE); box(6, -1, 10, 7, 0, 13, Blocks.AIR);
|
||||
for (int step = 0; step <= 2; step++) set(6, -step, 9 + step, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
|
||||
hang(13, 5, 7); hang(12, 5, 13); target("distribution_yard", 8, 1, 6); target("cable_trench", 6, -1, 12);
|
||||
target("secondary_entry", 16, 1, 13); loot(14, 1, 14, 13, 14);
|
||||
}
|
||||
void greenhouse() {
|
||||
shell(1, 0, 2, 11, 6, 14);
|
||||
for (int y = 2; y <= 5; y++) for (int z = 3; z <= 13; z++) { set(1, y, z, Blocks.GLASS); set(11, y, z, Blocks.GLASS); }
|
||||
northWindows(2, 5, 2, 2); northWindows(7, 10, 2, 2); door(6, 1, 2, Direction.SOUTH);
|
||||
for (int x = 2; x <= 10; x++) for (int z = 3; z <= 13; z++) set(x, 6, z, x == 6 ? Blocks.SMOOTH_STONE : Blocks.GLASS);
|
||||
for (int x : new int[] {2, 3, 4, 8, 9, 10}) for (int z = 4; z <= 10; z++) {
|
||||
set(x, 0, z, Blocks.FARMLAND.defaultBlockState().setValue(BlockStateProperties.MOISTURE, 7));
|
||||
set(x, 1, z, Blocks.WHEAT.defaultBlockState().setValue(BlockStateProperties.AGE_7,
|
||||
2 + LostCityPalette.roll(s.seed(), x, 1, z, 6)));
|
||||
}
|
||||
set(3, 0, 6, Blocks.WATER); set(9, 0, 6, Blocks.WATER);
|
||||
set(3, 1, 6, Blocks.AIR); set(9, 1, 6, Blocks.AIR);
|
||||
shell(11, 0, 9, 15, 5, 15); door(11, 1, 12, Direction.EAST); door(13, 1, 15, Direction.NORTH);
|
||||
set(12, 1, 10, Blocks.CRAFTING_TABLE); set(13, 1, 10, Blocks.FLOWER_POT); set(9, 1, 12, Blocks.COMPOSTER);
|
||||
hang(6, 6, 5); hang(13, 5, 12); target("glasshouse_aisle", 6, 1, 7); target("potting_shed", 13, 1, 12);
|
||||
target("secondary_entry", 13, 1, 16); loot(14, 1, 14, 13, 14);
|
||||
}
|
||||
void dock() {
|
||||
box(0, 0, 0, 16, 0, 16, Blocks.DARK_OAK_PLANKS); box(0, 1, 16, 16, 1, 16, Blocks.DARK_OAK_FENCE);
|
||||
box(0, 1, 1, 0, 1, 15, Blocks.DARK_OAK_FENCE); box(16, 1, 1, 16, 1, 15, Blocks.DARK_OAK_FENCE);
|
||||
shell(2, 0, 3, 7, 5, 10); door(7, 1, 6, Direction.WEST); seat(3, 1, 5, Direction.EAST);
|
||||
shell(2, 0, 10, 7, 5, 15); door(5, 1, 10, Direction.SOUTH); door(7, 1, 13, Direction.WEST);
|
||||
box(12, 1, 5, 12, 11, 5, Blocks.STRIPPED_OAK_LOG); box(8, 11, 5, 14, 11, 5, Blocks.STRIPPED_OAK_LOG);
|
||||
box(14, 7, 5, 14, 10, 5, Blocks.IRON_CHAIN); hang(10, 11, 5); hang(5, 5, 6); hang(5, 5, 12);
|
||||
target("departure_deck", 8, 1, 14); target("waiting_room", 5, 1, 6); target("secondary_entry", 8, 1, 13);
|
||||
loot(3, 1, 14, 4, 14);
|
||||
}
|
||||
void radio() {
|
||||
shell(2, 0, 3, 10, 6, 12); door(8, 1, 3, Direction.SOUTH); northWindows(3, 5, 3, 2);
|
||||
bed(4, 1, 5, Direction.NORTH); set(8, 1, 5, Blocks.CARTOGRAPHY_TABLE);
|
||||
shell(2, 0, 8, 6, 6, 12); door(6, 1, 10, Direction.WEST); door(2, 1, 10, Direction.EAST);
|
||||
box(11, 5, 6, 15, 5, 10, Blocks.SMOOTH_STONE);
|
||||
for (int x : new int[] {11, 15}) for (int z : new int[] {6, 10}) box(x, 1, z, x, 4, z, Blocks.POLISHED_ANDESITE);
|
||||
box(13, 1, 8, 13, 18, 8, Blocks.POLISHED_ANDESITE); box(11, 17, 8, 15, 17, 8, Blocks.IRON_BARS);
|
||||
box(13, 19, 8, 13, 21, 8, Blocks.IRON_BARS); stairs(11, 1, 0, 5);
|
||||
hang(7, 6, 6); hang(4, 6, 10);
|
||||
target("caretaker_room", 7, 1, 6); target("antenna_platform", 12, 6, 9); target("secondary_entry", 1, 1, 10);
|
||||
loot(3, 1, 11, 4, 11);
|
||||
}
|
||||
void photo() {
|
||||
shell(2, 0, 2, 14, 6, 14); door(8, 1, 2, Direction.SOUTH); northWindows(3, 6, 2, 2); northWindows(10, 13, 2, 2);
|
||||
box(3, 1, 8, 13, 5, 8, Blocks.SMOOTH_STONE); door(4, 1, 8, Direction.SOUTH);
|
||||
box(8, 1, 9, 8, 5, 13, Blocks.SMOOTH_STONE); door(8, 1, 11, Direction.EAST);
|
||||
box(10, 1, 5, 12, 1, 5, Blocks.DARK_OAK_PLANKS); set(3, 1, 4, Blocks.BOOKSHELF);
|
||||
for (int z : new int[] {10, 12}) set(3, 1, z, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 3));
|
||||
box(11, 4, 10, 11, 5, 12, Blocks.IRON_CHAIN); set(12, 1, 10, Blocks.CRAFTING_TABLE);
|
||||
door(12, 1, 14, Direction.NORTH); hang(8, 6, 4); hang(5, 6, 11); hang(12, 6, 12);
|
||||
target("club_room", 8, 1, 4); target("darkroom", 5, 1, 11); target("drying_room", 10, 1, 11);
|
||||
target("secondary_entry", 12, 1, 15); loot(13, 1, 13, 12, 13);
|
||||
}
|
||||
void impossible() {
|
||||
shell(3, 0, 2, 11, 4, 7); door(8, 1, 2, Direction.SOUTH);
|
||||
for (int z = 3; z <= 5; z++) { set(4, 1, z, Blocks.BARREL); set(4, 2, z, Blocks.BARREL); }
|
||||
seat(10, 1, 4, Direction.WEST);
|
||||
shell(1, 0, 7, 15, 10, 15); door(8, 1, 7, Direction.SOUTH);
|
||||
for (int x = 2; x <= 14; x++) for (int z = 8; z <= 14; z++) set(x, 0, z,
|
||||
((x + z) & 1) == 0 ? Blocks.CALCITE : Blocks.POLISHED_ANDESITE);
|
||||
for (int x : new int[] {3, 6, 10, 13}) box(x, 1, 11, x, 9, 11, Blocks.CALCITE);
|
||||
shell(1, 0, 12, 5, 5, 15); door(5, 1, 13, Direction.WEST); door(3, 1, 15, Direction.NORTH);
|
||||
set(2, 2, 12, Blocks.GLASS); set(2, 1, 13, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 2));
|
||||
hang(8, 4, 4); hang(8, 10, 11); hang(3, 5, 13);
|
||||
target("lockers", 8, 1, 4); target("oversized_tiled_hall", 8, 1, 11);
|
||||
target("secondary_entry", 3, 1, 16); loot(2, 1, 14, 3, 14);
|
||||
}
|
||||
void refuge() {
|
||||
box(3, 4, 5, 14, 4, 15, Blocks.DARK_OAK_PLANKS);
|
||||
for (int x : new int[] {3, 14}) for (int z : new int[] {5, 15}) box(x, 1, z, x, 3, z, Blocks.STONE_BRICKS);
|
||||
shell(5, 4, 6, 13, 10, 14); gable(5, 6, 13, 14, 10); door(8, 5, 6, Direction.SOUTH);
|
||||
bed(6, 5, 8, Direction.NORTH); set(12, 5, 8, Blocks.CRAFTING_TABLE);
|
||||
shell(6, 0, 9, 11, 4, 14); door(6, 1, 11, Direction.EAST);
|
||||
// Carry the ladder through the floor so opening the hatch leaves a climbable lip.
|
||||
for (int y = 1; y <= 4; y++) set(10, y, 11, Blocks.LADDER.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST));
|
||||
set(10, 5, 11, Blocks.DARK_OAK_TRAPDOOR.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST)
|
||||
.setValue(BlockStateProperties.HALF, Half.BOTTOM));
|
||||
stairs(7, 1, 0, 4); box(2, 1, 3, 3, 11, 4, Blocks.STONE_BRICKS); box(2, 11, 3, 14, 11, 4, Blocks.STRIPPED_OAK_LOG);
|
||||
hang(10, 10, 8); hang(8, 4, 12);
|
||||
target("suspended_hut", 9, 5, 8); target("hidden_floor_hatch", 10, 5, 11); target("secondary_entry", 5, 1, 11);
|
||||
loot(7, 1, 13, 8, 13);
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package fr.koka.sanctuary.worldgen.heritage;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Optional;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureType;
|
||||
|
||||
/** Island-scale routes are split into small independent native starts before rendering. */
|
||||
public final class HeritageStructure19 extends Structure {
|
||||
public static final MapCodec<HeritageStructure19> CODEC = simpleCodec(HeritageStructure19::new);
|
||||
public HeritageStructure19(StructureSettings settings) { super(settings); }
|
||||
|
||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && generator.rootGeneration() != 22) || !generator.experimentalStructures()) return Optional.empty();
|
||||
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
|
||||
if (island == null || !island.origin() || (island.generation() != generator.rootGeneration())) return Optional.empty();
|
||||
var plan = generator.heritagePlan19();
|
||||
int x = context.chunkPos().x(), z = context.chunkPos().z();
|
||||
var pieces = new ArrayList<net.minecraft.world.level.levelgen.structure.StructurePiece>();
|
||||
// Local route segments render before buildings. No piece needs a distant link endpoint loaded.
|
||||
for (var segment : plan.segments())
|
||||
if (segment.startChunkX() == x && segment.startChunkZ() == z)
|
||||
pieces.add((generator.rootGeneration() == 21 || generator.rootGeneration() == 22) ? new HeritagePiece21(segment) : generator.rootGeneration() == 20 ? new HeritagePiece20(segment) : new HeritagePiece19(segment));
|
||||
for (var site : plan.sites()) if (site.startChunkX() == x && site.startChunkZ() == z)
|
||||
pieces.add((generator.rootGeneration() == 21 || generator.rootGeneration() == 22) ? new HeritagePiece21(site) : generator.rootGeneration() == 20 ? new HeritagePiece20(site) : new HeritagePiece19(site));
|
||||
if (pieces.isEmpty()) return Optional.empty();
|
||||
for (var piece : pieces) {
|
||||
var box = piece.getBoundingBox();
|
||||
if (Math.abs((long) Math.floorDiv(box.minX(), 16) - x) > 7
|
||||
|| Math.abs((long) Math.floorDiv(box.maxX(), 16) - x) > 7
|
||||
|| Math.abs((long) Math.floorDiv(box.minZ(), 16) - z) > 7
|
||||
|| Math.abs((long) Math.floorDiv(box.maxZ(), 16) - z) > 7)
|
||||
throw new IllegalStateException("Heritage piece exceeds its local native reference contract");
|
||||
}
|
||||
var locator = pieces.stream().filter(piece -> piece instanceof HeritagePiece19 old && old.isSite() || piece instanceof HeritagePiece20 detail && detail.isSite() || piece instanceof HeritagePiece21 current && current.isSite()).findFirst().orElse(pieces.getFirst()).getLocatorPosition();
|
||||
return Optional.of(new GenerationStub(locator, builder -> pieces.forEach(builder::addPiece)));
|
||||
}
|
||||
@Override public StructureType<?> type() { return HeritageStructures19.TYPE; }
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package fr.koka.sanctuary.worldgen.heritage;
|
||||
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureType;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceType;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
|
||||
/** Experimental generation19 uses independent native starts, references and saved room pieces. */
|
||||
public final class HeritageStructures19 {
|
||||
public static final ResourceKey<Structure> KEY = ResourceKey.create(Registries.STRUCTURE, SanctuaryMod.id("heritage_networks"));
|
||||
public static final StructureType<HeritageStructure19> TYPE = Registry.register(BuiltInRegistries.STRUCTURE_TYPE,
|
||||
SanctuaryMod.id("heritage_networks_v19"), () -> HeritageStructure19.CODEC);
|
||||
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
|
||||
SanctuaryMod.id("heritage_piece_v19"), HeritagePiece19::new);
|
||||
public static final StructurePieceType PIECE21 = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
|
||||
SanctuaryMod.id("heritage_piece_v21"), HeritagePiece21::new);
|
||||
public static final StructurePieceType PIECE20 = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
|
||||
SanctuaryMod.id("heritage_piece_v20"), HeritagePiece20::new);
|
||||
private static final ThreadLocal<Integer> PLACEMENT_DEPTH = ThreadLocal.withInitial(() -> 0);
|
||||
|
||||
private HeritageStructures19() {}
|
||||
public static void register() { /* Initialize serializers before the dynamic structure registry is read. */ }
|
||||
public static boolean isPlacing() { return PLACEMENT_DEPTH.get() > 0; }
|
||||
static void placing(Runnable renderer) {
|
||||
int previous = PLACEMENT_DEPTH.get();
|
||||
PLACEMENT_DEPTH.set(previous + 1);
|
||||
try { renderer.run(); }
|
||||
finally {
|
||||
if (previous == 0) PLACEMENT_DEPTH.remove();
|
||||
else PLACEMENT_DEPTH.set(previous);
|
||||
}
|
||||
}
|
||||
public static void generate(ExpansionChunkGenerator generator, RegistryAccess registries,
|
||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && generator.rootGeneration() != 22)
|
||||
|| !sanctuary.experimentalStructures()) return;
|
||||
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
||||
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
|
||||
var plan = sanctuary.heritagePlan19();
|
||||
if (!hasStart(plan, chunk.getPos().x(), chunk.getPos().z())) return;
|
||||
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||
Structure structure = holder.value();
|
||||
StructureStart previous = manager.getStartForStructure(structure, chunk);
|
||||
if (previous != null && previous.isValid()) return;
|
||||
var random = generator.randomState(island);
|
||||
var sampler = random.createClimateSampler(SamplerContext.builder().enableCaches().build());
|
||||
var start = structure.generate(holder, level, registries, generator, generator.getBiomeSource(), sampler,
|
||||
random, templates, island.seed(), chunk.getPos(), previous == null ? 0 : previous.getReferences(),
|
||||
chunk, structure.biomes()::contains);
|
||||
if (start.isValid()) manager.setStartForStructure(structure, start, chunk);
|
||||
}
|
||||
/** Starts are loaded at STRUCTURE_STARTS; locating never creates an expansion or rebuilds an existing start. */
|
||||
public static Pair<BlockPos, Holder<Structure>> findNearest(ServerLevel level, HolderSet<Structure> structures,
|
||||
BlockPos origin, int radius, boolean skipReferenced) {
|
||||
if (!Level.OVERWORLD.equals(level.dimension())
|
||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && generator.rootGeneration() != 22) || !generator.experimentalStructures()
|
||||
|| !level.structureManager().shouldGenerateStructures()) return null;
|
||||
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||
if (!structures.contains(holder)) return null;
|
||||
var candidates = new ArrayList<HeritagePlan19.Site>();
|
||||
int originChunkX = origin.getX() >> 4, originChunkZ = origin.getZ() >> 4;
|
||||
for (var network : generator.heritagePlan19().sites()) {
|
||||
var island = generator.islandForChunk(network.startChunkX(), network.startChunkZ());
|
||||
if (island == null || !island.origin() || island.generation() != generator.rootGeneration() || !island.status().equals("ready")) continue;
|
||||
if (Math.abs((long) network.startChunkX() - originChunkX) <= radius
|
||||
&& Math.abs((long) network.startChunkZ() - originChunkZ) <= radius) candidates.add(network);
|
||||
}
|
||||
candidates.sort(Comparator.comparingDouble(n -> origin.distSqr(locator(n))));
|
||||
var visited = new HashSet<Long>();
|
||||
for (var network : candidates) {
|
||||
long key = ((long) network.startChunkX() << 32) ^ (network.startChunkZ() & 0xffffffffL);
|
||||
if (!visited.add(key)) continue;
|
||||
var chunk = level.getChunk(network.startChunkX(), network.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
StructureStart start = level.structureManager().getStartForStructure(holder.value(), chunk);
|
||||
if (start == null || !start.isValid() || skipReferenced && !start.canBeReferenced()) continue;
|
||||
if (skipReferenced) level.structureManager().addReference(start);
|
||||
var entrance = start.getPieces().stream().filter(p -> (p instanceof HeritagePiece19 site && site.isSite())
|
||||
|| (p instanceof HeritagePiece20 detail && detail.isSite()) || (p instanceof HeritagePiece21 current && current.isSite())).findFirst().orElseThrow();
|
||||
return Pair.of(entrance.getLocatorPosition(), holder);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
static BlockPos locator(HeritagePlan19.Site site) { return HeritagePiece19.locator(site); }
|
||||
static boolean hasStart(HeritagePlan19 plan, int x, int z) {
|
||||
return plan.sites().stream().anyMatch(s -> s.startChunkX() == x && s.startChunkZ() == z)
|
||||
|| plan.segments().stream().anyMatch(s -> s.startChunkX() == x && s.startChunkZ() == z);
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package fr.koka.sanctuary.worldgen.heritage;
|
||||
|
||||
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityPlan;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
|
||||
/** Read-only pre-decoration terrain. No loaded chunks or player-edited blocks enter a plan. */
|
||||
public final class HeritageTerrain19 implements HeritagePlanner19.Terrain {
|
||||
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
|
||||
private final ExpansionIsland island;
|
||||
private final NoiseBasedChunkGenerator generator;
|
||||
private final RandomState random;
|
||||
private final LevelHeightAccessor height;
|
||||
private final Predicate<BlockPos> protection;
|
||||
private final Map<Long, NoiseColumn> columns = new HashMap<>();
|
||||
private List<HeritagePlan19.Landmark> landmarks = List.of();
|
||||
|
||||
public HeritageTerrain19(ExpansionIsland island, NoiseBasedChunkGenerator generator, RandomState random,
|
||||
LevelHeightAccessor height, Predicate<BlockPos> protection) {
|
||||
this.island = island; this.generator = generator; this.random = random;
|
||||
this.height = height; this.protection = protection;
|
||||
}
|
||||
private NoiseColumn column(int x, int z) {
|
||||
if (!owns(x, z)) return null;
|
||||
long key = ((long) x << 32) ^ (z & 0xffffffffL);
|
||||
var previous = columns.get(key);
|
||||
if (previous != null) return previous;
|
||||
if (columns.size() >= HeritagePlan19.MAX_COLUMNS) return null;
|
||||
var result = generator.getBaseColumn(x, z, height, random);
|
||||
columns.put(key, result);
|
||||
return result;
|
||||
}
|
||||
@Override public boolean owns(int x, int z) { return island.contains(x, z); }
|
||||
@Override public boolean solid(int x, int y, int z) {
|
||||
if (y < 0 || y >= 384) return false;
|
||||
var column = column(x, z);
|
||||
return column != null && column.getBlock(y).isSolid() && column.getBlock(y).getFluidState().isEmpty();
|
||||
}
|
||||
@Override public boolean protectedAt(int x, int y, int z) {
|
||||
if (y < 0 || y >= 384) return true;
|
||||
var column = column(x, z);
|
||||
return column == null || !column.getBlock(y).getFluidState().isEmpty()
|
||||
|| protection.test(new BlockPos(x, y, z));
|
||||
}
|
||||
@Override public int surfaceY(int x, int z) {
|
||||
var column = column(x, z);
|
||||
if (column == null) return -1;
|
||||
for (int y = 380; y >= 1; y--) {
|
||||
var state = column.getBlock(y);
|
||||
if (!state.isAir()) return state.isSolid() && state.getFluidState().isEmpty() ? y : -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@Override public List<HeritagePlan19.Landmark> landmarks() { return landmarks; }
|
||||
public int columnsUsed() { return columns.size(); }
|
||||
|
||||
/** Select actual dry ground abutting existing walkable platforms. Their protected blocks stay intact. */
|
||||
public void discoverLandmarks(LostCityPlan city, RuinsPlan17 ruins) {
|
||||
var result = new ArrayList<HeritagePlan19.Landmark>();
|
||||
for (var network : ruins.networks()) {
|
||||
var access = network.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS).findFirst().orElse(null);
|
||||
if (access == null) continue;
|
||||
int deck = access.maxY() - 2;
|
||||
for (int direction = 0; direction < 4; direction++) {
|
||||
var entry = dryEntry(access.centerX() + DX[direction] * 3, access.centerZ() + DZ[direction] * 3, deck, direction);
|
||||
if (entry == null) continue;
|
||||
var kind = network.family() == RuinsPlan17.Family.MINE ? HeritagePlan19.LandmarkKind.MINE : HeritagePlan19.LandmarkKind.UTILITY;
|
||||
result.add(new HeritagePlan19.Landmark("old_" + network.id(), kind, entry));
|
||||
break;
|
||||
}
|
||||
}
|
||||
int cityEntries = 0;
|
||||
outer: for (var district : city.districts()) for (var parcel : district.parcels()) {
|
||||
if (parcel.kind() != LostCityPlan.Kind.STREET) continue;
|
||||
boolean eastWest = parcel.width() > parcel.depth();
|
||||
for (int direction : eastWest ? new int[]{1, 3} : new int[]{0, 2}) {
|
||||
int x = direction == 1 ? parcel.width() - 1 : direction == 3 ? 0 : parcel.width() / 2;
|
||||
int z = direction == 2 ? parcel.depth() - 1 : direction == 0 ? 0 : parcel.depth() / 2;
|
||||
var entry = dryEntry(parcel.minX() + x + DX[direction], parcel.minZ() + z + DZ[direction], parcel.surfaceY(x, z), direction);
|
||||
if (entry == null) continue;
|
||||
result.add(new HeritagePlan19.Landmark("old_city_" + cityEntries++, HeritagePlan19.LandmarkKind.CITY, entry));
|
||||
if (cityEntries == 2) break outer;
|
||||
}
|
||||
}
|
||||
landmarks = List.copyOf(result);
|
||||
}
|
||||
private HeritagePlan19.Entry dryEntry(int x, int z, int deck, int direction) {
|
||||
int y = surfaceY(x, z);
|
||||
if (y < 32 || y > 348 || Math.abs(y - deck) > 1) return null;
|
||||
// A three-wide arrival can abut a platform without cutting its wall or footings.
|
||||
for (int side = -1; side <= 1; side++) {
|
||||
int px = x + DZ[direction] * side, pz = z + DX[direction] * side;
|
||||
var column = column(px, pz);
|
||||
if (column == null || surfaceY(px, pz) != y || !solid(px, y, pz)) return null;
|
||||
for (int by = y - 1; by <= y + 3; by++) if (protectedAt(px, by, pz)) return null;
|
||||
for (int by = y + 1; by <= y + 3; by++) if (!column.getBlock(by).isAir()) return null;
|
||||
}
|
||||
return new HeritagePlan19.Entry(x, y + 1, z, direction);
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package fr.koka.sanctuary.worldgen.heritage;
|
||||
|
||||
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityPlan;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
|
||||
/** Read-only pre-decoration terrain. No loaded chunks or player-edited blocks enter a plan. */
|
||||
public final class HeritageTerrain21 implements HeritagePlanner21.Terrain {
|
||||
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
|
||||
private final ExpansionIsland island;
|
||||
private final NoiseBasedChunkGenerator generator;
|
||||
private final RandomState random;
|
||||
private final LevelHeightAccessor height;
|
||||
private final Predicate<BlockPos> protection;
|
||||
private final Map<Long, NoiseColumn> columns = new HashMap<>();
|
||||
private List<HeritagePlan19.Landmark> landmarks = List.of();
|
||||
|
||||
public HeritageTerrain21(ExpansionIsland island, NoiseBasedChunkGenerator generator, RandomState random,
|
||||
LevelHeightAccessor height, Predicate<BlockPos> protection) {
|
||||
this.island = island; this.generator = generator; this.random = random;
|
||||
this.height = height; this.protection = protection;
|
||||
}
|
||||
private NoiseColumn column(int x, int z) {
|
||||
if (!owns(x, z)) return null;
|
||||
long key = ((long) x << 32) ^ (z & 0xffffffffL);
|
||||
var previous = columns.get(key);
|
||||
if (previous != null) return previous;
|
||||
if (columns.size() >= HeritagePlan19.MAX_COLUMNS) return null;
|
||||
var result = generator.getBaseColumn(x, z, height, random);
|
||||
columns.put(key, result);
|
||||
return result;
|
||||
}
|
||||
@Override public boolean owns(int x, int z) { return island.contains(x, z); }
|
||||
@Override public boolean solid(int x, int y, int z) {
|
||||
if (y < 0 || y >= 384) return false;
|
||||
var column = column(x, z);
|
||||
return column != null && column.getBlock(y).isSolid() && column.getBlock(y).getFluidState().isEmpty();
|
||||
}
|
||||
@Override public boolean protectedAt(int x, int y, int z) {
|
||||
if (y < 0 || y >= 384) return true;
|
||||
var column = column(x, z);
|
||||
return column == null || !column.getBlock(y).getFluidState().isEmpty()
|
||||
|| protection.test(new BlockPos(x, y, z));
|
||||
}
|
||||
@Override public int surfaceY(int x, int z) {
|
||||
var column = column(x, z);
|
||||
if (column == null) return -1;
|
||||
for (int y = 380; y >= 1; y--) {
|
||||
var state = column.getBlock(y);
|
||||
if (!state.isAir()) return state.isSolid() && state.getFluidState().isEmpty() ? y : -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@Override public List<HeritagePlan19.Landmark> landmarks() { return landmarks; }
|
||||
public int columnsUsed() { return columns.size(); }
|
||||
|
||||
/** Select actual dry ground abutting existing walkable platforms. Their protected blocks stay intact. */
|
||||
public void discoverLandmarks(LostCityPlan city, RuinsPlan17 ruins) {
|
||||
var result = new ArrayList<HeritagePlan19.Landmark>();
|
||||
for (var network : ruins.networks()) {
|
||||
var access = network.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS).findFirst().orElse(null);
|
||||
if (access == null) continue;
|
||||
int deck = access.maxY() - 2;
|
||||
for (int direction = 0; direction < 4; direction++) {
|
||||
var entry = dryEntry(access.centerX() + DX[direction] * 3, access.centerZ() + DZ[direction] * 3, deck, direction);
|
||||
if (entry == null) continue;
|
||||
var kind = network.family() == RuinsPlan17.Family.MINE ? HeritagePlan19.LandmarkKind.MINE : HeritagePlan19.LandmarkKind.UTILITY;
|
||||
result.add(new HeritagePlan19.Landmark("old_" + network.id(), kind, entry));
|
||||
break;
|
||||
}
|
||||
}
|
||||
int cityEntries = 0;
|
||||
for (var district : city.districts()) {
|
||||
int districtEntries = 0;
|
||||
districtLoop: for (var parcel : district.parcels()) {
|
||||
if (parcel.kind() != LostCityPlan.Kind.STREET || parcel.width() < 3 || parcel.depth() < 3) continue;
|
||||
for (int direction = 0; direction < 4; direction++) {
|
||||
int x = direction == 1 ? parcel.width() - 1 : direction == 3 ? 0 : parcel.width() / 2;
|
||||
int z = direction == 2 ? parcel.depth() - 1 : direction == 0 ? 0 : parcel.depth() / 2;
|
||||
var entry = dryEntry(parcel.minX() + x + DX[direction], parcel.minZ() + z + DZ[direction], parcel.surfaceY(x, z), direction);
|
||||
if (entry == null) continue;
|
||||
result.add(new HeritagePlan19.Landmark("old_city_" + cityEntries++, HeritagePlan19.LandmarkKind.CITY, entry));
|
||||
if (++districtEntries == 2 || result.size() >= 16) break districtLoop;
|
||||
}
|
||||
}
|
||||
if (result.size() >= 16) break;
|
||||
}
|
||||
landmarks = List.copyOf(result);
|
||||
}
|
||||
private HeritagePlan19.Entry dryEntry(int x, int z, int deck, int direction) {
|
||||
int y = surfaceY(x, z);
|
||||
if (y < 32 || y > 348 || Math.abs(y - deck) > 1) return null;
|
||||
// A three-wide arrival can abut a platform without cutting its wall or footings.
|
||||
for (int side = -1; side <= 1; side++) {
|
||||
int px = x + DZ[direction] * side, pz = z + DX[direction] * side;
|
||||
var column = column(px, pz);
|
||||
if (column == null || surfaceY(px, pz) != y || !solid(px, y, pz)) return null;
|
||||
for (int by = y - 1; by <= y + 3; by++) if (protectedAt(px, by, pz)) return null;
|
||||
for (int by = y + 1; by <= y + 3; by++) if (!column.getBlock(by).isAir()) return null;
|
||||
}
|
||||
return new HeritagePlan19.Entry(x, y + 1, z, direction);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package fr.koka.sanctuary.worldgen.ruins;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Set;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
|
||||
/** Saved generation20 room geometry is rendered without replanning or reading neighboring chunks. */
|
||||
public final class RuinsPiece20 extends StructurePiece {
|
||||
private static final Set<String> ROOM_KEYS = Set.of("Schema", "Kind", "MinX", "MinY", "MinZ", "Width",
|
||||
"Height", "Depth", "Openings", "RoomSeed", "Supports");
|
||||
private static final Set<String> SUPPORT_KEYS = Set.of("X", "MinY", "Z", "MaxY");
|
||||
private final RuinsPlan17.Room room;
|
||||
|
||||
public RuinsPiece20(RuinsPlan17.Room room) {
|
||||
super(RuinsStructures17.PIECE20, 0, bounds(room));
|
||||
this.room = room;
|
||||
setOrientation(null);
|
||||
}
|
||||
public RuinsPiece20(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||
super(RuinsStructures17.PIECE20, tag);
|
||||
room = decodeRoom(tag.getCompound("SanctuaryRoom")
|
||||
.orElseThrow(() -> new IllegalArgumentException("Missing saved forgotten room")));
|
||||
if (!getBoundingBox().equals(bounds(room)))
|
||||
throw new IllegalArgumentException("Forgotten room disagrees with its saved native bounding box");
|
||||
setOrientation(null);
|
||||
}
|
||||
public RuinsPlan17.Room room() { return room; }
|
||||
@Override public BlockPos getLocatorPosition() { return locator(room); }
|
||||
public static BlockPos locator(RuinsPlan17.Room room) {
|
||||
return new BlockPos(room.minX() + room.width() / 2,
|
||||
room.kind() == RuinsPlan17.Kind.ACCESS ? room.minY() + room.height() - 2 : room.floorY() + 1,
|
||||
room.minZ() + room.depth() / 2);
|
||||
}
|
||||
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||
tag.put("SanctuaryRoom", encodeRoom(room));
|
||||
}
|
||||
public static CompoundTag encodeRoom(RuinsPlan17.Room room) {
|
||||
var tag = new CompoundTag(); tag.putInt("Schema", 1); tag.putString("Kind", room.kind().name());
|
||||
tag.putInt("MinX", room.minX()); tag.putInt("MinY", room.minY()); tag.putInt("MinZ", room.minZ());
|
||||
tag.putInt("Width", room.width()); tag.putInt("Height", room.height()); tag.putInt("Depth", room.depth());
|
||||
tag.putInt("Openings", room.openings()); tag.putLong("RoomSeed", room.seed());
|
||||
var supports = new ListTag();
|
||||
for (var support : room.supports()) {
|
||||
var value = new CompoundTag(); value.putInt("X", support.x()); value.putInt("MinY", support.minY());
|
||||
value.putInt("Z", support.z()); value.putInt("MaxY", support.maxY()); supports.add(value);
|
||||
}
|
||||
tag.put("Supports", supports);
|
||||
return tag;
|
||||
}
|
||||
public static RuinsPlan17.Room decodeRoom(CompoundTag tag) {
|
||||
if (!tag.keySet().equals(ROOM_KEYS)) throw new IllegalArgumentException("Incomplete or unknown forgotten room fields");
|
||||
if (integer(tag, "Schema") != 1) throw new IllegalArgumentException("Unsupported forgotten room schema");
|
||||
var kind = RuinsPlan17.Kind.valueOf(tag.getString("Kind").orElseThrow(() -> new IllegalArgumentException("Missing room kind")));
|
||||
type(tag, "RoomSeed", Tag.TAG_LONG); type(tag, "Supports", Tag.TAG_LIST);
|
||||
var saved = tag.getList("Supports").orElseThrow();
|
||||
if (saved.size() > 64) throw new IllegalArgumentException("Too many saved forgotten room supports");
|
||||
var supports = new ArrayList<RuinsPlan17.Support>();
|
||||
for (var entry : saved) {
|
||||
if (!(entry instanceof CompoundTag value) || !value.keySet().equals(SUPPORT_KEYS))
|
||||
throw new IllegalArgumentException("Invalid forgotten room support fields");
|
||||
supports.add(new RuinsPlan17.Support(integer(value, "X"), integer(value, "MinY"), integer(value, "Z"), integer(value, "MaxY")));
|
||||
}
|
||||
return new RuinsPlan17.Room(kind, integer(tag, "MinX"), integer(tag, "MinY"), integer(tag, "MinZ"),
|
||||
integer(tag, "Width"), integer(tag, "Height"), integer(tag, "Depth"), integer(tag, "Openings"),
|
||||
tag.getLong("RoomSeed").orElseThrow(), supports);
|
||||
}
|
||||
private static int integer(CompoundTag tag, String key) {
|
||||
type(tag, key, Tag.TAG_INT);
|
||||
return tag.getInt(key).orElseThrow();
|
||||
}
|
||||
private static void type(CompoundTag tag, String key, byte id) {
|
||||
if (tag.get(key) == null || tag.get(key).getId() != id)
|
||||
throw new IllegalArgumentException("Invalid saved forgotten room field: " + key);
|
||||
}
|
||||
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
|
||||
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
|
||||
RuinsStructures17.placing(() -> RuinsRenderer20.render(room, new LostCityRenderer.BlockWriter() {
|
||||
private BlockPos world(int x, int y, int z) { return new BlockPos(room.minX() + x, room.minY() + y, room.minZ() + z); }
|
||||
@Override public void set(int x, int y, int z, BlockState block) {
|
||||
var position = world(x, y, z);
|
||||
if (getBoundingBox().isInside(position) && clip.isInside(position))
|
||||
placeBlock(level, block, position.getX(), position.getY(), position.getZ(), clip);
|
||||
}
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
|
||||
var position = world(x, y, z);
|
||||
if (getBoundingBox().isInside(position) && clip.isInside(position))
|
||||
createChest(level, clip, RandomSource.create(seed), position, loot, null);
|
||||
}
|
||||
}));
|
||||
}
|
||||
public static BoundingBox bounds(RuinsPlan17.Room room) {
|
||||
int minY = room.minY(), maxY = Math.addExact(room.minY(), room.height() - 1);
|
||||
for (var support : room.supports()) {
|
||||
minY = Math.min(minY, support.minY()); maxY = Math.max(maxY, support.maxY());
|
||||
}
|
||||
return new BoundingBox(room.minX(), minY, room.minZ(), Math.addExact(room.minX(), room.width() - 1),
|
||||
maxY, Math.addExact(room.minZ(), room.depth() - 1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
package fr.koka.sanctuary.worldgen.ruins;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityPalette;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.AttachFace;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.storage.loot.BuiltInLootTables;
|
||||
|
||||
/** Generation20 architectural details: original, physical rooms reconstructed solely from their saved inputs.
|
||||
* Coordinates given to the writer are local to the saved room. There are no terrain reads, entities,
|
||||
* active machines or free fluids. Openings are restored last so connected pieces share dry thresholds. */
|
||||
public final class RuinsRenderer20 {
|
||||
private static final Direction[] DIRECTIONS = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
|
||||
private RuinsRenderer20() {}
|
||||
|
||||
/** Local standing positions for native path checks, including approaches to useful fixtures. */
|
||||
public record Target(String name, int x, int y, int z) {}
|
||||
|
||||
public static List<Target> targets(RuinsPlan17.Room room) {
|
||||
var targets = new ArrayList<Target>();
|
||||
if (room.kind() == RuinsPlan17.Kind.ACCESS) {
|
||||
targets.add(new Target("surface_hatch_approach", room.width() / 2, room.height() - 2, room.depth() / 2));
|
||||
targets.add(new Target("shaft_bottom", room.width() / 2, 2, room.depth() / 2));
|
||||
} else {
|
||||
targets.add(new Target("central_crossing", room.width() / 2, 2, room.depth() / 2));
|
||||
switch (room.kind()) {
|
||||
case WORKSHOP -> {
|
||||
targets.add(decorTarget(room, "workbench", 2, 2, 2));
|
||||
targets.add(decorTarget(room, "unfinished_work_platform", room.width() - 3, 4, room.depth() - 3));
|
||||
}
|
||||
case STORE -> targets.add(decorTarget(room, "supply_chest", room.width() - 3, 2, room.depth() - 2));
|
||||
case ARCHIVE -> targets.add(decorTarget(room, "reading_desk", 2, 2, 2));
|
||||
case PUMP_HALL, CONTROL_ROOM -> targets.add(decorTarget(room, "controls", 2, 2, 2));
|
||||
default -> { }
|
||||
}
|
||||
}
|
||||
for (int direction = 0; direction < 4; direction++) if ((room.openings() & 1 << direction) != 0) {
|
||||
int x = direction == 1 ? room.width() - 1 : direction == 3 ? 0 : room.width() / 2;
|
||||
int z = direction == 2 ? room.depth() - 1 : direction == 0 ? 0 : room.depth() / 2;
|
||||
targets.add(new Target("opening_" + DIRECTIONS[direction].getName(), x, 2, z));
|
||||
}
|
||||
return List.copyOf(targets);
|
||||
}
|
||||
|
||||
public static void render(RuinsPlan17.Room room, LostCityRenderer.BlockWriter output) {
|
||||
var buffer = fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20.buffer(output);
|
||||
var b = new Builder(room, buffer);
|
||||
b.shell();
|
||||
switch (room.kind()) {
|
||||
case ACCESS -> b.access();
|
||||
case CORRIDOR -> b.corridor();
|
||||
case MINE_GALLERY -> b.mine();
|
||||
case WORKSHOP -> b.workshop();
|
||||
case STORE -> b.store();
|
||||
case TILED_HALL -> b.tiledHall();
|
||||
case ARCHIVE -> b.archive();
|
||||
case SERVICE_HALL -> b.service();
|
||||
case PUMP_HALL -> b.pumps();
|
||||
case CONTROL_ROOM -> b.controls();
|
||||
}
|
||||
if (room.kind() != RuinsPlan17.Kind.ACCESS) { b.architecture(); b.lights(); }
|
||||
b.openings();
|
||||
for (var support : room.supports()) for (int y = support.minY(); y <= support.maxY(); y++)
|
||||
buffer.set(support.x() - room.minX(), y - room.minY(), support.z() - room.minZ(),
|
||||
Blocks.STONE_BRICKS.defaultBlockState());
|
||||
buffer.flush();
|
||||
}
|
||||
|
||||
private static boolean turn(RuinsPlan17.Room room) { return (room.seed() & 1L) != 0; }
|
||||
private static Target decorTarget(RuinsPlan17.Room room, String name, int x, int y, int z) {
|
||||
return new Target(name, turn(room) ? room.width() - 1 - x : x, y, turn(room) ? room.depth() - 1 - z : z);
|
||||
}
|
||||
|
||||
private static final class Builder {
|
||||
final RuinsPlan17.Room room;
|
||||
final LostCityRenderer.BlockWriter out;
|
||||
final int w, h, d, cx, cz;
|
||||
final boolean reverse;
|
||||
Builder(RuinsPlan17.Room room, LostCityRenderer.BlockWriter out) {
|
||||
this.room = room; this.out = out; w = room.width(); h = room.height(); d = room.depth();
|
||||
cx = w / 2; cz = d / 2; reverse = turn(room);
|
||||
}
|
||||
void set(int x, int y, int z, BlockState state) { out.set(x, y, z, state); }
|
||||
void set(int x, int y, int z, net.minecraft.world.level.block.Block block) { set(x, y, z, block.defaultBlockState()); }
|
||||
void decor(int x, int y, int z, BlockState state) {
|
||||
out.set(reverse ? w - 1 - x : x, y, reverse ? d - 1 - z : z,
|
||||
reverse ? state.rotate(Rotation.CLOCKWISE_180) : state);
|
||||
}
|
||||
void decor(int x, int y, int z, net.minecraft.world.level.block.Block block) { decor(x, y, z, block.defaultBlockState()); }
|
||||
boolean crossing(int x, int z) { return Math.abs(x - cx) <= 1 || Math.abs(z - cz) <= 1; }
|
||||
|
||||
void shell() {
|
||||
for (int y = 0; y < h; y++) for (int z = 0; z < d; z++) for (int x = 0; x < w; x++) {
|
||||
BlockState state;
|
||||
boolean wall = x == 0 || x == w - 1 || z == 0 || z == d - 1;
|
||||
if (y == 0) state = Blocks.STONE_BRICKS.defaultBlockState();
|
||||
else if (y == 1) state = floor(x, z);
|
||||
else if (wall || y == h - 1) state = wallMaterial(x, y, z);
|
||||
else state = Blocks.AIR.defaultBlockState();
|
||||
set(x, y, z, state);
|
||||
}
|
||||
// A continuous ceiling cornice and stone thresholds remain intact despite the weathering.
|
||||
for (int x = 0; x < w; x++) for (int z = 0; z < d; z++) if (x == 0 || x == w - 1 || z == 0 || z == d - 1)
|
||||
set(x, h - 2, z, trim());
|
||||
}
|
||||
net.minecraft.world.level.block.Block trim() {
|
||||
return switch (room.kind()) {
|
||||
case ARCHIVE -> Blocks.STRIPPED_OAK_WOOD;
|
||||
case TILED_HALL, SERVICE_HALL -> Blocks.POLISHED_DIORITE;
|
||||
case WORKSHOP, STORE -> Blocks.TUFF_BRICKS;
|
||||
case MINE_GALLERY -> Blocks.STRIPPED_OAK_WOOD;
|
||||
default -> Blocks.POLISHED_TUFF;
|
||||
};
|
||||
}
|
||||
BlockState wallMaterial(int x, int y, int z) {
|
||||
if (y <= 2) return Blocks.STONE_BRICKS.defaultBlockState();
|
||||
if (y == h - 1) return (room.kind() == RuinsPlan17.Kind.ARCHIVE
|
||||
? Blocks.BIRCH_PLANKS : Blocks.SMOOTH_STONE).defaultBlockState();
|
||||
int along = x == 0 || x == w - 1 ? z : x;
|
||||
net.minecraft.world.level.block.Block wall = switch (room.kind()) {
|
||||
case ARCHIVE -> along % 4 == 0 ? Blocks.STRIPPED_OAK_WOOD : Blocks.BIRCH_PLANKS;
|
||||
case WORKSHOP -> along % 4 == 0 ? Blocks.POLISHED_ANDESITE : Blocks.TUFF_BRICKS;
|
||||
case STORE -> along % 4 == 0 ? Blocks.STRIPPED_OAK_WOOD : Blocks.POLISHED_GRANITE;
|
||||
case MINE_GALLERY -> along % 4 == 0 ? Blocks.STRIPPED_OAK_WOOD : Blocks.TUFF;
|
||||
case TILED_HALL -> y == 3 ? Blocks.POLISHED_DIORITE : Blocks.CALCITE;
|
||||
case SERVICE_HALL -> y == 3 ? Blocks.BIRCH_PLANKS : Blocks.SMOOTH_STONE;
|
||||
case PUMP_HALL -> along % 4 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS;
|
||||
case CONTROL_ROOM -> along % 4 == 0 ? Blocks.POLISHED_ANDESITE : Blocks.SMOOTH_STONE;
|
||||
default -> along % 4 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS;
|
||||
};
|
||||
return wall.defaultBlockState();
|
||||
}
|
||||
BlockState floor(int x, int z) {
|
||||
boolean border = x == 1 || x == w - 2 || z == 1 || z == d - 2;
|
||||
return switch (room.kind()) {
|
||||
case MINE_GALLERY -> (x == cx ? Blocks.DARK_OAK_PLANKS : (x + z) % 5 == 0 ? Blocks.ANDESITE : Blocks.STONE).defaultBlockState();
|
||||
case ARCHIVE -> (border ? Blocks.STRIPPED_OAK_WOOD : Blocks.BIRCH_PLANKS).defaultBlockState();
|
||||
case WORKSHOP -> (border ? Blocks.POLISHED_GRANITE : Blocks.SMOOTH_STONE).defaultBlockState();
|
||||
case STORE -> (border ? Blocks.TUFF_BRICKS : Blocks.OAK_PLANKS).defaultBlockState();
|
||||
case TILED_HALL -> ((x / 2 + z / 2) % 2 == 0 ? Blocks.CALCITE : Blocks.POLISHED_DIORITE).defaultBlockState();
|
||||
case SERVICE_HALL -> (border ? Blocks.POLISHED_DIORITE : Blocks.SMOOTH_STONE).defaultBlockState();
|
||||
case PUMP_HALL, CONTROL_ROOM -> (x == cx || z == cz ? Blocks.POLISHED_ANDESITE : Blocks.SMOOTH_STONE).defaultBlockState();
|
||||
default -> (border && Math.floorMod(x + z + room.seed(), 4) == 0 ? Blocks.CHISELED_TUFF_BRICKS
|
||||
: border ? Blocks.TUFF_BRICKS : Blocks.POLISHED_TUFF).defaultBlockState();
|
||||
};
|
||||
}
|
||||
|
||||
void architecture() {
|
||||
// High inset cornices soften the square excavation without taking away the
|
||||
// three-wide walking cross. The outer stone shell stays closed and watertight.
|
||||
var stair = switch (room.kind()) {
|
||||
case ARCHIVE -> Blocks.OAK_STAIRS;
|
||||
case MINE_GALLERY, STORE -> Blocks.OAK_STAIRS;
|
||||
case TILED_HALL, SERVICE_HALL -> Blocks.POLISHED_DIORITE_STAIRS;
|
||||
default -> Blocks.TUFF_BRICK_STAIRS;
|
||||
};
|
||||
for (int x = 2; x < w - 2; x++) {
|
||||
set(x, h - 2, 1, stair.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH)
|
||||
.setValue(BlockStateProperties.HALF, net.minecraft.world.level.block.state.properties.Half.TOP));
|
||||
set(x, h - 2, d - 2, stair.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH)
|
||||
.setValue(BlockStateProperties.HALF, net.minecraft.world.level.block.state.properties.Half.TOP));
|
||||
}
|
||||
for (int z = 2; z < d - 2; z++) {
|
||||
set(1, h - 2, z, stair.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST)
|
||||
.setValue(BlockStateProperties.HALF, net.minecraft.world.level.block.state.properties.Half.TOP));
|
||||
set(w - 2, h - 2, z, stair.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST)
|
||||
.setValue(BlockStateProperties.HALF, net.minecraft.world.level.block.state.properties.Half.TOP));
|
||||
}
|
||||
// Exposed joists read as timber construction in the archive and interrupted
|
||||
// workshop; technical rooms use a repeated rib in their own mineral palette.
|
||||
if (w >= 9 && d >= 9) for (int z = 3; z < d - 2; z += 4)
|
||||
for (int x = 2; x < w - 2; x++) set(x, h - 2, z,
|
||||
(room.kind() == RuinsPlan17.Kind.ARCHIVE || room.kind() == RuinsPlan17.Kind.WORKSHOP
|
||||
? Blocks.STRIPPED_OAK_WOOD : trim()).defaultBlockState()
|
||||
.trySetValue(BlockStateProperties.AXIS, Direction.Axis.X));
|
||||
// Fitted thresholds and carved voussoirs distinguish doorways from random holes.
|
||||
for (int direction = 0; direction < 4; direction++) if ((room.openings() & 1 << direction) != 0)
|
||||
for (int side = -1; side <= 1; side++) {
|
||||
int x = direction == 1 ? w - 1 : direction == 3 ? 0 : cx + side;
|
||||
int z = direction == 2 ? d - 1 : direction == 0 ? 0 : cz + side;
|
||||
set(x, 5, z, side == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS);
|
||||
}
|
||||
}
|
||||
|
||||
void access() {
|
||||
int exit = Integer.numberOfTrailingZeros(room.openings() == 0 ? 1 : room.openings());
|
||||
int wall = (exit + 2) & 3;
|
||||
int x = wall == 1 ? w - 2 : wall == 3 ? 1 : cx;
|
||||
int z = wall == 2 ? d - 2 : wall == 0 ? 1 : cz;
|
||||
Direction facing = DIRECTIONS[wall].getOpposite();
|
||||
if ((room.openings() & 1 << wall) != 0) {
|
||||
// A branching shaft cannot attach its lowest ladder to another open doorway.
|
||||
// This corner post leaves all three doorway cells and the central crossing usable.
|
||||
x = 1; z = 2; facing = Direction.SOUTH;
|
||||
for (int y = 2; y <= h - 3; y++) set(1, y, 1, Blocks.STONE_BRICKS);
|
||||
}
|
||||
BlockState ladder = Blocks.LADDER.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing);
|
||||
for (int y = 2; y <= h - 3; y++) set(x, y, z, ladder);
|
||||
for (int zz = 0; zz < d; zz++) for (int xx = 0; xx < w; xx++) {
|
||||
set(xx, h - 3, zz, xx == x && zz == z ? ladder : Blocks.SMOOTH_STONE.defaultBlockState());
|
||||
set(xx, h - 2, zz, Blocks.AIR); set(xx, h - 1, zz, Blocks.AIR);
|
||||
}
|
||||
set(x, h - 2, z, Blocks.DARK_OAK_TRAPDOOR.defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, facing));
|
||||
}
|
||||
void corridor() {
|
||||
boolean alongX = w > d;
|
||||
int length = alongX ? w : d;
|
||||
for (int n = 2; n < length - 1; n += 4) {
|
||||
if (alongX) {
|
||||
for (int y = 2; y <= h - 1; y++) { set(n, y, 0, Blocks.POLISHED_ANDESITE); set(n, y, d - 1, Blocks.POLISHED_ANDESITE); }
|
||||
} else for (int y = 2; y <= h - 1; y++) { set(0, y, n, Blocks.POLISHED_ANDESITE); set(w - 1, y, n, Blocks.POLISHED_ANDESITE); }
|
||||
}
|
||||
}
|
||||
void mine() {
|
||||
for (int z : new int[] {2, d - 3}) {
|
||||
for (int x : new int[] {2, w - 3}) for (int y = 2; y <= h - 2; y++) decor(x, y, z, Blocks.STRIPPED_OAK_LOG);
|
||||
for (int x = 2; x <= w - 3; x++) decor(x, h - 2, z, Blocks.STRIPPED_OAK_LOG.defaultBlockState()
|
||||
.setValue(BlockStateProperties.AXIS, Direction.Axis.X));
|
||||
}
|
||||
for (int z = 1; z < d - 1; z++) set(cx, 2, z, Blocks.RAIL);
|
||||
// A short second siding ends at a buffer; the main track and cross passage remain open.
|
||||
if (w >= 13) {
|
||||
for (int z = 3; z < d - 2; z++) decor(w - 5, 2, z, Blocks.RAIL);
|
||||
decor(w - 5, 2, d - 2, Blocks.DARK_OAK_FENCE);
|
||||
}
|
||||
decor(1, 2, 1, Blocks.BARREL);
|
||||
decor(w - 2, 2, d - 2, Blocks.ANDESITE);
|
||||
if (LostCityPalette.roll(room.seed(), 1, 0, 0, 2) == 0) decor(w - 2, 3, d - 2, Blocks.COBBLESTONE);
|
||||
}
|
||||
void workshop() {
|
||||
decor(1, 2, 1, Blocks.CRAFTING_TABLE);
|
||||
decor(2, 2, 1, Blocks.FURNACE.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
|
||||
decor(1, 2, d - 3, Blocks.GRINDSTONE.defaultBlockState()
|
||||
.setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR)
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST));
|
||||
decor(w - 2, 2, 1, Blocks.STONECUTTER.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST));
|
||||
decor(1, 2, d - 2, Blocks.CAULDRON);
|
||||
decor(2, 2, d - 2, Blocks.ANVIL);
|
||||
// Two grounded posts and a small planked deck preserve the trace of unfinished repairs.
|
||||
for (int x = w - 3; x <= w - 2; x++) {
|
||||
decor(x, 2, d - 3, Blocks.STRIPPED_OAK_LOG);
|
||||
for (int z = d - 3; z <= d - 2; z++) decor(x, 3, z, Blocks.DARK_OAK_PLANKS);
|
||||
}
|
||||
for (int y = 2; y <= 3; y++) decor(w - 3, y, d - 4, Blocks.LADDER.defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
|
||||
decor(w - 2, 2, d - 2, Blocks.BARREL);
|
||||
}
|
||||
void store() {
|
||||
for (int z = 1; z < d - 1; z++) if (!crossing(1, z)) {
|
||||
decor(1, 2, z, Blocks.BARREL);
|
||||
if ((z & 1) == 0) decor(1, 3, z, Blocks.BARREL);
|
||||
decor(1, 4, z, Blocks.DARK_OAK_SLAB);
|
||||
}
|
||||
for (int x = cx + 2; x <= w - 2; x++) decor(x, 2, 1, Blocks.DARK_OAK_PLANKS);
|
||||
decor(w - 2, 3, 1, Blocks.BARREL);
|
||||
loot(w - 2, 2, d - 2, BuiltInLootTables.VILLAGE_PLAINS_HOUSE);
|
||||
}
|
||||
void tiledHall() {
|
||||
// Repeated columns are interrupted by complete, centered cross passages.
|
||||
for (int x : new int[] {2, w - 3}) for (int z : new int[] {2, d - 3})
|
||||
for (int y = 2; y < h - 1; y++) decor(x, y, z, y == 2 || y == h - 2 ? Blocks.SMOOTH_STONE : Blocks.CALCITE);
|
||||
if (w >= 13) for (int x : new int[] {3, w - 4}) for (int z = 1; z <= d - 2; z++)
|
||||
if (Math.abs(z - cz) > 1) for (int y = 2; y < h - 1; y++)
|
||||
decor(x, y, z, y == h - 2 ? Blocks.POLISHED_ANDESITE : Blocks.SMOOTH_STONE);
|
||||
for (int x = cx - 1; x <= cx + 1; x++) {
|
||||
decor(x, h - 2, 1, Blocks.POLISHED_ANDESITE);
|
||||
decor(x, h - 2, d - 2, Blocks.POLISHED_ANDESITE);
|
||||
}
|
||||
// Mismatched surviving benches give these physically reachable side niches a former use.
|
||||
decor(1, 2, 1, seat(Direction.WEST));
|
||||
decor(w - 2, 2, 1, seat(Direction.EAST));
|
||||
}
|
||||
void archive() {
|
||||
for (int z = 1; z <= d - 2; z++) if (Math.abs(z - cz) > 1)
|
||||
for (int y = 2; y <= 4; y++) decor(w - 2, y, z,
|
||||
y == 4 && (z & 1) == 0 ? Blocks.DARK_OAK_PLANKS : Blocks.BOOKSHELF);
|
||||
decor(1, 2, 1, Blocks.BOOKSHELF);
|
||||
decor(1, 2, 2, Blocks.LECTERN.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST));
|
||||
decor(1, 2, d - 2, Blocks.CARTOGRAPHY_TABLE);
|
||||
decor(2, 2, d - 3, Blocks.BARREL);
|
||||
loot(1, 2, d - 3, BuiltInLootTables.VILLAGE_CARTOGRAPHER);
|
||||
}
|
||||
void service() {
|
||||
for (int z = 1; z < d - 1; z++) if (Math.abs(z - cz) > 1) {
|
||||
decor(1, 2, z, Blocks.POLISHED_ANDESITE);
|
||||
decor(1, 3, z, Blocks.IRON_BARS);
|
||||
}
|
||||
for (int z : new int[] {1, d - 2}) for (int x = 1; x < w - 1; x++)
|
||||
decor(x, h - 2, z, Blocks.STRIPPED_OAK_LOG.defaultBlockState().setValue(BlockStateProperties.AXIS, Direction.Axis.X));
|
||||
decor(w - 2, 2, 1, Blocks.BARREL);
|
||||
decor(w - 2, 2, d - 2, Blocks.CRAFTING_TABLE);
|
||||
decor(2, 2, d - 2, seat(Direction.SOUTH));
|
||||
}
|
||||
void pumps() {
|
||||
for (int z : new int[] {1, d - 2}) {
|
||||
decor(1, 2, z, Blocks.DISPENSER.defaultBlockState().setValue(BlockStateProperties.FACING, Direction.EAST));
|
||||
decor(2, 2, z, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 3));
|
||||
decor(1, 3, z, Blocks.POLISHED_ANDESITE);
|
||||
decor(1, 4, z, Blocks.IRON_BARS);
|
||||
decor(w - 2, 2, z, Blocks.CAULDRON);
|
||||
}
|
||||
for (int z = 1; z <= d - 2; z++) decor(1, h - 2, z, Blocks.POLISHED_ANDESITE);
|
||||
decor(w - 2, 2, 2, Blocks.SMOOTH_STONE);
|
||||
decor(w - 2, 3, 2, Blocks.LEVER.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR)
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST));
|
||||
}
|
||||
void controls() {
|
||||
for (int x = 1; x <= cx - 2; x++) {
|
||||
decor(x, 2, 1, Blocks.SMOOTH_STONE);
|
||||
decor(x, 3, 1, Blocks.LEVER.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR)
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
|
||||
decor(x, 3, 0, Blocks.REDSTONE_LAMP);
|
||||
}
|
||||
decor(1, 2, 2, seat(Direction.WEST));
|
||||
decor(w - 2, 2, 1, Blocks.CRAFTING_TABLE);
|
||||
decor(w - 2, 2, d - 2, Blocks.BARREL);
|
||||
// An inspection window remains backed by a complete wall: there is no opening into unverified rock or water.
|
||||
for (int z = cz + 2; z < d - 1; z++) decor(0, 3, z, Blocks.GLASS);
|
||||
decor(1, 2, d - 2, Blocks.CARTOGRAPHY_TABLE);
|
||||
}
|
||||
BlockState seat(Direction back) { return Blocks.DARK_OAK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, back); }
|
||||
void lights() {
|
||||
// Every lantern hangs directly from the full masonry ceiling; never from a bottom slab.
|
||||
for (int[] p : new int[][] {{1, 1}, {w - 2, d - 2}})
|
||||
set(p[0], h - 2, p[1], Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
|
||||
}
|
||||
void openings() {
|
||||
for (int direction = 0; direction < 4; direction++) if ((room.openings() & 1 << direction) != 0)
|
||||
for (int side = -1; side <= 1; side++) {
|
||||
int x = direction == 1 ? w - 1 : direction == 3 ? 0 : cx + side;
|
||||
int z = direction == 2 ? d - 1 : direction == 0 ? 0 : cz + side;
|
||||
set(x, 1, z, Blocks.SMOOTH_STONE);
|
||||
for (int y = 2; y <= 4; y++) set(x, y, z, Blocks.AIR);
|
||||
}
|
||||
}
|
||||
void loot(int x, int y, int z, net.minecraft.resources.ResourceKey<net.minecraft.world.level.storage.loot.LootTable> table) {
|
||||
out.chest(reverse ? w - 1 - x : x, y, reverse ? d - 1 - z : z, table, room.seed() ^ 0x17A6C41L);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,9 @@ public final class RuinsStructure17 extends Structure {
|
||||
|
||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| generator.rootGeneration() != 17 || !generator.experimentalStructures()) return Optional.empty();
|
||||
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && generator.rootGeneration() != 22) || !generator.experimentalStructures()) return Optional.empty();
|
||||
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
|
||||
if (island == null || !island.origin() || island.generation() != 17) return Optional.empty();
|
||||
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return Optional.empty();
|
||||
var networks = generator.ruinsPlan17().networks().stream()
|
||||
.filter(n -> n.startChunkX() == context.chunkPos().x() && n.startChunkZ() == context.chunkPos().z()).toList();
|
||||
if (networks.isEmpty()) return Optional.empty();
|
||||
@@ -30,9 +30,9 @@ public final class RuinsStructure17 extends Structure {
|
||||
return Optional.of(new GenerationStub(RuinsStructures17.locator(networks.getFirst()), pieces -> {
|
||||
// Shafts come last so their surface hatches cannot be filled by a connected room.
|
||||
networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() != RuinsPlan17.Kind.ACCESS)
|
||||
.forEach(r -> pieces.addPiece(new RuinsPiece17(r))));
|
||||
.forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
|
||||
networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS)
|
||||
.forEach(r -> pieces.addPiece(new RuinsPiece17(r))));
|
||||
.forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
|
||||
}));
|
||||
}
|
||||
@Override public StructureType<?> type() { return RuinsStructures17.TYPE; }
|
||||
|
||||
+8
-6
@@ -35,6 +35,8 @@ public final class RuinsStructures17 {
|
||||
SanctuaryMod.id("forgotten_networks_v17"), () -> RuinsStructure17.CODEC);
|
||||
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
|
||||
SanctuaryMod.id("forgotten_room_v17"), RuinsPiece17::new);
|
||||
public static final StructurePieceType PIECE20 = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
|
||||
SanctuaryMod.id("forgotten_room_v20"), RuinsPiece20::new);
|
||||
private static final ThreadLocal<Integer> PLACEMENT_DEPTH = ThreadLocal.withInitial(() -> 0);
|
||||
|
||||
private RuinsStructures17() {}
|
||||
@@ -53,10 +55,10 @@ public final class RuinsStructures17 {
|
||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || generator.rootGeneration() != 17
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && generator.rootGeneration() != 22)
|
||||
|| !sanctuary.experimentalStructures()) return;
|
||||
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
||||
if (island == null || !island.origin() || island.generation() != 17) return;
|
||||
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
|
||||
var plan = sanctuary.ruinsPlan17();
|
||||
if (plan.networks().stream().noneMatch(n -> n.startChunkX() == chunk.getPos().x()
|
||||
&& n.startChunkZ() == chunk.getPos().z())) return;
|
||||
@@ -76,7 +78,7 @@ public final class RuinsStructures17 {
|
||||
BlockPos origin, int radius, boolean skipReferenced) {
|
||||
if (!Level.OVERWORLD.equals(level.dimension())
|
||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| generator.rootGeneration() != 17 || !generator.experimentalStructures()
|
||||
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && generator.rootGeneration() != 22) || !generator.experimentalStructures()
|
||||
|| !level.structureManager().shouldGenerateStructures()) return null;
|
||||
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||
if (!structures.contains(holder)) return null;
|
||||
@@ -84,7 +86,7 @@ public final class RuinsStructures17 {
|
||||
int originChunkX = origin.getX() >> 4, originChunkZ = origin.getZ() >> 4;
|
||||
for (var network : generator.ruinsPlan17().networks()) {
|
||||
var island = generator.islandForChunk(network.startChunkX(), network.startChunkZ());
|
||||
if (island == null || !island.origin() || island.generation() != 17 || !island.status().equals("ready")) continue;
|
||||
if (island == null || !island.origin() || island.generation() != generator.rootGeneration() || !island.status().equals("ready")) continue;
|
||||
if (Math.abs((long) network.startChunkX() - originChunkX) <= radius
|
||||
&& Math.abs((long) network.startChunkZ() - originChunkZ) <= radius) candidates.add(network);
|
||||
}
|
||||
@@ -97,8 +99,8 @@ public final class RuinsStructures17 {
|
||||
StructureStart start = level.structureManager().getStartForStructure(holder.value(), chunk);
|
||||
if (start == null || !start.isValid() || skipReferenced && !start.canBeReferenced()) continue;
|
||||
if (skipReferenced) level.structureManager().addReference(start);
|
||||
var entrance = start.getPieces().stream().filter(p -> p instanceof RuinsPiece17 room
|
||||
&& room.room().kind() == RuinsPlan17.Kind.ACCESS).findFirst().orElse(start.getPieces().getFirst());
|
||||
var entrance = start.getPieces().stream().filter(p -> (p instanceof RuinsPiece17 room && room.room().kind() == RuinsPlan17.Kind.ACCESS)
|
||||
|| (p instanceof RuinsPiece20 detail && detail.room().kind() == RuinsPlan17.Kind.ACCESS)).findFirst().orElse(start.getPieces().getFirst());
|
||||
return Pair.of(entrance.getLocatorPosition(), holder);
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package fr.koka.sanctuary.worldgen.transit;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Set;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
|
||||
/** Saved generation21 room geometry is rendered without replanning or reading neighboring chunks. */
|
||||
public final class TransitPiece21 extends StructurePiece {
|
||||
private static final Set<String> MODULE_KEYS = Set.of("Schema", "Kind", "MinX", "MinY", "MinZ", "Width",
|
||||
"Height", "Depth", "Facing", "Openings", "ModuleSeed", "Supports");
|
||||
private static final Set<String> SUPPORT_KEYS = Set.of("X", "MinY", "Z", "MaxY");
|
||||
private final TransitPlan21.Module room;
|
||||
|
||||
public TransitPiece21(TransitPlan21.Module room) {
|
||||
super(TransitStructures21.PIECE, 0, bounds(room));
|
||||
this.room = room;
|
||||
setOrientation(null);
|
||||
}
|
||||
public TransitPiece21(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||
super(TransitStructures21.PIECE, tag);
|
||||
room = decodeModule(tag.getCompound("SanctuaryModule")
|
||||
.orElseThrow(() -> new IllegalArgumentException("Missing saved transit room")));
|
||||
if (!getBoundingBox().equals(bounds(room)))
|
||||
throw new IllegalArgumentException("Transit room disagrees with its saved native bounding box");
|
||||
setOrientation(null);
|
||||
}
|
||||
public TransitPlan21.Module module() { return room; }
|
||||
@Override public BlockPos getLocatorPosition() { return locator(room); }
|
||||
public static BlockPos locator(TransitPlan21.Module room) {
|
||||
return new BlockPos(room.minX() + room.width() / 2,
|
||||
room.kind() == TransitPlan21.Kind.ACCESS ? room.surfaceY() + 1 : room.floorY() + 1,
|
||||
room.kind() == TransitPlan21.Kind.ACCESS ? room.minZ() : room.minZ() + room.depth() / 2);
|
||||
}
|
||||
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||
tag.put("SanctuaryModule", encodeModule(room));
|
||||
}
|
||||
public static CompoundTag encodeModule(TransitPlan21.Module room) {
|
||||
var tag = new CompoundTag(); tag.putInt("Schema", 1); tag.putString("Kind", room.kind().name());
|
||||
tag.putInt("MinX", room.minX()); tag.putInt("MinY", room.minY()); tag.putInt("MinZ", room.minZ());
|
||||
tag.putInt("Width", room.width()); tag.putInt("Height", room.height()); tag.putInt("Depth", room.depth());
|
||||
tag.putInt("Facing", room.facing()); tag.putInt("Openings", room.openings()); tag.putLong("ModuleSeed", room.seed());
|
||||
var supports = new ListTag();
|
||||
for (var support : room.supports()) {
|
||||
var value = new CompoundTag(); value.putInt("X", support.x()); value.putInt("MinY", support.minY());
|
||||
value.putInt("Z", support.z()); value.putInt("MaxY", support.maxY()); supports.add(value);
|
||||
}
|
||||
tag.put("Supports", supports);
|
||||
return tag;
|
||||
}
|
||||
public static TransitPlan21.Module decodeModule(CompoundTag tag) {
|
||||
if (!tag.keySet().equals(MODULE_KEYS)) throw new IllegalArgumentException("Incomplete or unknown transit room fields");
|
||||
if (integer(tag, "Schema") != 1) throw new IllegalArgumentException("Unsupported transit room schema");
|
||||
var kind = TransitPlan21.Kind.valueOf(tag.getString("Kind").orElseThrow(() -> new IllegalArgumentException("Missing room kind")));
|
||||
type(tag, "ModuleSeed", Tag.TAG_LONG); type(tag, "Supports", Tag.TAG_LIST);
|
||||
var saved = tag.getList("Supports").orElseThrow();
|
||||
if (saved.size() > 64) throw new IllegalArgumentException("Too many saved transit room supports");
|
||||
var supports = new ArrayList<TransitPlan21.Support>();
|
||||
for (var entry : saved) {
|
||||
if (!(entry instanceof CompoundTag value) || !value.keySet().equals(SUPPORT_KEYS))
|
||||
throw new IllegalArgumentException("Invalid transit room support fields");
|
||||
supports.add(new TransitPlan21.Support(integer(value, "X"), integer(value, "MinY"), integer(value, "Z"), integer(value, "MaxY")));
|
||||
}
|
||||
return new TransitPlan21.Module(kind, integer(tag, "MinX"), integer(tag, "MinY"), integer(tag, "MinZ"),
|
||||
integer(tag, "Width"), integer(tag, "Height"), integer(tag, "Depth"), integer(tag, "Facing"), integer(tag, "Openings"),
|
||||
tag.getLong("ModuleSeed").orElseThrow(), supports);
|
||||
}
|
||||
private static int integer(CompoundTag tag, String key) {
|
||||
type(tag, key, Tag.TAG_INT);
|
||||
return tag.getInt(key).orElseThrow();
|
||||
}
|
||||
private static void type(CompoundTag tag, String key, byte id) {
|
||||
if (tag.get(key) == null || tag.get(key).getId() != id)
|
||||
throw new IllegalArgumentException("Invalid saved transit room field: " + key);
|
||||
}
|
||||
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
|
||||
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
|
||||
TransitStructures21.placing(() -> TransitRenderer21.render(room, new LostCityRenderer.BlockWriter() {
|
||||
private BlockPos world(int x, int y, int z) { return new BlockPos(room.minX() + x, room.minY() + y, room.minZ() + z); }
|
||||
@Override public void set(int x, int y, int z, BlockState block) {
|
||||
var position = world(x, y, z);
|
||||
if (getBoundingBox().isInside(position) && clip.isInside(position))
|
||||
placeBlock(level, block, position.getX(), position.getY(), position.getZ(), clip);
|
||||
}
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
|
||||
var position = world(x, y, z);
|
||||
if (getBoundingBox().isInside(position) && clip.isInside(position))
|
||||
createChest(level, clip, RandomSource.create(seed), position, loot, null);
|
||||
}
|
||||
}));
|
||||
}
|
||||
public static BoundingBox bounds(TransitPlan21.Module room) {
|
||||
int minY = room.minY(), maxY = Math.addExact(room.minY(), room.height() - 1);
|
||||
for (var support : room.supports()) {
|
||||
minY = Math.min(minY, support.minY()); maxY = Math.max(maxY, support.maxY());
|
||||
}
|
||||
return new BoundingBox(room.minX(), minY, room.minZ(), Math.addExact(room.minX(), room.width() - 1),
|
||||
maxY, Math.addExact(room.minZ(), room.depth() - 1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package fr.koka.sanctuary.worldgen.transit;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
|
||||
/** Distinct native piece ID selects the corrected22 corner without migrating any saved21 piece. */
|
||||
public final class TransitPiece22 extends StructurePiece {
|
||||
private final TransitPlan21.Module room;
|
||||
public TransitPiece22(TransitPlan21.Module room) {
|
||||
super(TransitStructures21.PIECE22, 0, TransitPiece21.bounds(room));
|
||||
this.room = room; setOrientation(null);
|
||||
}
|
||||
public TransitPiece22(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||
super(TransitStructures21.PIECE22, tag);
|
||||
room = TransitPiece21.decodeModule(tag.getCompound("SanctuaryModule")
|
||||
.orElseThrow(() -> new IllegalArgumentException("Missing saved transit22 room")));
|
||||
if (!getBoundingBox().equals(TransitPiece21.bounds(room)))
|
||||
throw new IllegalArgumentException("Transit22 room disagrees with its saved native bounding box");
|
||||
setOrientation(null);
|
||||
}
|
||||
public TransitPlan21.Module module() { return room; }
|
||||
@Override public BlockPos getLocatorPosition() { return TransitPiece21.locator(room); }
|
||||
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||
tag.put("SanctuaryModule", TransitPiece21.encodeModule(room));
|
||||
}
|
||||
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
|
||||
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
|
||||
TransitStructures21.placing(() -> TransitRenderer22.render(room, new LostCityRenderer.BlockWriter() {
|
||||
private BlockPos world(int x, int y, int z) { return new BlockPos(room.minX() + x, room.minY() + y, room.minZ() + z); }
|
||||
@Override public void set(int x, int y, int z, BlockState block) {
|
||||
var pos = world(x, y, z);
|
||||
if (getBoundingBox().isInside(pos) && clip.isInside(pos)) placeBlock(level, block, pos.getX(), pos.getY(), pos.getZ(), clip);
|
||||
}
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
|
||||
var pos = world(x, y, z);
|
||||
if (getBoundingBox().isInside(pos) && clip.isInside(pos)) createChest(level, clip, RandomSource.create(seed), pos, loot, null);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package fr.koka.sanctuary.worldgen.transit;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
/** Immutable final geometry. Each module owns a small native start, never an island-wide start. */
|
||||
public record TransitPlan21(List<Module> modules, int samples, String diagnostic) {
|
||||
public static final int MAX_SAMPLES = 3_000_000;
|
||||
public static final TransitPlan21 EMPTY = new TransitPlan21(List.of(), 0, "disabled");
|
||||
public enum Kind { GALLERY, JUNCTION, HALL, ACCESS }
|
||||
public record Support(int x, int minY, int z, int maxY) {
|
||||
public Support {
|
||||
if (minY < 8 || maxY > 375 || minY > maxY || maxY - minY >= 16)
|
||||
throw new IllegalArgumentException("Invalid transit footing");
|
||||
}
|
||||
}
|
||||
/** World-aligned bounds. Openings N1/E2/S4/W8 are seven wide, local y2..8.
|
||||
* Access top deck is maxY-4. Its north gate, at centerX/minZ, has a certified natural approach. */
|
||||
public record Module(Kind kind, int minX, int minY, int minZ, int width, int height, int depth,
|
||||
int facing, int openings, long seed, List<Support> supports) {
|
||||
public Module {
|
||||
supports = List.copyOf(supports);
|
||||
if (kind == null || width < 9 || width > 25 || depth < 9 || depth > 25
|
||||
|| (width & 1) == 0 || (depth & 1) == 0 || height < 10 || height > 160
|
||||
|| minY < 16 || minY + height > 383 || facing < 0 || facing > 3
|
||||
|| openings < 1 || openings > 15 || supports.size() > 64)
|
||||
throw new IllegalArgumentException("Invalid transit module");
|
||||
if (kind == Kind.ACCESS && (width != 17 || depth != 17 || height < 20))
|
||||
throw new IllegalArgumentException("Transit stair access needs its complete 17x17 shaft");
|
||||
if (kind != Kind.ACCESS && height != 10) throw new IllegalArgumentException("Unexpected gallery height");
|
||||
var seen = new HashSet<Long>();
|
||||
for (var s : supports) if (s.x() < minX || s.x() >= minX + width || s.z() < minZ
|
||||
|| s.z() >= minZ + depth || s.maxY() != minY - 1 || !seen.add(key(s.x(), s.z())))
|
||||
throw new IllegalArgumentException("Transit footing does not belong to its floor");
|
||||
}
|
||||
public int maxX() { return minX + width - 1; }
|
||||
public int maxY() { return minY + height - 1; }
|
||||
public int maxZ() { return minZ + depth - 1; }
|
||||
public int centerX() { return minX + width / 2; }
|
||||
public int centerZ() { return minZ + depth / 2; }
|
||||
public int floorY() { return minY + 1; }
|
||||
public int surfaceY() { return maxY() - 4; }
|
||||
public int startChunkX() { return centerX() >> 4; }
|
||||
public int startChunkZ() { return centerZ() >> 4; }
|
||||
public int boundsMinY() { return supports.stream().mapToInt(Support::minY).min().orElse(minY); }
|
||||
public boolean contains(int x, int y, int z) {
|
||||
if (x < minX || x > maxX() || z < minZ || z > maxZ()) return false;
|
||||
if (y >= minY && y <= maxY()) return true;
|
||||
return supports.stream().anyMatch(s -> x == s.x() && z == s.z() && y >= s.minY() && y <= s.maxY());
|
||||
}
|
||||
public boolean protects(int x, int y, int z) {
|
||||
if (contains(x, y, z)) return true;
|
||||
if (kind == Kind.ACCESS && x >= centerX() - 2 && x <= centerX() + 2
|
||||
&& z >= minZ - 3 && z < minZ && y >= surfaceY() - 2 && y <= surfaceY() + 3) return true;
|
||||
// Two untouched layers carry the slab and the certified short piers.
|
||||
if (x >= minX && x <= maxX() && z >= minZ && z <= maxZ() && y >= minY - 2 && y < minY) return true;
|
||||
return supports.stream().anyMatch(s -> x == s.x() && z == s.z() && y >= s.minY() - 2 && y < s.minY());
|
||||
}
|
||||
}
|
||||
public TransitPlan21 {
|
||||
modules = List.copyOf(modules);
|
||||
if (modules.size() > 64 || samples < 0 || samples > MAX_SAMPLES || diagnostic == null || diagnostic.length() > 1024)
|
||||
throw new IllegalArgumentException("Invalid transit planning result");
|
||||
if (!modules.isEmpty() && modules.stream().filter(m -> m.kind() == Kind.ACCESS).count() != 2)
|
||||
throw new IllegalArgumentException("A through-gallery needs two surface accesses");
|
||||
for (int i = 0; i < modules.size(); i++) for (int j = i + 1; j < modules.size(); j++) {
|
||||
var a = modules.get(i); var b = modules.get(j);
|
||||
if (a.minX() <= b.maxX() && a.maxX() >= b.minX() && a.minZ() <= b.maxZ() && a.maxZ() >= b.minZ())
|
||||
throw new IllegalArgumentException("Transit modules overlap");
|
||||
}
|
||||
}
|
||||
public boolean isEmpty() { return modules.isEmpty(); }
|
||||
public List<Module> modulesForChunk(int x, int z) {
|
||||
return modules.stream().filter(m -> m.startChunkX() == x && m.startChunkZ() == z).toList();
|
||||
}
|
||||
public boolean contains(int x, int y, int z) { return modules.stream().anyMatch(m -> m.contains(x, y, z)); }
|
||||
public boolean protects(int x, int y, int z) { return modules.stream().anyMatch(m -> m.protects(x, y, z)); }
|
||||
public boolean intersectsProtection(int x0, int y0, int z0, int x1, int y1, int z1) {
|
||||
for (var m : modules) if (m.kind() == Kind.ACCESS && x0 <= m.centerX() + 2 && x1 >= m.centerX() - 2
|
||||
&& z0 < m.minZ() && z1 >= m.minZ() - 3 && y0 <= m.surfaceY() + 3 && y1 >= m.surfaceY() - 2) return true;
|
||||
for (var m : modules) if (x0 <= m.maxX() && x1 >= m.minX() && z0 <= m.maxZ() && z1 >= m.minZ()
|
||||
&& y0 <= m.maxY() && y1 >= m.boundsMinY() - 2) return true;
|
||||
return false;
|
||||
}
|
||||
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
|
||||
}
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
package fr.koka.sanctuary.worldgen.transit;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind;
|
||||
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Module;
|
||||
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Support;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/** A bounded read-only through-route. Failed candidates never publish partial corridors or shafts. */
|
||||
public final class TransitPlanner21 {
|
||||
public static final int MAX_COLUMNS = 12_000;
|
||||
private TransitPlanner21() {}
|
||||
public interface Terrain {
|
||||
boolean owns(int x, int z);
|
||||
boolean solid(int x, int y, int z);
|
||||
boolean protectedAt(int x, int y, int z);
|
||||
int surfaceY(int x, int z);
|
||||
default String obstruction(int x, int y, int z) { return "protected terrain"; }
|
||||
}
|
||||
public static TransitPlan21 plan(long seed, int centerX, int centerZ, int diameter, Terrain terrain) {
|
||||
if (diameter < 512 || diameter > 1024) throw new IllegalArgumentException("Unsupported transit island diameter");
|
||||
var probe = new Probe(terrain);
|
||||
int count = (int) Math.ceil(diameter * .42 / 17);
|
||||
if ((count & 1) == 0) count++;
|
||||
String last = "no dry supported through-route";
|
||||
var choices = new ArrayList<Choice>();
|
||||
// Reserve most of the native-column budget for the exact scan of selected routes.
|
||||
probe.sampleLimit = 1_800_000; probe.columnLimit = 7_500;
|
||||
try {
|
||||
search: for (int advance : new int[]{0, -34, 34}) for (boolean alongX : new boolean[]{true, false}) for (int shift : new int[]{-diameter / 8, diameter / 8, -diameter / 16, diameter / 16, 0, -3 * diameter / 16, 3 * diameter / 16}) {
|
||||
int cx = centerX + (alongX ? advance : shift), cz = centerZ + (alongX ? shift : advance);
|
||||
for (int floor : new int[]{216, 200, 224, 208, 184, 232, 160}) {
|
||||
var route = candidate(seed, cx, cz, count, floor, alongX, probe, false);
|
||||
String label = "axis=" + (alongX ? "east-west" : "north-south") + " offset=" + shift + " advance=" + advance + " floor=" + floor;
|
||||
if (route == null) {
|
||||
last = probe.reason;
|
||||
diagnostic("preselection reject " + label + " " + last + " columns=" + probe.columns.size());
|
||||
continue;
|
||||
}
|
||||
int cost = route.stream().mapToInt(m -> m.supports().stream().mapToInt(pier -> pier.maxY() - pier.minY() + 1).sum()
|
||||
+ (m.kind() == Kind.ACCESS ? m.surfaceY() - m.floorY() : 0)).sum();
|
||||
// Try the intermediate levels first; within each level prefer the better supported axis.
|
||||
int heightOrder = switch (floor) { case 216 -> 0; case 200 -> 1; case 224 -> 2; case 208 -> 3; case 184 -> 4; case 232 -> 5; default -> 6; };
|
||||
choices.add(new Choice(route, floor, alongX, heightOrder * 100_000 + cost, label));
|
||||
diagnostic("preselection admitted " + label + " bearing_cost=" + cost);
|
||||
if (choices.size() >= 8) break search;
|
||||
}
|
||||
}
|
||||
} catch (BudgetExceeded exhausted) { diagnostic("prescreen budget reached; retain " + choices.size() + " candidates"); }
|
||||
probe.sampleLimit = TransitPlan21.MAX_SAMPLES; probe.columnLimit = MAX_COLUMNS;
|
||||
choices.sort(java.util.Comparator.comparingInt(Choice::cost));
|
||||
try {
|
||||
for (var choice : choices) {
|
||||
var finalRoute = new ArrayList<Module>();
|
||||
for (var rough : choice.route()) {
|
||||
var complete = certify(rough, probe, true);
|
||||
if (complete == null) { last = probe.reason; break; }
|
||||
finalRoute.add(complete);
|
||||
}
|
||||
if (finalRoute.size() != choice.route().size()) {
|
||||
diagnostic("exact reject " + choice.label() + " " + last); continue;
|
||||
}
|
||||
addBranch(finalRoute, choice.floor(), choice.alongX(), seed, probe);
|
||||
return new TransitPlan21(finalRoute, probe.samples, "accepted " + choice.label()
|
||||
+ " span=" + ((count + 1) * 17) + " columns=" + probe.columns.size()
|
||||
+ " candidates=" + choices.size() + " hall=" + finalRoute.stream().anyMatch(m -> m.kind() == Kind.HALL));
|
||||
}
|
||||
} catch (BudgetExceeded exhausted) { last = "bounded exact search exhausted: " + probe.reason; }
|
||||
return new TransitPlan21(List.of(), probe.samples, last + "; candidates=" + choices.size() + "; columns=" + probe.columns.size());
|
||||
}
|
||||
private record Choice(List<Module> route, int floor, boolean alongX, int cost, String label) {}
|
||||
private static List<Module> candidate(long seed, int cx, int cz, int count, int floor,
|
||||
boolean alongX, Probe probe, boolean full) {
|
||||
int first = -(count + 1) * 17 / 2;
|
||||
int ax = cx + (alongX ? first : 0), az = cz + (alongX ? 0 : first);
|
||||
int bx = cx - (alongX ? first : 0), bz = cz - (alongX ? 0 : first);
|
||||
// Cheap route witnesses reject deep obstacles before two tall shafts are scanned in full.
|
||||
for (int d = first + 17; d <= -first - 17; d += 8) for (int side : new int[]{-4, 0, 4})
|
||||
for (int y : new int[]{floor - 2, floor, floor + 4, floor + 8}) {
|
||||
int x = cx + (alongX ? d : side), z = cz + (alongX ? side : d);
|
||||
if (probe.protectedAt(x, y, z)) {
|
||||
probe.reason = "coarse route [" + x + "," + y + "," + z + "] " + probe.terrain.obstruction(x, y, z); return null;
|
||||
}
|
||||
}
|
||||
// Certify the two indispensable exits before spending the route's column budget.
|
||||
Module left = access(ax, az, floor, alongX ? 2 : 4, seed ^ 0x2101L, probe, full);
|
||||
if (left == null) return null;
|
||||
Module right = access(bx, bz, floor, alongX ? 8 : 1, seed ^ 0x2102L, probe, full);
|
||||
if (right == null) return null;
|
||||
var result = new ArrayList<Module>(); result.add(left);
|
||||
int unanchoredRun = 0;
|
||||
for (int n = 0; n < count; n++) {
|
||||
int d = first + 17 * (n + 1);
|
||||
int x = cx + (alongX ? d - 8 : -4), z = cz + (alongX ? -4 : d - 8);
|
||||
var module = new Module(Kind.GALLERY, x, floor - 1, z, alongX ? 17 : 9, 10, alongX ? 9 : 17,
|
||||
alongX ? 1 : 0, alongX ? 10 : 5, seed ^ (n * 0x9E3779B97F4A7C15L), List.of());
|
||||
module = certify(module, probe, full);
|
||||
if (module == null) return null;
|
||||
unanchoredRun = probe.bearings.get(module) == 15 ? 0 : unanchoredRun + 1;
|
||||
if (unanchoredRun > 2) { probe.reason = "unsupported span exceeds 34 blocks"; return null; }
|
||||
result.add(module);
|
||||
}
|
||||
result.add(right);
|
||||
return result;
|
||||
}
|
||||
private static Module access(int cx, int cz, int floor, int opening, long seed, Probe probe, boolean full) {
|
||||
int top = -1, low = 384;
|
||||
for (int x = cx - 2; x <= cx + 2; x++) for (int z = cz - 11; z <= cz - 9; z++) {
|
||||
int surface = probe.surface(x, z); top = Math.max(top, surface); low = Math.min(low, surface);
|
||||
}
|
||||
if (low < 0 || top - low > 1) { probe.reason = "access approach relief [" + cx + "," + cz + "] " + low + ".." + top; return null; }
|
||||
if (top < floor + 18 || top > floor + 145) { probe.reason = "surface access height"; return null; }
|
||||
// Its visible north portico meets real ground; its three-wide approach is never an unsupported threshold.
|
||||
for (int x = cx - 2; x <= cx + 2; x++) for (int z = cz - 11; z <= cz - 9; z++) {
|
||||
int y = probe.surface(x, z);
|
||||
if (y < top - 1 || y > top || probe.protectedAt(x, y, z)
|
||||
|| probe.protectedAt(x, top + 1, z) || probe.protectedAt(x, top + 2, z)) {
|
||||
probe.reason = "access approach [" + x + "," + top + "," + z + "] terrain=" + y; return null;
|
||||
}
|
||||
}
|
||||
for (int x : new int[]{cx - 8, cx, cx + 8}) for (int z : new int[]{cz - 8, cz, cz + 8}) {
|
||||
int y = probe.surface(x, z);
|
||||
// The portico alone needs level terrain. A buried shaft can follow a hillside;
|
||||
// its local walls and checked footings carry the deck without flattening the hill.
|
||||
if (y < Math.max(floor + 8, top - 24) || y > top + 16) { probe.reason = "access hillside [" + x + "," + top + "," + z + "] terrain=" + y; return null; }
|
||||
}
|
||||
return certify(new Module(Kind.ACCESS, cx - 8, floor - 1, cz - 8, 17,
|
||||
top - (floor - 1) + 5, 17, 0, opening, seed, List.of()), probe, full);
|
||||
}
|
||||
private static void addBranch(List<Module> route, int floor, boolean alongX, long seed, Probe probe) {
|
||||
int index = 1 + (route.size() - 2) / 2;
|
||||
var junction = route.get(index);
|
||||
if (probe.bearings.getOrDefault(junction, 0) != 15) return;
|
||||
for (int side : new int[]{-1, 1}) {
|
||||
try {
|
||||
int x = alongX ? junction.centerX() - 4 : side < 0 ? junction.minX() - 17 : junction.maxX() + 1;
|
||||
int z = alongX ? side < 0 ? junction.minZ() - 17 : junction.maxZ() + 1 : junction.centerZ() - 4;
|
||||
int width = alongX ? 9 : 17, depth = alongX ? 17 : 9;
|
||||
var passage = certify(new Module(Kind.GALLERY, x, floor - 1, z, width, 10, depth,
|
||||
alongX ? 0 : 1, alongX ? 5 : 10, seed ^ 0x21B1L ^ side, List.of()), probe, true);
|
||||
if (passage == null) continue;
|
||||
int hx = alongX ? junction.centerX() - 8 : side < 0 ? passage.minX() - 21 : passage.maxX() + 1;
|
||||
int hz = alongX ? side < 0 ? passage.minZ() - 21 : passage.maxZ() + 1 : junction.centerZ() - 8;
|
||||
int opening = alongX ? side < 0 ? 4 : 1 : side < 0 ? 2 : 8;
|
||||
var hall = certify(new Module(Kind.HALL, hx, floor - 1, hz, alongX ? 17 : 21, 10,
|
||||
alongX ? 21 : 17, alongX ? 0 : 1, opening, seed ^ 0x21A1L ^ side, List.of()), probe, true);
|
||||
if (hall == null) continue;
|
||||
int branch = alongX ? side < 0 ? 1 : 4 : side < 0 ? 8 : 2;
|
||||
route.set(index, new Module(Kind.JUNCTION, junction.minX(), junction.minY(), junction.minZ(),
|
||||
junction.width(), 10, junction.depth(), junction.facing(), junction.openings() | branch,
|
||||
junction.seed(), junction.supports()));
|
||||
route.add(passage); route.add(hall); return;
|
||||
} catch (BudgetExceeded exhausted) { return; } // The already certified traversal remains complete.
|
||||
}
|
||||
}
|
||||
private static Module certify(Module module, Probe probe, boolean full) {
|
||||
var supports = new ArrayList<Support>();
|
||||
int bearingQuadrants = 0;
|
||||
for (int z = module.minZ();; z = Math.min(z + 4, module.maxZ())) {
|
||||
for (int x = module.minX();; x = Math.min(x + 4, module.maxX())) {
|
||||
if (!probe.owns(x, z)) { probe.reason = "outside island"; return null; }
|
||||
if (!full) for (int y = module.minY(); y <= module.maxY(); y += 4)
|
||||
if (probe.protectedAt(x, y, z)) {
|
||||
probe.reason = "coarse " + module.kind() + " [" + x + "," + y + "," + z + "] " + probe.terrain.obstruction(x, y, z); return null;
|
||||
}
|
||||
int anchor = module.minY() - 1;
|
||||
while (anchor >= module.minY() - 17 && !(probe.solid(x, anchor, z) && probe.solid(x, anchor - 1, z))) anchor--;
|
||||
if (anchor >= module.minY() - 17) {
|
||||
for (int y = anchor - 1; y < module.minY(); y++) if (probe.protectedAt(x, y, z)) {
|
||||
probe.reason = "protected footing"; return null;
|
||||
}
|
||||
if (anchor < module.minY() - 1) supports.add(new Support(x, anchor + 1, z, module.minY() - 1));
|
||||
bearingQuadrants |= 1 << ((x < module.centerX() ? 0 : 1) + (z < module.centerZ() ? 0 : 2));
|
||||
}
|
||||
if (x == module.maxX()) break;
|
||||
}
|
||||
if (z == module.maxZ()) break;
|
||||
}
|
||||
// The continuous two-block deck can span gaps between its bearings. Require
|
||||
// untouched rock in each quarter, not a pier at every point above a cavity.
|
||||
if (bearingQuadrants != 15 && module.kind() != Kind.GALLERY) { probe.reason = "unsupported quarters " + module.kind()
|
||||
+ " [" + module.minX() + "," + module.minY() + "," + module.minZ() + "] mask=" + bearingQuadrants; return null; }
|
||||
if (full) {
|
||||
// Hydrology and all previous structures are checked at every edited voxel after the cheap footing test.
|
||||
for (int z = module.minZ(); z <= module.maxZ(); z++) for (int x = module.minX(); x <= module.maxX(); x++) {
|
||||
if (!probe.owns(x, z)) { probe.reason = "outside island"; return null; }
|
||||
for (int y = module.minY() - 2; y <= module.maxY(); y++) if (probe.protectedAt(x, y, z)) {
|
||||
probe.reason = "volume " + module.kind() + " [" + x + "," + y + "," + z + "] " + probe.terrain.obstruction(x, y, z); return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
var certified = new Module(module.kind(), module.minX(), module.minY(), module.minZ(), module.width(), module.height(),
|
||||
module.depth(), module.facing(), module.openings(), module.seed(), supports);
|
||||
probe.bearings.put(certified, bearingQuadrants);
|
||||
return certified;
|
||||
}
|
||||
private static void diagnostic(String message) {
|
||||
if (Boolean.getBoolean("sanctuary.test.transit21") || Boolean.getBoolean("sanctuary.transit.plannerDiagnostics"))
|
||||
System.getLogger(TransitPlanner21.class.getName()).log(System.Logger.Level.INFO, "Transit21 " + message);
|
||||
}
|
||||
private static final class Probe {
|
||||
final Terrain terrain; final Set<Long> columns = new HashSet<>(); int samples; int sampleLimit = TransitPlan21.MAX_SAMPLES, columnLimit = MAX_COLUMNS; String reason = "budget";
|
||||
final java.util.Map<Module, Integer> bearings = new java.util.HashMap<>();
|
||||
Probe(Terrain terrain) { this.terrain = terrain; }
|
||||
void visit(int x, int z) {
|
||||
if (samples >= sampleLimit) throw new BudgetExceeded();
|
||||
samples++;
|
||||
long key = (long) x << 32 ^ z & 0xffffffffL;
|
||||
if (!columns.contains(key) && columns.size() >= columnLimit) throw new BudgetExceeded();
|
||||
columns.add(key);
|
||||
}
|
||||
boolean owns(int x, int z) { visit(x, z); return terrain.owns(x, z); }
|
||||
boolean solid(int x, int y, int z) { visit(x, z); return terrain.solid(x, y, z); }
|
||||
boolean protectedAt(int x, int y, int z) { visit(x, z); return terrain.protectedAt(x, y, z); }
|
||||
int surface(int x, int z) { visit(x, z); return terrain.surfaceY(x, z); }
|
||||
}
|
||||
private static final class BudgetExceeded extends RuntimeException { private static final long serialVersionUID = 1L; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package fr.koka.sanctuary.worldgen.transit;
|
||||
|
||||
/** The main traversal is core infrastructure in22. Reserve it before optional networks and sites.
|
||||
* Its established civil engineering may bridge a short cavity with certified bearings; the stricter
|
||||
* surface-building policy must not remove this underground destination. No terrain is generated here. */
|
||||
public final class TransitPlanner22 {
|
||||
public static final int MAX_COLUMNS = TransitPlanner21.MAX_COLUMNS;
|
||||
public static final int MAX_PIER = 16, MAX_WEAK_SPAN = 34;
|
||||
private TransitPlanner22() {}
|
||||
public static TransitPlan21 plan(long seed, int centerX, int centerZ, int diameter, TransitPlanner21.Terrain terrain) {
|
||||
return TransitPlanner21.plan(seed, centerX, centerZ, diameter, terrain);
|
||||
}
|
||||
}
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
package fr.koka.sanctuary.worldgen.transit;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
|
||||
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind;
|
||||
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Module;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.storage.loot.BuiltInLootTables;
|
||||
|
||||
/** A seven-wide abandoned industrial route: five roadway cells and one pavement on each side. */
|
||||
public final class TransitRenderer21 {
|
||||
private TransitRenderer21() {}
|
||||
public record Target(String name, int x, int y, int z) {}
|
||||
public static List<Target> targets(Module m) {
|
||||
var result = new ArrayList<Target>();
|
||||
result.add(new Target("roadway", m.width() / 2, 2, m.depth() / 2));
|
||||
if ((m.openings() & 1) != 0) result.add(new Target("north_connection", m.width() / 2, 2, 0));
|
||||
if ((m.openings() & 2) != 0) result.add(new Target("east_connection", m.width() - 1, 2, m.depth() / 2));
|
||||
if ((m.openings() & 4) != 0) result.add(new Target("south_connection", m.width() / 2, 2, m.depth() - 1));
|
||||
if ((m.openings() & 8) != 0) result.add(new Target("west_connection", 0, 2, m.depth() / 2));
|
||||
if (m.kind() == Kind.ACCESS) {
|
||||
result.add(new Target("surface_portico", 8, m.surfaceY() - m.minY() + 1, 0));
|
||||
result.add(new Target("surface_landing", 8, m.surfaceY() - m.minY() + 1, 12));
|
||||
}
|
||||
if (m.kind() == Kind.HALL) {
|
||||
result.add(new Target("maintenance_bench", 3, 2, 4));
|
||||
result.add(new Target("shared_tool_chest", m.width() - 4, 2, m.depth() - 4));
|
||||
result.add(new Target("unfinished_rock_face", m.facing() == 1 ? m.width() / 2 : 3,
|
||||
2, m.facing() == 1 ? 3 : m.depth() / 2));
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
public static void render(Module module, LostCityRenderer.BlockWriter output) {
|
||||
var connected = ConnectedBlocks20.buffer(output);
|
||||
new Painter(module, connected).render();
|
||||
connected.flush();
|
||||
}
|
||||
private static final class Painter {
|
||||
final Module m; final LostCityRenderer.BlockWriter out;
|
||||
Painter(Module module, LostCityRenderer.BlockWriter out) { m = module; this.out = out; }
|
||||
void set(int x, int y, int z, Block block) { set(x, y, z, block.defaultBlockState()); }
|
||||
void set(int x, int y, int z, BlockState state) { out.set(x, y, z, state); }
|
||||
void box(int x0, int y0, int z0, int x1, int y1, int z1, Block block) {
|
||||
for (int x = x0; x <= x1; x++) for (int z = z0; z <= z1; z++) for (int y = y0; y <= y1; y++) set(x, y, z, block);
|
||||
}
|
||||
BlockState stair(Direction direction) {
|
||||
return Blocks.STONE_BRICK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, direction);
|
||||
}
|
||||
void render() {
|
||||
int w = m.width(), d = m.depth();
|
||||
for (var support : m.supports()) box(support.x() - m.minX(), support.minY() - m.minY(), support.z() - m.minZ(),
|
||||
support.x() - m.minX(), -1, support.z() - m.minZ(), Blocks.STONE_BRICKS);
|
||||
box(0, 0, 0, w - 1, 0, d - 1, Blocks.STONE_BRICKS);
|
||||
box(0, 1, 0, w - 1, 1, d - 1, Blocks.SMOOTH_STONE);
|
||||
box(0, 2, 0, w - 1, m.height() - 1, d - 1, Blocks.AIR);
|
||||
if (m.kind() == Kind.ACCESS) { access(); return; }
|
||||
for (int x = 0; x < w; x++) for (int z = 0; z < d; z++) {
|
||||
boolean edge = x == 0 || x == w - 1 || z == 0 || z == d - 1;
|
||||
if (edge) for (int y = 2; y <= 8; y++) set(x, y, z,
|
||||
y == 2 || y == 7 ? Blocks.TUFF_BRICKS : ((x + z) % 5 == 0 ? Blocks.STONE_BRICKS : Blocks.SMOOTH_STONE));
|
||||
set(x, 9, z, Blocks.STONE_BRICKS);
|
||||
boolean alongX = m.facing() == 1;
|
||||
int across = alongX ? z - d / 2 : x - w / 2;
|
||||
if (Math.abs(across) <= 2) set(x, 1, z, Blocks.POLISHED_ANDESITE);
|
||||
else if (Math.abs(across) == 3) set(x, 2, z, Blocks.SMOOTH_STONE_SLAB);
|
||||
}
|
||||
openings();
|
||||
if (m.kind() == Kind.JUNCTION) {
|
||||
// The side branch's mouth and central crossing retain all five roadway cells.
|
||||
for (int x = w / 2 - 2; x <= w / 2 + 2; x++) for (int z = 0; z < d; z++) set(x, 2, z, Blocks.AIR);
|
||||
for (int z = d / 2 - 2; z <= d / 2 + 2; z++) for (int x = 0; x < w; x++) set(x, 2, z, Blocks.AIR);
|
||||
}
|
||||
// Ceiling ribs and luminaires are physically joined to the continuous roof slab.
|
||||
for (int n = 3; n < (m.facing() == 1 ? w : d) - 2; n += 6) {
|
||||
int x = m.facing() == 1 ? n : w / 2, z = m.facing() == 1 ? d / 2 : n;
|
||||
set(x, 8, z, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
|
||||
if (m.facing() == 1) box(x, 8, 1, x, 8, 2, Blocks.POLISHED_TUFF);
|
||||
else box(1, 8, z, 2, 8, z, Blocks.POLISHED_TUFF);
|
||||
}
|
||||
if (m.kind() == Kind.HALL) hall();
|
||||
exposedRock();
|
||||
}
|
||||
void openings() {
|
||||
for (int y = 2; y <= 8; y++) {
|
||||
if ((m.openings() & 1) != 0) box(m.width() / 2 - 3, y, 0, m.width() / 2 + 3, y, 0, Blocks.AIR);
|
||||
if ((m.openings() & 4) != 0) box(m.width() / 2 - 3, y, m.depth() - 1, m.width() / 2 + 3, y, m.depth() - 1, Blocks.AIR);
|
||||
if ((m.openings() & 2) != 0) box(m.width() - 1, y, m.depth() / 2 - 3, m.width() - 1, y, m.depth() / 2 + 3, Blocks.AIR);
|
||||
if ((m.openings() & 8) != 0) box(0, y, m.depth() / 2 - 3, 0, y, m.depth() / 2 + 3, Blocks.AIR);
|
||||
}
|
||||
}
|
||||
void hall() {
|
||||
// Flush crossings join the work bays to the road through the raised pavements.
|
||||
// They also provide an accessible route without requiring a jump over the curb.
|
||||
if (m.facing() == 0) {
|
||||
for (int x : new int[]{m.width() / 2 - 3, m.width() / 2 + 3})
|
||||
for (int z0 : new int[]{4, m.depth() - 4}) box(x, 2, z0 - 1, x, 2, z0 + 1, Blocks.AIR);
|
||||
} else {
|
||||
for (int z : new int[]{m.depth() / 2 - 3, m.depth() / 2 + 3})
|
||||
for (int x0 : new int[]{3, m.width() - 4}) box(x0 - 1, 2, z, x0 + 1, 2, z, Blocks.AIR);
|
||||
}
|
||||
// Maintenance workshop occupies side bays; the centered arrival and return remain unobstructed.
|
||||
for (int z : new int[]{3, m.depth() - 4}) {
|
||||
box(1, 2, z, 1, 7, z, Blocks.CHISELED_TUFF_BRICKS);
|
||||
box(m.width() - 2, 2, z, m.width() - 2, 7, z, Blocks.CHISELED_TUFF_BRICKS);
|
||||
box(1, 8, z, m.width() - 2, 8, z, Blocks.TUFF_BRICKS);
|
||||
}
|
||||
set(2, 2, 3, Blocks.CRAFTING_TABLE); set(2, 2, 4, Blocks.GRINDSTONE);
|
||||
set(2, 2, 5, Blocks.FURNACE.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST));
|
||||
for (int z = m.depth() - 6; z <= m.depth() - 3; z++) set(2, 2, z, Blocks.BARREL);
|
||||
set(m.width() - 3, 2, 3, Blocks.CAULDRON);
|
||||
out.chest(m.width() - 3, 2, m.depth() - 4, BuiltInLootTables.VILLAGE_TOOLSMITH, m.seed() ^ 0x21A1100L);
|
||||
}
|
||||
void exposedRock() {
|
||||
// Abandoned cuts expose finite, ordinary mineable ore in the saved wall geometry.
|
||||
// No ticking reward or regeneration: once mined, these blocks remain gone.
|
||||
if (m.kind() != Kind.HALL && (m.kind() != Kind.GALLERY || Math.floorMod(m.seed(), 4) != 0)) return;
|
||||
boolean alongX = m.facing() == 1;
|
||||
int middle = alongX ? m.width() / 2 : m.depth() / 2;
|
||||
Block[] ore = {Blocks.COAL_ORE, Blocks.COPPER_ORE, Blocks.IRON_ORE,
|
||||
Blocks.GOLD_ORE, Blocks.REDSTONE_ORE, Blocks.LAPIS_ORE};
|
||||
Block deposit = ore[Math.floorMod(m.seed() ^ 0x21CAFE, ore.length)];
|
||||
for (int across = -2; across <= 2; across++) for (int y = 2; y <= 6; y++) {
|
||||
int x = alongX ? middle + across : 0, z = alongX ? 0 : middle + across;
|
||||
Block rock = y == 2 ? Blocks.TUFF : y >= 5 ? Blocks.STONE : Blocks.DEEPSLATE;
|
||||
if (Math.abs(across) + Math.abs(y - 4) <= 1) rock = deposit;
|
||||
if (m.kind() == Kind.HALL && y == 3 && across == -1) rock = Blocks.DEEPSLATE_DIAMOND_ORE;
|
||||
if (m.kind() == Kind.HALL && y == 4 && across == 1) rock = Blocks.EMERALD_ORE;
|
||||
set(x, y, z, rock);
|
||||
}
|
||||
if (m.kind() == Kind.HALL) {
|
||||
// An unfinished excavation bay interrupts the masonry: irregular rock
|
||||
// shoulders and a low stone arch leave its mineral face accessible.
|
||||
for (int across = -3; across <= 3; across++) for (int reach = 0; reach <= 2; reach++) {
|
||||
int x = alongX ? middle + across : reach, z = alongX ? reach : middle + across;
|
||||
set(x, 1, z, Math.abs(across) == 3 ? Blocks.MOSSY_COBBLESTONE : Blocks.TUFF);
|
||||
int ceiling = 7 + ((across + reach) & 1);
|
||||
box(x, ceiling, z, x, 8, z, reach == 0 ? Blocks.DEEPSLATE : Blocks.STONE);
|
||||
if (Math.abs(across) == 3) box(x, 2, z, x, 6 - reach, z,
|
||||
reach == 2 ? Blocks.COBBLESTONE : Blocks.TUFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
void access() {
|
||||
int top = m.surfaceY() - m.minY();
|
||||
// Enclosed local shaft, not a filled foundation under the whole route.
|
||||
for (int n = 0; n <= 16; n++) {
|
||||
box(n, 2, 0, n, top, 0, Blocks.SMOOTH_STONE);
|
||||
box(n, 2, 16, n, top, 16, Blocks.SMOOTH_STONE);
|
||||
box(0, 2, n, 0, top, n, Blocks.STONE_BRICKS);
|
||||
box(16, 2, n, 16, top, n, Blocks.STONE_BRICKS);
|
||||
}
|
||||
box(1, top, 1, 15, top, 15, Blocks.SMOOTH_STONE);
|
||||
// Narrow bearing walls sit beside the flights, with the bottom route crossing at z8..9.
|
||||
box(2, 2, 3, 2, top - 1, 7, Blocks.STONE_BRICKS);
|
||||
box(14, 2, 3, 14, top - 1, 7, Blocks.STONE_BRICKS);
|
||||
openings();
|
||||
int rise = top - 1;
|
||||
for (int step = 1; step <= rise; step++) {
|
||||
int flight = (step - 1) / 5, within = (step - 1) % 5 + 1;
|
||||
boolean south = (flight & 1) == 0;
|
||||
int z = south ? 2 + within : 8 - within, x0 = south ? 3 : 11, y = 1 + step;
|
||||
box(x0, y, z, x0 + 2, Math.min(top + 3, y + 3), z, Blocks.AIR);
|
||||
for (int x = x0; x <= x0 + 2; x++) set(x, y, z, stair(south ? Direction.SOUTH : Direction.NORTH));
|
||||
if (within == 5) {
|
||||
int z0 = south ? 8 : 1;
|
||||
box(3, y, z0, 13, y, z0 + 1, Blocks.SMOOTH_STONE);
|
||||
box(3, y + 1, z0, 13, Math.min(top + 3, y + 3), z0 + 1, Blocks.AIR);
|
||||
if (y + 3 <= top) {
|
||||
set(6, y + 3, z0, Blocks.SMOOTH_STONE);
|
||||
set(6, y + 2, z0, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
|
||||
// The bracket reaches an actual shaft wall; the last open-air landing needs no hanging lamp.
|
||||
box(6, y + 3, south ? z0 : 0, 6, y + 3, south ? 16 : z0, Blocks.STONE_BRICKS);
|
||||
}
|
||||
}
|
||||
}
|
||||
// A north landing just below the surface needs a descending threshold instead
|
||||
// of a slab sealing its headroom. The last flight still reaches the real top deck.
|
||||
int belowNorthLanding = rise % 10;
|
||||
if (belowNorthLanding >= 1 && belowNorthLanding <= 3) {
|
||||
for (int z = 0; z <= belowNorthLanding; z++) for (int x = 7; x <= 9; x++) {
|
||||
box(x, top - z, z, x, top + 3, z, Blocks.AIR);
|
||||
set(x, top - z - 1, z, Blocks.STONE_BRICKS);
|
||||
set(x, top - z, z, stair(Direction.NORTH));
|
||||
}
|
||||
if (belowNorthLanding == 3) {
|
||||
for (int x : new int[]{6, 10}) {
|
||||
set(x, top - 3, 3, Blocks.SMOOTH_STONE);
|
||||
box(x, top - 2, 3, x, top - 1, 3, Blocks.AIR);
|
||||
}
|
||||
}
|
||||
}
|
||||
// A short, grounded portico identifies each naturally approachable surface gate.
|
||||
for (int n = 0; n <= 16; n++) {
|
||||
if (n < 6 || n > 10) set(n, top + 1, 0, Blocks.IRON_BARS);
|
||||
set(n, top + 1, 16, Blocks.IRON_BARS);
|
||||
set(0, top + 1, n, Blocks.IRON_BARS); set(16, top + 1, n, Blocks.IRON_BARS);
|
||||
}
|
||||
for (int x : new int[]{6, 10}) box(x, top + 1, 0, x, top + 3, 0, Blocks.STONE_BRICKS);
|
||||
box(6, top + 4, 0, 10, top + 4, 1, Blocks.STONE_BRICK_SLAB);
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package fr.koka.sanctuary.worldgen.transit;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** New22 corners retain complete walls on sides that have no saved opening.
|
||||
* Existing21 pieces keep their historical paint sequence and geometry. */
|
||||
public final class TransitRenderer22 {
|
||||
private TransitRenderer22() {}
|
||||
public static void render(TransitPlan21.Module module, LostCityRenderer.BlockWriter output) {
|
||||
if (module.kind() != TransitPlan21.Kind.JUNCTION) { TransitRenderer21.render(module, output); return; }
|
||||
var buffer = ConnectedBlocks20.buffer(output);
|
||||
TransitRenderer21.render(module, buffer);
|
||||
// The roadway crossing and its two slab curbs must stop at an unopened wall.
|
||||
for (int direction = 0; direction < 4; direction++) if ((module.openings() & 1 << direction) == 0)
|
||||
for (int across = -3; across <= 3; across++) {
|
||||
int x = direction == 1 ? module.width() - 1 : direction == 3 ? 0 : module.width() / 2 + across;
|
||||
int z = direction == 0 ? 0 : direction == 2 ? module.depth() - 1 : module.depth() / 2 + across;
|
||||
buffer.set(x, 2, z, Blocks.TUFF_BRICKS.defaultBlockState());
|
||||
}
|
||||
buffer.flush();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
package fr.koka.sanctuary.worldgen.transit;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind;
|
||||
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Module;
|
||||
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Support;
|
||||
import fr.koka.sanctuary.worldgen.transit.TransitPlanner21.Terrain;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/** Generation22 fallback alignments around protected volumes. It is called with a fresh terrain
|
||||
* sampler after the historical straight-route search fails. No partial route is ever published. */
|
||||
public final class TransitRoute22 {
|
||||
public static final int MAX_COLUMNS = 12_000;
|
||||
private TransitRoute22() {}
|
||||
public static TransitPlan21 plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
|
||||
if (!List.of(512, 724, 1024).contains(diameter)) throw new IllegalArgumentException("Unsupported transit detour diameter");
|
||||
var probe = new Probe(terrain);
|
||||
var choices = new ArrayList<Choice>(); String last = "no supported protected-volume detour";
|
||||
int farRadius = (int) Math.ceil(diameter * .21 / 17) * 17;
|
||||
int minimumRadius = (int) Math.ceil(diameter / 8.0 / 17) * 17;
|
||||
var radii = new java.util.LinkedHashSet<Integer>(List.of(Math.max(minimumRadius, farRadius - 34), minimumRadius, farRadius));
|
||||
// Search actual entrance pairs before repeatedly testing their identical bad porticos at seven depths.
|
||||
// Fine translations also remove the previous assumption that every entrance must lie on x/z=17n.
|
||||
int[][] offsets = {{0, 0}, {-17, 0}, {17, 0}, {0, -17}, {0, 17}, {-8, -8}, {8, 8}, {-8, 8}, {8, -8},
|
||||
{-34, 0}, {34, 0}, {0, -34}, {0, 34}, {-34, -34}, {34, 34}, {-34, 34}, {34, -34}};
|
||||
probe.columnLimit = 5_500; probe.sampleLimit = 1_200_000;
|
||||
int alignments = 0;
|
||||
try {
|
||||
search: for (var offset : offsets) for (int radius : radii) for (int shape = 0; shape < 8; shape++)
|
||||
for (boolean alongX : new boolean[]{true, false}) {
|
||||
var line = alignment(cx + offset[0], cz + offset[1], radius, 0, shape, alongX);
|
||||
alignments++;
|
||||
int firstTop = approachTop(line.getFirst(), probe), lastTop = approachTop(line.getLast(), probe);
|
||||
String geometry = "shape=" + shape + " axis=" + (alongX ? "east-west" : "north-south")
|
||||
+ " radius=" + radius + " translation=" + offset[0] + "," + offset[1];
|
||||
if (firstTop < 0 || lastTop < 0) { last = "no suitable pair of natural porticos"; diagnostic("portico reject " + geometry); continue; }
|
||||
int selectedAtGeometry = 0;
|
||||
for (int floor : floors(firstTop, lastTop)) {
|
||||
var route = candidate(seed, line, floor, probe);
|
||||
String label = geometry + " floor=" + floor;
|
||||
if (route == null) { last = probe.reason; diagnostic("reject " + label + " " + last + " columns=" + probe.columns.size()); continue; }
|
||||
int cost = route.stream().mapToInt(m -> m.supports().stream().mapToInt(p -> p.maxY() - p.minY() + 1).sum()
|
||||
+ (m.kind() == Kind.ACCESS ? m.surfaceY() - m.floorY() : 0)).sum();
|
||||
choices.add(new Choice(route, cost + line.size() * 4, label));
|
||||
diagnostic("preselected " + label + " modules=" + route.size());
|
||||
if (choices.size() >= 8) break search;
|
||||
// Keep alternative footprints available if exact hydrology rejects one alignment.
|
||||
if (++selectedAtGeometry >= 2) break;
|
||||
}
|
||||
}
|
||||
} catch (BudgetExceeded exhausted) { diagnostic("prescreen budget; retain " + choices.size()); }
|
||||
probe.columnLimit = MAX_COLUMNS; probe.sampleLimit = TransitPlan21.MAX_SAMPLES;
|
||||
choices.sort(java.util.Comparator.comparingInt(Choice::cost));
|
||||
try {
|
||||
for (var choice : choices) {
|
||||
var modules = new ArrayList<Module>();
|
||||
for (var rough : choice.modules()) {
|
||||
var complete = certify(rough, probe, true);
|
||||
if (complete == null) { last = probe.reason; break; }
|
||||
modules.add(complete);
|
||||
}
|
||||
if (modules.size() != choice.modules().size()) { diagnostic("exact reject " + choice.label() + " " + last); continue; }
|
||||
int span = Math.max(Math.abs(modules.getFirst().centerX() - modules.getLast().centerX()),
|
||||
Math.abs(modules.getFirst().centerZ() - modules.getLast().centerZ()));
|
||||
return new TransitPlan21(modules, probe.samples, "accepted detour " + choice.label() + " span=" + span
|
||||
+ " columns=" + probe.columns.size() + " bends=" + modules.stream().filter(m -> m.kind() == Kind.JUNCTION).count());
|
||||
}
|
||||
} catch (BudgetExceeded exhausted) { last = "bounded detour search exhausted: " + probe.reason; }
|
||||
return new TransitPlan21(List.of(), probe.samples, last + "; candidates=" + choices.size() + "; alignments=" + alignments + "; columns=" + probe.columns.size());
|
||||
}
|
||||
private static List<Integer> floors(int a, int b) {
|
||||
int lowerTop = Math.min(a, b), upperTop = Math.max(a, b);
|
||||
var result = new java.util.LinkedHashSet<Integer>();
|
||||
for (int y : new int[]{lowerTop - 22, lowerTop - 38, lowerTop - 54, lowerTop - 70, 216, 200, 184, 160})
|
||||
if (y >= 32 && y >= upperTop - 145 && y <= lowerTop - 18) result.add(y);
|
||||
return List.copyOf(result);
|
||||
}
|
||||
/** Height-independent portion of the existing21 portico certificate. Failure is cached per entrance. */
|
||||
private static int approachTop(Point point, Probe probe) {
|
||||
Integer old = probe.approaches.get(point); if (old != null) return old;
|
||||
int top = -1, low = 384;
|
||||
for (int x = point.x() - 2; x <= point.x() + 2; x++) for (int z = point.z() - 11; z <= point.z() - 9; z++) {
|
||||
int y = probe.surface(x, z); low = Math.min(low, y); top = Math.max(top, y);
|
||||
}
|
||||
boolean valid = low >= 0 && top <= 378 && top - low <= 1;
|
||||
if (valid) for (int x = point.x() - 2; x <= point.x() + 2; x++) for (int z = point.z() - 11; z <= point.z() - 9; z++) {
|
||||
int y = probe.surface(x, z);
|
||||
if (probe.protectedAt(x, y, z) || probe.protectedAt(x, top + 1, z) || probe.protectedAt(x, top + 2, z)) valid = false;
|
||||
}
|
||||
if (valid) for (int x : new int[]{point.x() - 8, point.x(), point.x() + 8})
|
||||
for (int z : new int[]{point.z() - 8, point.z(), point.z() + 8}) {
|
||||
int y = probe.surface(x, z); if (y < top - 24 || y > top + 16) valid = false;
|
||||
}
|
||||
probe.approaches.put(point, valid ? top : -1);
|
||||
return valid ? top : -1;
|
||||
}
|
||||
private record Choice(List<Module> modules, int cost, String label) {}
|
||||
private record Point(int x, int z) {}
|
||||
private static List<Point> alignment(int cx, int cz, int radius, int shift, int shape, boolean alongX) {
|
||||
var points = new ArrayList<Point>();
|
||||
if (shape < 6) {
|
||||
int offset = (shape / 2 + 1) * 34 * ((shape & 1) == 0 ? -1 : 1);
|
||||
points.add(new Point(-radius, 0)); line(points, -radius, offset); line(points, radius, offset); line(points, radius, 0);
|
||||
} else {
|
||||
int offset = 68;
|
||||
points.add(new Point(-radius, -offset));
|
||||
if (shape == 6) line(points, radius, -offset); else line(points, -radius, offset);
|
||||
line(points, radius, offset);
|
||||
}
|
||||
return points.stream().map(p -> new Point(cx + (alongX ? p.x() : p.z() + shift),
|
||||
cz + (alongX ? p.z() + shift : p.x()))).toList();
|
||||
}
|
||||
private static void line(List<Point> points, int x, int z) {
|
||||
var p = points.getLast(); int dx = Integer.signum(x - p.x()) * 17, dz = Integer.signum(z - p.z()) * 17;
|
||||
while (p.x() != x || p.z() != z) { p = new Point(p.x() + dx, p.z() + dz); points.add(p); }
|
||||
}
|
||||
private static int direction(Point from, Point to) {
|
||||
return from.x() == to.x() ? to.z() > from.z() ? 2 : 0 : to.x() > from.x() ? 1 : 3;
|
||||
}
|
||||
private static List<Module> candidate(long seed, List<Point> points, int floor, Probe probe) {
|
||||
// A sparse route-body test rejects a wrong alignment before full shafts and slabs are sampled.
|
||||
for (var p : points) for (int dx : new int[]{-4, 0, 4}) for (int dz : new int[]{-4, 0, 4})
|
||||
for (int y : new int[]{floor - 2, floor, floor + 4, floor + 8}) if (probe.protectedAt(p.x() + dx, y, p.z() + dz)) {
|
||||
probe.reason = "protected alignment [" + p.x() + "," + y + "," + p.z() + "]"; return null;
|
||||
}
|
||||
var first = points.getFirst(); var last = points.getLast();
|
||||
var entrance = access(first.x(), first.z(), floor, 1 << direction(first, points.get(1)), seed ^ 0x2201L, probe, false);
|
||||
if (entrance == null) return null;
|
||||
var exit = access(last.x(), last.z(), floor, 1 << direction(last, points.get(points.size() - 2)), seed ^ 0x2202L, probe, false);
|
||||
if (exit == null) return null;
|
||||
var result = new ArrayList<Module>(); result.add(entrance); int unanchored = 0;
|
||||
for (int i = 1; i < points.size() - 1; i++) {
|
||||
var p = points.get(i); int a = direction(p, points.get(i - 1)), b = direction(p, points.get(i + 1));
|
||||
boolean bend = ((a + 2) & 3) != b, alongX = a == 1 || a == 3;
|
||||
int width = bend || alongX ? 17 : 9, depth = bend || !alongX ? 17 : 9;
|
||||
var module = certify(new Module(bend ? Kind.JUNCTION : Kind.GALLERY, p.x() - width / 2, floor - 1,
|
||||
p.z() - depth / 2, width, 10, depth, alongX ? 1 : 0, (1 << a) | (1 << b),
|
||||
seed ^ (i * 0x9E3779B97F4A7C15L), List.of()), probe, false);
|
||||
if (module == null) return null;
|
||||
unanchored = probe.bearings.get(module) == 15 ? 0 : unanchored + 1;
|
||||
if (unanchored > 2) { probe.reason = "unsupported detour span exceeds 34 blocks"; return null; }
|
||||
result.add(module);
|
||||
}
|
||||
result.add(exit);
|
||||
return List.copyOf(result);
|
||||
}
|
||||
private static Module access(int cx, int cz, int floor, int opening, long seed, Probe probe, boolean full) {
|
||||
int top = -1, low = 384;
|
||||
for (int x = cx - 2; x <= cx + 2; x++) for (int z = cz - 11; z <= cz - 9; z++) {
|
||||
int surface = probe.surface(x, z); top = Math.max(top, surface); low = Math.min(low, surface);
|
||||
}
|
||||
if (low < 0 || top - low > 1) { probe.reason = "access approach relief [" + cx + "," + cz + "] " + low + ".." + top; return null; }
|
||||
if (top < floor + 18 || top > floor + 145) { probe.reason = "surface access height"; return null; }
|
||||
// Its visible north portico meets real ground; its three-wide approach is never an unsupported threshold.
|
||||
for (int x = cx - 2; x <= cx + 2; x++) for (int z = cz - 11; z <= cz - 9; z++) {
|
||||
int y = probe.surface(x, z);
|
||||
if (y < top - 1 || y > top || probe.protectedAt(x, y, z)
|
||||
|| probe.protectedAt(x, top + 1, z) || probe.protectedAt(x, top + 2, z)) {
|
||||
probe.reason = "access approach [" + x + "," + top + "," + z + "] terrain=" + y; return null;
|
||||
}
|
||||
}
|
||||
for (int x : new int[]{cx - 8, cx, cx + 8}) for (int z : new int[]{cz - 8, cz, cz + 8}) {
|
||||
int y = probe.surface(x, z);
|
||||
// The portico alone needs level terrain. A buried shaft can follow a hillside;
|
||||
// its local walls and checked footings carry the deck without flattening the hill.
|
||||
if (y < Math.max(floor + 8, top - 24) || y > top + 16) { probe.reason = "access hillside [" + x + "," + top + "," + z + "] terrain=" + y; return null; }
|
||||
}
|
||||
return certify(new Module(Kind.ACCESS, cx - 8, floor - 1, cz - 8, 17,
|
||||
top - (floor - 1) + 5, 17, 0, opening, seed, List.of()), probe, full);
|
||||
}
|
||||
private static Module certify(Module module, Probe probe, boolean full) {
|
||||
var supports = new ArrayList<Support>();
|
||||
int bearingQuadrants = 0;
|
||||
for (int z = module.minZ();; z = Math.min(z + 4, module.maxZ())) {
|
||||
for (int x = module.minX();; x = Math.min(x + 4, module.maxX())) {
|
||||
if (!probe.owns(x, z)) { probe.reason = "outside island"; return null; }
|
||||
if (!full) for (int y = module.minY(); y <= module.maxY(); y += 4)
|
||||
if (probe.protectedAt(x, y, z)) {
|
||||
probe.reason = "coarse " + module.kind() + " [" + x + "," + y + "," + z + "] " + probe.terrain.obstruction(x, y, z); return null;
|
||||
}
|
||||
int anchor = module.minY() - 1;
|
||||
while (anchor >= module.minY() - 17 && !(probe.solid(x, anchor, z) && probe.solid(x, anchor - 1, z))) anchor--;
|
||||
if (anchor >= module.minY() - 17) {
|
||||
for (int y = anchor - 1; y < module.minY(); y++) if (probe.protectedAt(x, y, z)) {
|
||||
probe.reason = "protected footing"; return null;
|
||||
}
|
||||
if (anchor < module.minY() - 1) supports.add(new Support(x, anchor + 1, z, module.minY() - 1));
|
||||
bearingQuadrants |= 1 << ((x < module.centerX() ? 0 : 1) + (z < module.centerZ() ? 0 : 2));
|
||||
}
|
||||
if (x == module.maxX()) break;
|
||||
}
|
||||
if (z == module.maxZ()) break;
|
||||
}
|
||||
// The continuous two-block deck can span gaps between its bearings. Require
|
||||
// untouched rock in each quarter, not a pier at every point above a cavity.
|
||||
if (bearingQuadrants != 15 && module.kind() != Kind.GALLERY) { probe.reason = "unsupported quarters " + module.kind()
|
||||
+ " [" + module.minX() + "," + module.minY() + "," + module.minZ() + "] mask=" + bearingQuadrants; return null; }
|
||||
if (full) {
|
||||
// Hydrology and all previous structures are checked at every edited voxel after the cheap footing test.
|
||||
for (int z = module.minZ(); z <= module.maxZ(); z++) for (int x = module.minX(); x <= module.maxX(); x++) {
|
||||
if (!probe.owns(x, z)) { probe.reason = "outside island"; return null; }
|
||||
for (int y = module.minY() - 2; y <= module.maxY(); y++) if (probe.protectedAt(x, y, z)) {
|
||||
probe.reason = "volume " + module.kind() + " [" + x + "," + y + "," + z + "] " + probe.terrain.obstruction(x, y, z); return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
var certified = new Module(module.kind(), module.minX(), module.minY(), module.minZ(), module.width(), module.height(),
|
||||
module.depth(), module.facing(), module.openings(), module.seed(), supports);
|
||||
probe.bearings.put(certified, bearingQuadrants);
|
||||
return certified;
|
||||
}
|
||||
private static void diagnostic(String message) {
|
||||
if (Boolean.getBoolean("sanctuary.test.transit22") || Boolean.getBoolean("sanctuary.transit.plannerDiagnostics"))
|
||||
System.getLogger(TransitRoute22.class.getName()).log(System.Logger.Level.INFO, "TransitRoute22 " + message);
|
||||
}
|
||||
private static final class Probe {
|
||||
final Terrain terrain; final Set<Long> columns = new HashSet<>(); int samples; int sampleLimit = TransitPlan21.MAX_SAMPLES, columnLimit = MAX_COLUMNS; String reason = "budget";
|
||||
final java.util.Map<Module, Integer> bearings = new java.util.HashMap<>();
|
||||
final java.util.Map<Point, Integer> approaches = new java.util.HashMap<>();
|
||||
Probe(Terrain terrain) { this.terrain = terrain; }
|
||||
void visit(int x, int z) {
|
||||
if (samples >= sampleLimit) throw new BudgetExceeded();
|
||||
samples++;
|
||||
long key = (long) x << 32 ^ z & 0xffffffffL;
|
||||
if (!columns.contains(key) && columns.size() >= columnLimit) throw new BudgetExceeded();
|
||||
columns.add(key);
|
||||
}
|
||||
boolean owns(int x, int z) { visit(x, z); return terrain.owns(x, z); }
|
||||
boolean solid(int x, int y, int z) { visit(x, z); return terrain.solid(x, y, z); }
|
||||
boolean protectedAt(int x, int y, int z) { visit(x, z); return terrain.protectedAt(x, y, z); }
|
||||
int surface(int x, int z) { visit(x, z); return terrain.surfaceY(x, z); }
|
||||
}
|
||||
private static final class BudgetExceeded extends RuntimeException { private static final long serialVersionUID = 1L; }
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package fr.koka.sanctuary.worldgen.transit;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.util.Optional;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureType;
|
||||
|
||||
/** A local group of neighboring modules; long routes deliberately have multiple native starts. */
|
||||
public final class TransitStructure21 extends Structure {
|
||||
public static final MapCodec<TransitStructure21> CODEC = simpleCodec(TransitStructure21::new);
|
||||
public TransitStructure21(StructureSettings settings) { super(settings); }
|
||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| (generator.rootGeneration() != 21 && generator.rootGeneration() != 22) || generator.rootGeneration() != 22 && !generator.experimentalStructures()) return Optional.empty();
|
||||
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
|
||||
if (island == null || !island.origin() || (island.generation() != 21 && island.generation() != 22)) return Optional.empty();
|
||||
var modules = generator.transitPlan21().modulesForChunk(context.chunkPos().x(), context.chunkPos().z());
|
||||
if (modules.isEmpty()) return Optional.empty();
|
||||
for (var module : modules) {
|
||||
if (Math.abs((module.minX() >> 4) - module.startChunkX()) > 7
|
||||
|| Math.abs((module.maxX() >> 4) - module.startChunkX()) > 7
|
||||
|| Math.abs((module.minZ() >> 4) - module.startChunkZ()) > 7
|
||||
|| Math.abs((module.maxZ() >> 4) - module.startChunkZ()) > 7)
|
||||
throw new IllegalStateException("Transit module exceeds local native reference range");
|
||||
}
|
||||
return Optional.of(new GenerationStub(TransitPiece21.locator(modules.getFirst()), pieces ->
|
||||
modules.forEach(module -> pieces.addPiece(generator.rootGeneration() == 22
|
||||
&& module.kind() == TransitPlan21.Kind.JUNCTION && Integer.bitCount(module.openings()) == 2
|
||||
? new TransitPiece22(module) : new TransitPiece21(module)))));
|
||||
}
|
||||
@Override public StructureType<?> type() { return TransitStructures21.TYPE; }
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package fr.koka.sanctuary.worldgen.transit;
|
||||
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.util.Comparator;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureType;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceType;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
|
||||
public final class TransitStructures21 {
|
||||
public static final ResourceKey<Structure> KEY = ResourceKey.create(Registries.STRUCTURE, SanctuaryMod.id("industrial_transit"));
|
||||
public static final StructureType<TransitStructure21> TYPE = Registry.register(BuiltInRegistries.STRUCTURE_TYPE,
|
||||
SanctuaryMod.id("industrial_transit_v21"), () -> TransitStructure21.CODEC);
|
||||
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
|
||||
SanctuaryMod.id("industrial_transit_module_v21"), TransitPiece21::new);
|
||||
public static final StructurePieceType PIECE22 = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
|
||||
SanctuaryMod.id("industrial_transit_module_v22"), TransitPiece22::new);
|
||||
private static final ThreadLocal<Integer> PLACEMENT_DEPTH = ThreadLocal.withInitial(() -> 0);
|
||||
private TransitStructures21() {}
|
||||
public static void register() {}
|
||||
public static boolean isPlacing() { return PLACEMENT_DEPTH.get() > 0; }
|
||||
static void placing(Runnable action) {
|
||||
int previous = PLACEMENT_DEPTH.get(); PLACEMENT_DEPTH.set(previous + 1);
|
||||
try { action.run(); } finally { if (previous == 0) PLACEMENT_DEPTH.remove(); else PLACEMENT_DEPTH.set(previous); }
|
||||
}
|
||||
public static void generate(ExpansionChunkGenerator generator, RegistryAccess registries,
|
||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 21 && generator.rootGeneration() != 22)
|
||||
|| generator.rootGeneration() != 22 && !sanctuary.experimentalStructures()) return;
|
||||
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
||||
if (island == null || !island.origin() || (island.generation() != 21 && island.generation() != 22)) return;
|
||||
if (sanctuary.transitPlan21().modulesForChunk(chunk.getPos().x(), chunk.getPos().z()).isEmpty()) return;
|
||||
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||
StructureStart previous = manager.getStartForStructure(holder.value(), chunk);
|
||||
if (previous != null && previous.isValid()) return;
|
||||
var random = generator.randomState(island);
|
||||
var sampler = random.createClimateSampler(SamplerContext.builder().enableCaches().build());
|
||||
var start = holder.value().generate(holder, level, registries, generator, generator.getBiomeSource(), sampler,
|
||||
random, templates, island.seed(), chunk.getPos(), previous == null ? 0 : previous.getReferences(),
|
||||
chunk, holder.value().biomes()::contains);
|
||||
if (start.isValid()) manager.setStartForStructure(holder.value(), start, chunk);
|
||||
}
|
||||
/** Locate selects a saved surface portico rather than teleporting the player into a deep road. */
|
||||
public static Pair<BlockPos, Holder<Structure>> findNearest(ServerLevel level, HolderSet<Structure> structures,
|
||||
BlockPos origin, int radius, boolean skipReferenced) {
|
||||
if (!Level.OVERWORLD.equals(level.dimension())
|
||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| (generator.rootGeneration() != 21 && generator.rootGeneration() != 22) || generator.rootGeneration() != 22 && !generator.experimentalStructures()
|
||||
|| !level.structureManager().shouldGenerateStructures()) return null;
|
||||
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||
if (!structures.contains(holder)) return null;
|
||||
var candidates = generator.transitPlan21().modules().stream()
|
||||
.filter(m -> m.kind() == TransitPlan21.Kind.ACCESS)
|
||||
.filter(m -> Math.abs((long) m.startChunkX() - (origin.getX() >> 4)) <= radius
|
||||
&& Math.abs((long) m.startChunkZ() - (origin.getZ() >> 4)) <= radius)
|
||||
.sorted(Comparator.comparingDouble(m -> origin.distSqr(TransitPiece21.locator(m)))).toList();
|
||||
for (var module : candidates) {
|
||||
var chunk = level.getChunk(module.startChunkX(), module.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
var start = level.structureManager().getStartForStructure(holder.value(), chunk);
|
||||
if (start == null || !start.isValid() || skipReferenced && !start.canBeReferenced()) continue;
|
||||
if (skipReferenced) level.structureManager().addReference(start);
|
||||
return Pair.of(TransitPiece21.locator(module), holder);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package fr.koka.sanctuary.worldgen.transit;
|
||||
|
||||
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
|
||||
/** Read-only, bounded native-noise cache. Unknown columns always reject construction. */
|
||||
public final class TransitTerrain21 implements TransitPlanner21.Terrain {
|
||||
public static final int MAX_COLUMNS = 12_000;
|
||||
private final ExpansionIsland island;
|
||||
private final NoiseBasedChunkGenerator generator;
|
||||
private final RandomState random;
|
||||
private final LevelHeightAccessor height;
|
||||
private final Predicate<BlockPos> protection;
|
||||
private final Map<Long, NoiseColumn> columns = new HashMap<>();
|
||||
|
||||
public TransitTerrain21(ExpansionIsland island, NoiseBasedChunkGenerator generator, RandomState random,
|
||||
LevelHeightAccessor height, Predicate<BlockPos> protection) {
|
||||
this.island = island; this.generator = generator; this.random = random;
|
||||
this.height = height; this.protection = protection;
|
||||
}
|
||||
private NoiseColumn column(int x, int z) {
|
||||
if (!owns(x, z)) return null;
|
||||
long key = ((long) x << 32) ^ (z & 0xffffffffL);
|
||||
var found = columns.get(key);
|
||||
if (found != null) return found;
|
||||
if (columns.size() >= MAX_COLUMNS) return null;
|
||||
var result = generator.getBaseColumn(x, z, height, random);
|
||||
columns.put(key, result);
|
||||
return result;
|
||||
}
|
||||
@Override public boolean owns(int x, int z) { return island.contains(x, z); }
|
||||
@Override public boolean solid(int x, int y, int z) {
|
||||
if (y < 0 || y >= 384) return false;
|
||||
var column = column(x, z);
|
||||
return column != null && column.getBlock(y).isSolid() && column.getBlock(y).getFluidState().isEmpty();
|
||||
}
|
||||
@Override public boolean protectedAt(int x, int y, int z) {
|
||||
if (y < 0 || y >= 384) return true;
|
||||
var column = column(x, z);
|
||||
return column == null || !column.getBlock(y).getFluidState().isEmpty()
|
||||
|| protection.test(new BlockPos(x, y, z));
|
||||
}
|
||||
@Override public int surfaceY(int x, int z) {
|
||||
var column = column(x, z);
|
||||
if (column == null) return -1;
|
||||
for (int y = 380; y >= 1; y--) {
|
||||
var state = column.getBlock(y);
|
||||
if (!state.isAir()) return state.isSolid() && state.getFluidState().isEmpty() ? y : -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@Override public String obstruction(int x, int y, int z) {
|
||||
var sample = column(x, z);
|
||||
if (sample == null) return "unknown/unowned column";
|
||||
var state = sample.getBlock(y);
|
||||
if (!state.getFluidState().isEmpty()) return "native fluid " + state;
|
||||
return protection.test(new BlockPos(x, y, z)) ? "earlier structure/hydrology mask" : "none";
|
||||
}
|
||||
public int columnsUsed() { return columns.size(); }
|
||||
}
|
||||
@@ -127,5 +127,27 @@
|
||||
"sanctuary.expansion.error.void_reuse_incomplete": "Void reuse is incomplete. The reservation is retained; check the server log.",
|
||||
"sanctuary.world_options.experimental": "Experimental structures",
|
||||
"sanctuary.world_options.experimental_hint": "Mines, strange rooms and abandoned facilities. This choice is saved with the new world.",
|
||||
"structure.sanctuary.forgotten_networks": "Forgotten networks"
|
||||
"structure.sanctuary.forgotten_networks": "Forgotten networks",
|
||||
"sanctuary.underground_hall.entry": "Underground hall entrance: %s, %s, %s",
|
||||
"sanctuary.world_options.heritage22_hint": "Adds experimental buildings and networks. The main traversal and underground hall belong to the island and remain without this option.",
|
||||
"sanctuary.world_options.heritage_hint": "Adds forgotten networks, buildings and abandoned routes to the new island. This choice is saved with the world.",
|
||||
"structure.sanctuary.heritage_networks": "Abandoned infrastructure",
|
||||
"sanctuary.heritage.kind.freight_station": "Freight station",
|
||||
"sanctuary.heritage.kind.post_office": "Post office",
|
||||
"sanctuary.heritage.kind.communal_depot": "Communal depot",
|
||||
"sanctuary.heritage.kind.expedition_office": "Expedition office and map archive",
|
||||
"sanctuary.heritage.kind.observatory": "Observatory and weather station",
|
||||
"sanctuary.heritage.kind.rail_workshop": "Rail workshop",
|
||||
"sanctuary.heritage.kind.pump_station": "Pump station",
|
||||
"sanctuary.heritage.kind.substation": "Utility substation",
|
||||
"sanctuary.heritage.kind.greenhouse": "Greenhouse and seed store",
|
||||
"sanctuary.heritage.kind.void_dock": "Expedition dock",
|
||||
"sanctuary.heritage.kind.radio_relay": "Radio relay",
|
||||
"sanctuary.heritage.kind.photo_club": "Photography club",
|
||||
"sanctuary.heritage.kind.impossible_service": "Changing rooms and strange service rooms",
|
||||
"sanctuary.heritage.kind.construction_refuge": "Construction refuge",
|
||||
"sanctuary.heritage.unavailable": "This survey requires a recent Sanctuary world with experimental structures enabled.",
|
||||
"sanctuary.heritage.header": "Abandoned infrastructure: %s planned sites and %s links. Entrance coordinates:",
|
||||
"sanctuary.heritage.entry": "%s : %s / %s / %s",
|
||||
"sanctuary.transit.entry": "Industrial galleries — entrance: %s %s %s"
|
||||
}
|
||||
|
||||
@@ -127,5 +127,27 @@
|
||||
"sanctuary.expansion.error.void_reuse_incomplete": "La réutilisation du vide est incomplète. La réservation est conservée ; consultez le journal du serveur.",
|
||||
"sanctuary.world_options.experimental": "Structures expérimentales",
|
||||
"sanctuary.world_options.experimental_hint": "Mines, salles étranges et installations abandonnées. Ce choix reste attaché au nouveau monde.",
|
||||
"structure.sanctuary.forgotten_networks": "Réseaux oubliés"
|
||||
"structure.sanctuary.forgotten_networks": "Réseaux oubliés",
|
||||
"sanctuary.underground_hall.entry": "Accès à la salle souterraine : %s, %s, %s",
|
||||
"sanctuary.world_options.heritage22_hint": "Ajoute les bâtiments et réseaux expérimentaux. La grande traversée et la salle souterraine font partie de l’île et restent présentes sans cette option.",
|
||||
"sanctuary.world_options.heritage_hint": "Ajoute les anciens réseaux, bâtiments et voies abandonnées sur la nouvelle île. Ce choix est enregistré avec le monde.",
|
||||
"structure.sanctuary.heritage_networks": "Anciennes infrastructures",
|
||||
"sanctuary.heritage.kind.freight_station": "Gare de marchandises",
|
||||
"sanctuary.heritage.kind.post_office": "Centre postal",
|
||||
"sanctuary.heritage.kind.communal_depot": "Dépôt communal",
|
||||
"sanctuary.heritage.kind.expedition_office": "Bureau des expéditions et cartothèque",
|
||||
"sanctuary.heritage.kind.observatory": "Observatoire et station météo",
|
||||
"sanctuary.heritage.kind.rail_workshop": "Atelier ferroviaire",
|
||||
"sanctuary.heritage.kind.pump_station": "Station de pompage",
|
||||
"sanctuary.heritage.kind.substation": "Sous-station",
|
||||
"sanctuary.heritage.kind.greenhouse": "Serre et graineterie",
|
||||
"sanctuary.heritage.kind.void_dock": "Quai d’expédition",
|
||||
"sanctuary.heritage.kind.radio_relay": "Relais radio",
|
||||
"sanctuary.heritage.kind.photo_club": "Club photo",
|
||||
"sanctuary.heritage.kind.impossible_service": "Vestiaires et salles étranges",
|
||||
"sanctuary.heritage.kind.construction_refuge": "Refuge de chantier",
|
||||
"sanctuary.heritage.unavailable": "Ce relevé nécessite un monde Sanctuary récent avec les structures expérimentales activées.",
|
||||
"sanctuary.heritage.header": "Anciennes infrastructures : %s lieux et %s liaisons prévus. Coordonnées des entrées :",
|
||||
"sanctuary.heritage.entry": "%s : %s / %s / %s",
|
||||
"sanctuary.transit.entry": "Galeries industrielles — entrée : %s %s %s"
|
||||
}
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:iron_axe"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:torch",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:bread",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:iron_shovel"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:bread",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:torch",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:compass"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:paper",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:map",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:minecart"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:rail",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:coal",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:iron_hoe"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:wheat_seeds",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:pumpkin_seeds",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:clock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:torch",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:string",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:spyglass"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:paper",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:book",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:book"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:paper",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:glass_pane",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:compass"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:paper",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:string",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:bucket"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:string",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:torch",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:compass"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:redstone",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:paper",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:iron_pickaxe"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:rail",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:coal",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:clock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:redstone",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:copper_ingot",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:lead"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:torch",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:bread",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user