Compare commits

...
Author SHA1 Message Date
koka 2facf6f722 docs: record verified alpha19 release and Prism update
Build Sanctuary / build (push) Canceled after 0s
2026-09-09 22:49:20 +02:00
koka c23e72b8e6 feat(worldgen): add reversible alpha19 heritage structures and routes
Build Sanctuary / build (push) Canceled after 0s
2026-09-09 22:42:32 +02:00
koka f2da922bad Document structure ideas and abandoned island routes
Build Sanctuary / build (push) Canceled after 0s
2026-09-09 20:03:42 +02:00
56 changed files with 4259 additions and 55 deletions
+24
View File
@@ -1,5 +1,29 @@
# Changelog
## 0.1.0-alpha.19 — 2026-09-09
- Quatorze nouvelles infrastructures abandonnées sur l’île initiale, avec pièces,
accès, mobilier, secrets et outils vanilla associés. La présence varie selon
la graine et le relief ; les quatorze types ont été rencontrés en tests natifs.
- Voies de fret aux rails incomplets et chemins entre destinations réelles.
Le témoin Moyen, graine 0, contient treize lieux et sept liaisons, dont un
fret de 499 blocs entre dépôt et pompage. Tracés enregistrés par chunk.
- Option FR/EN « Structures expérimentales » dans le bouton natif Personnaliser,
trois tailles conservées. Codec `sanctuary:island_v19` et journal 19 ; les
anciens mondes gardent leurs plans et futurs chunks. Un nouveau monde avec
loption désactivée conserve le socle urbain sans les expériences 17/19.
- Commande opérateur `/sanctuary structures` pour relever les entrées.
La ville et ses sous-sols sont conservés ; les systèmes de la bêta restent
à construire.
`check build assemblePack` passe, dont les huit GameTests généraux. Les trois
formats sont vérifiés nativement ; le témoin moyen se rouvre avec ses 112 pièces,
13 coffres, 68 cibles accessibles et la modification témoin. Les identités 17
restent identiques à la release publiée. JAR, MRpack et ZIP Prism vérifiés ;
release et canal packwiz publiés. Deux synchronisations isolées puis deux dans
la même instance Prism passent, avec 767 fichiers personnels et réglages suivis
conservés. Voir [les preuves et limites](docs/testing-alpha19.md).
## 0.1.0-alpha.17 — 2026-09-09
- Prototype de trois réseaux oubliés sur l’île initiale : mines et ateliers,
+22 -5
View File
@@ -15,11 +15,28 @@ infrastructures qui permettront progressivement d'ouvrir de nouveaux continents.
Ce dépôt démarre la nouvelle base Fabric. La [vision complète](docs/vision.md)
conserve les intentions ; le [backlog](docs/backlog.md) suit les
tickets. Les systèmes d’économie, de progression collective et les dimensions
décrits dans la vision ne sont pas encore implémentés. **Lalpha.17 est publiée** :
trois familles de réseaux oubliés expérimentaux rejoignent la ville et ses grands
sous-sols sur les nouvelles îles de départ. Le canal packwiz et la même instance
décrits dans la vision ne sont pas encore implémentés. **Lalpha.19 est publiée** :
quatorze types dinfrastructures abandonnées et des liaisons de fret rejoignent
la ville et ses réseaux souterrains sur les nouvelles îles de départ.
Le canal packwiz et la même instance
Prism sont à jour ; voir [Distribution](docs/packwiz.md).
## Alpha.19 — anciennes infrastructures
Le chantier ajoute une première passe des quatorze lieux du
[catalogue](docs/structures-audit.md), avec leurs accès, des petits butins vanilla
et des voies de fret ou chemins abandonnés entre destinations réelles.
Loption **Structures expérimentales** reste enregistrée à la création du monde.
La nouvelle génération garde les anciennes sauvegardes intactes. Aucun système
de progression, mailbox, caméra ou réacteur nest activé.
Les trois tailles sont vérifiées en génération native ; création et réouverture
conservent les structures et les coffres. `check build assemblePack` passe.
Pour voir le grand fret : **Moyen, graine 0**, option activée ;
`/sanctuary structures` liste les entrées. Voir [les preuves et la carte](docs/testing-alpha19.md).
La [release alpha.19](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.19)
et le canal packwiz sont publiés. La même instance Prism est synchronisée deux
fois, avec 767 fichiers personnels et réglages suivis conservés.
## Alpha.17 — prototype des réseaux oubliés
La nouvelle génération expérimentale recherche trois petits réseaux sur l’île
@@ -409,7 +426,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.19 |
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 +455,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.19.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).
+50
View File
@@ -700,6 +700,56 @@ Retour arrière par branche/artefact et option désactivée
pour un autre nouveau monde. Aucun retour binaire vers 16 dans une sauvegarde 17
nest déclaré compatible ; les mondes dessai restent séparés.
### WG-21 — Audit des structures et des anciennes circulations
**Branche :** `codex/structures-audit`.
**Demande :** avant de poursuivre limplémentation, proposer des structures
contemporaines, des axes abandonnés à l’échelle de l’île et leurs liens possibles
avec lexpansion, la mailbox et les objets utiles cachés dans des lieux secrets.
**État : audit documenté, propositions à discuter.** Le
[catalogue et schéma de relations](structures-audit.md) distinguent les trois
petits ensembles actuels des futurs tracés entre destinations, proposent des
voies de fret dégradées et des usages pour les bâtiments. Les outils vanilla
servent didées de butin ; la caméra et les systèmes restent pour la bêta.
Aucune structure, règle de butin, version binaire ou instance Prism modifiée.
La sélection des lieux et la réalisation des axes feront lobjet du chantier
suivant ; ce catalogue nest pas une liste dajouts tous validés.
### WG-22 — Anciennes infrastructures et liaisons — alpha.19
**Branche :** `codex/heritage-worldgen-alpha19`.
**Demande validée :** première passe de toutes les nouvelles structures du
catalogue WG-21 et de leurs liaisons avant la bêta, en conservant la possibilité
de revenir à une génération sans ce chantier.
- Ajouter les quatorze lieux avec architectures, entrées, pièces, mobilier et
petits butins vanilla associés à leur usage. Présence selon le terrain ;
relever les lieux réellement générés et donner les coordonnées des témoins.
- Planifier des voies de fret et chemins entre destinations réelles, avec
sections abandonnées lisibles, appuis et passages vérifiés. Rechercher les
raccords aux anciennes installations dans un budget borné.
- Garder une majorité naturelle à l’île ; conserver le terrain, lhydrologie,
la ville et les réseaux existants comme base. Première île uniquement.
- Nouveau codec 19, option expérimentale sauvegardée à la création du monde.
Option désactivée : socle urbain seul. Les mondes 17 et antérieurs gardent
leur génération, leurs pièces et leurs futurs chunks. Aucune conversion,
suppression de structures ou rétrogradation dune sauvegarde 19.
- Les bâtiments préparent les futurs services ; mailbox, deposit boxes,
caméra, réacteur, progression et récompenses personnelles restent pour la
bêta. Les coffres de cette passe sont partagés et utilisent du butin vanilla.
- Vérifier créations/réouvertures, option désactivée, anciens mondes, supports,
circulations et vrais placements. `check build assemblePack`, artefacts,
publication packwiz et mise à jour de la même instance Prism.
**État : livré en alpha.19 ; code, artefacts et synchronisation Prism vérifiés.** Voir
[les essais natifs et la carte des lieux](testing-alpha19.md). Version 19 demandée
explicitement ; aucune version 18 publiée
dans ce dépôt à louverture du chantier. Le catalogue est maintenant accepté
pour une première passe, sans imposer sa présence exhaustive sur chaque graine.
## 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.
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

+45 -1
View File
@@ -13,10 +13,54 @@ 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.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 utilise cette
version, au commit `ea883b7`. JAR, [MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.19/Sanctuary-0.1.0-alpha.19.mrpack)
et ZIP damorçage Prism ont été vérifiés par téléchargement public.
SHA-256 du JAR :
```text
a159b00a2778020800e81381e0608495a4d7909f1f853f0b8812cd4017865b0b
```
Première passe de quatorze architectures et de leurs voies de fret ou chemins
abandonnés sur l’île initiale. Les trois tailles sont vérifiées nativement ;
les quatorze types ont chacun été rencontrés et parcourus sur ces témoins.
Créer un **nouveau monde Sanctuary, Moyen, graine 0**, puis garder
**Structures expérimentales** activées dans **Personnaliser** : ce témoin
contient treize lieux et sept liaisons, dont un fret de 499 blocs.
`/sanctuary structures` relève les entrées. La présence des bâtiments et les
raccords dépendent du relief ; voir [les preuves et la carte](testing-alpha19.md).
`check build assemblePack` et les huit GameTests généraux passent.
Création et réouverture natives conservent 112 pièces et 13 coffres ; 68 cibles
sont accessibles. Le mode désactivé passe. Les empreintes de la génération 17
restent identiques aux preuves publiées, y compris après réouverture.
Deux synchronisations isolées puis deux dans la même instance Prism Mac
réussissent. Un seul JAR Sanctuary alpha.19 est actif ; les **767 fichiers
personnels et réglages suivis** conservent leurs hashes. Aucune sauvegarde
personnelle na été ouverte, convertie ou régénérée. Le dossier reste
`Sanctuary-0.1.0-alpha.1`, affiché « Sanctuary Beta ».
Le JAR 17 et le suivi packwiz sont sauvegardés dans
`sanctuary-backups/before-0.1.0-alpha.19/`. Reçus locaux ignorés :
`build/alpha19-isolated-validation.json` et `build/alpha19-prism-validation.json`.
Pour comparer, créer un autre nouveau monde avec loption désactivée : le socle
urbain demeure, sans les expériences 17/19. Cette option ne supprime pas de
structures dans une sauvegarde existante. Les anciens mondes conservent leur
génération ; une sauvegarde 19 ne doit pas être ouverte avec lancien binaire 17.
Les bâtiments préparent les futurs usages, mais mailbox, caméra, réacteur et
progression restent pour la bêta. Aucun essai visuel dans le client ni essai
Windows natif na été effectué.
## Alpha.17 publiée — 9 septembre 2026
[Sanctuary 0.1.0-alpha.17](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.17)
est publiée depuis le commit source `0969ed5`. Le canal stable utilise cette
est publiée depuis le commit source `0969ed5`. Le canal stable utilisait alors cette
version, au commit `6e0c297`. JAR, [MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.17/Sanctuary-0.1.0-alpha.17.mrpack)
et ZIP damorçage Prism ont été vérifiés par téléchargement public.
SHA-256 du JAR :
+164
View File
@@ -0,0 +1,164 @@
# Structures contemporaines et anciennes circulations de Sanctuary
Audit de conception du 9 septembre 2026, après lessai de lalpha.17.
Ticket WG-21 ; branche `codex/structures-audit`.
**Propositions à discuter : aucune nouvelle génération ni règle de progression
nest implémentée par ce document.** Le pack reste en alpha.17.
## Ce que produit actuellement le mod
Lalpha.17 recherche au maximum trois petits ensembles, un par famille. Chaque
ensemble rassemble trois à cinq pièces autour dun accès de surface, avec des
salles généralement de 9 × 9 blocs. Leur position est cherchée indépendamment.
Les rails sont des tronçons intérieurs de la galerie minière. Les rues et ponts
de Lost City desservent son quartier compact. Aucun itinéraire ne relie ces
ensembles à l’échelle de l’île : le manque daxes perçu en jeu correspond donc
au fonctionnement actuel.
Les ateliers possèdent des outils et équipements vanilla utilisables. Les
coffres emploient les tables de maison des plaines et de cartographe ; le mod
na pas encore de distribution de trésors conçue pour chaque lieu secret.
La mailbox, les deposit boxes, la caméra, la machine dexpansion et les
récompenses personnelles restent des systèmes futurs.
Sources locales : [planificateur17](../mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/ruins/RuinsPlanner17.java),
[rendu17](../mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/ruins/RuinsRenderer17.java),
[preuves alpha.17](testing-alpha17.md), [vision](vision.md).
## Faire comprendre les usages avant dintroduire leurs systèmes
Chaque lieu devrait répondre à trois questions : pourquoi était-il construit
ici, par où arrivaient ses occupants ou ses marchandises, et que peut en faire
un joueur aujourdhui ? Une plateforme de chargement, un vestiaire, des rayonnages
et une sortie de service racontent davantage son usage quune pièce portant un nom.
La chaîne proposée pour lexpansion est : **observer → préparer une expédition
→ rassembler des ressources → ouvrir un continent → construire ses liaisons**.
Les lieux rendent ces activités compréhensibles. Ils nimposent pas encore une
campagne, un coût ou un ordre obligatoire de déblocage.
La poste rend lisible la différence entre recevoir personnellement un colis et
contribuer à un stock collectif dans le dépôt. La salle dexpansion déjà présente
peut devenir le point où ces efforts collectifs prennent sens. Une future
mailbox ne doit pas être confondue avec un coffre ordinaire décoré en alpha.
Ces bâtiments pourraient présenter les services en bêta sans obliger les joueurs
à revenir au même guichet pour chaque action ; les règles dusage restent ouvertes.
## Catalogue proposé
Les objets cités sont des **idées de butin**, pas des ajouts de cette livraison.
Les variantes dune même famille peuvent se remplacer selon la graine et le
relief ; ce catalogue ne demande pas de tout placer sur chaque île.
| Lieu | Forme, pièces et implantation | Découverte en alpha à concevoir | Lien possible avec la bêta |
| --- | --- | --- | --- |
| **Gare de marchandises** | Quai bas, petite halle, balance, guichet, poste daiguillage et voie de garage ; à la jonction ville/industrie. | Comprendre la ligne abandonnée, trouver un wagonnet et quelques rails dans la réserve. | Transport des productions vers le dépôt collectif ; réparations faites par les joueurs. |
| **Centre postal et atelier de tri** | Hall public, comptoir, casiers individuels, salle de tri, quai arrière, bureau du facteur. | Suivre physiquement le passage dun colis de lentrée à son casier ; boussole dans le bureau fermé. | Découverte de la mailbox personnelle et de la livraison des achats. |
| **Halle du dépôt communal** | Grande porte, travées numérotées, aire de déchargement, mezzanine dinventaire et atelier annexe. | Stock modeste, outils de manutention suggérés par le décor, chantier de rayonnages interrompu. | Contributions via les deposit boxes, stocks collectifs et préparation dexpansions. |
| **Bureau des expéditions et cartothèque** | Salle de préparation, table de cartographie, vestiaire, réserves de voyage, balcon orienté vers le vide. | Boussole, papier et cartes vierges ; comparer les directions depuis le balcon. | Relevés de ressources, choix de climat et préparation des départs. Les cartes préremplies restent un travail distinct. |
| **Observatoire et station météo** | Petite tour sur une crête, plateforme dégagée, salle des instruments, bureau abrité. | Une longue-vue vanilla dans une armoire repérable ; observation du ciel et du relief. | Constellations, signatures climatiques et futur instrument dobservation éventuel. |
| **Dépôt de maintenance ferroviaire** | Hangar court avec fosse dentretien, établis, pièces détachées, vestiaire et établi personnel caché. | Réparer des tronçons, récupérer un outil utile ; retrouver le travail interrompu dun ancien occupant. | Production, maintenance et futures machines. |
| **Station de pompage et château deau** | Prise deau, bassin, galerie de vannes, passerelles, réservoir et exutoire ; implantation dictée par leau et les altitudes. | Traverser les bassins, rejoindre un local sec par une coursive ; seau ou canne à pêche. | Eau pour les ateliers et cultures. Aucun système de pompage actif nest supposé déjà disponible. |
| **Sous-station et galerie de maintenance** | Cour technique, salle de distribution, chemins de câbles, tranchée laissée ouverte et pièce de service. | Horloge dans le poste de garde, redstone modeste dans latelier ; repérer les départs de galeries. | Préparer visuellement les infrastructures à réactiver. Ni réacteur ni alimentation obligatoire de lexpansion décidés ici. |
| **Serre municipale et graineterie** | Serre partiellement effondrée, tables de rempotage, irrigation, remise et chambre de conservation. | Graines vanilla adaptées au lieu, pot de fleurs, outils de culture. | Récoltes localisées et cuisine d**Its Alive !** ; pas de plantes futures distribuées en alpha. |
| **Quai dexpédition au bord du vide** | Plateforme ancrée, hangar, rambarde, grue inachevée et petite salle dattente. | Point de vue et lieu de départ évident ; accès depuis la ville ou le dépôt. | Bateaux volants, Happy Ghast, biplans, ziplines ou ponts construits par les joueurs. Aucune île future présumée en face. |
| **Relais radio et cabane du gardien** | Antenne sobre sur un promontoire, escalier dentretien, logement minuscule et réserve sous le plancher. | Repère visible de loin ; boussole ou horloge, passage discret vers un atelier. | Réseau de services et rendez-vous collectifs, si ce rôle est retenu. |
| **Club photo et laboratoire caché** | Petite boutique ou salle associative, réserve, chambre noire et séchoir ; accès secondaire depuis une cour ou un hôtel. | Un atelier secret identifiable, avec une récompense vanilla provisoire. | Lieu naturel pour découvrir une **caméra en bêta** ; aucune caméra factice ou promesse de butin inexistant en alpha. |
| **Vestiaires et salles de service impossibles** | Derrière les grandes piscines : vestiaires, laverie, couloir de livraison, puis vaste salle carrelée hors proportion. | Étrangeté progressive, passages secondaires, récompense au bout dun détour lisible. | Donjons physiques et secrets ; distincts de la dimension Backrooms des objets perdus. |
| **Refuge de chantier dans un ouvrage inachevé** | Cabane dans une pile de viaduc, atelier suspendu avec ancrages, escalier arrêté à une plateforme et réserves laissées sur place. | Un lit, un outil et une installation récupérable ; sentir quun autre joueur s’était établi ici. | Réappropriation par les joueurs, bases individuelles et chemins à terminer. |
## Des axes à l’échelle de l’île
Une **ancienne ligne de fret**, quelques embranchements et un chemin de service
peuvent suffire à donner une organisation au territoire. Son tracé suit la pente,
entre brièvement dans la roche et franchit les ravins avec des ouvrages ponctuels.
La majorité de l’île reste naturelle. Une longue voie ne signifie pas une grande
gare ni une grande ville supplémentaire.
```mermaid
flowchart LR
M[Mine] --- A[Atelier et maintenance]
A --- G[Gare de marchandises]
G --- D[Dépôt communal]
G --- P[Centre postal]
D --- Q[Quai d'expédition]
P --- C[Cartothèque]
C --- O[Observatoire]
D -. passage technique .- E[Salle d'expansion existante]
E -. galerie de service .- H[Pompage et égouts]
H -. accès secondaire .- S[Salles étranges]
```
Ce schéma décrit les relations proposées, pas un plan rigide à reproduire ni
des liaisons toutes ferroviaires. Mine, atelier, gare et dépôt partagent la ligne
de fret ; les autres raccords peuvent être des rues, sentiers, escaliers ou
galeries. Les réseaux techniques et les lieux étranges gardent leurs itinéraires
propres, avec seulement quelques intersections utiles.
Pour une rupture crédible, il faut concevoir **le trajet complet entre deux
destinations présentes**, puis son état dabandon :
- **Rails arrachés** : le ballast et les traverses montrent le chemin à réparer.
- **Petit pont effondré** : les culées et une pile conservée indiquent la portée
manquante ; un détour permet de rejoindre lautre côté.
- **Tunnel éboulé** : les deux portails suivent le même axe ; une galerie de
maintenance donne un autre parcours lorsque le relief le permet.
- **Travaux jamais terminés** : terrassement, échafaudage, matériaux et butoir
forment une fin intentionnelle. Un chantier peut réellement sarrêter là.
- **Voie envahie** : végétation et débris alternent avec des sections utilisables.
Les coupures ne doivent pas reproduire laspect dun raccord de chunks cassé.
Les rails qui subsistent ont un support ; les tronçons réparables suivent des
pentes et courbes praticables en wagonnet. Un souterrain conserve des appuis et
un passage pour le joueur. Les raccords impossibles sont abandonnés proprement,
sans publier une porte donnant dans la roche ni annoncer une gare inexistante.
Une liaison vers un nouveau continent reste à construire après son ouverture.
Le quai exprime la possibilité de partir ; il ne prédit pas un terrain encore
absent, sa position exacte ou la route que choisiront les joueurs.
## Récompenser la découverte dun lieu secret
Un secret devrait offrir **un objet principal cohérent avec son ancien usage** :
longue-vue dans lobservatoire, boussole dans la cartothèque, horloge dans le poste
daiguillage, outil dans latelier. Quelques fournitures accompagnent cet objet.
Le plaisir vient de trouver un équipement utile tôt et de comprendre son origine ;
un objet vanilla fabriquable ne devient pas artificiellement une relique unique.
Laccès se lit dans le décor : fenêtre donnant sur une pièce sans porte visible,
escalier derrière une étagère déplacée, trappe de maintenance, conduit débouchant
sur un établi éclairé. La pièce peut être cachée sans exiger de casser tous les
murs au hasard. Des variations portent sur le plan, laccès et lusage, pas
seulement sur la couleur dune salle identique.
Le bâti est lui-même une récompense récupérable. Les rails, équipements datelier,
blocs de fer et autres matériaux du décor doivent compter dans linventaire
des ressources réellement générées ; une infrastructure trop généreuse pourrait
remplacer tout le minage du début de partie. Cela se mesure après génération.
Pour un premier essai de butin en alpha, les coffres pourraient rester des
conteneurs vanilla partagés, sans renouvellement automatique. La coexistence
des **reliques collectives uniques** et des **récompenses personnelles** nécessite
le contrat de bêta. Laccès à la mailbox ou à lexpansion ne devrait pas dépendre
dun objet ordinaire que le premier explorateur peut emporter et perdre.
## Ordre de travail proposé
1. Relier une destination existante et une nouvelle destination avec un axe
visible à l’échelle de l’île : atelier/mines, gare et dépôt. Vérifier les
trajets, les supports et quelques ruptures réparables en jeu.
2. Donner à la ville une poste et une cartothèque ; placer un observatoire dans
le relief, avec des accès logiques et un premier atelier secret.
3. Raccorder ponctuellement les galeries techniques aux égouts, piscines et
salles étranges existantes. Garder des secteurs indépendants et des accès
secondaires plutôt que transformer tous les sous-sols en un unique donjon.
4. Définir puis essayer les petites tables de butin par usage avant dintroduire
les objets et règles de la bêta.
Avant d’écrire ce générateur, valider la place de la ligne de fret, les premiers
lieux retenus et le rôle des ruptures : réparations libres, détours ou chantiers
à prolonger. Les essais doivent distinguer un axe planifié, ses tronçons réellement
générés, ceux qui sont praticables et ceux qui sont volontairement interrompus.
La recherche des raccords doit rester bornée, sans explorer ou charger toute
l’île pour chercher un passage. Les mondes publiés gardent leur génération ;
aucune transformation dune sauvegarde existante nest autorisée par cet audit.
+173
View File
@@ -0,0 +1,173 @@
# Alpha.19 — anciennes infrastructures de Sanctuary
**État : alpha.19 publiée, même instance Prism synchronisée et vérifiée.** Ticket WG-22, branche
`codex/heritage-worldgen-alpha19`. Minecraft 26.3-pre-2, mêmes dépendances que
lalpha.17. La numérotation 19 est demandée explicitement ; aucune 18 nest
intercalée dans ce dépôt.
## Périmètre et réversibilité
Première passe des quatorze lieux du [catalogue](structures-audit.md) : gare de
marchandises, centre postal, dépôt communal, cartothèque, observatoire,
maintenance ferroviaire, pompage, sous-station, serre, quai dexpédition,
relais radio, club photo, vestiaires/salles étranges et refuge de chantier.
Les plans recherchent des sites admissibles et des voies de fret ou chemins
entre destinations réelles ; ils ne garantissent pas tous les lieux sur chaque
graine. Les mesures et coordonnées des essais figurent ci-dessous.
Créer un **nouveau monde Sanctuary** et utiliser **Personnaliser** :
Petit 512, Moyen 724 par défaut ou Grand 1 024, avec **Structures expérimentales**.
Loption est activée par défaut et enregistrée avec le générateur 19. La désactiver
pour un autre nouveau monde conserve le socle urbain sans les réseaux
expérimentaux 17 et les infrastructures 19. Elle ne retire aucun bloc ni ne change
les règles dune sauvegarde existante.
Les mondes 17 et antérieurs gardent leurs codecs, plans, pièces et futurs chunks.
Le preset 17 public est conservé octet par octet sous un identifiant historique
caché à la création. Le générateur 19 et son journal sont distincts. Il nexiste
aucune conversion ou rétrogradation de sauvegarde 19 vers un ancien binaire.
Lancienne release reste disponible pour ses propres mondes, et loption dun
nouveau monde permet de comparer les deux configurations du nouveau binaire.
Les nouveaux lieux sont réservés à l’île initiale. La ville et les anciens
réseaux servent de base ; leurs protections et celles de lhydrologie restent
respectées. Les liaisons vers de futurs continents seront construites après
leur apparition, sans présumer leurs emplacements dans cette génération.
## Objets et systèmes
Les bâtiments suggèrent leurs usages futurs par leurs pièces et aménagements.
Mailbox, deposit boxes, caméra, réacteur, progression et récompenses
personnelles ne sont pas activés. Chaque nouveau lieu dispose dune table
de coffre propre à la génération 19, avec un outil vanilla lié à son usage et
quelques fournitures. Les coffres sont partagés et ne se renouvellent pas.
La valeur des rails, équipements et blocs récupérables du décor sajoute au
butin. Leur distribution doit être constatée sur les témoins, sans transformer
cette première passe en une garantie d’équilibrage économique pour la bêta.
## Vérifications effectuées
- Placement des quatorze architectures, ouvertures, circulation, orientation
et appuis du mobilier ; accès réel au coffre et retour possible.
- Plans bornés sur les trois tailles, terrain non occupé, supports, continuité
des tracés et distinction entre ballast praticable et rails retirés.
- Pièces natives et références locales, y compris pour les longues voies ;
géométrie, orientation et rails sauvegardés dans un schéma strict.
- Création et réouverture dun monde 19 avec modification témoin ; conservation
des starts, butins différés, accès, routes et supports.
- Option désactivée, paramètres sauvegardés, génération 17 comparée aux preuves
de la version publiée, refus des migrations implicites de journaux.
- `./gradlew check build assemblePack`, artefacts et index, publication immuable
puis canal packwiz et deux synchronisations de la même instance Prism.
Les smokes des quatorze architectures, de leurs quatre orientations, des
plans sur les trois tailles et des pièces natives passent. Le mode désactivé
est vérifié dans un monde 19 neuf : aucun site ni liaison expérimental.
`./gradlew check build assemblePack` passe, avec les huit GameTests généraux,
125 chunks FULL, 5 923 blocs deau conservés et 1 800 ticks de fluides.
Le JAR, le MRpack, les hashes du pack et le ZIP Prism sont vérifiés.
La release et le canal packwiz sont publiés. Deux synchronisations isolées puis
deux dans la même instance Prism passent ; 767 fichiers personnels et réglages
suivis conservent leurs hashes. Le JAR précédent et le suivi packwiz sont
sauvegardés dans `sanctuary-backups/before-0.1.0-alpha.19/`.
Voir [le reçu de distribution](packwiz.md#alpha19-publiée--9-septembre-2026).
Aucun essai visuel dans le client ni essai Windows natif na été effectué.
## Témoin natif Moyen, graine 0
Créer un nouveau monde avec **Structures expérimentales activées**.
`/sanctuary structures` liste les entrées prévues sans charger de chunks ;
`/locate structure sanctuary:heritage_networks` retrouve lentrée dun site.
Le relevé ci-dessous provient des placements et parcours vérifiés par le moteur.
![Entrées et tracés du témoin alpha.19](images/heritage19-seed0.svg)
Treize des quatorze lieux apparaissent. **La cartothèque est absente de cette
graine** : la recherche na pas trouvé demplacement admissible. La ville et les
trois réseaux souterrains 17 sont conservés. Les raccords recherchés vers la
ville et lancienne mine ne sont pas admis sur ce témoin.
Les sept liaisons comprennent un tracé de fret **dépôt → pompage de 499 blocs**,
dune étendue de 245 blocs sur X et 181 sur Z. Son ballast est praticable ;
478 rails sont présents et 21 sont volontairement manquants. Les autres voies
et chemins relient gare, dépôt, atelier, poste, club photo, serre et vestiaires.
Le quai, lobservatoire, le relais, la sous-station et le refuge gardent leurs
propres accès au terrain, même sans liaison avec ce réseau.
La création et la réouverture dans deux JVM distinctes passent chacune les deux
GameTests : **112 pièces natives, 111 chunks FULL, 80 487 blocs contrôlés,
2 882 contrôles de portance, 13 coffres différés et 68 cibles accessibles**.
Trois tirages de chacune des quatorze tables de butin sont vérifiés
séparément, sans ouvrir les coffres persistants. Une modification témoin à
`-204 / 255 / -13` est conservée à la réouverture.
Empreinte stable du plan :
`7f476c2ecca703a05bf88a550c61cc5cbeff2594024c43ccd9f3620bbc2ad3de`.
JVM de création `36366`, de réouverture `36481`. Reçus locaux ignorés :
`build/alpha19-evidence/heritage19-create.json` et `heritage19-reload.json`.
La planification supplémentaire utilise 15 144 colonnes natives, dans un plafond
de 16 000, en 20,7 s sur le Mac de développement. Ce temps concerne ce plan,
pas le chargement total du jeu. Les routes sont découpées par chunk ; aucun
start ne couvre toute leur longueur ni ne charge une destination lointaine.
La prospection 19 consulte les plans hydrologiques de toute lenveloppe de
départ, avant de publier les sites. Le contrôle général utilise donc pour 19
une limite géographique calculée : 5 régions possibles pour Petit, 9 pour
Moyen/Grand, avec les budgets de colonnes et de sondes contrôlés séparément.
Le témoin Moyen en consulte 5 ; les quatre régions périphériques représentent
3,38 s de calcul hydrologique supplémentaire dans le relevé du build.
Ces consultations ne matérialisent aucun chunk. Les générations historiques
conservent le contrôle initial de 4 régions maximum autour du spawn.
```sh
./gradlew :sanctuary:runGameTest -PsanctuaryHeritage19Tests=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false
./gradlew :sanctuary:runGameTest -PsanctuaryHeritage19Tests=true -PsanctuaryExpansionReload=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false
```
## Autres tailles et couverture du catalogue
Les deux GameTests natifs passent aussi sur Petit et Grand, graine 42.
Les quatorze architectures ont ainsi chacune été rencontrées et parcourues
sur le terrain réel, en complément des rotations vérifiées par les smokes.
| Taille et graine | Lieux | Liaisons | Pièces natives | Chunks FULL |
| --- | ---: | ---: | ---: | ---: |
| Petit 512, 42 | 10 | 2 | 35 | 44 |
| Moyen 724, 0 | 13 | 7 | 112 | 111 |
| Grand 1 024, 42 | 13 | 4 | 47 | 61 |
Pour visiter la cartothèque : **Petit, graine 42**, entrée
`-101 / 251 / -25`, ou **Grand, graine 42**, entrée `-252 / 249 / 146`.
Le Petit testé na ni poste, ni observatoire, ni atelier ferroviaire, ni quai.
Le Grand testé na pas de quai. Aucun grand axe nest admis sur ces deux témoins :
les portées maximales de leurs liaisons sont respectivement de 32 et 55 blocs.
Le témoin Moyen, graine 0, reste celui conseillé pour découvrir le grand fret.
Ces résultats rendent explicite la variation de cette première passe ; ils ne
constituent pas une garantie de présence ou de connectivité pour chaque graine.
Reçus locaux ignorés : `build/alpha19-evidence/small42-create.json` (JVM 36591)
et `large42-create.json` (JVM 36669).
## Compatibilité 17 vérifiée
Deux JVM de développement distinctes créent puis rouvrent le témoin 17
**Moyen, graine 0**, avec le nouveau binaire. Les deux GameTests passent dans
chaque exécution. Le codec, les starts natifs, les butins différés et les
parcours restent identiques aux preuves de lalpha.17 publiée. Le bloc dor
placé par le test est conservé à la réouverture.
Empreinte du plan 17 :
`7358ecd7841c03be1977a7f45f37c0c3ef83a32dc27e8d3ec4f0d85d6ee5d3ef`.
Les trois starts restent ceux documentés dans [le témoin 17](testing-alpha17.md).
Reçus locaux ignorés : `build/alpha19-evidence/legacy17-create.json` et
`legacy17-reload.json`.
```sh
./gradlew :sanctuary:runGameTest -PsanctuaryRuins17Tests=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false
./gradlew :sanctuary:runGameTest -PsanctuaryRuins17Tests=true -PsanctuaryExpansionReload=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false
```
Ces commandes visent uniquement le monde de développement ignoré. La première
le recrée ; la seconde conserve ce même monde et lit ses preuves précédentes.
+2 -2
View File
@@ -8,6 +8,6 @@ loader_version=0.19.5
loom_version=1.17.20
fabric_api_version=0.160.0+26.3
mod_version=0.1.0-alpha.17
pack_version=0.1.0-alpha.17
mod_version=0.1.0-alpha.19
pack_version=0.1.0-alpha.19
maven_group=fr.koka.sanctuary
+16 -2
View File
@@ -40,13 +40,14 @@ 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' : '19'))
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.lostCity16', providers.gradleProperty('sanctuaryLostCity16Tests').getOrElse('false'))
systemProperty('sanctuary.test.ruins17', providers.gradleProperty('sanctuaryRuins17Tests').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 +70,12 @@ tasks.named('processGametestResources') {
inputs.property('lostCity16', lostCity16)
def ruins17 = providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('ruins17', ruins17)
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: 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 +418,14 @@ 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) }
}
@@ -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); }
}
}
@@ -210,8 +210,30 @@ public final class PopulationDiagnostics {
}
void prepare() throws IOException {
helper.assertTrue(regionsAtInitialSpawn > 0 && regionsAtInitialSpawn <= 4,
"Initial spawn must plan only nearby regions, not materialize the large island: " + regionsAtInitialSpawn);
boolean heritage = level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator sanctuary
&& sanctuary.rootGeneration() == 19 && sanctuary.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));
@@ -45,8 +45,11 @@ 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 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");
helper.assertTrue(generator.rootGeneration() == 19 && generator.source().generation() == 19 && generator.experimentalStructures(), "The public preset uses generation19 with its new-world option");
helper.assertTrue(generator.initialDiameter() == 724, "The public preset defaults to Medium / 724 blocks");
for (var tag : List.of(WorldPresetTags.NORMAL, WorldPresetTags.EXTENDED)) {
var visible = presets.getOrThrow(tag).stream().map(holder -> holder.getRegisteredName())
@@ -56,11 +59,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() == 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");
"Generation19 omitted experimental option defaults to enabled");
helper.assertTrue(!((SanctuaryChunkGenerator) historical16.overworld()).withExperimentalStructures(true).experimentalStructures(),
"The new-world switch cannot activate experimental generation in a historical16 generator");
serialized.remove("initial_diameter");
@@ -21,7 +21,7 @@ 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.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 +46,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", 19);
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19) throw new IllegalArgumentException("Unsupported test generation");
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 19 ? "sanctuary" : "sanctuary_v" + generation));
}
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,9 @@ 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);
fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.register();
fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.register();
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("population_island"), PopulationIslandDensity.CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("population_island"), PopulationIslandBiomeSource.CODEC);
@@ -51,11 +51,11 @@ public final class SanctuaryWorldOptionsScreen extends Screen {
contents.addChild(sizeButton);
diameterHint = contents.addChild(new MultiLineTextWidget(diameterHint(), font)
.setMaxWidth(310).setCentered(true));
if (generator.rootGeneration() == 17) {
if (generator.rootGeneration() == 17 || generator.rootGeneration() == 19) {
contents.addChild(CycleButton.builder((Boolean enabled) -> enabled ? CommonComponents.OPTION_ON : CommonComponents.OPTION_OFF, experimental)
.withValues(true, false).create(0, 0, 280, 20, Component.translatable("sanctuary.world_options.experimental"),
(button, value) -> experimental = value));
contents.addChild(new MultiLineTextWidget(Component.translatable("sanctuary.world_options.experimental_hint"), font)
contents.addChild(new MultiLineTextWidget(Component.translatable(generator.rootGeneration() == 19 ? "sanctuary.world_options.heritage_hint" : "sanctuary.world_options.experimental_hint"), font)
.setMaxWidth(310).setCentered(true));
}
layout.addToContents(contents);
@@ -122,6 +122,7 @@ 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);
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 or 19 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; }
public boolean origin() { return id.equals("sanctuary"); }
public double referenceDiameter() { return origin() && diameter == 724 ? Math.sqrt(2) * 512 : 512; }
public double horizontalScale() { return origin() ? 1.0 : diameter / referenceDiameter(); }
@@ -25,7 +25,8 @@ 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() && generator.protectsCityDecoration(pos))
callback.setReturnValue(false);
}
@@ -40,7 +41,7 @@ public abstract class UnifiedDecorationProtectionMixin {
if (state.is(Blocks.SUGAR_CANE)) return;
// Generation16 reserves its architectural volume before decoration. Only its own
// native pieces may replace water there; published generations retain the old rule.
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17)
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19)
&& fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
&& generator.protectsCityDecoration(pos)) return;
if (generator.protectsDecoration(pos) && !region.getBlockState(pos).equals(state)) {
@@ -42,6 +42,10 @@ 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)));
private final List<Holder<Biome>> allBiomes;
private final PopulationIslandBiomeSource population;
private final boolean cities;
@@ -75,7 +79,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 == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; }
@Override public BiomeResolver createResolver(Climate.Sampler sampler) {
BiomeResolver woodlands = population.createResolver(sampler);
@@ -45,9 +45,17 @@ 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)));
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 volatile Context originContext;
private final int generation;
private net.minecraft.world.level.LevelHeightAccessor heightAccessor;
@@ -83,12 +91,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) && 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 == 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 +105,10 @@ 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) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| generation == 19 && heritagePlan19.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ);
}
public IslandCapacity capacity() {
return switch (initialDiameter()) {
@@ -134,11 +144,13 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
context(islands.getFirst());
prepareCities(islands);
prepareRuins(islands);
prepareHeritage(islands);
}
@Override public void setIslands(List<ExpansionIsland> islands) {
// Complete and publish city geometry before workers can observe the new owner list.
prepareCities(islands);
prepareRuins(islands);
prepareHeritage(islands);
super.setIslands(islands);
}
private void prepareCities(List<ExpansionIsland> islands) {
@@ -149,7 +161,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) && !cityStructuresEnabled) {
cityProtection16 = fr.koka.sanctuary.worldgen.city.LostCityProtection16.EMPTY;
activeCityPlans = java.util.Map.of();
source().setCityPlans(java.util.Map.of());
@@ -165,7 +177,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
}
cityProtection15 = protection.build();
}
if (generation == 16 || generation == 17) {
if (generation == 16 || generation == 17 || generation == 19) {
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
for (var island : islands) {
protection.add(published.get(island.id()), undergroundPlan16(island));
@@ -180,7 +192,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
source().setCityPlans(published);
}
private void prepareRuins(List<ExpansionIsland> islands) {
if (generation != 17 || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return;
if ((generation != 17 && generation != 19) || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var terrain = context(root);
var sampled = new fr.koka.sanctuary.worldgen.ruins.RuinsTerrain17(root, terrain.generator(), terrain.random(),
@@ -192,6 +204,22 @@ 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 || !experimentalStructures || !cityStructuresEnabled || heritagePrepared) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var terrain = context(root);
var sampled = new fr.koka.sanctuary.worldgen.heritage.HeritageTerrain19(root, terrain.generator(), terrain.random(),
heightAccessor, pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()));
long start = System.nanoTime();
sampled.discoverLandmarks(lostCityPlan(root), ruinsPlan17);
var plan = fr.koka.sanctuary.worldgen.heritage.HeritagePlanner19.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
heritagePlan19 = plan;
heritagePrepared = true;
SanctuaryMod.LOGGER.info("Heritage19 seed {}: {} sites, {} routes, {} segments, {} probes, {} native columns in {} ms; {}",
root.seed(), plan.sites().size(), plan.links().size(), plan.segments().size(), plan.samples(), sampled.columnsUsed(),
(System.nanoTime() - start) / 1_000_000, plan.diagnostics());
}
private 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();
@@ -217,7 +245,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
return cityPlans.computeIfAbsent(cityIdentity(island), ignored -> {
long start = System.nanoTime();
var terrain = context(island);
if (generation == 16 || generation == 17) {
if (generation == 16 || generation == 17 || generation == 19) {
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 +316,9 @@ 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) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 17 || generation == 19) && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ())
|| generation == 19 && heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ());
if (generation == 15) return cityProtection15.contains(pos.getX(), pos.getY(), pos.getZ());
if (generation != 14) return false;
var island = islandAt(pos.getX(), pos.getZ());
@@ -311,20 +340,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) && 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) 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) 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) return null;
lostCityPlan(island);
return cityReservations16.get(cityIdentity(island));
}
@@ -336,8 +365,10 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
net.minecraft.core.BlockPos origin, int radius, boolean skipReferenced) {
if (structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.city.LostCityStructures.KEY))
return fr.koka.sanctuary.worldgen.city.LostCityStructures.findNearest(level, structures, origin, radius, skipReferenced);
if (generation == 17 && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.KEY))
if ((generation == 17 || generation == 19) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.KEY))
return fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.findNearest(level, structures, origin, radius, skipReferenced);
if (generation == 19 && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.KEY))
return fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.findNearest(level, structures, origin, radius, skipReferenced);
return super.findNearestMapStructure(level, structures, origin, radius, skipReferenced);
}
@Override protected Context context(ExpansionIsland island) {
@@ -365,7 +396,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) && cityStructuresEnabled) {
var reserved = cityReservations16.get(cityIdentity(island));
if (reserved != null) {
var mutable = new net.minecraft.core.BlockPos.MutableBlockPos();
@@ -29,17 +29,17 @@ public final class LostCityStructure extends Structure {
throw new IllegalStateException("Lost City district exceeds native structure reference range");
}
return Optional.of(new GenerationStub(LostCityStructures.locator(district), pieces -> {
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17) && generator.cityReservation16(island) != null)
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19) && generator.cityReservation16(island) != null)
pieces.addPiece(new LostCityFoundationPiece16(generator.cityReservation16(island), district.seed()));
if (generator.rootGeneration() == 15) for (var support : generator.citySupports(island)) {
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
pieces.addPiece(new LostCitySupportPiece15(support));
}
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17)) for (var support : generator.citySupports16(island)) {
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19)) 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() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19) ? 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,7 +49,7 @@ 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)) 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)
@@ -0,0 +1,34 @@
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.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);
return plan.sites().size();
}))));
}
}
@@ -0,0 +1,185 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import java.util.ArrayList;
import java.util.Set;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.ResourceKey;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.loot.LootTable;
/** Self-contained schema19 geometry. Reload never needs the planner or a distant route destination. */
public final class HeritagePiece19 extends StructurePiece {
private static final Set<String> SITE_KEYS = Set.of("Schema", "Type", "Id", "Kind", "MinX", "FloorY", "MinZ",
"Width", "Depth", "Height", "Facing", "Seed", "FoundationDepth", "Supports", "Entry");
private static final Set<String> ROUTE_KEYS = Set.of("Schema", "Type", "Id", "FromId", "ToId", "Kind",
"StartChunkX", "StartChunkZ", "Cells", "Seed");
private static final Set<String> SUPPORT_KEYS = Set.of("X", "MinY", "Z", "MaxY");
private static final Set<String> ENTRY_KEYS = Set.of("X", "Y", "Z", "Facing");
private static final Set<String> CELL_KEYS = Set.of("X", "FloorY", "Z", "FoundationDepth", "Rail", "RailPresent");
private final HeritagePlan19.Site site;
private final HeritagePlan19.Segment segment;
public HeritagePiece19(HeritagePlan19.Site site) {
super(HeritageStructures19.PIECE, 0, bounds(site));
this.site = site; this.segment = null; setOrientation(null);
}
public HeritagePiece19(HeritagePlan19.Segment segment) {
super(HeritageStructures19.PIECE, 0, bounds(segment));
this.site = null; this.segment = segment; setOrientation(null);
}
public HeritagePiece19(StructurePieceSerializationContext context, CompoundTag tag) {
super(HeritageStructures19.PIECE, tag);
var payload = tag.getCompound("SanctuaryHeritage").orElseThrow(() -> new IllegalArgumentException("Missing saved heritage geometry"));
String type = string(payload, "Type");
if (type.equals("SITE")) { site = decodeSite(payload); segment = null; }
else if (type.equals("ROUTE")) { site = null; segment = decodeSegment(payload); }
else throw new IllegalArgumentException("Unknown heritage piece type");
if (!getBoundingBox().equals(isSite() ? bounds(site) : bounds(segment)))
throw new IllegalArgumentException("Heritage geometry disagrees with its saved native bounds");
setOrientation(null);
}
public boolean isSite() { return site != null; }
public HeritagePlan19.Site site() { if (site == null) throw new IllegalStateException("This heritage piece is a route"); return site; }
public HeritagePlan19.Segment segment() { if (segment == null) throw new IllegalStateException("This heritage piece is a site"); return segment; }
@Override public BlockPos getLocatorPosition() {
if (isSite()) return locator(site);
var cell = segment.cells().getFirst(); return new BlockPos(cell.x(), cell.floorY() + 1, cell.z());
}
public static BlockPos locator(HeritagePlan19.Site site) { return new BlockPos(site.entry().x(), site.entry().y(), site.entry().z()); }
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
tag.put("SanctuaryHeritage", isSite() ? encodeSite(site) : encodeSegment(segment));
}
public static CompoundTag encodeSite(HeritagePlan19.Site site) {
var tag = header("SITE", site.id(), site.kind().name(), site.seed());
tag.putInt("MinX", site.minX()); tag.putInt("FloorY", site.floorY()); tag.putInt("MinZ", site.minZ());
tag.putInt("Width", site.width()); tag.putInt("Depth", site.depth()); tag.putInt("Height", site.height());
tag.putInt("Facing", site.facing()); tag.putInt("FoundationDepth", site.foundationDepth());
var entry = new CompoundTag(); entry.putInt("X", site.entry().x()); entry.putInt("Y", site.entry().y());
entry.putInt("Z", site.entry().z()); entry.putInt("Facing", site.entry().facing()); tag.put("Entry", entry);
var supports = new ListTag();
for (var support : site.supports()) {
var value = new CompoundTag(); value.putInt("X", support.x()); value.putInt("MinY", support.minY());
value.putInt("Z", support.z()); value.putInt("MaxY", support.maxY()); supports.add(value);
}
tag.put("Supports", supports); return tag;
}
public static CompoundTag encodeSegment(HeritagePlan19.Segment segment) {
var tag = header("ROUTE", segment.id(), segment.kind().name(), segment.seed());
tag.putString("FromId", segment.fromId()); tag.putString("ToId", segment.toId());
tag.putInt("StartChunkX", segment.startChunkX()); tag.putInt("StartChunkZ", segment.startChunkZ());
var cells = new ListTag();
for (var cell : segment.cells()) {
var value = new CompoundTag(); value.putInt("X", cell.x()); value.putInt("FloorY", cell.floorY());
value.putInt("Z", cell.z()); value.putInt("FoundationDepth", cell.foundationDepth());
value.putString("Rail", cell.rail().name()); value.putBoolean("RailPresent", cell.railPresent()); cells.add(value);
}
tag.put("Cells", cells); return tag;
}
public static HeritagePlan19.Site decodeSite(CompoundTag tag) {
schema(tag, "SITE", SITE_KEYS);
var entry = compound(tag, "Entry", ENTRY_KEYS);
var saved = list(tag, "Supports", 9); var supports = new ArrayList<HeritagePlan19.Support>();
for (var value : saved) {
var support = element(value, SUPPORT_KEYS);
supports.add(new HeritagePlan19.Support(coordinate(support, "X"), integer(support, "MinY"),
coordinate(support, "Z"), integer(support, "MaxY")));
}
return new HeritagePlan19.Site(id(tag, "Id"), HeritagePlan19.Kind.valueOf(string(tag, "Kind")),
coordinate(tag, "MinX"), integer(tag, "FloorY"), coordinate(tag, "MinZ"), integer(tag, "Width"),
integer(tag, "Depth"), integer(tag, "Height"), integer(tag, "Facing"), seed(tag), integer(tag, "FoundationDepth"),
supports, new HeritagePlan19.Entry(coordinate(entry, "X"), integer(entry, "Y"), coordinate(entry, "Z"), integer(entry, "Facing")));
}
public static HeritagePlan19.Segment decodeSegment(CompoundTag tag) {
schema(tag, "ROUTE", ROUTE_KEYS);
var saved = list(tag, "Cells", 256); var cells = new ArrayList<HeritagePlan19.Cell>();
for (var value : saved) {
var cell = element(value, CELL_KEYS);
type(cell, "RailPresent", Tag.TAG_BYTE);
byte present = cell.getByte("RailPresent").orElseThrow();
if (present != 0 && present != 1) throw new IllegalArgumentException("Invalid saved rail presence");
cells.add(new HeritagePlan19.Cell(coordinate(cell, "X"), integer(cell, "FloorY"), coordinate(cell, "Z"),
integer(cell, "FoundationDepth"), HeritagePlan19.Rail.valueOf(string(cell, "Rail")), present == 1));
}
String from = id(tag, "FromId"), to = id(tag, "ToId");
var kind = HeritagePlan19.RouteKind.valueOf(string(tag, "Kind"));
if (from.equals(to) && kind != HeritagePlan19.RouteKind.PATH)
throw new IllegalArgumentException("Only a local pedestrian landing may join a destination to itself");
return new HeritagePlan19.Segment(id(tag, "Id"), from, to, kind,
integer(tag, "StartChunkX"), integer(tag, "StartChunkZ"), cells, seed(tag));
}
private static CompoundTag header(String type, String id, String kind, long seed) {
var value = new CompoundTag(); value.putInt("Schema", 19); value.putString("Type", type);
value.putString("Id", id); value.putString("Kind", kind); value.putLong("Seed", seed); return value;
}
private static void schema(CompoundTag tag, String expected, Set<String> keys) {
if (!tag.keySet().equals(keys) || integer(tag, "Schema") != 19 || !string(tag, "Type").equals(expected))
throw new IllegalArgumentException("Unsupported or incomplete saved heritage schema");
}
private static CompoundTag compound(CompoundTag tag, String key, Set<String> keys) {
type(tag, key, Tag.TAG_COMPOUND); return element(tag.get(key), keys);
}
private static CompoundTag element(Tag tag, Set<String> keys) {
if (!(tag instanceof CompoundTag value) || !value.keySet().equals(keys))
throw new IllegalArgumentException("Unsupported or incomplete saved heritage element");
return value;
}
private static ListTag list(CompoundTag tag, String key, int maximum) {
type(tag, key, Tag.TAG_LIST); var value = tag.getList(key).orElseThrow();
if (value.size() > maximum) throw new IllegalArgumentException("Oversized saved heritage list"); return value;
}
private static long seed(CompoundTag tag) { type(tag, "Seed", Tag.TAG_LONG); return tag.getLong("Seed").orElseThrow(); }
private static String id(CompoundTag tag, String key) {
var value = string(tag, key);
if (value.isBlank() || value.length() > 128) throw new IllegalArgumentException("Invalid saved heritage identifier"); return value;
}
private static String string(CompoundTag tag, String key) { type(tag, key, Tag.TAG_STRING); return tag.getString(key).orElseThrow(); }
private static int coordinate(CompoundTag tag, String key) {
int value = integer(tag, key);
if (value < -30_000_000 || value > 30_000_000) throw new IllegalArgumentException("Heritage coordinate outside world bounds"); return value;
}
private static int integer(CompoundTag tag, String key) { type(tag, key, Tag.TAG_INT); return tag.getInt(key).orElseThrow(); }
private static void type(CompoundTag tag, String key, byte id) {
if (tag.get(key) == null || tag.get(key).getId() != id) throw new IllegalArgumentException("Invalid saved heritage field: " + key);
}
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
final int x0 = isSite() ? site.minX() : segment.minX();
final int y0 = isSite() ? site.floorY() : segment.minY();
final int z0 = isSite() ? site.minZ() : segment.minZ();
var writer = new LostCityRenderer.BlockWriter() {
private BlockPos world(int x, int y, int z) { return new BlockPos(Math.addExact(x0, x), Math.addExact(y0, y), Math.addExact(z0, z)); }
@Override public void set(int x, int y, int z, BlockState block) {
var position = world(x, y, z);
if (getBoundingBox().isInside(position) && clip.isInside(position))
placeBlock(level, block, position.getX(), position.getY(), position.getZ(), clip);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
var position = world(x, y, z);
if (getBoundingBox().isInside(position) && clip.isInside(position))
createChest(level, clip, RandomSource.create(seed), position, loot, null);
}
};
HeritageStructures19.placing(() -> {
if (isSite()) HeritageRenderer19.render(site, writer);
else HeritageRenderer19.renderSegment(segment, writer);
});
}
public static BoundingBox bounds(HeritagePlan19.Site site) {
return new BoundingBox(site.minX(), site.boundsMinY(), site.minZ(), site.maxX(), site.boundsMaxY(), site.maxZ());
}
public static BoundingBox bounds(HeritagePlan19.Segment segment) {
return new BoundingBox(segment.minX(), segment.boundsMinY(), segment.minZ(), segment.maxX(), segment.boundsMaxY(), segment.maxZ());
}
}
@@ -0,0 +1,200 @@
package fr.koka.sanctuary.worldgen.heritage;
import java.util.HashSet;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** Immutable generation19 catalogue and real-destination routes. Pieces save geometry, not terrain queries. */
public final class HeritagePlan19 {
private final List<Site> sites;
private final List<Landmark> landmarks;
private final List<Link> links;
private final List<Segment> segments;
private final int samples, columns;
private final Map<Kind, String> diagnostics;
private final Map<Long, List<Range>> protectedColumns;
private final Map<Long, List<Box>> protectedChunks;
public static final int MAX_COLUMNS = 16_000, MAX_SAMPLES = 800_000;
public static final HeritagePlan19 EMPTY = new HeritagePlan19(List.of(), List.of(), List.of(), List.of(), 0, 0, Map.of());
public enum Kind { FREIGHT_STATION, POST_OFFICE, COMMUNAL_DEPOT, EXPEDITION_OFFICE, OBSERVATORY,
RAIL_WORKSHOP, PUMP_STATION, SUBSTATION, GREENHOUSE, VOID_DOCK, RADIO_RELAY, PHOTO_CLUB,
IMPOSSIBLE_SERVICE, CONSTRUCTION_REFUGE }
public enum RouteKind { FREIGHT, PATH }
public enum LandmarkKind { CITY, MINE, UTILITY }
/** NONE is a side column or path; a missing rail retains its intended shape with railPresent=false. */
public enum Rail { NONE, NORTH_SOUTH, EAST_WEST, ASCENDING_EAST, ASCENDING_WEST, ASCENDING_NORTH,
ASCENDING_SOUTH, SOUTH_EAST, SOUTH_WEST, NORTH_WEST, NORTH_EAST }
/** Coordinates of the player's feet, immediately outside the facade. Facing0=N,1=E,2=S,3=W. */
public record Entry(int x, int y, int z, int facing) {
public Entry { if (!coordinate(x) || !coordinate(z) || y < 2 || y > 380 || facing < 0 || facing > 3) throw new IllegalArgumentException("Invalid heritage entry"); }
}
public record Support(int x, int minY, int z, int maxY) {
public Support { if (!coordinate(x) || !coordinate(z) || minY < 2 || maxY < minY || maxY - minY >= 12 || maxY > 370) throw new IllegalArgumentException("Invalid heritage support"); }
}
public record Site(String id, Kind kind, int minX, int floorY, int minZ, int width, int depth, int height,
int facing, long seed, int foundationDepth, List<Support> supports, Entry entry) {
public Site {
supports = List.copyOf(supports);
if (!identifier(id) || !coordinate(minX) || !coordinate(minZ) || kind == null || width != 17 || depth != 17 || height < 7 || height > 24
|| floorY < 32 || floorY + height > 380 || facing < 0 || facing > 3
|| foundationDepth < 3 || foundationDepth > 8 || supports.size() > 9 || entry == null)
throw new IllegalArgumentException("Invalid heritage site");
int ex = facing == 1 ? minX + width : facing == 3 ? minX - 1 : minX + width / 2;
int ez = facing == 2 ? minZ + depth : facing == 0 ? minZ - 1 : minZ + depth / 2;
if (entry.x() != ex || entry.z() != ez || entry.y() != floorY + 1 || entry.facing() != facing)
throw new IllegalArgumentException("Heritage entry does not meet its rotated facade");
var positions = new HashSet<Long>();
for (var support : supports) if (support.x() < minX || support.x() >= minX + width
|| support.z() < minZ || support.z() >= minZ + depth || support.maxY() != floorY - foundationDepth - 1
|| !positions.add(key(support.x(), support.z()))) throw new IllegalArgumentException("Invalid site footing");
}
public int maxX() { return minX + width - 1; }
public int maxZ() { return minZ + depth - 1; }
public int minY() { return floorY - foundationDepth; }
public int maxY() { return floorY + height; }
public int boundsMinY() { return supports.stream().mapToInt(Support::minY).min().orElse(minY()); }
public int boundsMaxY() { return maxY(); }
public int centerX() { return minX + width / 2; }
public int centerZ() { return minZ + depth / 2; }
public int startChunkX() { return centerX() >> 4; }
public int startChunkZ() { return centerZ() >> 4; }
}
/** A pre-existing, read-only destination. Its coordinate is never granted a mask bypass. */
public record Landmark(String id, LandmarkKind kind, Entry entry) {
public Landmark { if (!identifier(id) || kind == null || entry == null) throw new IllegalArgumentException("Invalid heritage landmark"); }
}
/** Floor/block-support Y, not player feet. Endpoints may be read-only landmark coordinates. */
public record Point(int x, int floorY, int z) {
public Point { if (!coordinate(x) || !coordinate(z) || floorY < 2 || floorY > 370) throw new IllegalArgumentException("Invalid route point"); }
}
public record Link(String id, String fromId, String toId, RouteKind kind, List<Point> path) {
public Link {
path = List.copyOf(path);
if (!identifier(id) || !identifier(fromId) || !identifier(toId) || fromId.equals(toId)
|| kind == null || path.size() < 2 || path.size() > 1024) throw new IllegalArgumentException("Invalid heritage link");
for (int i = 1; i < path.size(); i++) {
var a = path.get(i - 1); var b = path.get(i);
if (Math.abs(a.x() - b.x()) + Math.abs(a.z() - b.z()) != 1 || Math.abs(a.floorY() - b.floorY()) > 1)
throw new IllegalArgumentException("Heritage route has a discontinuous centerline");
}
}
}
/** Each written ground column belongs to exactly one Segment. Clear local y1..3 before rail placement. */
public record Cell(int x, int floorY, int z, int foundationDepth, Rail rail, boolean railPresent) {
public Cell {
if (!coordinate(x) || !coordinate(z) || floorY < 14 || floorY > 350 || foundationDepth < 1 || foundationDepth > 12 || rail == null
|| rail == Rail.NONE && railPresent) throw new IllegalArgumentException("Invalid heritage route cell");
}
public int minY() { return floorY - foundationDepth; }
public int maxY() { return floorY + 3; }
}
/** Chunk-local route piece: stable ownership prevents neighboring links painting a shared column twice. */
public record Segment(String id, String fromId, String toId, RouteKind kind, int startChunkX, int startChunkZ,
List<Cell> cells, long seed) {
public Segment {
cells = List.copyOf(cells);
if (!identifier(id) || !identifier(fromId) || !identifier(toId) || fromId.equals(toId) && kind != RouteKind.PATH || kind == null || cells.isEmpty() || cells.size() > 256)
throw new IllegalArgumentException("Invalid heritage segment");
var positions = new HashSet<Long>();
for (var cell : cells) if ((cell.x() >> 4) != startChunkX || (cell.z() >> 4) != startChunkZ
|| !positions.add(key(cell.x(), cell.z()))) throw new IllegalArgumentException("Route cells escaped their owning chunk");
}
public int minX() { return cells.stream().mapToInt(Cell::x).min().orElseThrow(); }
public int maxX() { return cells.stream().mapToInt(Cell::x).max().orElseThrow(); }
public int minZ() { return cells.stream().mapToInt(Cell::z).min().orElseThrow(); }
public int maxZ() { return cells.stream().mapToInt(Cell::z).max().orElseThrow(); }
public int minY() { return cells.stream().mapToInt(Cell::minY).min().orElseThrow(); }
public int maxY() { return cells.stream().mapToInt(Cell::maxY).max().orElseThrow(); }
public int boundsMinY() { return minY(); }
public int boundsMaxY() { return maxY(); }
}
public HeritagePlan19(List<Site> sites, List<Landmark> landmarks, List<Link> links, List<Segment> segments,
int samples, int columns, Map<Kind, String> diagnostics) {
sites = List.copyOf(sites); landmarks = List.copyOf(landmarks); links = List.copyOf(links); segments = List.copyOf(segments);
diagnostics = Map.copyOf(diagnostics);
if (sites.size() > 14 || landmarks.size() > 16 || links.size() > 24 || segments.size() > 512
|| samples < 0 || samples > MAX_SAMPLES || columns < 0 || columns > MAX_COLUMNS)
throw new IllegalArgumentException("Invalid heritage plan budget");
var destinations = new HashSet<String>();
for (var site : sites) if (!destinations.add(site.id())) throw new IllegalArgumentException("Duplicate heritage destination");
for (var landmark : landmarks) if (!destinations.add(landmark.id())) throw new IllegalArgumentException("Duplicate heritage destination");
for (var link : links) if (!destinations.contains(link.fromId()) || !destinations.contains(link.toId()))
throw new IllegalArgumentException("A heritage route promises a nonexistent destination");
var occupied = new HashSet<Long>();
for (var segment : segments) {
if (!destinations.contains(segment.fromId()) || !destinations.contains(segment.toId()))
throw new IllegalArgumentException("A heritage segment promises a nonexistent destination");
for (var cell : segment.cells()) if (!occupied.add(key(cell.x(), cell.z())))
throw new IllegalArgumentException("Heritage segments write the same column");
}
this.sites = sites; this.landmarks = landmarks; this.links = links; this.segments = segments;
this.samples = samples; this.columns = columns; this.diagnostics = diagnostics;
var columnIndex = new HashMap<Long, List<Range>>(); var chunkIndex = new HashMap<Long, List<Box>>();
for (var site : sites) {
index(columnIndex, chunkIndex, new Box(site.minX(), site.minY() - 2, site.minZ(), site.maxX(), site.maxY(), site.maxZ()));
for (var support : site.supports()) index(columnIndex, chunkIndex,
new Box(support.x(), support.minY() - 2, support.z(), support.x(), support.maxY(), support.z()));
}
for (var segment : segments) for (var cell : segment.cells()) index(columnIndex, chunkIndex,
new Box(cell.x(), cell.minY() - 1, cell.z(), cell.x(), cell.maxY(), cell.z()));
var usedDestinations = new HashSet<String>();
for (var link : links) { usedDestinations.add(link.fromId()); usedDestinations.add(link.toId()); }
for (var landmark : landmarks) if (usedDestinations.contains(landmark.id())) {
Entry e = landmark.entry();
for (int side = -1; side <= 1; side++) {
int x = e.x() + ((e.facing() & 1) == 0 ? side : 0);
int z = e.z() + ((e.facing() & 1) == 1 ? side : 0);
// Read-only dependencies: these cells never enter contains() or any native writer.
index(columnIndex, chunkIndex, new Box(x, e.y() - 2, z, x, e.y() + 2, z));
}
}
columnIndex.replaceAll((key, values) -> List.copyOf(values)); chunkIndex.replaceAll((key, values) -> List.copyOf(values));
protectedColumns = Map.copyOf(columnIndex); protectedChunks = Map.copyOf(chunkIndex);
}
public List<Site> sites() { return sites; }
public List<Landmark> landmarks() { return landmarks; }
public List<Link> links() { return links; }
public List<Segment> segments() { return segments; }
public int samples() { return samples; }
public int columns() { return columns; }
public Map<Kind, String> diagnostics() { return diagnostics; }
public boolean isEmpty() { return sites.isEmpty(); }
public boolean contains(int x, int y, int z) {
for (var site : sites) {
if (x < site.minX() || x > site.maxX() || z < site.minZ() || z > site.maxZ()) continue;
if (y >= site.minY() && y <= site.maxY()) return true;
for (var support : site.supports()) if (x == support.x() && z == support.z() && y >= support.minY() && y <= support.maxY()) return true;
}
for (var segment : segments) if ((x >> 4) == segment.startChunkX() && (z >> 4) == segment.startChunkZ())
for (var cell : segment.cells()) if (cell.x() == x && cell.z() == z && y >= cell.minY() && y <= cell.maxY()) return true;
return false;
}
/** Protect the original rock below slabs as well as every written volume. */
public boolean protects(int x, int y, int z) {
for (var range : protectedColumns.getOrDefault(key(x, z), List.of())) if (y >= range.minY() && y <= range.maxY()) return true;
return false;
}
public boolean intersectsProtection(int x0, int y0, int z0, int x1, int y1, int z1) {
if (x0 > x1 || y0 > y1 || z0 > z1) throw new IllegalArgumentException("Reversed heritage query");
for (int cx = x0 >> 4; cx <= x1 >> 4; cx++) for (int cz = z0 >> 4; cz <= z1 >> 4; cz++)
for (var box : protectedChunks.getOrDefault(key(cx, cz), List.of()))
if (x0 <= box.maxX() && x1 >= box.minX() && z0 <= box.maxZ() && z1 >= box.minZ()
&& y0 <= box.maxY() && y1 >= box.minY()) return true;
return false;
}
private record Range(int minY, int maxY) {}
private record Box(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {}
private static void index(Map<Long, List<Range>> columns, Map<Long, List<Box>> chunks, Box box) {
for (int x = box.minX(); x <= box.maxX(); x++) for (int z = box.minZ(); z <= box.maxZ(); z++)
columns.computeIfAbsent(key(x, z), ignored -> new ArrayList<>()).add(new Range(box.minY(), box.maxY()));
for (int x = box.minX() >> 4; x <= box.maxX() >> 4; x++) for (int z = box.minZ() >> 4; z <= box.maxZ() >> 4; z++)
chunks.computeIfAbsent(key(x, z), ignored -> new ArrayList<>()).add(box);
}
private static boolean coordinate(int value) { return Math.abs((long) value) <= 29_999_000; }
private static boolean identifier(String value) { return value != null && !value.isBlank() && value.length() <= 128; }
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
}
@@ -0,0 +1,545 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.*;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/** Generation19 only. A finite catalogue is admitted before complete, repairable routes are published.
* Everything samples native columns; no chunk, player object or future continent is requested. */
public final class HeritagePlanner19 {
public static final int MAX_COLUMNS = HeritagePlan19.MAX_COLUMNS, MAX_SAMPLES = HeritagePlan19.MAX_SAMPLES;
public static final int MAX_SITE_CUT = 6, MAX_ROUTE_CUT = 3;
private static final int SITE_COLUMNS = 11_000, SITE_SAMPLES = 540_000;
private static final int INVALID = Integer.MIN_VALUE;
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
private HeritagePlanner19() {}
public interface Terrain {
boolean owns(int x, int z);
int surfaceY(int x, int z);
boolean solid(int x, int y, int z);
boolean protectedAt(int x, int y, int z);
default List<Landmark> landmarks() { return List.of(); }
}
public static HeritagePlan19 plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
if (!List.of(512, 724, 1024).contains(diameter)) throw new IllegalArgumentException("Heritage19 requires a starter size");
var probe = new Probe(terrain, cx, cz, diameter);
List<Landmark> landmarks = List.copyOf(terrain.landmarks()).stream().limit(16).toList();
var sites = new ArrayList<Site>(); var cells = new LinkedHashMap<Long, OwnedCell>(); var diagnostics = new EnumMap<Kind, String>(Kind.class);
for (var kind : Kind.values()) {
int remaining = Kind.values().length - kind.ordinal() - 1;
probe.columnLimit = SITE_COLUMNS - remaining * (SITE_COLUMNS / 14);
probe.sampleLimit = SITE_SAMPLES - remaining * (SITE_SAMPLES / 14);
SiteDraft admitted = null; int attempts = 0;
var reasons = new java.util.TreeMap<String, Integer>(); probe.reasons = reasons;
try {
for (var candidate : candidates(seed, kind, cx, cz, diameter, sites, landmarks, probe)) {
attempts++;
admitted = site(seed, kind, candidate, sites, landmarks, cells, probe);
if (admitted != null) break;
}
} catch (BudgetExceeded exhausted) { reasons.merge(exhausted.columns ? "column-budget" : "probe-budget", 1, Integer::sum); }
if (admitted != null) {
sites.add(admitted.site());
Request landing = new Request(admitted.site().id() + "_landing", admitted.site().id(), admitted.site().id(), RouteKind.PATH);
for (var cell : admitted.landings()) cells.put(key(cell.x(), cell.z()), new OwnedCell(landing, cell));
}
diagnostics.put(kind, (admitted == null ? "not admitted" : "admitted") + "; candidates=" + attempts + "; " + reasons);
}
probe.columnLimit = MAX_COLUMNS; probe.sampleLimit = MAX_SAMPLES;
var links = new ArrayList<Link>();
var routeResults = new LinkedHashMap<String, String>();
var routeReasons = new java.util.TreeMap<String, Integer>(); probe.reasons = routeReasons;
Map<String, Endpoint> endpoints = new LinkedHashMap<>();
for (var site : sites) endpoints.put(site.id(), new Endpoint(site.id(), site.entry(), false));
for (var landmark : landmarks) if (!endpoints.containsKey(landmark.id()))
endpoints.put(landmark.id(), new Endpoint(landmark.id(), landmark.entry(), true));
List<Request> requests = requests(sites, landmarks).stream()
.filter(r -> endpoints.containsKey(r.from()) && endpoints.containsKey(r.to())).toList();
requests = new ArrayList<>(requests);
requests.sort(Comparator.comparingInt((Request r) -> r.kind() == RouteKind.FREIGHT ? 0
: span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry()) >= diameter / 4 ? 1 : 2)
.thenComparingInt(r -> span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry())));
int adaptiveLong = 0;
for (int requestIndex = 0; requestIndex < requests.size(); requestIndex++) {
var request = requests.get(requestIndex);
int remaining = requests.size() - requestIndex - 1;
// Failed alignments must not starve every later destination. Unused allowances carry forward.
boolean longAxis = span(endpoints.get(request.from()).entry(), endpoints.get(request.to()).entry()) >= diameter / 4;
int allowance = longAxis && adaptiveLong++ < 2 ? 4_500 : 1_400;
probe.columnLimit = Math.min(MAX_COLUMNS - remaining * 120, probe.columns.size() + allowance);
probe.sampleLimit = Math.min(MAX_SAMPLES - remaining * 4_000, probe.samples + (longAxis ? 150_000 : 75_000));
Endpoint from = endpoints.get(request.from()), to = endpoints.get(request.to());
if (from == null || to == null) continue;
int beforeRouteColumns = probe.columns.size();
try {
RouteDraft route = route(seed, request, from, to, sites, landmarks, cells, probe);
if (route == null) { probe.reject("no-complete-route"); routeResults.put(request.id(), "refused; columns=" + (probe.columns.size() - beforeRouteColumns)); continue; }
links.add(new Link(request.id(), request.from(), request.to(), request.kind(), route.path()));
routeResults.put(request.id(), "admitted; span=" + span(from.entry(), to.entry()) + "; length=" + route.path().size() + "; columns=" + (probe.columns.size() - beforeRouteColumns));
for (var cell : route.cells().values()) {
long key = key(cell.x(), cell.z());
OwnedCell old = cells.get(key);
if (old == null) cells.put(key, new OwnedCell(request, cell));
else if (cell.rail() != Rail.NONE && old.cell().rail() == Rail.NONE)
cells.put(key, new OwnedCell(old.request(), cell));
else if (cell.rail() != Rail.NONE && old.cell().rail() != Rail.NONE && cell.rail() != old.cell().rail()) {
// A missing switch is intentional; never emit a rail with contradictory connections.
var c = old.cell(); cells.put(key, new OwnedCell(old.request(),
new Cell(c.x(), c.floorY(), c.z(), c.foundationDepth(), c.rail(), false)));
}
}
} catch (BudgetExceeded exhausted) { probe.reject(exhausted.columns ? "route-column-budget" : "route-probe-budget"); routeResults.put(request.id(), "budget; columns=" + (probe.columns.size() - beforeRouteColumns)); }
}
var segments = segments(seed, cells);
if (Boolean.getBoolean("sanctuary.test.heritage19") || Boolean.getBoolean("sanctuary.heritage.plannerDiagnostics"))
System.getLogger(HeritagePlanner19.class.getName()).log(System.Logger.Level.INFO,
"Heritage19 seed=" + seed + " diameter=" + diameter + " sites=" + sites.stream().map(Site::kind).toList()
+ " links=" + links.size() + " segments=" + segments.size() + " columns=" + probe.columns.size()
+ " samples=" + probe.samples + " catalogue=" + diagnostics + " routes=" + routeReasons + " routeResults=" + routeResults);
return new HeritagePlan19(sites, landmarks, links, segments, probe.samples, probe.columns.size(), diagnostics);
}
private static List<Candidate> candidates(long seed, Kind kind, int cx, int cz, int diameter,
List<Site> sites, List<Landmark> landmarks, Probe probe) {
double angle = unit(mix(seed ^ 0x19ACEL)) * Math.PI * 2 + kind.ordinal() * 2.399963229728653;
double radius = diameter * (kind == Kind.VOID_DOCK ? .40 : kind == Kind.OBSERVATORY || kind == Kind.RADIO_RELAY ? .31 : .22);
int x = cx + (int) Math.round(Math.cos(angle) * radius), z = cz + (int) Math.round(Math.sin(angle) * radius);
Kind parent = switch (kind) {
case POST_OFFICE, COMMUNAL_DEPOT, RAIL_WORKSHOP -> Kind.FREIGHT_STATION;
case EXPEDITION_OFFICE, PHOTO_CLUB -> Kind.POST_OFFICE;
case SUBSTATION, CONSTRUCTION_REFUGE -> Kind.RAIL_WORKSHOP;
case GREENHOUSE, IMPOSSIBLE_SERVICE -> Kind.PUMP_STATION;
default -> null;
};
Site neighbor = parent == null ? null : find(sites, parent);
if (neighbor != null) {
double local = unit(mix(seed ^ kind.ordinal() * 179L)) * Math.PI * 2;
int distance = kind == Kind.RAIL_WORKSHOP ? 65 : 40;
x = neighbor.centerX() + (int) Math.round(Math.cos(local) * distance);
z = neighbor.centerZ() + (int) Math.round(Math.sin(local) * distance);
}
var candidates = new ArrayList<Candidate>();
for (int i = 0; i < 16; i++) {
double turn = angle + i * Math.PI / 3;
int offset = i == 0 ? 0 : i < 5 ? 24 : i < 8 ? 48 : i < 12 ? 72 : 96;
int px = x + (int) Math.round(Math.cos(turn) * offset), pz = z + (int) Math.round(Math.sin(turn) * offset);
if (Math.hypot((double) px - cx, (double) pz - cz) > diameter / 2.0 - 18) continue;
if (overlapsSites(px - 15, pz - 15, px + 15, pz + 15, sites)) continue;
int low = 384, high = -1; boolean dry = true;
for (int dz : new int[] {-8, 0, 8}) for (int dx : new int[] {-8, 0, 8}) {
int top = probe.surface(px + dx, pz + dz);
if (top < 48 || top > 330 || !probe.solid(px + dx, top, pz + dz)
|| probe.protectedAt(px + dx, top, pz + dz)) { dry = false; break; }
low = Math.min(low, top); high = Math.max(high, top);
}
if (!dry || high - low > 13) { probe.reject("coarse-terrain"); continue; }
int facing = direction(px, pz, neighbor == null ? cx : neighbor.centerX(), neighbor == null ? cz : neighbor.centerZ());
if (kind == Kind.VOID_DOCK) facing = (direction(px, pz, cx, cz) + 2) & 3;
int score = (i >= 8 ? 1_000 : 0) + high - low + (kind == Kind.OBSERVATORY || kind == Kind.RADIO_RELAY ? (300 - high) / 4 : 0);
candidates.add(new Candidate(px - 8, pz - 8, facing, score, mix(seed ^ kind.ordinal() * 463L ^ i)));
}
candidates.sort(Comparator.comparingInt(Candidate::score).thenComparingLong(Candidate::seed));
return candidates;
}
private static SiteDraft site(long seed, Kind kind, Candidate candidate, List<Site> previous,
List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
int x = candidate.x(), z = candidate.z(), low = 384, high = -1;
for (var landmark : landmarks) if (landmark.entry().x() >= x - 2 && landmark.entry().x() <= x + 18
&& landmark.entry().z() >= z - 2 && landmark.entry().z() <= z + 18) { probe.reject("existing-entry"); return null; }
var heights = new ArrayList<Integer>();
for (int dz = 0; dz < 17; dz++) for (int dx = 0; dx < 17; dx++) {
int top = probe.surface(x + dx, z + dz);
if (top < 48 || top > 330 || !probe.solid(x + dx, top, z + dz)) { probe.reject("dry-footprint"); return null; }
heights.add(top); low = Math.min(low, top); high = Math.max(high, top);
}
int minimum = Math.max(48, high - MAX_SITE_CUT), maximum = Math.min(330, low + 7);
if (minimum > maximum) { probe.reject("site-slope"); return null; }
heights.sort(Integer::compareTo);
int floor = Math.clamp(heights.get(heights.size() / 2), minimum, maximum);
int foundation = Math.max(3, floor - low + 1), height = height(kind);
int base = floor - foundation;
for (int dz = 0; dz < 17; dz++) for (int dx = 0; dx < 17; dx++)
for (int y = base; y <= floor + height; y++) if (probe.protectedAt(x + dx, y, z + dz)) {
probe.reject("site-protected-volume"); return null;
}
var supports = new ArrayList<Support>();
for (int dz : new int[] {1, 8, 15}) for (int dx : new int[] {1, 8, 15}) {
int bottom = base - 1, rock = bottom;
while (rock >= Math.max(2, base - 13) && (!probe.solid(x + dx, rock, z + dz)
|| !probe.solid(x + dx, rock - 1, z + dz))) rock--;
if (rock < Math.max(2, base - 13)) { probe.reject("site-no-bearing"); return null; }
for (int y = rock - 1; y <= bottom; y++) if (probe.protectedAt(x + dx, y, z + dz)) { probe.reject("support-protected"); return null; }
if (rock < bottom) supports.add(new Support(x + dx, rock + 1, z + dz, bottom));
}
int ex = candidate.facing() == 1 ? x + 17 : candidate.facing() == 3 ? x - 1 : x + 8;
int ez = candidate.facing() == 2 ? z + 17 : candidate.facing() == 0 ? z - 1 : z + 8;
Entry entry = new Entry(ex, floor + 1, ez, candidate.facing());
Site result = new Site(id(kind), kind, x, floor, z, 17, 17, height, candidate.facing(),
mix(seed ^ candidate.seed()), foundation, supports, entry);
var landings = landing(result.entry(), previous, probe);
if (landings == null) { probe.reject("entry-landing"); return null; }
for (var cell : landings) {
OwnedCell old = existing.get(key(cell.x(), cell.z()));
if (old != null && old.cell().floorY() != cell.floorY()) { probe.reject("landing-collision"); return null; }
}
return new SiteDraft(result, landings);
}
private static int height(Kind kind) {
return switch (kind) {
case FREIGHT_STATION, POST_OFFICE, RAIL_WORKSHOP, SUBSTATION, GREENHOUSE, PHOTO_CLUB -> 10;
case EXPEDITION_OFFICE, IMPOSSIBLE_SERVICE -> 12;
case COMMUNAL_DEPOT, VOID_DOCK, CONSTRUCTION_REFUGE -> 14;
case PUMP_STATION -> 18; case OBSERVATORY -> 20; case RADIO_RELAY -> 22;
};
}
private static List<Request> requests(List<Site> sites, List<Landmark> landmarks) {
var result = new ArrayList<Request>();
request(result, Kind.FREIGHT_STATION, Kind.COMMUNAL_DEPOT, RouteKind.FREIGHT);
request(result, Kind.FREIGHT_STATION, Kind.RAIL_WORKSHOP, RouteKind.FREIGHT);
request(result, Kind.COMMUNAL_DEPOT, Kind.PUMP_STATION, RouteKind.FREIGHT);
Site workshop = find(sites, Kind.RAIL_WORKSHOP);
for (var landmark : landmarks) if (landmark.kind() == LandmarkKind.MINE && workshop != null)
result.add(new Request("mine_freight", landmark.id(), workshop.id(), RouteKind.FREIGHT));
request(result, Kind.FREIGHT_STATION, Kind.POST_OFFICE, RouteKind.PATH);
request(result, Kind.POST_OFFICE, Kind.EXPEDITION_OFFICE, RouteKind.PATH);
request(result, Kind.EXPEDITION_OFFICE, Kind.OBSERVATORY, RouteKind.PATH);
request(result, Kind.COMMUNAL_DEPOT, Kind.VOID_DOCK, RouteKind.PATH);
request(result, Kind.RAIL_WORKSHOP, Kind.SUBSTATION, RouteKind.PATH);
request(result, Kind.SUBSTATION, Kind.PUMP_STATION, RouteKind.PATH);
request(result, Kind.PUMP_STATION, Kind.GREENHOUSE, RouteKind.PATH);
request(result, Kind.POST_OFFICE, Kind.PHOTO_CLUB, RouteKind.PATH);
request(result, Kind.PUMP_STATION, Kind.IMPOSSIBLE_SERVICE, RouteKind.PATH);
request(result, Kind.OBSERVATORY, Kind.RADIO_RELAY, RouteKind.PATH);
request(result, Kind.RAIL_WORKSHOP, Kind.CONSTRUCTION_REFUGE, RouteKind.PATH);
Site post = find(sites, Kind.POST_OFFICE);
for (var landmark : landmarks) if (landmark.kind() == LandmarkKind.CITY && post != null) {
result.add(new Request("city_post_" + result.size(), landmark.id(), post.id(), RouteKind.PATH)); break;
}
return result;
}
private static void request(List<Request> requests, Kind from, Kind to, RouteKind kind) {
requests.add(new Request(id(from) + "_" + id(to), id(from), id(to), kind));
}
/** Always persisted, including for an isolated destination: five steps return to natural terrain. */
private static List<Cell> landing(Entry entry, List<Site> sites, Probe probe) {
int facing = entry.facing(), floor = entry.y() - 1;
int endX = entry.x() + DX[facing] * 4, endZ = entry.z() + DZ[facing] * 4;
int target = probe.surface(endX, endZ);
if (target < 14 || target > 350 || Math.abs(target - floor) > 3) return null;
var result = new ArrayList<Cell>();
for (int step = 0; step < 5; step++) {
// Keep the first two rows flat for the facade and the first rail turn.
int y = floor + (int) Math.round((target - floor) * Math.max(0, step - 1) / 3.0);
for (int cross = -1; cross <= 1; cross++) {
int x = entry.x() + DX[facing] * step + DZ[facing] * cross;
int z = entry.z() + DZ[facing] * step - DX[facing] * cross;
if (overlapsSites(x, z, x, z, sites)) return null;
Cell cell = routeCell(x, y, z, Rail.NONE, false, probe);
if (cell == null) return null;
result.add(cell);
if (step == 4) {
int beyond = probe.surface(x + DX[facing], z + DZ[facing]);
if (Math.abs(beyond - y) > 1 || !probe.solid(x + DX[facing], beyond, z + DZ[facing])) return null;
}
}
}
return List.copyOf(result);
}
private static RouteDraft route(long seed, Request request, Endpoint from, Endpoint to, List<Site> sites,
List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
Entry a = from.entry(), b = to.entry();
XZ start = new XZ(a.x(), a.z()), end = new XZ(b.x(), b.z());
XZ outA = start.move(a.facing(), 6), outB = end.move(b.facing(), 6);
int middleX = (outA.x() + outB.x()) / 2, middleZ = (outA.z() + outB.z()) / 2;
var alternatives = List.of(
List.of(outA, new XZ(outB.x(), outA.z()), outB),
List.of(outA, new XZ(outA.x(), outB.z()), outB),
List.of(outA, new XZ(middleX, outA.z()), new XZ(middleX, outB.z()), outB),
List.of(outA, new XZ(outA.x(), middleZ), new XZ(outB.x(), middleZ), outB),
List.of(outA, new XZ(outA.x(), middleZ - 16), new XZ(outB.x(), middleZ - 16), outB),
List.of(outA, new XZ(middleX + 16, outA.z()), new XZ(middleX + 16, outB.z()), outB));
for (var waypoints : alternatives) {
var points = new ArrayList<XZ>(); points.add(start);
boolean okay = true;
for (var waypoint : waypoints) if (!line(points, waypoint)) { okay = false; break; }
if (!okay || !line(points, end) || points.size() > 1024 || new HashSet<>(points).size() != points.size()) continue;
RouteDraft result = profile(seed, request, from, to, points, sites, landmarks, existing, probe);
if (result != null) return result;
}
return adaptive(seed, request, from, to, start, end, outA, outB, sites, landmarks, existing, probe);
}
/** Adaptive fallback. The coarse graph only proposes an alignment; it grants no writing permission. */
private static RouteDraft adaptive(long seed, Request request, Endpoint from, Endpoint to, XZ start, XZ end,
XZ outA, XZ outB, List<Site> sites, List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
int stepSize = span(from.entry(), to.entry()) >= 160 ? 8 : 4;
int certificateReserve = Math.min(2_200, distance(outA, outB) * 3 + 80);
var excluded = new HashSet<XZ>();
for (int attempt = 0; attempt < 3; attempt++) {
var open = new java.util.PriorityQueue<SearchNode>(Comparator.comparingDouble(SearchNode::priority)
.thenComparingDouble(SearchNode::cost).thenComparingInt(n -> n.position().x()).thenComparingInt(n -> n.position().z()));
var best = new HashMap<XZ, Double>(); var parents = new HashMap<XZ, XZ>(); var closed = new HashSet<XZ>();
var terrainHeights = new HashMap<XZ, Integer>();
int begin = coarseRoute(outA, sites, terrainHeights, probe);
if (begin == INVALID) return null;
open.add(new SearchNode(outA, 0, distance(outA, outB))); best.put(outA, 0.0);
List<XZ> found = null;
int searchColumns = Math.max(probe.columns.size() + 150, probe.columnLimit - certificateReserve);
while (!open.isEmpty() && closed.size() < 500 && probe.columns.size() < searchColumns) {
var current = open.remove(); XZ p = current.position();
if (!closed.add(p) || current.cost() > best.getOrDefault(p, Double.POSITIVE_INFINITY)) continue;
if (distance(p, outB) <= stepSize * 2) {
var reversed = new ArrayList<XZ>(); XZ ancestor = p;
while (ancestor != null) { reversed.add(ancestor); ancestor = parents.get(ancestor); }
java.util.Collections.reverse(reversed);
for (int order = 0; order < 2 && found == null; order++) {
var candidate = new ArrayList<XZ>(); candidate.add(start); boolean valid = true;
for (XZ step : reversed) if (!line(candidate, step)) { valid = false; break; }
XZ turn = order == 0 ? new XZ(outB.x(), p.z()) : new XZ(p.x(), outB.z());
if (valid && line(candidate, turn) && line(candidate, outB) && line(candidate, end)
&& new HashSet<>(candidate).size() == candidate.size()) found = candidate;
}
if (found != null) break;
}
int top = terrainHeights.get(p); XZ parent = parents.get(p);
for (int d = 0; d < 4; d++) {
if (parent == null && d == ((from.entry().facing() + 2) & 3)) continue;
XZ next = p.move(d, stepSize);
if (closed.contains(next) || excluded.contains(next)
|| Math.min(distance(next, outA), distance(next, outB)) > distance(outA, outB) + 64) continue;
int height = coarseRoute(next, sites, terrainHeights, probe);
if (height == INVALID || Math.abs(height - top) > stepSize + 3) continue;
double turnCost = parent == null || parent.x() == next.x() || parent.z() == next.z() ? 0 : 1.5;
double cost = current.cost() + stepSize + Math.abs(height - top) * 1.1 + turnCost;
if (cost >= best.getOrDefault(next, Double.POSITIVE_INFINITY)) continue;
best.put(next, cost); parents.put(next, p);
open.add(new SearchNode(next, cost, cost + distance(next, outB) * 1.6));
}
}
if (found == null) { probe.reject("adaptive-no-alignment"); return null; }
RouteDraft certified = profile(seed, request, from, to, found, sites, landmarks, existing, probe);
if (certified != null) { probe.reject("adaptive-admitted"); return certified; }
if (probe.routeBlock == null) return null;
// A failed exact corridor removes only nearby coarse nodes on the next proposal.
XZ bad = probe.routeBlock;
int gx = outA.x() + (int) Math.round((bad.x() - outA.x()) / (double) stepSize) * stepSize;
int gz = outA.z() + (int) Math.round((bad.z() - outA.z()) / (double) stepSize) * stepSize;
excluded.add(new XZ(gx, gz));
for (int d = 0; d < 4; d++) excluded.add(new XZ(gx, gz).move(d, stepSize));
}
return null;
}
private record SearchNode(XZ position, double cost, double priority) {}
private static int distance(XZ a, XZ b) { return Math.abs(a.x() - b.x()) + Math.abs(a.z() - b.z()); }
private static int coarseRoute(XZ p, List<Site> sites, Map<XZ, Integer> cache, Probe probe) {
Integer old = cache.get(p); if (old != null) return old;
if (overlapsSites(p.x() - 2, p.z() - 2, p.x() + 2, p.z() + 2, sites)) { cache.put(p, INVALID); return INVALID; }
int top = probe.surface(p.x(), p.z());
if (top < 14 || top > 350 || !probe.solid(p.x(), top, p.z()) || probe.protectedAt(p.x(), top, p.z())) {
cache.put(p, INVALID); return INVALID;
}
for (int d = 0; d < 4; d++) {
XZ side = p.move(d, 1); int sideTop = probe.surface(side.x(), side.z());
if (sideTop < 14 || sideTop > 350 || Math.abs(sideTop - top) > 4 || probe.protectedAt(side.x(), sideTop, side.z())) {
cache.put(p, INVALID); return INVALID;
}
}
cache.put(p, top); return top;
}
/** Finite orthogonal alignments, each certified at full block resolution before publication. */
private static boolean line(List<XZ> points, XZ destination) {
XZ p = points.getLast();
if (p.x() != destination.x() && p.z() != destination.z()) return false;
int dx = Integer.signum(destination.x() - p.x()), dz = Integer.signum(destination.z() - p.z());
while (!p.equals(destination)) {
p = new XZ(p.x() + dx, p.z() + dz); points.add(p);
if (points.size() > 1024) return false;
}
return true;
}
private static RouteDraft profile(long seed, Request request, Endpoint from, Endpoint to, List<XZ> points,
List<Site> sites, List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
int size = points.size(); probe.routeBlock = null;
int[] lo = new int[size], hi = new int[size], desired = new int[size], group = new int[size];
boolean[] flat = new boolean[size - 1];
for (int i = 1; i < size - 1; i++) if (turn(points, i)) { flat[i - 1] = true; flat[i] = true; }
for (int i = 0; i < size; i++) {
XZ point = points.get(i); probe.routeBlock = point; lo[i] = 14; hi[i] = 350;
desired[i] = probe.surface(point.x(), point.z());
boolean readOnly = i == 0 && from.readOnly() || i == size - 1 && to.readOnly();
for (XZ column : strip(points, i)) {
if (overlapsSites(column.x(), column.z(), column.x(), column.z(), sites)) { probe.reject("route-building"); return null; }
int top = probe.surface(column.x(), column.z());
if (top < 14 || top > 350 || !probe.solid(column.x(), top, column.z())) { probe.reject("route-dry-ground"); return null; }
lo[i] = Math.max(lo[i], top - MAX_ROUTE_CUT); hi[i] = Math.min(hi[i], top + 4);
OwnedCell old = existing.get(key(column.x(), column.z()));
if (old != null) { lo[i] = Math.max(lo[i], old.cell().floorY()); hi[i] = Math.min(hi[i], old.cell().floorY()); }
if (readOnly && top != (i == 0 ? from.entry().y() : to.entry().y()) - 1) return null;
}
if (i == 0 || i == size - 1) {
int exact = (i == 0 ? from.entry().y() : to.entry().y()) - 1;
lo[i] = Math.max(lo[i], exact); hi[i] = Math.min(hi[i], exact);
}
if (lo[i] > hi[i]) { probe.reject("route-grade-interval"); return null; }
}
// Curves and both adjacent rails share one elevation; a Minecraft curved rail cannot slope.
int groups = 1;
for (int i = 1; i < size; i++) { if (!flat[i - 1]) groups++; group[i] = groups - 1; }
int[] gl = new int[groups], gh = new int[groups], gd = new int[groups], y = new int[groups];
java.util.Arrays.fill(gl, 14); java.util.Arrays.fill(gh, 350);
for (int i = 0; i < size; i++) { int g = group[i]; gl[g] = Math.max(gl[g], lo[i]); gh[g] = Math.min(gh[g], hi[i]); gd[g] = desired[i]; }
for (int g = 1; g < groups; g++) { gl[g] = Math.max(gl[g], gl[g - 1] - 1); gh[g] = Math.min(gh[g], gh[g - 1] + 1); }
for (int g = groups - 2; g >= 0; g--) { gl[g] = Math.max(gl[g], gl[g + 1] - 1); gh[g] = Math.min(gh[g], gh[g + 1] + 1); }
for (int g = 0; g < groups; g++) {
if (gl[g] > gh[g]) { probe.reject("route-grade"); return null; }
int lower = g == 0 ? gl[g] : Math.max(gl[g], y[g - 1] - 1), upper = g == 0 ? gh[g] : Math.min(gh[g], y[g - 1] + 1);
y[g] = Math.clamp(gd[g], lower, upper);
}
var path = new ArrayList<Point>(); var floors = new LinkedHashMap<Long, Integer>();
for (int i = 0; i < size; i++) {
XZ point = points.get(i); int floor = y[group[i]]; path.add(new Point(point.x(), floor, point.z()));
if (i == 0 && from.readOnly() || i == size - 1 && to.readOnly()) continue;
for (XZ column : strip(points, i)) {
long key = key(column.x(), column.z()); Integer old = floors.putIfAbsent(key, floor);
if (old != null && old != floor) { probe.reject("route-self-grade"); return null; }
// Historical endpoints and their lateral landing are read-only, even at another nearby turn.
for (var landmark : landmarks) if (landingColumn(landmark.entry(), column)) {
boolean actualEnd = from.readOnly() && landmark.id().equals(from.id())
|| to.readOnly() && landmark.id().equals(to.id());
if (!actualEnd || floor != landmark.entry().y() - 1) { probe.reject("route-historical-entry"); return null; }
floors.remove(key); break;
}
}
}
var cells = new LinkedHashMap<Long, Cell>();
for (var entry : floors.entrySet()) {
int x = (int) (entry.getKey() >> 32), z = (int) (long) entry.getKey();
probe.routeBlock = new XZ(x, z);
Cell cell = routeCell(x, entry.getValue(), z, Rail.NONE, false, probe);
if (cell == null) { probe.reject("route-column-certificate"); return null; }
cells.put(entry.getKey(), cell);
}
if (request.kind() == RouteKind.FREIGHT) for (int i = 0; i < path.size(); i++) {
Point point = path.get(i); long key = key(point.x(), point.z()); Cell cell = cells.get(key);
if (cell == null) continue;
Rail rail = rail(path, i);
// A one-block valley cannot carry two ascending slopes on the same rail. Keep a repairable rail gap.
boolean intact = rail != Rail.NONE && Math.floorMod(mix(seed ^ key ^ request.id().hashCode()), 19) != 0;
if (rail == Rail.NONE) rail = points.get(Math.max(0, i - 1)).x() == points.get(Math.min(size - 1, i + 1)).x() ? Rail.NORTH_SOUTH : Rail.EAST_WEST;
cells.put(key, new Cell(cell.x(), cell.floorY(), cell.z(), cell.foundationDepth(), rail, intact));
}
return new RouteDraft(List.copyOf(path), cells);
}
private static boolean landingColumn(Entry entry, XZ column) {
int dx = column.x() - entry.x(), dz = column.z() - entry.z();
return DX[entry.facing()] * dx + DZ[entry.facing()] * dz == 0 && Math.abs(dx) + Math.abs(dz) <= 1;
}
private static List<XZ> strip(List<XZ> points, int i) {
XZ p = points.get(i); var result = new ArrayList<XZ>();
if (turn(points, i)) {
for (int dx = -1; dx <= 1; dx++) for (int dz = -1; dz <= 1; dz++) result.add(new XZ(p.x() + dx, p.z() + dz));
} else {
XZ other = points.get(i == points.size() - 1 ? i - 1 : i + 1);
int dx = Integer.signum(other.x() - p.x()), dz = Integer.signum(other.z() - p.z());
for (int cross = -1; cross <= 1; cross++) result.add(new XZ(p.x() + dz * cross, p.z() - dx * cross));
}
return result;
}
private static boolean turn(List<XZ> points, int i) {
if (i == 0 || i == points.size() - 1) return false;
XZ a = points.get(i - 1), b = points.get(i + 1);
return a.x() != b.x() && a.z() != b.z();
}
private static Rail rail(List<Point> path, int i) {
Point p = path.get(i), a = path.get(Math.max(0, i - 1)), b = path.get(Math.min(path.size() - 1, i + 1));
if (a.floorY() > p.floorY() && b.floorY() > p.floorY()) return Rail.NONE;
Point high = a.floorY() > p.floorY() ? a : b.floorY() > p.floorY() ? b : null;
if (high != null) return switch (direction(p.x(), p.z(), high.x(), high.z())) {
case 0 -> Rail.ASCENDING_NORTH; case 1 -> Rail.ASCENDING_EAST; case 2 -> Rail.ASCENDING_SOUTH; default -> Rail.ASCENDING_WEST;
};
if (a.x() == b.x()) return Rail.NORTH_SOUTH;
if (a.z() == b.z()) return Rail.EAST_WEST;
boolean n = a.z() < p.z() || b.z() < p.z(), s = a.z() > p.z() || b.z() > p.z();
boolean e = a.x() > p.x() || b.x() > p.x();
return n ? e ? Rail.NORTH_EAST : Rail.NORTH_WEST : s && e ? Rail.SOUTH_EAST : Rail.SOUTH_WEST;
}
private static Cell routeCell(int x, int floor, int z, Rail rail, boolean present, Probe probe) {
int top = probe.surface(x, z);
if (floor < 14 || floor > 350 || top < floor - 4 || top > floor + MAX_ROUTE_CUT) return null;
int foundation = Math.max(1, floor - top + 1), base = floor - foundation;
if (!probe.solid(x, base - 1, z) || !probe.solid(x, base, z)) return null;
for (int y = base - 1; y <= floor + 3; y++) if (probe.protectedAt(x, y, z)) return null;
return new Cell(x, floor, z, foundation, rail, present);
}
private record XZ(int x, int z) { XZ move(int facing, int distance) { return new XZ(x + DX[facing] * distance, z + DZ[facing] * distance); } }
private record SegmentKey(Request request, int cx, int cz) {}
private static List<Segment> segments(long seed, Map<Long, OwnedCell> cells) {
var groups = new LinkedHashMap<SegmentKey, List<Cell>>();
for (var owned : cells.values()) {
Cell c = owned.cell(); var key = new SegmentKey(owned.request(), c.x() >> 4, c.z() >> 4);
groups.computeIfAbsent(key, ignored -> new ArrayList<>()).add(c);
}
var result = new ArrayList<Segment>();
for (var entry : groups.entrySet()) {
var k = entry.getKey(); var r = k.request();
// IDs are stable, bounded and independent of native generation order.
String id = "piece_" + r.id() + "_" + k.cx() + "_" + k.cz();
result.add(new Segment(id, r.from(), r.to(), r.kind(), k.cx(), k.cz(), entry.getValue(), mix(seed ^ key(k.cx(), k.cz()) ^ r.id().hashCode())));
}
return List.copyOf(result);
}
private static int span(Entry a, Entry b) { return Math.max(Math.abs(a.x() - b.x()), Math.abs(a.z() - b.z())); }
private static Site find(List<Site> sites, Kind kind) { return sites.stream().filter(site -> site.kind() == kind).findFirst().orElse(null); }
private static String id(Kind kind) { return kind.name().toLowerCase(Locale.ROOT); }
private static int direction(int x, int z, int tx, int tz) { return Math.abs(tx - x) >= Math.abs(tz - z) ? tx > x ? 1 : 3 : tz > z ? 2 : 0; }
private static boolean overlapsSites(int x0, int z0, int x1, int z1, List<Site> sites) {
for (var site : sites) if (x0 <= site.maxX() && x1 >= site.minX() && z0 <= site.maxZ() && z1 >= site.minZ()) return true;
return false;
}
private record SiteDraft(Site site, List<Cell> landings) {}
private record Candidate(int x, int z, int facing, int score, long seed) {}
private record Endpoint(String id, Entry entry, boolean readOnly) {}
private record Request(String id, String from, String to, RouteKind kind) {}
private record OwnedCell(Request request, Cell cell) {}
private record RouteDraft(List<Point> path, Map<Long, Cell> cells) {}
private static final class BudgetExceeded extends RuntimeException {
final boolean columns; BudgetExceeded(boolean columns) { this.columns = columns; }
}
private static final class Probe {
final Terrain terrain; final int cx, cz; final double radius;
final Map<Long, Integer> columns = new HashMap<>();
XZ routeBlock; Map<String, Integer> reasons = new HashMap<>(); int samples, columnLimit, sampleLimit;
Probe(Terrain terrain, int cx, int cz, int diameter) { this.terrain = java.util.Objects.requireNonNull(terrain); this.cx = cx; this.cz = cz; radius = diameter / 2.0 - 2; }
int surface(int x, int z) {
if (Math.hypot((double) x - cx, (double) z - cz) > radius || !terrain.owns(x, z)) return INVALID;
long key = key(x, z); var cached = columns.get(key); if (cached != null) return cached;
if (columns.size() >= columnLimit) throw new BudgetExceeded(true);
count(); int top = terrain.surfaceY(x, z); columns.put(key, top); return top;
}
boolean solid(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) == INVALID) return false; count(); return terrain.solid(x, y, z); }
boolean protectedAt(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) == INVALID) return true; count(); return terrain.protectedAt(x, y, z); }
void count() { if (samples >= sampleLimit) throw new BudgetExceeded(false); samples++; }
void reject(String reason) { reasons.merge(reason, 1, Integer::sum); }
}
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
private static double unit(long value) { return (value >>> 11) * 0x1.0p-53; }
private static long mix(long value) { value = (value ^ value >>> 30) * 0xbf58476d1ce4e5b9L; value = (value ^ value >>> 27) * 0x94d049bb133111ebL; return value ^ value >>> 31; }
}
@@ -0,0 +1,361 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.worldgen.city.LostCityPalette;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import net.minecraft.core.Direction;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.AttachFace;
import net.minecraft.world.level.block.state.properties.BedPart;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.DoorHingeSide;
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
import net.minecraft.world.level.block.state.properties.Half;
import net.minecraft.world.level.block.state.properties.RailShape;
/** Fourteen original, physical generation19 places. The saved site controls all geometry and loot.
* Site writes are local to minX/floorY/minZ; route writes are local to the segment's minimum bounds. */
public final class HeritageRenderer19 {
private HeritageRenderer19() {}
public record Target(String name, int x, int y, int z) {}
/** These are already rotated local standing positions, not world coordinates. */
public static List<Target> targets(HeritagePlan19.Site site) {
var b = new Builder(site, null); b.layout(); return List.copyOf(b.targets);
}
public static void render(HeritagePlan19.Site site, LostCityRenderer.BlockWriter writer) {
var b = new Builder(site, writer); b.ground(); b.layout();
// Footings have absolute saved coordinates and must not undergo a second facade rotation.
for (var support : site.supports()) for (int y = support.minY(); y <= support.maxY(); y++)
writer.set(support.x() - site.minX(), y - site.floorY(), support.z() - site.minZ(),
y == support.minY() ? Blocks.SMOOTH_STONE.defaultBlockState() : Blocks.STONE_BRICKS.defaultBlockState());
}
public static void renderSegment(HeritagePlan19.Segment segment, LostCityRenderer.BlockWriter writer) {
int x0 = segment.minX(), y0 = segment.minY(), z0 = segment.minZ();
for (var cell : segment.cells()) {
int x = cell.x() - x0, z = cell.z() - z0, floor = cell.floorY() - y0;
for (int y = cell.minY() - y0; y <= floor; y++) {
Block material = Blocks.STONE;
if (y == floor) material = segment.kind() == HeritagePlan19.RouteKind.FREIGHT
? cell.rail() != HeritagePlan19.Rail.NONE && Math.floorMod(cell.x() + cell.z(), 3) == 0
? Blocks.DARK_OAK_PLANKS : Blocks.GRAVEL
: LostCityPalette.roll(segment.seed(), cell.x(), y, cell.z(), 4) == 0 ? Blocks.ANDESITE : Blocks.STONE;
writer.set(x, y, z, material.defaultBlockState());
}
for (int y = floor + 1; y <= floor + 3; y++) writer.set(x, y, z, Blocks.AIR.defaultBlockState());
if (cell.railPresent()) writer.set(x, floor + 1, z, Blocks.RAIL.defaultBlockState()
.setValue(BlockStateProperties.RAIL_SHAPE, RailShape.valueOf(cell.rail().name())));
}
}
private static final class Builder {
final HeritagePlan19.Site s;
final LostCityRenderer.BlockWriter out;
final List<Target> targets = new ArrayList<>();
final Rotation rotation;
Builder(HeritagePlan19.Site s, LostCityRenderer.BlockWriter out) {
this.s = s; this.out = out;
rotation = switch (s.facing()) { case 1 -> Rotation.CLOCKWISE_90; case 2 -> Rotation.CLOCKWISE_180;
case 3 -> Rotation.COUNTERCLOCKWISE_90; default -> Rotation.NONE; };
}
int rx(int x, int z) { return switch (s.facing()) { case 1 -> 16 - z; case 2 -> 16 - x; case 3 -> z; default -> x; }; }
int rz(int x, int z) { return switch (s.facing()) { case 1 -> x; case 2 -> 16 - z; case 3 -> 16 - x; default -> z; }; }
void set(int x, int y, int z, BlockState state) {
if (x < 0 || x > 16 || z < 0 || z > 16 || y < -s.foundationDepth() || y > s.height())
throw new IllegalArgumentException("Heritage " + s.kind() + " renderer escaped saved site: " + x + "/" + y + "/" + z);
if (out != null) out.set(rx(x, z), y, rz(x, z), state.rotate(rotation));
}
void set(int x, int y, int z, Block b) { set(x, y, z, b.defaultBlockState()); }
void box(int x0, int y0, int z0, int x1, int y1, int z1, Block b) {
for (int y = y0; y <= y1; y++) for (int z = z0; z <= z1; z++) for (int x = x0; x <= x1; x++) set(x, y, z, b);
}
void target(String name, int x, int y, int z) { targets.add(new Target(name, rx(x, z), y, rz(x, z))); }
void ground() {
for (int y = -s.foundationDepth(); y <= s.height(); y++) for (int z = 0; z <= 16; z++) for (int x = 0; x <= 16; x++)
set(x, y, z, y < 0 ? Blocks.STONE.defaultBlockState() : y == 0
? LostCityPalette.pavement(s.seed(), x, z) : Blocks.AIR.defaultBlockState());
}
void layout() {
target("main_entry", 8, 1, 0);
switch (s.kind()) {
case FREIGHT_STATION -> station();
case POST_OFFICE -> post();
case COMMUNAL_DEPOT -> depot();
case EXPEDITION_OFFICE -> expedition();
case OBSERVATORY -> observatory();
case RAIL_WORKSHOP -> railway();
case PUMP_STATION -> pumping();
case SUBSTATION -> substation();
case GREENHOUSE -> greenhouse();
case VOID_DOCK -> dock();
case RADIO_RELAY -> radio();
case PHOTO_CLUB -> photo();
case IMPOSSIBLE_SERVICE -> impossible();
case CONSTRUCTION_REFUGE -> refuge();
}
}
void shell(int x0, int floor, int z0, int x1, int ceiling, int z1) {
for (int y = floor; y <= ceiling; y++) for (int z = z0; z <= z1; z++) for (int x = x0; x <= x1; x++) {
boolean wall = x == x0 || x == x1 || z == z0 || z == z1;
set(x, y, z, y == floor || y == ceiling ? Blocks.SMOOTH_STONE.defaultBlockState()
: wall ? LostCityPalette.masonry(s.seed(), x, y, z) : Blocks.AIR.defaultBlockState());
}
}
void gable(int x0, int z0, int x1, int z1, int y) {
int rise = Math.min(3, (x1 - x0) / 2);
for (int step = 0; step <= rise; step++)
box(x0 + step, y + step, z0, x1 - step, y + step, z1, step == rise ? Blocks.STONE_BRICK_SLAB : Blocks.STONE_BRICKS);
}
void door(int x, int y, int z, Direction facing) { door(x, y, z, facing, false); }
void door(int x, int y, int z, Direction facing, boolean right) {
BlockState state = Blocks.DARK_OAK_DOOR.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing)
.setValue(BlockStateProperties.DOOR_HINGE, right ? DoorHingeSide.RIGHT : DoorHingeSide.LEFT)
.setValue(BlockStateProperties.OPEN, false).setValue(BlockStateProperties.POWERED, false);
set(x, y, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.LOWER));
set(x, y + 1, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.UPPER));
}
void doubleDoor(int x, int y, int z) { door(x, y, z, Direction.SOUTH, true); door(x + 1, y, z, Direction.SOUTH, false); }
void northWindows(int x0, int x1, int z, int y) { box(x0, y, z, x1, y + 1, z, Blocks.GLASS_PANE); }
void hang(int x, int ceiling, int z) { set(x, ceiling - 1, z, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true)); }
void seat(int x, int y, int z, Direction view) {
set(x, y, z, Blocks.DARK_OAK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, view.getOpposite()));
}
void lever(int x, int y, int z, Direction facing) {
set(x, y, z, Blocks.LEVER.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR)
.setValue(BlockStateProperties.HORIZONTAL_FACING, facing));
}
void rail(int x, int z, RailShape shape) { set(x, 1, z, Blocks.RAIL.defaultBlockState().setValue(BlockStateProperties.RAIL_SHAPE, shape)); }
void loot(int x, int y, int z, int approachX, int approachZ) {
target("loot_approach", approachX, y, approachZ);
if (out != null) out.chest(rx(x, z), y, rz(x, z), ResourceKey.create(Registries.LOOT_TABLE,
SanctuaryMod.id("chests/heritage19/" + s.kind().name().toLowerCase(Locale.ROOT))), s.seed() ^ 0x19CA7A10L);
}
void stairs(int x, int z, int base, int rise) {
for (int step = 1; step <= rise; step++) for (int xx = x; xx <= x + 1; xx++) {
box(xx, base + step + 1, z + step - 1, xx, base + step + 3, z + step - 1, Blocks.AIR);
set(xx, base + step - 1, z + step - 1, Blocks.STONE_BRICKS);
set(xx, base + step, z + step - 1, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
}
}
void bed(int x, int y, int z, Direction facing) {
BlockState state = Blocks.BED.white().defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing);
set(x, y, z, state.setValue(BlockStateProperties.BED_PART, BedPart.FOOT));
set(x + facing.getStepX(), y, z + facing.getStepZ(), state.setValue(BlockStateProperties.BED_PART, BedPart.HEAD));
int wx = x + facing.getStepX() * 2, wz = z + facing.getStepZ() * 2;
Direction along = facing.getClockWise();
for (int side = -1; side <= 1; side++) for (int up = 0; up <= 2; up++)
set(wx + along.getStepX() * side, y + up, wz + along.getStepZ() * side, Blocks.SMOOTH_STONE);
}
void station() {
shell(4, 0, 3, 12, 6, 11); gable(4, 3, 12, 11, 6);
door(8, 1, 3, Direction.SOUTH); northWindows(5, 6, 3, 2); northWindows(10, 11, 3, 2);
box(5, 1, 5, 6, 1, 5, Blocks.SMOOTH_STONE); set(5, 1, 8, Blocks.BARREL); seat(10, 1, 5, Direction.WEST);
for (int z = 2; z <= 14; z++) rail(2, z, RailShape.NORTH_SOUTH);
set(2, 1, 15, Blocks.DARK_OAK_FENCE);
rail(8, 0, RailShape.NORTH_SOUTH); rail(8, 1, RailShape.NORTH_WEST);
for (int x = 3; x <= 7; x++) rail(x, 1, RailShape.EAST_WEST);
rail(2, 1, RailShape.SOUTH_EAST);
box(0, 5, 5, 3, 5, 11, Blocks.DARK_OAK_PLANKS);
for (int z : new int[] {5, 11}) box(1, 1, z, 1, 4, z, Blocks.STRIPPED_OAK_LOG);
shell(9, 0, 11, 15, 4, 15); door(10, 1, 11, Direction.SOUTH); door(12, 1, 15, Direction.NORTH);
set(10, 1, 13, Blocks.SMOOTH_STONE); lever(10, 2, 13, Direction.EAST);
northWindows(13, 14, 15, 2); hang(8, 6, 7); hang(12, 4, 12);
target("ticket_counter", 8, 1, 5); target("freight_platform", 3, 1, 9); target("signal_office", 12, 1, 12);
target("secondary_entry", 12, 1, 16); loot(14, 1, 14, 13, 14);
}
void post() {
shell(2, 0, 2, 14, 6, 14); door(8, 1, 2, Direction.SOUTH);
northWindows(3, 6, 2, 2); northWindows(10, 13, 2, 2);
box(3, 1, 7, 13, 5, 7, Blocks.SMOOTH_STONE); door(8, 1, 7, Direction.SOUTH);
box(4, 1, 6, 6, 1, 6, Blocks.DARK_OAK_PLANKS);
for (int z = 9; z <= 11; z++) { set(3, 1, z, Blocks.BARREL); set(3, 2, z, Blocks.BARREL); }
set(6, 1, 10, Blocks.CRAFTING_TABLE); set(8, 1, 12, Blocks.BARREL);
shell(10, 0, 10, 14, 6, 14); door(10, 1, 12, Direction.EAST); door(14, 1, 12, Direction.WEST);
set(11, 1, 11, Blocks.CARTOGRAPHY_TABLE); hang(8, 6, 4); hang(7, 6, 10); hang(12, 6, 12);
target("public_hall", 8, 1, 4); target("sorting_aisle", 7, 1, 10); target("factor_office", 12, 1, 12);
target("secondary_entry", 15, 1, 12); loot(13, 1, 13, 12, 13);
}
void depot() {
shell(1, 0, 2, 15, 9, 14); gable(1, 2, 15, 14, 9);
box(7, 1, 2, 9, 3, 2, Blocks.AIR); door(4, 1, 2, Direction.SOUTH);
for (int z : new int[] {5, 8}) {
box(4, 1, z, 5, 1, z + 1, Blocks.DARK_OAK_PLANKS); set(4, 2, z, Blocks.BARREL);
box(11, 1, z, 13, 1, z + 1, Blocks.DARK_OAK_PLANKS); set(13, 2, z + 1, Blocks.BARREL);
}
box(2, 5, 10, 14, 5, 13, Blocks.SMOOTH_STONE);
for (int x = 5; x <= 14; x++) set(x, 6, 10, Blocks.IRON_BARS);
shell(10, 5, 10, 14, 9, 14); door(10, 6, 12, Direction.EAST);
set(11, 6, 11, Blocks.LECTERN.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
stairs(2, 5, 0, 5); door(15, 1, 12, Direction.WEST); hang(8, 9, 6); hang(12, 9, 12);
for (int z = 0; z <= 8; z++) rail(8, z, RailShape.NORTH_SOUTH);
set(8, 1, 9, Blocks.DARK_OAK_FENCE);
target("unloading_bay", 8, 1, 5); target("inventory_mezzanine", 5, 6, 12);
target("secondary_entry", 16, 1, 12); loot(13, 6, 13, 12, 13);
}
void expedition() {
shell(2, 0, 2, 12, 6, 12); door(8, 1, 2, Direction.SOUTH);
northWindows(3, 5, 2, 2); northWindows(10, 11, 2, 2);
set(4, 1, 4, Blocks.CARTOGRAPHY_TABLE); set(5, 1, 4, Blocks.LECTERN.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH)); set(3, 1, 6, Blocks.BARREL);
shell(2, 0, 8, 6, 6, 12); door(6, 1, 10, Direction.WEST); set(3, 1, 9, Blocks.BOOKSHELF);
box(3, 0, 13, 14, 0, 15, Blocks.DARK_OAK_PLANKS);
box(3, 1, 15, 14, 1, 15, Blocks.DARK_OAK_FENCE); door(10, 1, 12, Direction.NORTH);
seat(6, 1, 13, Direction.SOUTH); hang(8, 6, 5); hang(4, 6, 10);
target("expedition_table", 5, 1, 5); target("balcony", 10, 1, 14);
target("secondary_entry", 10, 1, 13); loot(3, 1, 11, 4, 11);
}
boolean tower(int x, int z) { return x >= 3 && x <= 13 && z >= 3 && z <= 13 && Math.abs(x - 8) + Math.abs(z - 8) <= 8; }
void observatory() {
for (int y = 0; y <= 12; y++) for (int z = 3; z <= 13; z++) for (int x = 3; x <= 13; x++) if (tower(x, z)) {
boolean wall = !tower(x - 1, z) || !tower(x + 1, z) || !tower(x, z - 1) || !tower(x, z + 1);
set(x, y, z, y == 0 || y == 6 || y == 12 ? Blocks.SMOOTH_STONE.defaultBlockState()
: wall ? LostCityPalette.masonry(s.seed(), x, y, z) : Blocks.AIR.defaultBlockState());
if (y == 12 && wall) set(x, 13, z, Blocks.IRON_BARS);
}
door(8, 1, 3, Direction.SOUTH); door(8, 1, 13, Direction.NORTH);
northWindows(7, 9, 3, 8); box(13, 8, 7, 13, 9, 9, Blocks.GLASS_PANE);
shell(9, 0, 9, 12, 4, 12); door(9, 1, 10, Direction.EAST); set(10, 1, 11, Blocks.BARREL);
set(10, 7, 5, Blocks.CARTOGRAPHY_TABLE); set(11, 7, 8, Blocks.SMOOTH_STONE); lever(11, 8, 8, Direction.WEST);
stairs(4, 5, 0, 6); stairs(4, 5, 6, 6);
box(4, 12, 11, 6, 12, 11, Blocks.SMOOTH_STONE); box(4, 13, 11, 6, 15, 11, Blocks.AIR);
box(10, 13, 9, 10, 18, 9, Blocks.DARK_OAK_FENCE); box(9, 18, 9, 11, 18, 9, Blocks.IRON_BARS);
hang(8, 6, 6); hang(10, 4, 11);
target("instruments", 9, 7, 8); target("sky_platform", 8, 13, 8);
target("secondary_entry", 8, 1, 14); loot(11, 1, 11, 11, 10);
}
void railway() {
shell(1, 0, 2, 15, 6, 14); gable(1, 2, 15, 14, 6); doubleDoor(7, 1, 2);
for (int z = 3; z <= 13; z++) for (int x : new int[] {5, 11}) set(x, 1, z, Blocks.RAIL);
box(4, 1, 2, 6, 3, 2, Blocks.AIR); rail(8, 0, RailShape.NORTH_SOUTH); rail(8, 1, RailShape.NORTH_WEST);
rail(7, 1, RailShape.EAST_WEST); rail(6, 1, RailShape.EAST_WEST); rail(5, 1, RailShape.SOUTH_EAST); rail(5, 2, RailShape.NORTH_SOUTH);
box(7, -2, 5, 9, -2, 11, Blocks.SMOOTH_STONE); box(7, -1, 5, 9, 0, 11, Blocks.AIR);
for (int step = 0; step <= 2; step++) set(8, -step, 4 + step, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
for (int z = 6; z <= 11; z++) { set(6, 1, z, Blocks.IRON_BARS); set(10, 1, z, Blocks.IRON_BARS); }
set(2, 1, 4, Blocks.CRAFTING_TABLE); set(3, 1, 4, Blocks.FURNACE.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
set(2, 1, 6, Blocks.GRINDSTONE.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR));
shell(1, 0, 10, 5, 5, 14); door(5, 1, 12, Direction.WEST); door(8, 1, 14, Direction.NORTH);
set(5, 1, 9, Blocks.DARK_OAK_FENCE);
set(2, 1, 11, Blocks.ANVIL); hang(8, 6, 8); hang(3, 5, 12);
target("maintenance_pit", 8, -1, 9); target("workbench", 3, 1, 5);
target("secondary_entry", 8, 1, 15); loot(2, 1, 13, 3, 13);
}
void pumping() {
shell(2, 0, 3, 9, 6, 12); door(8, 1, 3, Direction.SOUTH);
set(3, 1, 5, Blocks.DISPENSER.defaultBlockState().setValue(BlockStateProperties.FACING, Direction.EAST));
set(4, 1, 5, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 3));
set(3, 2, 5, Blocks.POLISHED_ANDESITE); box(3, 3, 5, 3, 5, 8, Blocks.IRON_BARS);
for (int x : new int[] {10, 15}) for (int z : new int[] {5, 11}) box(x, 1, z, x, 7, z, Blocks.POLISHED_ANDESITE);
shell(10, 8, 5, 15, 13, 11); box(11, 9, 6, 14, 11, 10, Blocks.WATER);
box(10, 3, 6, 14, 3, 13, Blocks.SMOOTH_STONE); stairs(10, 3, 0, 3);
shell(2, 0, 10, 6, 5, 15); door(5, 1, 10, Direction.SOUTH); door(5, 1, 15, Direction.NORTH);
set(3, 1, 11, Blocks.BARREL); hang(7, 6, 7); hang(4, 5, 12);
target("pump_room", 6, 1, 6); target("reservoir_walkway", 13, 4, 12);
target("secondary_entry", 5, 1, 16); loot(3, 1, 14, 4, 14);
}
void substation() {
for (int z = 2; z <= 14; z++) { set(1, 1, z, Blocks.IRON_BARS); set(15, 1, z, Blocks.IRON_BARS); }
for (int x = 1; x <= 15; x++) if (x < 7 || x > 9) set(x, 1, 2, Blocks.IRON_BARS);
for (int z : new int[] {5, 8}) {
box(3, 1, z, 4, 2, z + 1, Blocks.POLISHED_ANDESITE); box(3, 3, z, 4, 3, z, Blocks.IRON_BARS);
}
shell(10, 0, 4, 15, 5, 10); door(10, 1, 6, Direction.EAST); northWindows(12, 14, 4, 2);
set(12, 1, 5, Blocks.SMOOTH_STONE); lever(12, 2, 5, Direction.SOUTH); set(14, 1, 5, Blocks.CRAFTING_TABLE);
shell(10, 0, 10, 15, 5, 15); door(12, 1, 10, Direction.SOUTH); door(15, 1, 13, Direction.WEST);
box(6, -2, 10, 7, -2, 13, Blocks.SMOOTH_STONE); box(6, -1, 10, 7, 0, 13, Blocks.AIR);
for (int step = 0; step <= 2; step++) set(6, -step, 9 + step, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
hang(13, 5, 7); hang(12, 5, 13); target("distribution_yard", 8, 1, 6); target("cable_trench", 6, -1, 12);
target("secondary_entry", 16, 1, 13); loot(14, 1, 14, 13, 14);
}
void greenhouse() {
shell(1, 0, 2, 11, 6, 14);
for (int y = 2; y <= 5; y++) for (int z = 3; z <= 13; z++) { set(1, y, z, Blocks.GLASS); set(11, y, z, Blocks.GLASS); }
northWindows(2, 5, 2, 2); northWindows(7, 10, 2, 2); door(6, 1, 2, Direction.SOUTH);
for (int x = 2; x <= 10; x++) for (int z = 3; z <= 13; z++) set(x, 6, z, x == 6 ? Blocks.SMOOTH_STONE : Blocks.GLASS);
for (int x : new int[] {2, 3, 4, 8, 9, 10}) for (int z = 4; z <= 10; z++) {
set(x, 0, z, Blocks.FARMLAND.defaultBlockState().setValue(BlockStateProperties.MOISTURE, 7));
set(x, 1, z, Blocks.WHEAT.defaultBlockState().setValue(BlockStateProperties.AGE_7,
2 + LostCityPalette.roll(s.seed(), x, 1, z, 6)));
}
set(3, 0, 6, Blocks.WATER); set(9, 0, 6, Blocks.WATER);
set(3, 1, 6, Blocks.AIR); set(9, 1, 6, Blocks.AIR);
shell(11, 0, 9, 15, 5, 15); door(11, 1, 12, Direction.EAST); door(13, 1, 15, Direction.NORTH);
set(12, 1, 10, Blocks.CRAFTING_TABLE); set(13, 1, 10, Blocks.FLOWER_POT); set(9, 1, 12, Blocks.COMPOSTER);
hang(6, 6, 5); hang(13, 5, 12); target("glasshouse_aisle", 6, 1, 7); target("potting_shed", 13, 1, 12);
target("secondary_entry", 13, 1, 16); loot(14, 1, 14, 13, 14);
}
void dock() {
box(0, 0, 0, 16, 0, 16, Blocks.DARK_OAK_PLANKS); box(0, 1, 16, 16, 1, 16, Blocks.DARK_OAK_FENCE);
box(0, 1, 1, 0, 1, 15, Blocks.DARK_OAK_FENCE); box(16, 1, 1, 16, 1, 15, Blocks.DARK_OAK_FENCE);
shell(2, 0, 3, 7, 5, 10); door(7, 1, 6, Direction.WEST); seat(3, 1, 5, Direction.EAST);
shell(2, 0, 10, 7, 5, 15); door(5, 1, 10, Direction.SOUTH); door(7, 1, 13, Direction.WEST);
box(12, 1, 5, 12, 11, 5, Blocks.STRIPPED_OAK_LOG); box(8, 11, 5, 14, 11, 5, Blocks.STRIPPED_OAK_LOG);
box(14, 7, 5, 14, 10, 5, Blocks.IRON_CHAIN); hang(10, 11, 5); hang(5, 5, 6); hang(5, 5, 12);
target("departure_deck", 8, 1, 14); target("waiting_room", 5, 1, 6); target("secondary_entry", 8, 1, 13);
loot(3, 1, 14, 4, 14);
}
void radio() {
shell(2, 0, 3, 10, 6, 12); door(8, 1, 3, Direction.SOUTH); northWindows(3, 5, 3, 2);
bed(4, 1, 5, Direction.NORTH); set(8, 1, 5, Blocks.CARTOGRAPHY_TABLE);
shell(2, 0, 8, 6, 6, 12); door(6, 1, 10, Direction.WEST); door(2, 1, 10, Direction.EAST);
box(11, 5, 6, 15, 5, 10, Blocks.SMOOTH_STONE);
for (int x : new int[] {11, 15}) for (int z : new int[] {6, 10}) box(x, 1, z, x, 4, z, Blocks.POLISHED_ANDESITE);
box(13, 1, 8, 13, 18, 8, Blocks.POLISHED_ANDESITE); box(11, 17, 8, 15, 17, 8, Blocks.IRON_BARS);
box(13, 19, 8, 13, 21, 8, Blocks.IRON_BARS); stairs(11, 1, 0, 5);
hang(7, 6, 6); hang(4, 6, 10);
target("caretaker_room", 7, 1, 6); target("antenna_platform", 12, 6, 9); target("secondary_entry", 1, 1, 10);
loot(3, 1, 11, 4, 11);
}
void photo() {
shell(2, 0, 2, 14, 6, 14); door(8, 1, 2, Direction.SOUTH); northWindows(3, 6, 2, 2); northWindows(10, 13, 2, 2);
box(3, 1, 8, 13, 5, 8, Blocks.SMOOTH_STONE); door(4, 1, 8, Direction.SOUTH);
box(8, 1, 9, 8, 5, 13, Blocks.SMOOTH_STONE); door(8, 1, 11, Direction.EAST);
box(10, 1, 5, 12, 1, 5, Blocks.DARK_OAK_PLANKS); set(3, 1, 4, Blocks.BOOKSHELF);
for (int z : new int[] {10, 12}) set(3, 1, z, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 3));
box(11, 4, 10, 11, 5, 12, Blocks.IRON_CHAIN); set(12, 1, 10, Blocks.CRAFTING_TABLE);
door(12, 1, 14, Direction.NORTH); hang(8, 6, 4); hang(5, 6, 11); hang(12, 6, 12);
target("club_room", 8, 1, 4); target("darkroom", 5, 1, 11); target("drying_room", 10, 1, 11);
target("secondary_entry", 12, 1, 15); loot(13, 1, 13, 12, 13);
}
void impossible() {
shell(3, 0, 2, 11, 4, 7); door(8, 1, 2, Direction.SOUTH);
for (int z = 3; z <= 5; z++) { set(4, 1, z, Blocks.BARREL); set(4, 2, z, Blocks.BARREL); }
seat(10, 1, 4, Direction.WEST);
shell(1, 0, 7, 15, 10, 15); door(8, 1, 7, Direction.SOUTH);
for (int x = 2; x <= 14; x++) for (int z = 8; z <= 14; z++) set(x, 0, z,
((x + z) & 1) == 0 ? Blocks.CALCITE : Blocks.POLISHED_ANDESITE);
for (int x : new int[] {3, 6, 10, 13}) box(x, 1, 11, x, 9, 11, Blocks.CALCITE);
shell(1, 0, 12, 5, 5, 15); door(5, 1, 13, Direction.WEST); door(3, 1, 15, Direction.NORTH);
set(2, 2, 12, Blocks.GLASS); set(2, 1, 13, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 2));
hang(8, 4, 4); hang(8, 10, 11); hang(3, 5, 13);
target("lockers", 8, 1, 4); target("oversized_tiled_hall", 8, 1, 11);
target("secondary_entry", 3, 1, 16); loot(2, 1, 14, 3, 14);
}
void refuge() {
box(3, 4, 5, 14, 4, 15, Blocks.DARK_OAK_PLANKS);
for (int x : new int[] {3, 14}) for (int z : new int[] {5, 15}) box(x, 1, z, x, 3, z, Blocks.STONE_BRICKS);
shell(5, 4, 6, 13, 10, 14); gable(5, 6, 13, 14, 10); door(8, 5, 6, Direction.SOUTH);
bed(6, 5, 8, Direction.NORTH); set(12, 5, 8, Blocks.CRAFTING_TABLE);
shell(6, 0, 9, 11, 4, 14); door(6, 1, 11, Direction.EAST);
// Carry the ladder through the floor so opening the hatch leaves a climbable lip.
for (int y = 1; y <= 4; y++) set(10, y, 11, Blocks.LADDER.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST));
set(10, 5, 11, Blocks.DARK_OAK_TRAPDOOR.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST)
.setValue(BlockStateProperties.HALF, Half.BOTTOM));
stairs(7, 1, 0, 4); box(2, 1, 3, 3, 11, 4, Blocks.STONE_BRICKS); box(2, 11, 3, 14, 11, 4, Blocks.STRIPPED_OAK_LOG);
hang(10, 10, 8); hang(8, 4, 12);
target("suspended_hut", 9, 5, 8); target("hidden_floor_hatch", 10, 5, 11); target("secondary_entry", 5, 1, 11);
loot(7, 1, 13, 8, 13);
}
}
}
@@ -0,0 +1,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.experimentalStructures()) return Optional.empty();
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
if (island == null || !island.origin() || island.generation() != 19) return Optional.empty();
var plan = generator.heritagePlan19();
int x = context.chunkPos().x(), z = context.chunkPos().z();
var pieces = new ArrayList<HeritagePiece19>();
// Local route segments render before buildings. No piece needs a distant link endpoint loaded.
for (var segment : plan.segments())
if (segment.startChunkX() == x && segment.startChunkZ() == z)
pieces.add(new HeritagePiece19(segment));
for (var site : plan.sites()) if (site.startChunkX() == x && site.startChunkZ() == z)
pieces.add(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(HeritagePiece19::isSite).findFirst().orElse(pieces.getFirst()).getLocatorPosition();
return Optional.of(new GenerationStub(locator, builder -> pieces.forEach(builder::addPiece)));
}
@Override public StructureType<?> type() { return HeritageStructures19.TYPE; }
}
@@ -0,0 +1,110 @@
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);
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
|| !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.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()).findFirst().orElseThrow();
return Pair.of(entrance.getLocatorPosition(), holder);
}
return null;
}
static BlockPos locator(HeritagePlan19.Site site) { return HeritagePiece19.locator(site); }
static boolean hasStart(HeritagePlan19 plan, int x, int z) {
return plan.sites().stream().anyMatch(s -> s.startChunkX() == x && s.startChunkZ() == z)
|| plan.segments().stream().anyMatch(s -> s.startChunkX() == x && s.startChunkZ() == z);
}
}
@@ -0,0 +1,110 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import fr.koka.sanctuary.worldgen.city.LostCityPlan;
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.RandomState;
/** Read-only pre-decoration terrain. No loaded chunks or player-edited blocks enter a plan. */
public final class HeritageTerrain19 implements HeritagePlanner19.Terrain {
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
private final ExpansionIsland island;
private final NoiseBasedChunkGenerator generator;
private final RandomState random;
private final LevelHeightAccessor height;
private final Predicate<BlockPos> protection;
private final Map<Long, NoiseColumn> columns = new HashMap<>();
private List<HeritagePlan19.Landmark> landmarks = List.of();
public HeritageTerrain19(ExpansionIsland island, NoiseBasedChunkGenerator generator, RandomState random,
LevelHeightAccessor height, Predicate<BlockPos> protection) {
this.island = island; this.generator = generator; this.random = random;
this.height = height; this.protection = protection;
}
private NoiseColumn column(int x, int z) {
if (!owns(x, z)) return null;
long key = ((long) x << 32) ^ (z & 0xffffffffL);
var previous = columns.get(key);
if (previous != null) return previous;
if (columns.size() >= HeritagePlan19.MAX_COLUMNS) return null;
var result = generator.getBaseColumn(x, z, height, random);
columns.put(key, result);
return result;
}
@Override public boolean owns(int x, int z) { return island.contains(x, z); }
@Override public boolean solid(int x, int y, int z) {
if (y < 0 || y >= 384) return false;
var column = column(x, z);
return column != null && column.getBlock(y).isSolid() && column.getBlock(y).getFluidState().isEmpty();
}
@Override public boolean protectedAt(int x, int y, int z) {
if (y < 0 || y >= 384) return true;
var column = column(x, z);
return column == null || !column.getBlock(y).getFluidState().isEmpty()
|| protection.test(new BlockPos(x, y, z));
}
@Override public int surfaceY(int x, int z) {
var column = column(x, z);
if (column == null) return -1;
for (int y = 380; y >= 1; y--) {
var state = column.getBlock(y);
if (!state.isAir()) return state.isSolid() && state.getFluidState().isEmpty() ? y : -1;
}
return -1;
}
@Override public List<HeritagePlan19.Landmark> landmarks() { return landmarks; }
public int columnsUsed() { return columns.size(); }
/** Select actual dry ground abutting existing walkable platforms. Their protected blocks stay intact. */
public void discoverLandmarks(LostCityPlan city, RuinsPlan17 ruins) {
var result = new ArrayList<HeritagePlan19.Landmark>();
for (var network : ruins.networks()) {
var access = network.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS).findFirst().orElse(null);
if (access == null) continue;
int deck = access.maxY() - 2;
for (int direction = 0; direction < 4; direction++) {
var entry = dryEntry(access.centerX() + DX[direction] * 3, access.centerZ() + DZ[direction] * 3, deck, direction);
if (entry == null) continue;
var kind = network.family() == RuinsPlan17.Family.MINE ? HeritagePlan19.LandmarkKind.MINE : HeritagePlan19.LandmarkKind.UTILITY;
result.add(new HeritagePlan19.Landmark("old_" + network.id(), kind, entry));
break;
}
}
int cityEntries = 0;
outer: for (var district : city.districts()) for (var parcel : district.parcels()) {
if (parcel.kind() != LostCityPlan.Kind.STREET) continue;
boolean eastWest = parcel.width() > parcel.depth();
for (int direction : eastWest ? new int[]{1, 3} : new int[]{0, 2}) {
int x = direction == 1 ? parcel.width() - 1 : direction == 3 ? 0 : parcel.width() / 2;
int z = direction == 2 ? parcel.depth() - 1 : direction == 0 ? 0 : parcel.depth() / 2;
var entry = dryEntry(parcel.minX() + x + DX[direction], parcel.minZ() + z + DZ[direction], parcel.surfaceY(x, z), direction);
if (entry == null) continue;
result.add(new HeritagePlan19.Landmark("old_city_" + cityEntries++, HeritagePlan19.LandmarkKind.CITY, entry));
if (cityEntries == 2) break outer;
}
}
landmarks = List.copyOf(result);
}
private HeritagePlan19.Entry dryEntry(int x, int z, int deck, int direction) {
int y = surfaceY(x, z);
if (y < 32 || y > 348 || Math.abs(y - deck) > 1) return null;
// A three-wide arrival can abut a platform without cutting its wall or footings.
for (int side = -1; side <= 1; side++) {
int px = x + DZ[direction] * side, pz = z + DX[direction] * side;
var column = column(px, pz);
if (column == null || surfaceY(px, pz) != y || !solid(px, y, pz)) return null;
for (int by = y - 1; by <= y + 3; by++) if (protectedAt(px, by, pz)) return null;
for (int by = y + 1; by <= y + 3; by++) if (!column.getBlock(by).isAir()) return null;
}
return new HeritagePlan19.Entry(x, y + 1, z, direction);
}
}
@@ -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.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();
@@ -53,10 +53,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)
|| !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 +76,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.experimentalStructures()
|| !level.structureManager().shouldGenerateStructures()) return null;
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
if (!structures.contains(holder)) return null;
@@ -84,7 +84,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);
}
@@ -127,5 +127,24 @@
"sanctuary.expansion.error.void_reuse_incomplete": "Void reuse is incomplete. The reservation is retained; check the server log.",
"sanctuary.world_options.experimental": "Experimental structures",
"sanctuary.world_options.experimental_hint": "Mines, strange rooms and abandoned facilities. This choice is saved with the new world.",
"structure.sanctuary.forgotten_networks": "Forgotten networks"
"structure.sanctuary.forgotten_networks": "Forgotten networks",
"sanctuary.world_options.heritage_hint": "Adds forgotten networks, buildings and abandoned routes to the new island. This choice is saved with the world.",
"structure.sanctuary.heritage_networks": "Abandoned infrastructure",
"sanctuary.heritage.kind.freight_station": "Freight station",
"sanctuary.heritage.kind.post_office": "Post office",
"sanctuary.heritage.kind.communal_depot": "Communal depot",
"sanctuary.heritage.kind.expedition_office": "Expedition office and map archive",
"sanctuary.heritage.kind.observatory": "Observatory and weather station",
"sanctuary.heritage.kind.rail_workshop": "Rail workshop",
"sanctuary.heritage.kind.pump_station": "Pump station",
"sanctuary.heritage.kind.substation": "Utility substation",
"sanctuary.heritage.kind.greenhouse": "Greenhouse and seed store",
"sanctuary.heritage.kind.void_dock": "Expedition dock",
"sanctuary.heritage.kind.radio_relay": "Radio relay",
"sanctuary.heritage.kind.photo_club": "Photography club",
"sanctuary.heritage.kind.impossible_service": "Changing rooms and strange service rooms",
"sanctuary.heritage.kind.construction_refuge": "Construction refuge",
"sanctuary.heritage.unavailable": "This survey requires a Sanctuary alpha.19 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"
}
@@ -127,5 +127,24 @@
"sanctuary.expansion.error.void_reuse_incomplete": "La réutilisation du vide est incomplète. La réservation est conservée ; consultez le journal du serveur.",
"sanctuary.world_options.experimental": "Structures expérimentales",
"sanctuary.world_options.experimental_hint": "Mines, salles étranges et installations abandonnées. Ce choix reste attaché au nouveau monde.",
"structure.sanctuary.forgotten_networks": "Réseaux oubliés"
"structure.sanctuary.forgotten_networks": "Réseaux oubliés",
"sanctuary.world_options.heritage_hint": "Ajoute les anciens réseaux, bâtiments et voies abandonnées sur la nouvelle île. Ce choix est enregistré avec le monde.",
"structure.sanctuary.heritage_networks": "Anciennes infrastructures",
"sanctuary.heritage.kind.freight_station": "Gare de marchandises",
"sanctuary.heritage.kind.post_office": "Centre postal",
"sanctuary.heritage.kind.communal_depot": "Dépôt communal",
"sanctuary.heritage.kind.expedition_office": "Bureau des expéditions et cartothèque",
"sanctuary.heritage.kind.observatory": "Observatoire et station météo",
"sanctuary.heritage.kind.rail_workshop": "Atelier ferroviaire",
"sanctuary.heritage.kind.pump_station": "Station de pompage",
"sanctuary.heritage.kind.substation": "Sous-station",
"sanctuary.heritage.kind.greenhouse": "Serre et graineterie",
"sanctuary.heritage.kind.void_dock": "Quai dexpédition",
"sanctuary.heritage.kind.radio_relay": "Relais radio",
"sanctuary.heritage.kind.photo_club": "Club photo",
"sanctuary.heritage.kind.impossible_service": "Vestiaires et salles étranges",
"sanctuary.heritage.kind.construction_refuge": "Refuge de chantier",
"sanctuary.heritage.unavailable": "Ce relevé nécessite un monde Sanctuary alpha.19 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"
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:iron_axe"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:torch",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:bread",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:iron_shovel"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:bread",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:torch",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:compass"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:paper",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:map",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:minecart"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:rail",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:coal",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:iron_hoe"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:wheat_seeds",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:pumpkin_seeds",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:clock"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:torch",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:string",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:spyglass"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:paper",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:book",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:book"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:paper",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:glass_pane",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:compass"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:paper",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:string",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:bucket"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:string",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:torch",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:compass"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:redstone",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:paper",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:iron_pickaxe"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:rail",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:coal",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:clock"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:redstone",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:copper_ingot",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:lead"
}
]
},
{
"rolls": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
},
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:torch",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
},
{
"type": "minecraft:item",
"name": "minecraft:bread",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,55 @@
{
"replace": false,
"values": [
"#minecraft:is_overworld",
"sanctuary:cavern_bamboo_grove",
"sanctuary:cavern_birch_forest",
"sanctuary:cavern_clearing",
"sanctuary:cavern_dark_grove",
"sanctuary:cavern_dripstone_caves",
"sanctuary:cavern_dry_woodland",
"sanctuary:cavern_lush_caves",
"sanctuary:cavern_oak_forest",
"sanctuary:cavern_rocky_heath",
"sanctuary:cavern_sulfur_depths",
"sanctuary:layered_flower_meadow",
"sanctuary:layered_lower_grove",
"sanctuary:layered_meadow",
"sanctuary:layered_mossy_terraces",
"sanctuary:layered_rocky_terraces",
"sanctuary:lost_city",
"sanctuary:population_bamboo_grove",
"sanctuary:population_birch_forest",
"sanctuary:population_clearing",
"sanctuary:population_dark_grove",
"sanctuary:population_dripstone_caves",
"sanctuary:population_dry_woodland",
"sanctuary:population_lush_caves",
"sanctuary:population_oak_forest",
"sanctuary:population_rocky_heath",
"sanctuary:population_sulfur_depths",
"sanctuary:rift_bamboo_grove",
"sanctuary:rift_birch_forest",
"sanctuary:rift_clearing",
"sanctuary:rift_dark_grove",
"sanctuary:rift_dry_woodland",
"sanctuary:rift_oak_forest",
"sanctuary:rift_rocky_heath",
"sanctuary:rift_sulfur_depths",
"sanctuary:starter_forest",
"sanctuary:starter_forest_hydrology",
"sanctuary:temperate_birch_forest",
"sanctuary:temperate_dappled_forest",
"sanctuary:temperate_flower_forest",
"sanctuary:temperate_forest",
"sanctuary:temperate_plains",
"sanctuary:woodland_bamboo_grove",
"sanctuary:woodland_birch_forest",
"sanctuary:woodland_clearing",
"sanctuary:woodland_dark_grove",
"sanctuary:woodland_dry_woodland",
"sanctuary:woodland_oak_forest",
"sanctuary:woodland_rocky_heath",
"sanctuary:woodland_sulfur_depths"
]
}
@@ -0,0 +1,7 @@
{
"type": "sanctuary:heritage_networks_v19",
"biomes": "#sanctuary:has_structure/heritage_networks",
"step": "underground_structures",
"terrain_adaptation": "none",
"spawn_overrides": {}
}
@@ -3,7 +3,7 @@
"minecraft:overworld": {
"type": "sanctuary:sanctuary_384",
"generator": {
"type": "sanctuary:island_v17",
"type": "sanctuary:island_v19",
"biome_source": {
"biomes": [
"minecraft:the_void",
@@ -0,0 +1,81 @@
{
"dimensions": {
"minecraft:overworld": {
"type": "sanctuary:sanctuary_384",
"generator": {
"type": "sanctuary:island_v17",
"biome_source": {
"biomes": [
"minecraft:the_void",
"minecraft:forest",
"minecraft:birch_forest",
"minecraft:flower_forest",
"minecraft:plains",
"minecraft:meadow",
"minecraft:taiga",
"minecraft:snowy_taiga",
"minecraft:snowy_plains",
"minecraft:grove",
"minecraft:ice_spikes",
"minecraft:frozen_peaks",
"minecraft:old_growth_spruce_taiga",
"minecraft:dark_forest",
"minecraft:swamp",
"minecraft:pale_garden",
"minecraft:jungle",
"minecraft:sparse_jungle",
"minecraft:bamboo_jungle",
"minecraft:mangrove_swamp",
"minecraft:savanna",
"minecraft:savanna_plateau",
"minecraft:desert",
"minecraft:badlands",
"minecraft:eroded_badlands",
"minecraft:wooded_badlands",
"minecraft:windswept_hills",
"minecraft:windswept_gravelly_hills",
"minecraft:stony_peaks",
"minecraft:dappled_forest",
"minecraft:cherry_grove",
"minecraft:lush_caves",
"minecraft:dripstone_caves",
"minecraft:sulfur_caves",
"sanctuary:population_oak_forest",
"sanctuary:population_birch_forest",
"sanctuary:population_clearing",
"sanctuary:population_dry_woodland",
"sanctuary:population_rocky_heath",
"sanctuary:population_dark_grove",
"sanctuary:population_bamboo_grove",
"sanctuary:population_sulfur_depths",
"sanctuary:population_lush_caves",
"sanctuary:population_dripstone_caves",
"sanctuary:lost_city"
]
},
"initial_diameter": 724
}
},
"minecraft:the_end": {
"type": "minecraft:the_end",
"generator": {
"type": "minecraft:noise",
"biome_source": {
"type": "minecraft:the_end"
},
"settings": "minecraft:end"
}
},
"minecraft:the_nether": {
"type": "minecraft:the_nether",
"generator": {
"type": "minecraft:noise",
"biome_source": {
"type": "minecraft:multi_noise",
"preset": "minecraft:nether"
},
"settings": "minecraft:nether"
}
}
}
}
@@ -158,6 +158,21 @@ public final class ExpansionJournalSmoke {
require(Arrays.equals(experimentBytes, Files.readAllBytes(experimentDirectory.resolve(ExpansionJournal.FILE_NAME)))
&& Arrays.equals(ruinsBytes, Files.readAllBytes(ruinsDirectory.resolve(ExpansionJournal.FILE_NAME))),
"Rejected cross-generation changes preserve experiment and published16 bytes");
var heritageRoot = ExpansionIsland.origin(724, SEED, 19);
var heritageDirectory = scratch.resolve("heritage-19");
var heritageJournal = new ExpansionJournal(heritageDirectory, SEED, 724, 19);
var heritageChild = ExpansionPlacement.candidates(SEED, List.of(heritageRoot), "heritage", "sanctuary", "east", "lost_city", 128, "natural", 0).getFirst()
.withReuseToken("f".repeat(64));
heritageJournal.commit(List.of(heritageRoot, heritageChild));
require(new ExpansionJournal(heritageDirectory, SEED, 724, 19).islands().equals(List.of(heritageRoot, heritageChild)),
"Generation19 preserves the new world and its expansions across journal reload");
byte[] heritageBytes = Files.readAllBytes(heritageDirectory.resolve(ExpansionJournal.FILE_NAME));
reject(() -> new ExpansionJournal(heritageDirectory, SEED, 724, 17), "No implicit downgrade from19 to17");
reject(() -> new ExpansionJournal(experimentDirectory, SEED, 724, 19), "No implicit upgrade from17 to19");
reject(() -> heritageJournal.commit(List.of(heritageRoot, experimentChild)), "No generation17 child in19");
require(Arrays.equals(heritageBytes, Files.readAllBytes(heritageDirectory.resolve(ExpansionJournal.FILE_NAME)))
&& Arrays.equals(experimentBytes, Files.readAllBytes(experimentDirectory.resolve(ExpansionJournal.FILE_NAME))),
"Rejected migrations preserve both generation17 and19 journals");
try {
ExpansionPlacement.candidates(SEED, List.of(ExpansionIsland.origin(724, SEED, 13)), "city", "sanctuary", "east", "lost_city", 256, "natural", 0);
throw new AssertionError("Lost City must not silently activate in a saved alpha.13 world");
@@ -0,0 +1,85 @@
package fr.koka.sanctuary.worldgen.heritage;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.IntTag;
import net.minecraft.nbt.ListTag;
/** Exact, bounded independent schema19 persistence for catalogue sites and chunk-local abandoned routes. */
public final class HeritagePiece19Smoke {
public static void main(String[] args) {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); run();
System.out.println("HeritagePiece19Smoke: 14 kinds, 4 facades, every rail profile, missing rails, strict NBT and local native bounds passed.");
}
public static void run() {
for (var kind : HeritagePlan19.Kind.values()) for (int face = 0; face < 4; face++) {
int x = -32, z = 40, floor = 160;
var entry = new HeritagePlan19.Entry(face == 1 ? x + 17 : face == 3 ? x - 1 : x + 8,
floor + 1, face == 2 ? z + 17 : face == 0 ? z - 1 : z + 8, face);
var supportList = new ArrayList<HeritagePlan19.Support>();
supportList.add(new HeritagePlan19.Support(x + 1, 150, z + 1, 155));
var site = new HeritagePlan19.Site("site_" + kind.name(), kind, x, floor, z, 17, 17, 20, face,
Long.MIN_VALUE + kind.ordinal(), 4, supportList, entry);
supportList.clear();
var saved = HeritagePiece19.encodeSite(site);
require(HeritagePiece19.decodeSite(saved).equals(site) && HeritagePiece19.encodeSite(HeritagePiece19.decodeSite(saved)).equals(saved), "Site roundtrip changed geometry");
require(site.supports().size() == 1 && HeritagePiece19.bounds(site).minY() == 150, "Support input is mutable or excluded from native bounds");
require(HeritagePiece19.locator(site).getX() == entry.x() && HeritagePiece19.locator(site).getY() == entry.y()
&& HeritagePiece19.locator(site).getZ() == entry.z(), "Locate lost the rotated facade entry");
badSite(saved, t -> t.remove("Facing")); badSite(saved, t -> t.putInt("Schema", 17));
badSite(saved, t -> t.putString("Type", "ROUTE")); badSite(saved, t -> t.putString("Kind", "FUTURE"));
badSite(saved, t -> t.putDouble("FloorY", 160)); badSite(saved, t -> t.putInt("Seed", 1));
badSite(saved, t -> t.putInt("Future", 1)); badSite(saved, t -> t.putInt("MinX", Integer.MAX_VALUE));
badSite(saved, t -> t.putInt("Width", 18)); badSite(saved, t -> t.putInt("Facing", 4));
badSite(saved, t -> t.putInt("FoundationDepth", 0)); badSite(saved, t -> t.putString("Id", "x".repeat(129)));
badSite(saved, t -> t.getCompound("Entry").orElseThrow().putInt("X", entry.x() + 1));
badSite(saved, t -> t.getCompound("Entry").orElseThrow().putLong("Y", entry.y()));
badSite(saved, t -> t.getList("Supports").orElseThrow().getCompound(0).orElseThrow().putInt("MinY", 130));
badSite(saved, t -> t.getList("Supports").orElseThrow().getCompound(0).orElseThrow().putInt("X", site.maxX() + 1));
badSite(saved, t -> {
var list = t.getList("Supports").orElseThrow(); list.add(list.getFirst().copy());
});
}
var cells = new ArrayList<HeritagePlan19.Cell>();
int index = 0;
for (var rail : HeritagePlan19.Rail.values()) {
cells.add(new HeritagePlan19.Cell(-32 + index, 150 + index % 3, 32, 2 + index % 4, rail, rail != HeritagePlan19.Rail.NONE && index % 2 == 0)); index++;
}
var segment = new HeritagePlan19.Segment("freight/part0", "station", "mine", HeritagePlan19.RouteKind.FREIGHT, -2, 2, cells, Long.MAX_VALUE);
cells.clear();
var landing = new HeritagePlan19.Segment("station/landing", "station", "station", HeritagePlan19.RouteKind.PATH,
-2, 2, List.of(new HeritagePlan19.Cell(-32, 150, 32, 2, HeritagePlan19.Rail.NONE, false)), 42);
require(HeritagePiece19.decodeSegment(HeritagePiece19.encodeSegment(landing)).equals(landing), "Standalone entry landing lost its site binding");
var saved = HeritagePiece19.encodeSegment(segment);
require(HeritagePiece19.decodeSegment(saved).equals(segment) && HeritagePiece19.encodeSegment(HeritagePiece19.decodeSegment(saved)).equals(saved), "Route roundtrip changed shapes, supports or deliberate rail gaps");
require(segment.cells().size() == HeritagePlan19.Rail.values().length, "External route list mutation changed its saved cells");
require(HeritagePiece19.bounds(segment).intersectingChunks().count() == 1, "Segment escaped its one-chunk native start");
badRoute(saved, t -> t.putInt("Schema", 20)); badRoute(saved, t -> t.putString("Type", "SITE"));
badRoute(saved, t -> t.remove("FromId")); badRoute(saved, t -> t.putString("ToId", "station"));
badRoute(saved, t -> t.putString("Id", " ")); badRoute(saved, t -> t.putInt("StartChunkX", -1));
badRoute(saved, t -> t.putDouble("Seed", 42)); badRoute(saved, t -> t.put("Cells", new ListTag()));
badRoute(saved, t -> cell(t).putInt("RailPresent", 1)); badRoute(saved, t -> cell(t).putByte("RailPresent", (byte) 2));
badRoute(saved, t -> cell(t).putString("Rail", "FUTURE")); badRoute(saved, t -> cell(t).putBoolean("RailPresent", true));
badRoute(saved, t -> cell(t).putInt("X", -16)); badRoute(saved, t -> cell(t).putInt("FoundationDepth", 13));
badRoute(saved, t -> cell(t).putInt("FloorY", 2)); badRoute(saved, t -> cell(t).putInt("Extra", 0));
badRoute(saved, t -> { var list = t.getList("Cells").orElseThrow(); list.add(list.getFirst().copy()); });
badRoute(saved, t -> { var list = new ListTag(); list.add(IntTag.valueOf(1)); t.put("Cells", list); });
badRoute(saved, t -> { var list = t.getList("Cells").orElseThrow(); while (list.size() <= 256) list.add(list.getFirst().copy()); });
}
private static CompoundTag cell(CompoundTag tag) { return tag.getList("Cells").orElseThrow().getCompound(0).orElseThrow(); }
private static void badSite(CompoundTag original, Consumer<CompoundTag> mutation) {
var value = original.copy(); mutation.accept(value);
try { HeritagePiece19.decodeSite(value); } catch (IllegalArgumentException expected) { return; }
throw new AssertionError("Malformed saved site accepted: " + value);
}
private static void badRoute(CompoundTag original, Consumer<CompoundTag> mutation) {
var value = original.copy(); mutation.accept(value);
try { HeritagePiece19.decodeSegment(value); } catch (IllegalArgumentException expected) { return; }
throw new AssertionError("Malformed saved route accepted: " + value);
}
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
}
@@ -0,0 +1,197 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.*;
import java.util.ArrayDeque;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.IntBinaryOperator;
/** Pure geography contract: full catalogue, actual destinations, independent entrances and supported axes. */
public final class HeritagePlanner19Smoke {
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
public static void main(String[] args) {
var catalogue = EnumSet.noneOf(Kind.class);
int freight = 0, paths = 0, missingRails = 0, islandAxes = 0;
for (int diameter : new int[] {512, 724, 1024}) for (long seed : new long[] {0, 1, 42, -1}) {
Fixture terrain = new Fixture((x, z) -> 220);
var plan = HeritagePlanner19.plan(seed, 0, 0, diameter, terrain);
verify(plan, terrain);
require(plan.sites().size() >= 8, "Flat starter loses most of its small physical catalogue: " + plan.diagnostics());
require(plan.links().size() >= 2, "Flat starter has no useful inter-destination axes");
plan.sites().forEach(site -> catalogue.add(site.kind()));
freight += (int) plan.links().stream().filter(l -> l.kind() == RouteKind.FREIGHT).count();
islandAxes += (int) plan.links().stream().filter(l -> {
Point first = l.path().getFirst(), last = l.path().getLast();
return Math.max(Math.abs(first.x() - last.x()), Math.abs(first.z() - last.z())) >= diameter / 4;
}).count();
paths += (int) plan.links().stream().filter(l -> l.kind() == RouteKind.PATH).count();
missingRails += (int) plan.segments().stream().flatMap(s -> s.cells().stream())
.filter(c -> c.rail() != Rail.NONE && !c.railPresent()).count();
var repeat = HeritagePlanner19.plan(seed, 0, 0, diameter, new Fixture((x, z) -> 220));
require(plan.sites().equals(repeat.sites()) && plan.links().equals(repeat.links())
&& plan.segments().equals(repeat.segments()) && plan.samples() == repeat.samples()
&& plan.diagnostics().equals(repeat.diagnostics()), "Planning depends on generation order or mutable state");
}
require(catalogue.equals(EnumSet.allOf(Kind.class)), "A requested heritage family never appears: " + catalogue);
require(islandAxes > 0, "All published networks remain local; no axis reaches island scale");
require(freight > 0 && paths > 0 && missingRails > 0, "Both networks and repairable missing rails must be represented");
var flat = HeritagePlanner19.plan(0, 0, 0, 724, new Fixture((x, z) -> 220));
require(!flat.sites().equals(HeritagePlanner19.plan(1, 0, 0, 724, new Fixture((x, z) -> 220)).sites()), "Seed does not affect sites");
Fixture wet = new Fixture((x, z) -> 220); wet.protection = (x, y, z) -> true;
var blocked = HeritagePlanner19.plan(0, 0, 0, 724, wet); verify(blocked, wet);
require(blocked.isEmpty() && blocked.links().isEmpty(), "Protected water or earlier city is overwritten");
Fixture voidTerrain = new Fixture((x, z) -> -1);
var absent = HeritagePlanner19.plan(0, 0, 0, 724, voidTerrain); verify(absent, voidTerrain);
require(absent.isEmpty() && absent.segments().isEmpty(), "Void receives unsupported structures or fictional routes");
Fixture thin = new Fixture((x, z) -> 220); thin.rock = (x, y, z) -> y >= 219 && y <= 220;
var thinPlan = HeritagePlanner19.plan(0, 0, 0, 724, thin); verify(thinPlan, thin);
require(thinPlan.isEmpty(), "Thin shelves receive buildings without anchored footings");
// Every site keeps its local access even when a protected ring rejects every inter-site alignment.
Set<Long> allowed = new HashSet<>();
for (var site : flat.sites()) for (int x = site.minX(); x <= site.maxX(); x++) for (int z = site.minZ(); z <= site.maxZ(); z++) allowed.add(key(x, z));
for (var segment : flat.segments()) if (segment.fromId().equals(segment.toId())) for (var c : segment.cells()) allowed.add(key(c.x(), c.z()));
Fixture isolated = new Fixture((x, z) -> 220); isolated.protection = (x, y, z) -> !allowed.contains(key(x, z));
var islands = HeritagePlanner19.plan(0, 0, 0, 724, isolated); verify(islands, isolated);
require(!islands.sites().isEmpty(), "Independent access fixture has no retained site");
require(islands.sites().stream().anyMatch(site -> islands.links().stream().noneMatch(l -> l.fromId().equals(site.id()) || l.toId().equals(site.id()))),
"The isolated destination witness unexpectedly has every long route");
// Historical landmark feet are read-only and have no mask bypass, even when a route reaches them.
Site station = flat.sites().stream().filter(s -> s.kind() == Kind.FREIGHT_STATION).findFirst().orElseThrow();
Entry historical = new Entry(station.entry().x() + DX[station.facing()] * 30,
221, station.entry().z() + DZ[station.facing()] * 30, (station.facing() + 2) & 3);
Fixture landmark = new Fixture((x, z) -> 220);
landmark.landmarks = List.of(new Landmark("old_mine", LandmarkKind.MINE, historical));
landmark.protection = (x, y, z) -> x == historical.x() && z == historical.z();
var linked = HeritagePlanner19.plan(0, 0, 0, 724, landmark); verify(linked, landmark);
require(!linked.contains(historical.x(), historical.y(), historical.z()), "A historical endpoint receives route painting");
Landmark left = new Landmark("left", LandmarkKind.CITY, new Entry(0, 101, 0, 1));
Landmark right = new Landmark("right", LandmarkKind.MINE, new Entry(1, 101, 0, 3));
Landmark unused = new Landmark("unused", LandmarkKind.UTILITY, new Entry(30, 101, 0, 0));
var dependency = new HeritagePlan19(List.of(), List.of(left, right, unused),
List.of(new Link("existing_access", "left", "right", RouteKind.PATH, List.of(new Point(0, 100, 0), new Point(1, 100, 0)))),
List.of(), 0, 0, Map.of());
for (int z = -1; z <= 1; z++) for (int y = 99; y <= 103; y++) {
require(dependency.protects(0, y, z) && dependency.intersectsProtection(0, y, z, 0, y, z), "Used historical access lost its rock/headroom dependency");
require(!dependency.contains(0, y, z), "Read-only historical access received writing permission");
}
require(!dependency.protects(30, 101, 0), "Unused historical landmark reserves unrelated terrain");
// Smooth slopes exercise certified cut/fill and route grade without allowing an unsupported plateau.
Fixture sloped = new Fixture((x, z) -> 220 + Math.floorDiv(z, 12));
var slopes = HeritagePlanner19.plan(42, 0, 0, 724, sloped); verify(slopes, sloped);
require(slopes.sites().size() >= 5, "A modest slope makes the entire catalogue inadmissible");
Fixture shifted = new Fixture((x, z) -> 220);
var translated = HeritagePlanner19.plan(42, -1024, 512, 724, shifted); verify(translated, shifted);
require(!translated.isEmpty(), "Negative chunk coordinates prevent generation");
require(flat.diagnostics().size() == Kind.values().length && blocked.diagnostics().size() == Kind.values().length,
"Missing kinds have no explicit diagnostic");
expectInvalid(() -> new Link("self", "a", "a", RouteKind.PATH, List.of(new Point(0, 100, 0), new Point(1, 100, 0))));
expectInvalid(() -> new Segment("self", "a", "a", RouteKind.FREIGHT, 0, 0, List.of(new Cell(0, 100, 0, 1, Rail.NONE, false)), 0));
expectInvalid(() -> new Cell(0, 100, 0, 13, Rail.NONE, false));
System.out.println("HeritagePlanner19Smoke passed: fourteen families, deterministic placements, persisted isolated entrances, real route destinations, bounded slopes and supported rails, unique chunk-local cells, indexed protection and budgets");
}
private static void verify(HeritagePlan19 plan, Fixture terrain) {
require(plan.samples() == terrain.samples && plan.samples() <= HeritagePlan19.MAX_SAMPLES, "Probe accounting differs from native calls");
require(plan.columns() == terrain.columns.size() && plan.columns() <= HeritagePlan19.MAX_COLUMNS, "Column budget exceeded");
Map<Long, Cell> cells = new HashMap<>(); Map<String, Entry> destinations = new HashMap<>();
Set<Long> footprints = new HashSet<>();
for (var site : plan.sites()) {
destinations.put(site.id(), site.entry());
require(site.foundationDepth() <= 8 && site.maxY() <= 380, "Architecture exceeds its bounded envelope");
require(Math.abs((site.minX() >> 4) - site.startChunkX()) <= 7 && Math.abs((site.maxX() >> 4) - site.startChunkX()) <= 7
&& Math.abs((site.minZ() >> 4) - site.startChunkZ()) <= 7 && Math.abs((site.maxZ() >> 4) - site.startChunkZ()) <= 7, "Site exceeds native reference radius");
for (int z = site.minZ(); z <= site.maxZ(); z++) for (int x = site.minX(); x <= site.maxX(); x++) {
require(footprints.add(key(x, z)), "Two buildings overlap");
require(terrain.height.applyAsInt(x, z) - site.floorY() <= HeritagePlanner19.MAX_SITE_CUT, "Site cuts an excessive mound");
for (int y = site.minY(); y <= site.maxY(); y++) require(!terrain.protection.test(x, y, z), "Architecture intersects protected water/old structure");
require(plan.contains(x, site.floorY(), z) && plan.protects(x, site.maxY(), z)
&& plan.intersectsProtection(x, site.maxY(), z, x, site.maxY(), z), "Site is absent from indexed protection");
}
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 rock = support.isPresent() ? support.get().minY() - 1 : site.minY() - 1;
require(terrain.rock.test(x, rock, z) && terrain.rock.test(x, rock - 1, z), "Site has no two-block rock bearing");
}
for (var support : site.supports()) {
require(support.maxY() - support.minY() < 12 && support.maxY() == site.minY() - 1, "Unbounded/disconnected architectural pier");
require(plan.protects(support.x(), support.minY() - 2, support.z()), "Pier bearing exposed to decoration");
for (int y = support.minY(); y <= support.maxY(); y++) require(!terrain.protection.test(support.x(), y, support.z()), "Pier overwrites protected terrain");
}
}
plan.landmarks().forEach(l -> destinations.put(l.id(), l.entry()));
for (var segment : plan.segments()) {
require(destinations.containsKey(segment.fromId()) && destinations.containsKey(segment.toId()), "Segment has a fictional destination");
for (Cell cell : segment.cells()) {
require(cells.put(key(cell.x(), cell.z()), cell) == null, "Two segments write the same column");
require(!footprints.contains(key(cell.x(), cell.z())), "Route crosses a building envelope");
require((cell.x() >> 4) == segment.startChunkX() && (cell.z() >> 4) == segment.startChunkZ(), "Route piece is not chunk-local");
require(terrain.rock.test(cell.x(), cell.minY() - 1, cell.z()), "Route ballast floats without bearing");
require(terrain.height.applyAsInt(cell.x(), cell.z()) - cell.floorY() <= HeritagePlanner19.MAX_ROUTE_CUT, "Route makes excessive terrain cuts");
for (int y = cell.minY(); y <= cell.maxY(); y++) require(!terrain.protection.test(cell.x(), y, cell.z()), "Route overwrites protected water/old structure");
require(plan.contains(cell.x(), cell.floorY(), cell.z()) && plan.protects(cell.x(), cell.minY() - 1, cell.z())
&& plan.intersectsProtection(cell.x(), cell.minY() - 1, cell.z(), cell.x(), cell.minY() - 1, cell.z()), "Route/ballast missing from protection index");
require(!cell.railPresent() || cell.rail() != Rail.NONE, "Present rail has no saved geometry");
}
}
for (var site : plan.sites()) {
Entry e = site.entry();
for (int cross = -1; cross <= 1; cross++) {
Cell cell = cells.get(key(e.x() + DZ[e.facing()] * cross, e.z() - DX[e.facing()] * cross));
require(cell != null && cell.floorY() == e.y() - 1, "Unlinked entrance has no independently painted three-wide landing");
}
// A five-row access profile must meet natural terrain; never stop on an isolated retaining ledge.
var reached = new HashSet<Long>(); var queue = new ArrayDeque<Long>();
queue.add(key(e.x(), e.z())); reached.add(key(e.x(), e.z())); boolean meetsGround = false;
while (!queue.isEmpty()) {
long key = queue.remove(); Cell c = cells.get(key);
if (Math.abs(c.x() - e.x()) + Math.abs(c.z() - e.z()) >= 4
&& Math.abs(c.floorY() - terrain.height.applyAsInt(c.x(), c.z())) <= 1) meetsGround = true;
for (int d = 0; d < 4; d++) {
long next = key(c.x() + DX[d], c.z() + DZ[d]); Cell n = cells.get(next);
if (n != null && Math.abs(n.floorY() - c.floorY()) <= 1 && reached.add(next)) queue.add(next);
}
}
require(meetsGround, "An entrance platform has no traversable return to the sampled terrain");
}
for (var link : plan.links()) {
Entry from = destinations.get(link.fromId()), to = destinations.get(link.toId());
require(link.path().getFirst().equals(new Point(from.x(), from.y() - 1, from.z()))
&& link.path().getLast().equals(new Point(to.x(), to.y() - 1, to.z())), "Route does not actually join its promised entrances");
for (int i = 0; i < link.path().size(); i++) {
Point p = link.path().get(i); Cell c = cells.get(key(p.x(), p.z()));
boolean historical = plan.landmarks().stream().anyMatch(l -> l.entry().x() == p.x() && l.entry().z() == p.z() && l.entry().y() == p.floorY() + 1);
require(c != null && c.floorY() == p.floorY() || historical, "A complete route contains an unpainted gap");
if (i > 0) {
Point a = link.path().get(i - 1);
require(Math.abs(a.x() - p.x()) + Math.abs(a.z() - p.z()) == 1 && Math.abs(a.floorY() - p.floorY()) <= 1, "Route has a jump or excessive grade");
}
}
}
require(!plan.protects(100_000, 200, 100_000) && !plan.intersectsProtection(100_000, 0, 100_000, 100_002, 383, 100_002), "Protection escaped accepted constructions");
}
@FunctionalInterface private interface Volume { boolean test(int x, int y, int z); }
private static final class Fixture implements HeritagePlanner19.Terrain {
final IntBinaryOperator height; final Set<Long> columns = new HashSet<>(); int samples;
Volume protection = (x, y, z) -> false;
Volume rock; List<Landmark> landmarks = List.of();
Fixture(IntBinaryOperator height) { this.height = height; rock = (x, y, z) -> y >= 8 && y <= height.applyAsInt(x, z); }
public boolean owns(int x, int z) { return true; }
public int surfaceY(int x, int z) { samples++; columns.add(key(x, z)); return height.applyAsInt(x, z); }
public boolean solid(int x, int y, int z) { samples++; return rock.test(x, y, z); }
public boolean protectedAt(int x, int y, int z) { samples++; return protection.test(x, y, z); }
public List<Landmark> landmarks() { return landmarks; }
}
private static void expectInvalid(Runnable action) { boolean rejected = false; try { action.run(); } catch (IllegalArgumentException expected) { rejected = true; } require(rejected, "Invalid saved geometry was accepted"); }
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
}
@@ -0,0 +1,328 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.EmptyBlockGetter;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.DoorBlock;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.TrapDoorBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BedPart;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
import net.minecraft.world.level.block.state.properties.Half;
import net.minecraft.world.level.storage.loot.LootTable;
import net.minecraft.world.phys.AABB;
/** Native shapes and final geometry: distinguish real rooms, reachable secrets and supported routes. */
public final class HeritageRenderer19Smoke {
private HeritageRenderer19Smoke() {}
private static final Direction[] DIRECTIONS = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
public static void main(String[] args) {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); run();
System.out.println("HeritageRenderer19Smoke passed: fourteen distinct architectures, four rotations, actual doors/bed/seat/light supports, every target and secret reachable, contained water, immutable loot and shaped abandoned routes");
}
public static void run() {
Set<Set<BlockPos>> silhouettes = new HashSet<>();
for (var kind : HeritagePlan19.Kind.values()) for (long seed : new long[] {0, 43}) {
var canonical = render(site(kind, 0, seed));
if (seed == 0) {
Set<BlockPos> shape = new HashSet<>(); canonical.blocks.forEach((p, b) -> { if (p.getY() > 0 && !b.isAir()) shape.add(p); });
require(silhouettes.add(shape), kind + " only repaints another catalogue building");
}
for (int facing = 0; facing < 4; facing++) {
var c = facing == 0 ? canonical : render(site(kind, facing, seed));
var repeat = render(c.site);
require(c.blocks.equals(repeat.blocks) && c.loot.equals(repeat.loot), label(c) + " is not deterministic");
rotate(canonical, c);
var targets = HeritageRenderer19.targets(c.site);
var reached = walk(c, at(targets.getFirst()));
for (var target : targets) require(reached.contains(at(target)), label(c) + " cannot reach " + target.name() + " at " + at(target));
require(targets.stream().anyMatch(t -> t.name().equals("secondary_entry")), "Missing secondary physical approach");
var secondary = targets.stream().filter(t -> t.name().equals("secondary_entry")).findFirst().orElseThrow();
Set<BlockPos> returnPath = walk(c, at(secondary));
require(returnPath.contains(at(targets.getFirst())), label(c) + " service exit cannot return through the certified courtyard");
require(c.loot.size() == 1, label(c) + " must have one shared, deferred discovery chest");
var chest = c.loot.entrySet().iterator().next();
require(chest.getValue().table().identifier().toString().equals("sanctuary:chests/heritage19/" + kind.name().toLowerCase(java.util.Locale.ROOT)), "Loot table is not generation19-specific");
require(approachable(chest.getKey(), reached), label(c) + " hidden chest cannot be opened from a reachable standing cell");
for (var t : targets) if (t.name().equals("loot_approach")) require(at(t).distManhattan(chest.getKey()) == 1, "Secret witness is not next to the actual chest");
furnishings(c, reached);
refugeShaft(c, reached);
railConnection(c);
fluids(c);
require(c.blocks.values().stream().noneMatch(b -> b.is(Blocks.SPAWNER) || b.is(Blocks.LAVA)), "Heritage added an unrequested spawner or lava");
identity(c);
}
}
require(silhouettes.size() == 14, "Catalogue does not contain fourteen physical architectures");
routes(); chunkPartition();
}
private static HeritagePlan19.Site site(HeritagePlan19.Kind kind, int facing, long seed) {
int height = switch (kind) {
case COMMUNAL_DEPOT, VOID_DOCK, CONSTRUCTION_REFUGE -> 14;
case EXPEDITION_OFFICE, IMPOSSIBLE_SERVICE -> 12;
case OBSERVATORY -> 20; case PUMP_STATION -> 18; case RADIO_RELAY -> 22;
default -> 10;
};
int x = -43, y = 180, z = 59;
var entry = new HeritagePlan19.Entry(facing == 1 ? x + 17 : facing == 3 ? x - 1 : x + 8, y + 1,
facing == 2 ? z + 17 : facing == 0 ? z - 1 : z + 8, facing);
return new HeritagePlan19.Site("test", kind, x, y, z, 17, 17, height, facing, seed, 3,
List.of(new HeritagePlan19.Support(x, y - 7, z, y - 4)), entry);
}
private static String label(Capture c) { return c.site.kind() + " facing=" + c.site.facing() + " seed=" + c.site.seed(); }
private static Capture render(HeritagePlan19.Site site) { var c = new Capture(site); HeritageRenderer19.render(site, c); return c; }
private static BlockPos at(HeritageRenderer19.Target t) { return new BlockPos(t.x(), t.y(), t.z()); }
private static BlockPos rotate(BlockPos p, int facing) {
return switch (facing) { case 1 -> new BlockPos(16 - p.getZ(), p.getY(), p.getX());
case 2 -> new BlockPos(16 - p.getX(), p.getY(), 16 - p.getZ());
case 3 -> new BlockPos(p.getZ(), p.getY(), 16 - p.getX()); default -> p; };
}
private static void rotate(Capture base, Capture other) {
int facing = other.site.facing();
Rotation rotation = switch (facing) { case 1 -> Rotation.CLOCKWISE_90; case 2 -> Rotation.CLOCKWISE_180;
case 3 -> Rotation.COUNTERCLOCKWISE_90; default -> Rotation.NONE; };
for (var e : base.blocks.entrySet()) {
// Saved footings are already in world coordinates; they do not rotate with the facade.
if (e.getKey().getY() < -base.site.foundationDepth() || base.loot.containsKey(e.getKey())) continue;
require(other.block(rotate(e.getKey(), facing)).equals(e.getValue().rotate(rotation)), label(other) + " rotates shape and block state differently at " + e.getKey());
}
for (var e : base.loot.entrySet()) require(e.getValue().equals(other.loot.get(rotate(e.getKey(), facing))), "Rotating a site changes its deferred treasure");
}
private static void furnishings(Capture c, Set<BlockPos> reached) {
for (var e : c.blocks.entrySet()) {
var p = e.getKey(); var b = e.getValue();
if (b.getBlock() instanceof DoorBlock) {
var half = b.getValue(BlockStateProperties.DOUBLE_BLOCK_HALF);
var partner = half == DoubleBlockHalf.LOWER ? p.above() : p.below();
require(c.block(partner).equals(b.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF,
half == DoubleBlockHalf.LOWER ? DoubleBlockHalf.UPPER : DoubleBlockHalf.LOWER)), label(c) + " has a truncated door at " + p);
if (half == DoubleBlockHalf.LOWER) {
require(full(c.block(p.below()), p.below()), "Door has no full threshold");
Direction normal = b.getValue(BlockStateProperties.HORIZONTAL_FACING);
require(reached.contains(p.relative(normal)) && reached.contains(p.relative(normal.getOpposite())),
label(c) + " door does not connect two usable sides at " + p);
}
}
if (b.is(Blocks.LADDER)) require(full(c.block(p.relative(b.getValue(BlockStateProperties.HORIZONTAL_FACING).getOpposite())), p),
label(c) + " floating ladder at " + p);
if (b.is(Blocks.LANTERN)) require(b.getValue(BlockStateProperties.HANGING) && full(c.block(p.above()), p.above()),
label(c) + " lantern does not hang from a complete ceiling at " + p);
if (b.is(Blocks.LEVER) || b.is(Blocks.GRINDSTONE)) require(full(c.block(p.below()), p.below()), "Usable fixture has no full support");
if (b.is(Blocks.DARK_OAK_STAIRS)) {
var front = p.relative(b.getValue(BlockStateProperties.HORIZONTAL_FACING).getOpposite());
require(reached.contains(front), label(c) + " seat faces an obstruction at " + p);
}
if (b.hasProperty(BlockStateProperties.BED_PART)) {
Direction direction = b.getValue(BlockStateProperties.HORIZONTAL_FACING);
boolean head = b.getValue(BlockStateProperties.BED_PART) == BedPart.HEAD;
var partner = p.relative(head ? direction.getOpposite() : direction);
require(c.block(partner).equals(b.setValue(BlockStateProperties.BED_PART, head ? BedPart.FOOT : BedPart.HEAD)), "Bed halves disagree");
if (head) {
for (int y = 0; y < 3; y++) require(full(c.block(p.relative(direction).above(y)), p), "Bed head is not against its real wall");
boolean side = false;
for (Direction d : new Direction[] {direction.getClockWise(), direction.getCounterClockWise()})
side |= reached.contains(p.relative(d)) && reached.contains(partner.relative(d));
require(side, "Bed has no reachable full lateral approach");
}
}
if (b.is(Blocks.CRAFTING_TABLE) || b.is(Blocks.CARTOGRAPHY_TABLE) || b.is(Blocks.FURNACE)
|| b.is(Blocks.GRINDSTONE) || b.is(Blocks.LECTERN))
require(approachable(p, reached), label(c) + " functional furnishing cannot be approached at " + p);
}
}
private static boolean full(BlockState b, BlockPos p) {
return b.getFluidState().isEmpty() && b.getCollisionShape(EmptyBlockGetter.INSTANCE, p).toAabbs().stream()
.anyMatch(a -> a.minX == 0 && a.minY == 0 && a.minZ == 0 && a.maxX == 1 && a.maxY == 1 && a.maxZ == 1);
}
private static boolean approachable(BlockPos p, Set<BlockPos> reached) {
for (var d : DIRECTIONS) if (reached.contains(p.relative(d))) return true;
return false;
}
private static boolean clear(Capture c, BlockPos p) {
var body = new AABB(p.getX() + .2, p.getY(), p.getZ() + .2, p.getX() + .8, p.getY() + 1.8, p.getZ() + .8);
for (var at : new BlockPos[] {p, p.above()}) {
var b = opened(c.block(at)); if (!b.getFluidState().isEmpty()) return false;
for (var shape : b.getCollisionShape(EmptyBlockGetter.INSTANCE, at).toAabbs())
if (shape.move(at.getX(), at.getY(), at.getZ()).intersects(body)) return false;
}
return true;
}
private static boolean stand(Capture c, BlockPos p) {
if (p.getX() < 0 || p.getX() > 16 || p.getZ() < 0 || p.getZ() > 16
|| p.getY() < -c.site.foundationDepth() + 1 || p.getY() >= c.site.height() || !clear(c, p)) return false;
if (climb(c.block(p)) || climb(c.block(p.below()))) return true;
return opened(c.block(p.below())).getCollisionShape(EmptyBlockGetter.INSTANCE, p.below()).toAabbs().stream()
.anyMatch(a -> a.maxY >= .5 && a.minX <= .5 && a.maxX >= .5 && a.minZ <= .5 && a.maxZ >= .5);
}
private static BlockState opened(BlockState state) {
return state.getBlock() instanceof DoorBlock || state.getBlock() instanceof TrapDoorBlock
? state.setValue(BlockStateProperties.OPEN, true) : state;
}
private static void refugeShaft(Capture c, Set<BlockPos> reached) {
if (c.site.kind() != HeritagePlan19.Kind.CONSTRUCTION_REFUGE) return;
var hatch = rotate(new BlockPos(10, 5, 11), c.site.facing());
require(c.block(hatch).is(Blocks.DARK_OAK_TRAPDOOR)
&& c.block(hatch).getValue(BlockStateProperties.HALF) == Half.BOTTOM,
label(c) + " hatch is not at the climbable upper lip");
var direction = c.block(hatch).getValue(BlockStateProperties.HORIZONTAL_FACING);
require(stand(c, hatch) && reached.contains(hatch), label(c) + " open hatch has no reachable climbable support");
for (int y = 1; y <= 4; y++) {
var rung = new BlockPos(hatch.getX(), y, hatch.getZ());
require(c.block(rung).is(Blocks.LADDER)
&& c.block(rung).getValue(BlockStateProperties.HORIZONTAL_FACING) == direction,
label(c) + " hatch descent loses a matching ladder at " + rung);
require(stand(c, rung) && reached.contains(rung) && clear(c, rung.above()),
label(c) + " hatch descent cannot traverse vertically at " + rung);
}
}
private static boolean climb(BlockState b) { return b.is(Blocks.LADDER); }
private static Set<BlockPos> walk(Capture c, BlockPos start) {
require(stand(c, start), label(c) + " starts without a standing surface at " + start);
Set<BlockPos> seen = new HashSet<>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
while (!queue.isEmpty()) {
var p = queue.removeFirst();
for (var d : DIRECTIONS) for (int dy : new int[] {0, 1, -1}) {
var q = p.relative(d).offset(0, dy, 0);
if (dy > 0 && !clear(c, p.above())) continue;
if (stand(c, q) && seen.add(q)) queue.add(q);
}
for (int dy : new int[] {-1, 1}) {
var q = p.offset(0, dy, 0);
if ((climb(c.block(p)) || climb(c.block(q)) || climb(c.block(p.below()))) && stand(c, q) && seen.add(q)) queue.add(q);
}
}
return seen;
}
private static void fluids(Capture c) {
Set<BlockPos> water = new HashSet<>(); c.blocks.forEach((p, b) -> { if (!b.getFluidState().isEmpty()) water.add(p); });
int expected = c.site.kind() == HeritagePlan19.Kind.PUMP_STATION ? 60 : c.site.kind() == HeritagePlan19.Kind.GREENHOUSE ? 2 : 0;
require(water.size() == expected, label(c) + " has unexpected water quantity: " + water.size());
for (var p : water) {
require(c.block(p).is(Blocks.WATER), "Only explicitly contained water is allowed");
for (var d : new Direction[] {Direction.DOWN, Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}) {
var q = p.relative(d);
require(water.contains(q) || !c.block(q).isAir(), label(c) + " lets tank/irrigation water escape into air at " + q);
}
}
}
private static void identity(Capture c) {
switch (c.site.kind()) {
case FREIGHT_STATION -> { has(c, Blocks.RAIL); has(c, Blocks.LEVER); }
case POST_OFFICE -> { has(c, Blocks.BARREL); has(c, Blocks.CARTOGRAPHY_TABLE); }
case COMMUNAL_DEPOT -> { has(c, Blocks.BARREL); has(c, Blocks.LECTERN); }
case EXPEDITION_OFFICE -> { has(c, Blocks.CARTOGRAPHY_TABLE); has(c, Blocks.BOOKSHELF); }
case OBSERVATORY -> { has(c, Blocks.CARTOGRAPHY_TABLE); has(c, Blocks.IRON_BARS); }
case RAIL_WORKSHOP -> { has(c, Blocks.GRINDSTONE); has(c, Blocks.RAIL); has(c, Blocks.FURNACE); }
case PUMP_STATION -> { has(c, Blocks.DISPENSER); has(c, Blocks.WATER); }
case SUBSTATION -> { has(c, Blocks.LEVER); has(c, Blocks.IRON_BARS); }
case GREENHOUSE -> { has(c, Blocks.WHEAT); has(c, Blocks.FARMLAND); has(c, Blocks.COMPOSTER); }
case VOID_DOCK -> { has(c, Blocks.IRON_CHAIN); has(c, Blocks.STRIPPED_OAK_LOG); }
case RADIO_RELAY -> { has(c, Blocks.BED.white()); has(c, Blocks.IRON_BARS); }
case PHOTO_CLUB -> { has(c, Blocks.WATER_CAULDRON); has(c, Blocks.IRON_CHAIN); }
case IMPOSSIBLE_SERVICE -> { has(c, Blocks.CALCITE); has(c, Blocks.WATER_CAULDRON); }
case CONSTRUCTION_REFUGE -> { has(c, Blocks.BED.white()); has(c, Blocks.LADDER); has(c, Blocks.DARK_OAK_TRAPDOOR); }
}
}
private static void has(Capture c, Block b) { require(c.blocks.values().stream().anyMatch(v -> v.is(b)), label(c) + " loses its " + b); }
private static void railConnection(Capture c) {
BlockPos destination = switch (c.site.kind()) {
case FREIGHT_STATION -> new BlockPos(2, 1, 8);
case RAIL_WORKSHOP -> new BlockPos(5, 1, 7);
case COMMUNAL_DEPOT -> new BlockPos(8, 1, 7);
default -> null;
};
if (destination == null) return;
var start = rotate(new BlockPos(8, 1, 0), c.site.facing());
destination = rotate(destination, c.site.facing());
Set<BlockPos> seen = new HashSet<>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
while (!queue.isEmpty()) {
var p = queue.removeFirst(); var b = c.block(p);
require(b.is(Blocks.RAIL), label(c) + " freight entry has no supported rail");
for (var d : railEnds(b)) {
var next = p.relative(d); var state = c.block(next);
if (state.is(Blocks.RAIL) && railEnds(state).contains(d.getOpposite()) && seen.add(next)) queue.add(next);
}
}
require(seen.contains(destination), label(c) + " interior rail does not connect to its real external freight entry");
}
private static List<Direction> railEnds(BlockState b) {
return switch (b.getValue(BlockStateProperties.RAIL_SHAPE)) {
case NORTH_SOUTH, ASCENDING_NORTH, ASCENDING_SOUTH -> List.of(Direction.NORTH, Direction.SOUTH);
case EAST_WEST, ASCENDING_EAST, ASCENDING_WEST -> List.of(Direction.EAST, Direction.WEST);
case SOUTH_EAST -> List.of(Direction.SOUTH, Direction.EAST);
case SOUTH_WEST -> List.of(Direction.SOUTH, Direction.WEST);
case NORTH_WEST -> List.of(Direction.NORTH, Direction.WEST);
case NORTH_EAST -> List.of(Direction.NORTH, Direction.EAST);
};
}
private static void routes() {
var cells = new ArrayList<HeritagePlan19.Cell>();
int x = -15;
for (var rail : HeritagePlan19.Rail.values()) {
cells.add(new HeritagePlan19.Cell(x++, 160, 2, 2, rail, rail != HeritagePlan19.Rail.NONE));
}
cells.add(new HeritagePlan19.Cell(-1, 161, 2, 3, HeritagePlan19.Rail.ASCENDING_EAST, false));
var segment = new HeritagePlan19.Segment("route", "a", "b", HeritagePlan19.RouteKind.FREIGHT, -1, 0, cells, 7);
Map<BlockPos, BlockState> result = new HashMap<>();
HeritageRenderer19.renderSegment(segment, new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState b) {
var p = new BlockPos(segment.minX() + x, segment.minY() + y, segment.minZ() + z);
var cell = cells.stream().filter(c -> c.x() == p.getX() && c.z() == p.getZ()).findFirst().orElseThrow();
require(p.getY() >= cell.minY() && p.getY() <= cell.maxY(), "Route escaped its saved column mask"); result.put(p, b);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { throw new AssertionError("A route generated treasure"); }
});
for (var c : cells) {
var p = new BlockPos(c.x(), c.floorY() + 1, c.z());
require(full(result.get(p.below()), p.below()), "Rail or missing-rail witness lost its ballast support");
if (c.railPresent()) require(result.get(p).is(Blocks.RAIL)
&& result.get(p).getValue(BlockStateProperties.RAIL_SHAPE).name().equals(c.rail().name()), "Saved rail shape was replaced by a straight track");
else require(result.get(p).isAir(), "An intentional abandoned-track gap was repaired by rendering");
}
}
private static void chunkPartition() {
var s = site(HeritagePlan19.Kind.CONSTRUCTION_REFUGE, 3, 43); var expected = render(s); var parts = new Capture(s);
for (int cz = s.maxZ() >> 4; cz >= s.minZ() >> 4; cz--) for (int cx = s.maxX() >> 4; cx >= s.minX() >> 4; cx--) {
final int chunkX = cx, chunkZ = cz;
HeritageRenderer19.render(s, new LostCityRenderer.BlockWriter() {
boolean owns(int x, int z) { return ((s.minX() + x) >> 4) == chunkX && ((s.minZ() + z) >> 4) == chunkZ; }
@Override public void set(int x, int y, int z, BlockState state) { if (owns(x, z)) parts.set(x, y, z, state); }
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { if (owns(x, z)) parts.chest(x, y, z, table, seed); }
});
}
require(parts.blocks.equals(expected.blocks) && parts.loot.equals(expected.loot), "Native chunk order changes a site");
}
private record Loot(ResourceKey<LootTable> table, long seed) {}
private static final class Capture implements LostCityRenderer.BlockWriter {
final HeritagePlan19.Site site;
final Map<BlockPos, BlockState> blocks = new HashMap<>();
final Map<BlockPos, Loot> loot = new HashMap<>();
Capture(HeritagePlan19.Site site) { this.site = site; }
@Override public void set(int x, int y, int z, BlockState state) {
int wy = y + site.floorY();
boolean main = x >= 0 && x <= 16 && z >= 0 && z <= 16 && wy >= site.minY() && wy <= site.maxY();
boolean footing = site.supports().stream().anyMatch(s -> s.x() == x + site.minX() && s.z() == z + site.minZ() && wy >= s.minY() && wy <= s.maxY());
require(main || footing, label(this) + " writes outside its exact body and footing mask"); blocks.put(new BlockPos(x, y, z), state);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
require(loot.put(new BlockPos(x, y, z), new Loot(table, seed)) == null, "Duplicate shared treasure"); set(x, y, z, Blocks.CHEST.defaultBlockState());
}
BlockState block(BlockPos p) { return blocks.getOrDefault(p, Blocks.AIR.defaultBlockState()); }
}
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
}
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "0.1.0-alpha.17"
version = "0.1.0-alpha.19"
pack-format = "packwiz:1.1.0"
[index]