Compare commits

...
Author SHA1 Message Date
koka f2da922bad Document structure ideas and abandoned island routes
Build Sanctuary / build (push) Canceled after 0s
2026-09-09 20:03:42 +02:00
koka c049e0f2ce Record alpha.17 publication and preserved Prism instance
Build Sanctuary / build (push) Canceled after 0s
2026-09-09 19:16:07 +02:00
koka 0969ed52da Add alpha.17 experimental forgotten structure networks
Build Sanctuary / build (push) Canceled after 0s
2026-09-09 19:14:09 +02:00
39 changed files with 2442 additions and 48 deletions
+21
View File
@@ -1,5 +1,26 @@
# Changelog
## 0.1.0-alpha.17 — 2026-09-09
- Prototype de trois réseaux oubliés sur l’île initiale : mines et ateliers,
salles étranges, infrastructures abandonnées. Accès depuis la surface,
couloirs et embranchements, outils vanilla utilisables, mobilier et traces de
chantier. Placement borné, hors ville et eaux protégées, avec appuis vérifiés.
- Option FR/EN « Structures expérimentales » dans Personnaliser, enregistrée
avec la taille du nouveau monde. Les connexions entre réseaux, le réacteur,
la progression et les reliques ne sont pas encore implémentés.
- Codec `sanctuary:island_v17`, pièces natives indépendantes, journal 17.
Les mondes 16 et antérieurs conservent leurs plans et futurs chunks.
Loption ne supprime ni najoute rétroactivement des structures.
`check build assemblePack` passe. Création et réouverture natives : 15 pièces,
7 639 blocs contrôlés, 49 cibles accessibles, 12 appuis et 3 coffres conservés.
Le cas désactivé et la sauvegarde des trois tailles passent. La génération 16
conserve les empreintes natives et le journal des témoins publiés. Artefacts
vérifiés, release et canal packwiz publiés. Même instance Prism synchronisée
deux fois, 767 fichiers personnels et réglages suivis conservés. Voir
[le protocole et les coordonnées](docs/testing-alpha17.md).
## 0.1.0-alpha.16 — 2026-09-09
- Ville compacte obligatoire sur la nouvelle île initiale, structures activées.
+33 -5
View File
@@ -15,9 +15,37 @@ 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.16 est publiée** : une ville sur l’île initiale, de grandes
piscines, des parkings profonds et des égouts ouverts. Le canal packwiz et la
même instance Prism sont à jour ; voir [Distribution](docs/packwiz.md).
décrits dans la vision ne sont pas encore implémentés. **Lalpha.17 est publiée** :
trois familles de réseaux oubliés expérimentaux rejoignent la ville et ses grands
sous-sols sur les nouvelles îles de départ. Le canal packwiz et la même instance
Prism sont à jour ; voir [Distribution](docs/packwiz.md).
## Alpha.17 — prototype des réseaux oubliés
La nouvelle génération expérimentale recherche trois petits réseaux sur l’île
initiale : mines et ateliers, salles étranges, installations techniques
abandonnées. Chaque réseau possède une entrée de surface et des salles reliées,
avec mobilier, outils vanilla et traces de travaux interrompus. Les continents
conservent leur génération urbaine sans recevoir ces réseaux supplémentaires.
Dans **Personnaliser**, loption **Structures expérimentales** accompagne les
trois tailles habituelles. Elle est enregistrée avec chaque nouveau monde ;
elle permet de comparer un monde avec les nouveaux lieux et un autre avec le
socle urbain seul. La présence des réseaux dépend des emplacements admissibles
trouvés dans un budget borné. Les connexions entre familles restent à construire.
Le prototype demande un **nouveau monde**. Les mondes 16 et antérieurs gardent
leur génération. Revenir au binaire 16 ne permet pas douvrir un monde 17 : les
mondes dessai restent séparés. Voir [le contrat alpha.17](docs/testing-alpha17.md).
Le réacteur, la progression et les reliques restent prévus pour la suite.
`check build assemblePack`, création/réouverture et option désactivée passent.
Les empreintes natives de la génération 16 restent identiques aux témoins publiés.
La [release alpha.17](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.17)
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. Pour voir les
trois réseaux, créer un monde **Moyen, graine 0**, option activée ;
`/locate structure sanctuary:forgotten_networks` indique une entrée de surface.
## Alpha.16 — Sanctuary habitée avant nous
@@ -381,7 +409,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.16 |
| Sanctuary / pack | 0.1.0-alpha.17 |
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
@@ -410,7 +438,7 @@ décrits dans [Validation](docs/testing.md).
Résultats :
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.16.jar` : mod à installer avec
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.17.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).
+49
View File
@@ -668,6 +668,55 @@ Release et canal packwiz publiés ; même instance Prism synchronisée deux fois
avec les 767 fichiers personnels et réglages suivis conservés. Voir [le reçu](packwiz.md).
Les choix des futurs réseaux et donjons sont consignés dans la vision.
### WG-20 — Réseaux oubliés, prototype réversible — alpha.17
**Branche :** `codex/structures-experiment-alpha17`.
**Contrat :** rendre explorables les trois familles validées : mines et ateliers,
lieux étranges physiques dans l’île, infrastructures abandonnées. Les traces
privilégient des installations utilisables et des chantiers interrompus.
- Nouveau générateur 17 et option « Structures expérimentales » dans le bouton
natif Personnaliser. La taille et loption sont enregistrées avec le nouveau
monde ; aucune activation rétroactive ou suppression de structures existantes.
- Conserver les codecs, pièces, plans et futurs chunks des générations publiées,
dont 16. La ville 16 reste le socle de 17 ; les nouveaux réseaux disposent de
leurs pièces natives, emprises protégées et calculs bornés propres.
- Trois petits réseaux recherchés sur l’île initiale, avec accès de surface,
salles, galeries et mobilier. Les continents ne reçoivent pas ce prototype.
Les connexions entre réseaux seront évaluées après les premières implantations.
- Ne pas implémenter de réacteur, progression, reliques ou règles de récompenses
personnelles. Les coffres éventuels utilisent des tables vanilla différées.
- Tester terrain/supports, connexions internes, accès, réglage sauvegardé,
placement natif et réouverture. Livrer le pack et synchroniser linstance
existante selon le protocole packwiz. Donner graines et coordonnées dessai.
**État : livré en alpha.17.** Création/réouverture, option désactivée,
compatibilité 16 et `check build assemblePack` passent. Release et canal packwiz
publiés, même instance Prism synchronisée deux fois ; les 767 fichiers personnels
et réglages suivis sont conservés. Voir [le reçu](packwiz.md) et
[les coordonnées dessai](testing-alpha17.md).
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.
## 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.
+41 -1
View File
@@ -13,10 +13,50 @@ 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.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
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 :
```text
9ab7a9f03a46bc940d6bc014de85c6935eb5ba21f11ceea9b1222a9dae1faaec
```
Loption **Structures expérimentales** de **Personnaliser** ajoute trois familles
de petits réseaux sur les nouvelles îles initiales : mines et ateliers, salles
étranges, installations abandonnées. Le choix est enregistré avec la taille.
Le témoin **Moyen, graine 0**, option activée, possède trois réseaux accessibles
depuis la surface. Voir [les preuves et coordonnées](testing-alpha17.md).
`check build assemblePack`, les huit GameTests et les trois smokes dédiés passent.
Création et réouverture natives conservent 15 pièces, 3 coffres, 12 appuis et le
bloc témoin ; 49 cibles sont accessibles. Le cas désactivé passe. La génération 16
conserve les empreintes de ses structures et de son journal publiés.
Deux synchronisations isolées puis deux dans la même instance Prism Mac
réussissent. Un seul JAR Sanctuary alpha.17 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 16 et le suivi packwiz sont sauvegardés dans
`sanctuary-backups/before-0.1.0-alpha.17/`. Reçus locaux ignorés :
`build/alpha17-isolated-validation.json` et `build/alpha17-prism-validation.json`.
Créer un **nouveau monde Sanctuary** pour cet essai. Loption désactivée permet
un autre nouveau monde sans les réseaux supplémentaires ; elle ne retire pas
de blocs dun monde existant. Les anciennes sauvegardes gardent leur génération.
Ne pas ouvrir une sauvegarde 17 avec le binaire 16. Les connexions entre réseaux,
le réacteur, les reliques et la progression restent à construire. Aucun essai
Windows natif ni revue visuelle dans le client na été effectué.
## Alpha.16 publiée — 9 septembre 2026
[Sanctuary 0.1.0-alpha.16](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.16)
est publiée depuis le commit source `98fc13c`. Le canal stable utilise cette
est publiée depuis le commit source `98fc13c`. Le canal stable utilisait alors cette
version, au commit `927c862`. JAR, [MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.16/Sanctuary-0.1.0-alpha.16.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.
+99
View File
@@ -0,0 +1,99 @@
# Alpha.17 — réseaux oubliés, prototype réversible
**État : publié et installé dans la même instance Prism.** Ticket WG-20, branche
`codex/structures-experiment-alpha17`. Minecraft 26.3-pre-2, mêmes dépendances
que lalpha.16. Cet essai porte sur les lieux et leur génération.
## Contrat
Créer un **nouveau monde Sanctuary**, puis **Personnaliser** : Petit 512,
Moyen 724 par défaut, Grand 1 024 et **Structures expérimentales** (activées par
défaut). Le choix est enregistré dans le générateur 17, avec la taille. Désactiver
loption pour un autre nouveau monde permet de comparer avec le socle urbain 16.
Ce bouton ne supprime ni ne rajoute de structures dans une sauvegarde existante.
Les anciens mondes gardent leurs codecs, rendus, pièces et futurs chunks. Le
preset 16 historique est conservé à lidentique et caché à la création. Le 17
possède son propre journal ; aucune conversion ni rétrogradation de sauvegarde
nest proposée. Le retour au binaire 16 concerne seulement les mondes 16 et
antérieurs, jamais un monde 17.
Les nouvelles structures sont recherchées sur l’île initiale uniquement, hors
ville et eaux protégées. Trois petits réseaux distincts : mines et ateliers,
salles étranges, installations techniques abandonnées. Salles et couloirs se
raccordent à des accès de surface. Les fondations reposent sur le terrain ou sur
des appuis courts certifiés. La présence dépend des emplacements trouvés dans
un budget borné. Les jonctions entre familles restent une étape suivante.
Les établis, fourneaux et outils vanilla donnent des usages aux installations ;
rails, soutènements et chantiers interrompus suggèrent les anciens occupants.
Les lieux étranges sont physiques dans l’île, distincts de la future dimension
Backrooms. Aucun réacteur, progression, relique collective, récompense personnelle
ou nouveau spawner nest ajouté par ce prototype. Les coffres éventuels utilisent
les tables de butin vanilla.
## Vérification et essai
Le monde **Moyen, graine 0**, option expérimentale activée, reçoit trois réseaux
avec cinq pièces chacun. Les contrôles natifs portent sur **15 pièces**, **7 639
blocs**, **13 chunks FULL**, **12 appuis**, **49 cibles de parcours** et **3 coffres
à butin différé**. Les empreintes des starts, le plan et un bloc dor témoin sont
conservés après réouverture dans une seconde JVM. Les trappes sont ouvertes pour
vérifier le parcours réel dun joueur, puis refermées avant la sauvegarde.
| Réseau | Entrée X / Y / Z |
| --- | --- |
| Mine et atelier | **78 / 252 / 179** |
| Salles étranges et archives | **194 / 254 / 22** |
| Locaux techniques | **116 / 253 / 157** |
Avec les commandes autorisées :
```text
/locate structure sanctuary:forgotten_networks
```
La commande donne une entrée de surface. Le prototype recherche les trois
familles, sans les garantir sur chaque graine. Les neuf types de salles et
couloirs plus laccès sont testés sur des dimensions et ouvertures variées ;
les plafonds, appuis de mobilier, orientations et chemins sont contrôlés.
Les trois smokes dédiés passent. Les huit contrôles moteur du socle passent,
avec conservation des cavités et eaux naturelles. Les trois tailles et les deux
valeurs de loption passent le stockage réel des paramètres sur disque.
La planification des réseaux prend environ une seconde après celle de la ville
sur le témoin 0 de ce Mac : 915 colonnes lues et 11 974 sondages, pour des plafonds
respectifs de 5 000 et 250 000. Cela ne mesure ni le rendu client ni le chargement
complet sur une autre machine. Rapports locaux ignorés :
`build/alpha17-evidence/ruins17-create.json` et `ruins17-reload.json`.
Logs : `build/alpha17-native-create-final.log`, `alpha17-native-reload.log`,
`alpha17-first-native-base.log`, `alpha17-planner-piece-smokes.log`.
Lessai natif avec **Structures expérimentales désactivées** passe : plan vide,
zéro sondage expérimental et booléen conservé. Rapport :
`build/alpha17-evidence/ruins17-disabled.json`, log `build/alpha17-native-disabled.log`.
La création et la réouverture actives utilisent les processus 32029 et 32123 ; le
cas désactivé utilise 32176. Aucune sauvegarde personnelle na été ouverte.
La création et la réouverture des mondes de génération **16** passent sous le
binaire 17 : les 71 pièces de la ville initiale et du continent témoin gardent
leurs empreintes natives, ainsi que le journal, les paramètres et le bloc dor
témoin de la version publiée. Rapports :
`build/alpha17-evidence/legacy16-create.json` et `legacy16-reload.json`.
Le preset 16 historique est également identique octet par octet. La comparaison
avec les reçus publiés est consignée dans `legacy16-comparison.json` :
création sous 17 dans le processus 32244, réouverture dans 32429.
`./gradlew check build assemblePack` passe : 52 tâches, huit GameTests moteur
et les trois smokes du prototype. Le JAR de production exclut les classes de
test ; les versions, les hashes du pack, le MRpack et le ZIP damorçage Prism
sont vérifiés. Journal final : `build/alpha17-final-check-build-pack.log`.
La release et le canal packwiz sont publiés. Deux synchronisations isolées puis
deux dans la même instance Prism passent ; les 767 fichiers personnels et
réglages suivis gardent leurs hashes. Le JAR 16 et le suivi packwiz précédent
sont sauvegardés hors de `mods/`. Voir [le reçu de livraison](packwiz.md).
Aucun essai Windows natif ni revue visuelle dans le client nest revendiqué ;
le prochain essai de Koka validera lambiance.
+5
View File
@@ -231,6 +231,11 @@ L'ambition est de reprendre puis d'étendre la centaine de biomes du catalogue T
spawners pourront rester exploitables après exploration sans régénérer les
donjons. Les règles précises restent à concevoir. Le réacteur et ces nouveaux
systèmes ne font pas partie de lalpha.16.
Le prototype alpha.17 commence par trois petits réseaux indépendants, avec
accès de surface, ateliers, galeries, salles étranges et locaux techniques.
Loption « Structures expérimentales » se choisit pour un nouveau monde.
Les connexions entre familles et les systèmes de butin spécifiques restent
à construire ; voir [le contrat du prototype](testing-alpha17.md).
- **Secret rooms et challenge rooms** : des accès présents dans le monde mènent à des indoors aménagés, avec des équipements, des épreuves ou des gisements de fer, d'or, d'argent ou de titane.
### Dimensions
+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.16
pack_version=0.1.0-alpha.16
mod_version=0.1.0-alpha.17
pack_version=0.1.0-alpha.17
maven_group=fr.koka.sanctuary
+17 -2
View File
@@ -40,12 +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' : '16'))
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'))
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.experimentalStructures', providers.gradleProperty('sanctuaryExperimentalStructures').getOrElse('true'))
systemProperty('sanctuary.test.cityMatrix', providers.gradleProperty('sanctuaryCityMatrix').getOrElse('false'))
systemProperty('sanctuary.test.cityMatrixOnly', providers.gradleProperty('sanctuaryCityMatrixOnly').getOrElse('false'))
systemProperty('sanctuary.test.voidReuse', providers.gradleProperty('sanctuaryVoidReuseTests').getOrElse('false'))
@@ -65,10 +67,12 @@ tasks.named('processGametestResources') {
inputs.property('lostCity15', lostCity15)
def lostCity16 = providers.gradleProperty('sanctuaryLostCity16Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('lostCity16', lostCity16)
def ruins17 = providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('ruins17', ruins17)
def voidReuse = providers.gradleProperty('sanctuaryVoidReuseTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('voidReuse', voidReuse)
filesMatching('fabric.mod.json') {
expand(gametest_entrypoint: 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: 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')
@@ -400,3 +404,14 @@ tasks.register('lostCityFoundation16Smoke', JavaExec) {
mainClass = 'fr.koka.sanctuary.worldgen.city.LostCityFoundation16Smoke'
}
tasks.named('check') { dependsOn('lostCityFoundation16Smoke') }
// The experimental pieces and algorithms are additive; historical city suites remain unchanged.
['Planner', 'Renderer', 'Piece'].each { component ->
def smoke = tasks.register("ruins17${component}Smoke", JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = "fr.koka.sanctuary.worldgen.ruins.Ruins${component}17Smoke"
}
tasks.named('check') { dependsOn(smoke) }
}
@@ -0,0 +1,351 @@
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.ruins.RuinsPiece17;
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17;
import fr.koka.sanctuary.worldgen.ruins.RuinsRenderer17;
import fr.koka.sanctuary.worldgen.ruins.RuinsStructures17;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Predicate;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderSet;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.LevelResource;
import net.minecraft.world.level.storage.loot.LootTable;
import net.minecraft.world.phys.AABB;
/** Opt-in native proof confined to the new generation17 starter island in a disposable development world. */
public final class Ruins17WorldGameTests {
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
@GameTest(maxTicks = 12000)
public void forgottenNetworksPersistNatively(GameTestHelper helper) {
helper.assertTrue(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator,
"The forgotten-networks proof requires a Sanctuary generator");
var generator = (SanctuaryChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
helper.assertTrue(generator.rootGeneration() == 17, "Use a fresh generation17 test world; do not convert old saves");
new Scenario(helper, generator, Boolean.getBoolean("sanctuary.test.expansionReload")).advance();
}
private static final class Scenario {
final GameTestHelper helper;
final ServerLevel level;
final SanctuaryChunkGenerator generator;
final RuinsPlan17 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.ruinsPlan17();
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 starts, references, room blocks, player-sized routes and unopened loot on the starter island only. "
+ "No expansion is created and no personal save is opened. A single outer-wall gold edit in this disposable world "
+ "is saved and checked in a second JVM. Open hatches are restored after traversal; no free-fluid simulation is claimed.");
}
void advance() {
try {
if (!prepared) {
prepare(); prepared = true;
if (plan.isEmpty()) { finishDisabled(); return; }
}
if (nextChunk < chunks.size()) {
var pos = chunks.get(nextChunk++);
var loaded = level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true);
helper.assertTrue(loaded instanceof LevelChunk, "Every room footprint must reach native FULL: " + pos);
if (nextChunk % 10 == 0) SanctuaryMod.LOGGER.info("Forgotten networks17: loaded {}/{} footprint chunks", nextChunk, chunks.size());
helper.runAtTickTime(helper.getTick() + 1, this::advance); return;
}
verifyPlaced();
report.put("passed", true);
if (!reload) {
helper.assertTrue(gold != null && level.getBlockState(gold).isCollisionShapeFullBlock(level, gold),
"Gameplay witness must replace a solid outer wall away from every doorway");
level.setBlock(gold, Blocks.GOLD_BLOCK.defaultBlockState(), 3);
report.put("gold_witness", coords(gold));
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush native structures, deferred loot and the gold edit");
report.put("world_flushed_before_shutdown", true);
} else {
helper.assertTrue(level.getBlockState(gold).is(Blocks.GOLD_BLOCK), "Reload replaced the existing wall edit");
report.put("gold_witness", coords(gold)); report.put("gold_witness_preserved", true);
}
write(level, report, reload); helper.succeed();
} catch (Exception failure) {
report.put("passed", false); report.put("failure", failure.toString());
try { write(level, report, reload); } catch (IOException ignored) { }
throw new IllegalStateException("Forgotten networks17 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() == 17
&& copy.experimentalStructures() == generator.experimentalStructures(), "The generation17 codec must preserve its experimental switch");
report.put("generator_codec", encoded); report.put("plan_sha256", planHash(plan)); report.put("planning_samples", plan.samples());
helper.assertTrue(plan.samples() <= RuinsPlan17.MAX_SAMPLES, "Forgotten-network planning exceeded its budget");
if (reload) {
previous = JsonParser.parseString(Files.readString(proof(level))).getAsJsonObject();
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("world_flushed_before_shutdown").getAsBoolean(),
"Reload requires the successful, flushed creation proof");
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload must run in a separate JVM");
helper.assertTrue(previous.get("seed").getAsLong() == level.getSeed()
&& previous.get("size").getAsInt() == generator.initialDiameter()
&& previous.get("world_directory").getAsString().equals(world(level).toString()), "Reload used another world, seed or size");
helper.assertTrue(previous.get("generator_codec").equals(encoded) && previous.get("plan_sha256").getAsString().equals(planHash(plan)),
"Saved switch or planned generation17 room identities changed on reload");
if (previous.has("gold_witness")) gold = readPos(previous.getAsJsonArray("gold_witness"));
}
boolean enabled = generator.experimentalStructures() && level.structureManager().shouldGenerateStructures();
if (!enabled) {
helper.assertTrue(plan.isEmpty(), "Experimental=false or generateStructures=false must publish no forgotten network");
return;
}
helper.assertTrue(!plan.isEmpty(), "Enabled reference island has no experimental network");
var families = EnumSet.noneOf(RuinsPlan17.Family.class);
plan.networks().forEach(n -> families.add(n.family()));
report.put("families", families.stream().map(Enum::name).toList()); report.put("networks", plan.networks().size());
if (level.getSeed() == 0) helper.assertTrue(families.equals(EnumSet.allOf(RuinsPlan17.Family.class)),
"Seed0 reference must demonstrate all three experimental families; found " + families);
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(RuinsStructures17.KEY);
var origins = new HashSet<Long>(); var uniqueChunks = new HashSet<ChunkPos>();
for (var network : plan.networks()) {
long origin = new ChunkPos(network.startChunkX(), network.startChunkZ()).pack();
if (!origins.add(origin)) continue;
var chunk = level.getChunk(network.startChunkX(), network.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
var start = chunk.getStartForStructure(structure.value());
helper.assertTrue(start != null && start.isValid(), "Accepted experimental network has no native start: " + network.id());
starts.add(start);
for (var piece : start.getPieces()) {
helper.assertTrue(piece instanceof RuinsPiece17, "Forgotten network loaded a foreign or historical piece type");
piece.getBoundingBox().intersectingChunks().forEach(uniqueChunks::add);
}
}
helper.assertTrue(uniqueChunks.size() <= 225, "The bounded root-only proof unexpectedly covers more than225 chunks");
chunks.addAll(uniqueChunks.stream().sorted(java.util.Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList());
}
void finishDisabled() throws IOException {
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(RuinsStructures17.KEY);
var chunk = level.getChunk(0, 0, ChunkStatus.STRUCTURE_STARTS, true);
var start = chunk.getStartForStructure(structure.value());
helper.assertTrue(start == null || !start.isValid(), "Disabled experimental generation emitted a native start");
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
helper.assertTrue(located == null, "Disabled experimental structures must not be located or prepared");
report.put("empty_plan_verified", true); report.put("passed", true);
if (!reload) {
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save the disabled experiment setting");
report.put("world_flushed_before_shutdown", true);
}
write(level, report, reload); helper.succeed();
}
void verifyPlaced() {
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(RuinsStructures17.KEY);
var context = StructurePieceSerializationContext.fromLevel(level);
var sites = new ArrayList<Map<String, Object>>();
int blocks = 0, supports = 0, chests = 0, pieces = 0;
for (var start : starts) {
var origin = start.getChunkPos(); var saved = start.createTag(context, origin);
var copy = StructureStart.loadStaticStart(context, saved, level.getSeed());
helper.assertTrue(copy != null && copy.isValid() && copy.createTag(context, origin).equals(saved), "The native start changed on an NBT roundtrip");
var expectedRooms = plan.networks().stream().filter(n -> n.startChunkX() == origin.x() && n.startChunkZ() == origin.z())
.flatMap(n -> n.rooms().stream()).toList();
var actualRooms = start.getPieces().stream().map(RuinsPiece17.class::cast).map(RuinsPiece17::room).toList();
helper.assertTrue(actualRooms.size() == expectedRooms.size() && new HashSet<>(actualRooms).equals(new HashSet<>(expectedRooms)),
"Native start did not persist every planned room exactly once");
String nativeHash = hash(saved.toString());
if (reload) {
JsonObject prior = null;
for (var value : previous.getAsJsonArray("starts")) {
var site = value.getAsJsonObject(); var at = site.getAsJsonArray("start_chunk");
if (at.get(0).getAsInt() == origin.x() && at.get(1).getAsInt() == origin.z()) prior = site;
}
helper.assertTrue(prior != null && prior.get("native_start_sha256").getAsString().equals(nativeHash),
"Reload altered native room parameters, supports or reference count");
}
var loot = new ArrayList<Map<String, Object>>();
for (var piece : start.getPieces()) {
var roomPiece = (RuinsPiece17) piece; var room = roomPiece.room(); pieces++;
piece.getBoundingBox().intersectingChunks().forEach(at -> {
var full = level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true);
helper.assertTrue(full.getReferencesForStructure(structure.value()).contains(origin.pack()), "A saved FULL chunk lost the native structure reference");
});
var roomTag = roomPiece.createTag(context);
var roundtrip = new RuinsPiece17(context, roomTag);
helper.assertTrue(roundtrip.room().equals(room) && roundtrip.createTag(context).equals(roomTag), "The native room failed its exact saved-field roundtrip");
var corrupt = roomTag.copy(); var payload = corrupt.getCompound("SanctuaryRoom").orElseThrow(); payload.putInt("Schema", 99);
boolean rejected = false;
try { new RuinsPiece17(context, corrupt); } catch (IllegalArgumentException expected) { rejected = true; }
helper.assertTrue(rejected, "An unknown generation17 piece schema must be refused");
var expected = rendered(room); blocks += expected.blocks().size();
for (var entry : expected.blocks().entrySet()) {
var at = entry.getKey(); var wanted = entry.getValue();
helper.assertTrue(RuinsPiece17.bounds(room).isInside(at), "Renderer escaped its saved room/support bounds");
if (reload && at.equals(gold)) continue;
helper.assertTrue(level.getBlockState(at).is(wanted.getBlock()),
"Native clipping or later decoration changed " + room.kind() + " at " + at + ": expected " + wanted + " got " + level.getBlockState(at));
helper.assertTrue(!level.getBlockState(at).is(Blocks.SPAWNER), "Experimental architecture must not create a spawner");
}
for (var support : room.supports()) {
supports++;
var below = new BlockPos(support.x(), support.minY() - 1, support.z());
helper.assertTrue(level.getBlockState(below).isSolid() && level.getFluidState(below).isEmpty(), "Room footing has lost its native rock anchor");
}
for (var chest : expected.chests()) {
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Deferred native chest block entity is missing");
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
helper.assertTrue(chest.table().equals(entity.getLootTable()) && entity.getLootTableSeed() == chest.seed(),
"Native chest loot must remain deferred with its exact table and seed");
loot.add(Map.of("position", coords(chest.position()), "table", chest.table().identifier().toString(), "seed", chest.seed())); chests++;
}
if (!reload && gold == null && room.kind() != RuinsPlan17.Kind.ACCESS && room.kind() != RuinsPlan17.Kind.CORRIDOR)
gold = new BlockPos(room.minX(), room.floorY() + 1, room.minZ() + 1);
}
sites.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", nativeHash,
"pieces", start.getPieces().size(), "unopened_chests", loot));
}
var traversal = new ArrayList<Map<String, Object>>();
for (var network : plan.networks()) traversal.add(verifyAccess(helper, network));
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
helper.assertTrue(located != null && located.getSecond().is(RuinsStructures17.KEY), "Native locate cannot find a saved forgotten network");
helper.assertTrue(plan.networks().stream().flatMap(n -> n.rooms().stream()).filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS)
.anyMatch(r -> RuinsPiece17.locator(r).equals(located.getFirst())), "Locate must lead to a surface entrance rather than an underground floor");
report.put("starts", sites); report.put("native_pieces", pieces); report.put("native_blocks_checked", blocks);
report.put("full_chunks_checked", chunks.size()); report.put("rock_supports_checked", supports);
report.put("unopened_deferred_chests", chests); report.put("native_routes", traversal); report.put("locate", coords(located.getFirst()));
}
}
private record Chest(BlockPos position, ResourceKey<LootTable> table, long seed) {}
private record Rendered(Map<BlockPos, BlockState> blocks, List<Chest> chests) {}
private static Rendered rendered(RuinsPlan17.Room room) {
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
RuinsRenderer17.render(room, new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(at(room, x, y, z), state); }
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
var position = at(room, x, y, z); blocks.put(position, Blocks.CHEST.defaultBlockState()); chests.add(new Chest(position, table, RandomSource.create(seed).nextLong()));
}
});
return new Rendered(blocks, chests);
}
private static Map<String, Object> verifyAccess(GameTestHelper helper, RuinsPlan17.Network network) {
var level = helper.getLevel(); var access = network.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS).findFirst().orElseThrow();
var hatches = new HashMap<BlockPos, BlockState>();
rendered(access).blocks().forEach((at, state) -> { if (state.is(Blocks.DARK_OAK_TRAPDOOR)) hatches.put(at, level.getBlockState(at)); });
helper.assertTrue(hatches.size() == 1, "Each network must have one usable wooden surface hatch");
hatches.forEach((at, state) -> {
helper.assertTrue(state.is(Blocks.DARK_OAK_TRAPDOOR) && state.canSurvive(level, at), "Surface hatch is missing its native support");
level.setBlock(at, state.setValue(BlockStateProperties.OPEN, true), 2);
});
try {
Predicate<BlockPos> inside = pos -> network.rooms().stream().anyMatch(r -> r.contains(pos.getX(), pos.getY(), pos.getZ()));
var start = RuinsPiece17.locator(access); var reached = walk(helper, start, inside);
int targets = 0;
for (var room : network.rooms()) for (var target : RuinsRenderer17.targets(room)) {
var point = at(room, target.x(), target.y(), target.z());
helper.assertTrue(reached.contains(point), "Native network " + network.id() + " cannot reach " + room.kind() + "/" + target.name() + " at " + point);
targets++;
}
for (var hatch : hatches.keySet()) helper.assertTrue(reached.contains(hatch), "Surface approach cannot enter its ladder shaft");
return Map.of("id", network.id(), "family", network.family().name(), "rooms", network.rooms().size(),
"entrance", coords(start), "walkable_targets", targets, "standing_cells", reached.size());
} finally { hatches.forEach((at, state) -> level.setBlock(at, state, 2)); }
}
private static Set<BlockPos> walk(GameTestHelper helper, BlockPos start, Predicate<BlockPos> inside) {
var level = helper.getLevel(); helper.assertTrue(inside.test(start) && standable(level, start), "Blocked native surface approach: " + 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 traversal exceeded its room 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 BlockPos at(RuinsPlan17.Room room, int x, int y, int z) { return new BlockPos(room.minX() + x, room.minY() + y, room.minZ() + z); }
private static 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("ruins17-create-proof.json"); }
private static String planHash(RuinsPlan17 plan) { return hash(plan.networks().toString()); }
private static void write(ServerLevel level, Map<String, Object> report, boolean reload) throws IOException {
var path = Path.of("diagnostics", "ruins17-" + (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); }
}
}
@@ -42,8 +42,11 @@ final class UnifiedWorldSettingsDiagnostics {
var historical15 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v15"))).value().createWorldDimensions();
helper.assertTrue(historical15.overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 15 && old.source().generation() == 15,
"Historical alpha.15 keeps its own biome codec and city generator");
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 generator = (SanctuaryChunkGenerator) standard.overworld();
helper.assertTrue(generator.rootGeneration() == 16 && generator.source().generation() == 16, "The public preset uses generation16");
helper.assertTrue(generator.rootGeneration() == 17 && generator.source().generation() == 17 && generator.experimentalStructures(), "The public preset uses generation17 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())
@@ -53,8 +56,13 @@ final class UnifiedWorldSettingsDiagnostics {
}
var ops = RegistryOps.create(JsonOps.INSTANCE, access);
var activeCodec = generator.rootGeneration() == 16 ? SanctuaryChunkGenerator.CITY16_CODEC : generator.rootGeneration() == 15 ? SanctuaryChunkGenerator.CITY15_CODEC : generator.rootGeneration() == 14 ? SanctuaryChunkGenerator.CITY_CODEC : SanctuaryChunkGenerator.CODEC;
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 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");
helper.assertTrue(!((SanctuaryChunkGenerator) historical16.overworld()).withExperimentalStructures(true).experimentalStructures(),
"The new-world switch cannot activate experimental generation in a historical16 generator");
serialized.remove("initial_diameter");
helper.assertTrue(activeCodec.codec().parse(ops, serialized).getOrThrow().initialDiameter() == 724,
"An omitted new-generation size defaults to Medium");
@@ -67,8 +75,8 @@ final class UnifiedWorldSettingsDiagnostics {
Files.createDirectories(directory);
var storage = LevelStorageSource.createDefault(directory);
List<Map<String, Object>> results = new ArrayList<>();
for (int diameter : new int[]{512, 724, 1024}) {
var chosen = generator.withInitialDiameter(diameter);
for (int diameter : new int[]{512, 724, 1024}) for (boolean experimental : new boolean[]{false, true}) {
var chosen = generator.withInitialDiameter(diameter).withExperimentalStructures(experimental);
helper.assertTrue(generator.initialDiameter() == 724 && chosen.source() != generator.source(),
"Customizing a new world must copy the generator and its mutable biome source");
var dimensions = standard.replaceOverworldGenerator(access, chosen);
@@ -77,17 +85,17 @@ final class UnifiedWorldSettingsDiagnostics {
"Customizing the island preserves the other dimensions");
}
var original = new WorldGenSettings(new WorldOptions(helper.getLevel().getSeed(), false, true), dimensions);
String name = "diameter-" + diameter;
String name = "diameter-" + diameter + "-experimental-" + experimental;
try (var save = storage.createAccess(name)) {
LevelStorageSource.writeWorldGenSettings(access, storage.getLevelPath(name), original);
var restored = LevelStorageSource.readExistingSavedData(save, access, WorldGenSettings.TYPE).getOrThrow();
helper.assertTrue(restored.dimensions().overworld() instanceof SanctuaryChunkGenerator reloaded
&& reloaded.initialDiameter() == diameter && reloaded.rootGeneration() == generator.rootGeneration(),
&& reloaded.initialDiameter() == diameter && reloaded.rootGeneration() == generator.rootGeneration() && reloaded.experimentalStructures() == experimental,
"The selected diameter must survive real world-settings storage");
helper.assertTrue(WorldGenSettings.CODEC.encodeStart(ops, restored).getOrThrow()
.equals(WorldGenSettings.CODEC.encodeStart(ops, original).getOrThrow()),
"Seed, generation options and all dimensions survive the disk round trip");
results.add(Map.of("diameter", diameter, "disk_round_trip", true));
results.add(Map.of("diameter", diameter, "experimental_structures", experimental, "disk_round_trip", true));
}
}
Files.writeString(directory.resolve("round-trip.json"), new GsonBuilder().setPrettyPrinting().create().toJson(Map.of(
@@ -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.expansion") || Boolean.getBoolean("sanctuary.test.lostCity")
if (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", 16);
if (generation != 13 && generation != 14 && generation != 15 && generation != 16) throw new IllegalArgumentException("Unsupported test generation");
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 16 ? "sanctuary" : "sanctuary_v" + generation));
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));
}
if (Boolean.getBoolean("sanctuary.test.expansion")) return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("expansion_lab"));
int players = Integer.getInteger("sanctuary.test.players", 10);
@@ -70,7 +70,8 @@ public abstract class SanctuaryGameTestServerMixin {
if (Boolean.getBoolean("sanctuary.test.unified")) {
var generator = (fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator) dimensions.overworld();
return dimensions.replaceOverworldGenerator(context.datapackWorldRegistries(),
generator.withInitialDiameter(Integer.getInteger("sanctuary.test.diameter", 724)));
generator.withInitialDiameter(Integer.getInteger("sanctuary.test.diameter", 724))
.withExperimentalStructures(Boolean.parseBoolean(System.getProperty("sanctuary.test.experimentalStructures", "true"))));
}
return dimensions;
}
@@ -57,7 +57,10 @@ public final class SanctuaryMod implements ModInitializer {
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v15"), SanctuaryBiomeSource.CITY15_CODEC);
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v16"), SanctuaryChunkGenerator.CITY16_CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v16"), SanctuaryBiomeSource.CITY16_CODEC);
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();
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);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("population_groves"), PopulationGrovesFeature.CODEC);
@@ -12,12 +12,13 @@ import net.minecraft.client.gui.screens.worldselection.WorldCreationContext;
import net.minecraft.network.chat.CommonComponents;
import net.minecraft.network.chat.Component;
/** Edits only the pending new world's island size. Cancel never changes its dimensions. */
/** Edits size and the saved experiment switch of a pending new world. Cancel applies neither. */
public final class SanctuaryWorldOptionsScreen extends Screen {
private final CreateWorldScreen parent;
private final SanctuaryChunkGenerator generator;
private final HeaderAndFooterLayout layout = new HeaderAndFooterLayout(this);
private IslandSize selectedSize;
private boolean experimental;
private MultiLineTextWidget diameterHint;
public SanctuaryWorldOptionsScreen(CreateWorldScreen parent, WorldCreationContext context) {
@@ -28,6 +29,7 @@ public final class SanctuaryWorldOptionsScreen extends Screen {
}
generator = sanctuary;
selectedSize = IslandSize.fromDiameter(generator.initialDiameter());
experimental = generator.experimentalStructures();
}
@Override
@@ -49,6 +51,13 @@ public final class SanctuaryWorldOptionsScreen extends Screen {
contents.addChild(sizeButton);
diameterHint = contents.addChild(new MultiLineTextWidget(diameterHint(), font)
.setMaxWidth(310).setCentered(true));
if (generator.rootGeneration() == 17) {
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)
.setMaxWidth(310).setCentered(true));
}
layout.addToContents(contents);
LinearLayout actions = LinearLayout.horizontal().spacing(8);
@@ -65,7 +74,7 @@ public final class SanctuaryWorldOptionsScreen extends Screen {
private void applyAndClose() {
parent.getUiState().updateDimensions((registries, dimensions) -> dimensions.replaceOverworldGenerator(
registries, generator.withInitialDiameter(selectedSize.diameter)));
registries, generator.withInitialDiameter(selectedSize.diameter).withExperimentalStructures(experimental)));
onClose();
}
@@ -121,6 +121,7 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
@Override public void createStructures(RegistryAccess registries, ChunkGeneratorStructureState state, StructureManager structures,
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);
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 or 16 expansion");
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 (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; }
public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16 || generation == 17; }
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(); }
@@ -139,6 +139,9 @@ public final class ExpansionStructures {
if (fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime.protects(binding.generator(), binding.random(),
new BlockPos(centreX, total.minY(), centreZ), radius, total.getYSpan() + 16, 26)) return "protected-hydrology";
}
if (generator instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator city
&& city.intersectsExperimentalStructures(total.minX() - 1, total.minY() - (temple ? 24 : 12), total.minZ() - 1,
total.maxX() + 1, total.maxY() + 4, total.maxZ() + 1)) return "experimental-structure";
if (!inside(island, total.minX(), total.minZ(), 4) || !inside(island, total.maxX(), total.minZ(), 4)
|| !inside(island, total.minX(), total.maxZ(), 4) || !inside(island, total.maxX(), total.maxZ(), 4)) return "outside-island";
if (total.minY() < chunk.getMinY() || total.maxY() > chunk.getMaxY() - 4) return "outside-height";
@@ -24,7 +24,8 @@ public abstract class UnifiedDecorationProtectionMixin {
var owner = generator.islandForChunk(center.x(), center.z());
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() && generator.protectsCityDecoration(pos))
|| !fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
&& !fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.isPlacing() && generator.protectsCityDecoration(pos))
callback.setReturnValue(false);
}
@@ -39,7 +40,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
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17)
&& fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
&& generator.protectsCityDecoration(pos)) return;
if (generator.protectsDecoration(pos) && !region.getBlockState(pos).equals(state)) {
@@ -38,6 +38,10 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 16)));
public static final MapCodec<SanctuaryBiomeSource> RUINS17_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 17)));
private final List<Holder<Biome>> allBiomes;
private final PopulationIslandBiomeSource population;
private final boolean cities;
@@ -71,7 +75,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
cityPlans = java.util.Map.copyOf(plans);
}
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; }
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return 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);
@@ -39,6 +39,15 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
DIAMETER_CODEC.optionalFieldOf("initial_diameter", 724).forGetter(SanctuaryChunkGenerator::initialDiameter)
).apply(i, (source, diameter) -> new SanctuaryChunkGenerator(source, diameter, 16)));
public static final MapCodec<SanctuaryChunkGenerator> RUINS17_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
SanctuaryBiomeSource.RUINS17_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, 17, 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 Context originContext;
private final int generation;
private net.minecraft.world.level.LevelHeightAccessor heightAccessor;
@@ -70,14 +79,26 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
this(source, initialDiameter, 13);
}
private SanctuaryChunkGenerator(SanctuaryBiomeSource source, int initialDiameter, int generation) {
this(source, initialDiameter, generation, false);
}
private SanctuaryChunkGenerator(SanctuaryBiomeSource source, int initialDiameter, int generation, boolean experimental) {
super(source, initialDiameter, true);
this.experimentalStructures = generation == 17 && 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 == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; }
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return 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);
return new SanctuaryChunkGenerator(source().copy(), diameter, generation, experimentalStructures);
}
public boolean experimentalStructures() { return experimentalStructures; }
public SanctuaryChunkGenerator withExperimentalStructures(boolean enabled) {
return new SanctuaryChunkGenerator(source().copy(), initialDiameter(), generation, enabled);
}
public fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17() { return ruinsPlan17; }
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);
}
public IslandCapacity capacity() {
return switch (initialDiameter()) {
@@ -112,10 +133,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
// Publish one root context before any generation workers use it.
context(islands.getFirst());
prepareCities(islands);
prepareRuins(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);
super.setIslands(islands);
}
private void prepareCities(List<ExpansionIsland> islands) {
@@ -126,7 +149,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
source().setCityPlans(java.util.Map.of());
return;
}
if (generation == 16 && !cityStructuresEnabled) {
if ((generation == 16 || generation == 17) && !cityStructuresEnabled) {
cityProtection16 = fr.koka.sanctuary.worldgen.city.LostCityProtection16.EMPTY;
activeCityPlans = java.util.Map.of();
source().setCityPlans(java.util.Map.of());
@@ -142,7 +165,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
}
cityProtection15 = protection.build();
}
if (generation == 16) {
if (generation == 16 || generation == 17) {
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
for (var island : islands) {
protection.add(published.get(island.id()), undergroundPlan16(island));
@@ -156,6 +179,19 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
activeCityPlans = java.util.Map.copyOf(published);
source().setCityPlans(published);
}
private void prepareRuins(List<ExpansionIsland> islands) {
if (generation != 17 || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var terrain = context(root);
var sampled = new fr.koka.sanctuary.worldgen.ruins.RuinsTerrain17(root, terrain.generator(), terrain.random(),
heightAccessor, pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()));
long start = System.nanoTime();
var plan = fr.koka.sanctuary.worldgen.ruins.RuinsPlanner17.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
ruinsPlan17 = plan;
ruinsPrepared = true;
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 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();
@@ -181,7 +217,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
return cityPlans.computeIfAbsent(cityIdentity(island), ignored -> {
long start = System.nanoTime();
var terrain = context(island);
if (generation == 16) {
if (generation == 16 || generation == 17) {
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(),
@@ -252,7 +288,8 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
});
}
public boolean protectsCityDecoration(net.minecraft.core.BlockPos pos) {
if (generation == 16) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ());
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 == 15) return cityProtection15.contains(pos.getX(), pos.getY(), pos.getZ());
if (generation != 14) return false;
var island = islandAt(pos.getX(), pos.getZ());
@@ -274,20 +311,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 && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
return (generation == 16 || generation == 17) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
}
public fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan undergroundPlan16(ExpansionIsland island) {
if (generation != 16) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
if (generation != 16 && generation != 17) 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) return List.of();
if (generation != 16 && generation != 17) return List.of();
lostCityPlan(island);
return citySupports16.getOrDefault(cityIdentity(island), List.of());
}
public LostCityPlanner16.Fallback cityReservation16(ExpansionIsland island) {
if (generation != 16) return null;
if (generation != 16 && generation != 17) return null;
lostCityPlan(island);
return cityReservations16.get(cityIdentity(island));
}
@@ -299,6 +336,8 @@ 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))
return fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.findNearest(level, structures, origin, radius, skipReferenced);
return super.findNearestMapStructure(level, structures, origin, radius, skipReferenced);
}
@Override protected Context context(ExpansionIsland island) {
@@ -326,7 +365,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 && cityStructuresEnabled) {
if ((generation == 16 || generation == 17) && 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.cityReservation16(island) != null)
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17) && 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) for (var support : generator.citySupports16(island)) {
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17)) 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 ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
district.parcels().forEach(parcel -> pieces.addPiece((generator.rootGeneration() == 16 || generator.rootGeneration() == 17) ? 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) for (var network : generator.undergroundPlan16(island).networks()) {
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17)) 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,114 @@
package fr.koka.sanctuary.worldgen.ruins;
import java.util.ArrayList;
import java.util.Set;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.ResourceKey;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.loot.LootTable;
/** Saved generation17 room geometry is rendered without replanning or reading neighboring chunks. */
public final class RuinsPiece17 extends StructurePiece {
private static final Set<String> ROOM_KEYS = Set.of("Schema", "Kind", "MinX", "MinY", "MinZ", "Width",
"Height", "Depth", "Openings", "RoomSeed", "Supports");
private static final Set<String> SUPPORT_KEYS = Set.of("X", "MinY", "Z", "MaxY");
private final RuinsPlan17.Room room;
public RuinsPiece17(RuinsPlan17.Room room) {
super(RuinsStructures17.PIECE, 0, bounds(room));
this.room = room;
setOrientation(null);
}
public RuinsPiece17(StructurePieceSerializationContext context, CompoundTag tag) {
super(RuinsStructures17.PIECE, tag);
room = decodeRoom(tag.getCompound("SanctuaryRoom")
.orElseThrow(() -> new IllegalArgumentException("Missing saved forgotten room")));
if (!getBoundingBox().equals(bounds(room)))
throw new IllegalArgumentException("Forgotten room disagrees with its saved native bounding box");
setOrientation(null);
}
public RuinsPlan17.Room room() { return room; }
@Override public BlockPos getLocatorPosition() { return locator(room); }
public static BlockPos locator(RuinsPlan17.Room room) {
return new BlockPos(room.minX() + room.width() / 2,
room.kind() == RuinsPlan17.Kind.ACCESS ? room.minY() + room.height() - 2 : room.floorY() + 1,
room.minZ() + room.depth() / 2);
}
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
tag.put("SanctuaryRoom", encodeRoom(room));
}
public static CompoundTag encodeRoom(RuinsPlan17.Room room) {
var tag = new CompoundTag(); tag.putInt("Schema", 1); tag.putString("Kind", room.kind().name());
tag.putInt("MinX", room.minX()); tag.putInt("MinY", room.minY()); tag.putInt("MinZ", room.minZ());
tag.putInt("Width", room.width()); tag.putInt("Height", room.height()); tag.putInt("Depth", room.depth());
tag.putInt("Openings", room.openings()); tag.putLong("RoomSeed", room.seed());
var supports = new ListTag();
for (var support : room.supports()) {
var value = new CompoundTag(); value.putInt("X", support.x()); value.putInt("MinY", support.minY());
value.putInt("Z", support.z()); value.putInt("MaxY", support.maxY()); supports.add(value);
}
tag.put("Supports", supports);
return tag;
}
public static RuinsPlan17.Room decodeRoom(CompoundTag tag) {
if (!tag.keySet().equals(ROOM_KEYS)) throw new IllegalArgumentException("Incomplete or unknown forgotten room fields");
if (integer(tag, "Schema") != 1) throw new IllegalArgumentException("Unsupported forgotten room schema");
var kind = RuinsPlan17.Kind.valueOf(tag.getString("Kind").orElseThrow(() -> new IllegalArgumentException("Missing room kind")));
type(tag, "RoomSeed", Tag.TAG_LONG); type(tag, "Supports", Tag.TAG_LIST);
var saved = tag.getList("Supports").orElseThrow();
if (saved.size() > 64) throw new IllegalArgumentException("Too many saved forgotten room supports");
var supports = new ArrayList<RuinsPlan17.Support>();
for (var entry : saved) {
if (!(entry instanceof CompoundTag value) || !value.keySet().equals(SUPPORT_KEYS))
throw new IllegalArgumentException("Invalid forgotten room support fields");
supports.add(new RuinsPlan17.Support(integer(value, "X"), integer(value, "MinY"), integer(value, "Z"), integer(value, "MaxY")));
}
return new RuinsPlan17.Room(kind, integer(tag, "MinX"), integer(tag, "MinY"), integer(tag, "MinZ"),
integer(tag, "Width"), integer(tag, "Height"), integer(tag, "Depth"), integer(tag, "Openings"),
tag.getLong("RoomSeed").orElseThrow(), supports);
}
private static int integer(CompoundTag tag, String key) {
type(tag, key, Tag.TAG_INT);
return tag.getInt(key).orElseThrow();
}
private static void type(CompoundTag tag, String key, byte id) {
if (tag.get(key) == null || tag.get(key).getId() != id)
throw new IllegalArgumentException("Invalid saved forgotten room field: " + key);
}
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
RuinsStructures17.placing(() -> RuinsRenderer17.render(room, new LostCityRenderer.BlockWriter() {
private BlockPos world(int x, int y, int z) { return new BlockPos(room.minX() + x, room.minY() + y, room.minZ() + z); }
@Override public void set(int x, int y, int z, BlockState block) {
var position = world(x, y, z);
if (getBoundingBox().isInside(position) && clip.isInside(position))
placeBlock(level, block, position.getX(), position.getY(), position.getZ(), clip);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
var position = world(x, y, z);
if (getBoundingBox().isInside(position) && clip.isInside(position))
createChest(level, clip, RandomSource.create(seed), position, loot, null);
}
}));
}
public static BoundingBox bounds(RuinsPlan17.Room room) {
int minY = room.minY(), maxY = Math.addExact(room.minY(), room.height() - 1);
for (var support : room.supports()) {
minY = Math.min(minY, support.minY()); maxY = Math.max(maxY, support.maxY());
}
return new BoundingBox(room.minX(), minY, room.minZ(), Math.addExact(room.minX(), room.width() - 1),
maxY, Math.addExact(room.minZ(), room.depth() - 1));
}
}
@@ -0,0 +1,123 @@
package fr.koka.sanctuary.worldgen.ruins;
import java.util.HashSet;
import java.util.List;
/** Immutable generation17 experiment. Native pieces persist every room and its bounded footing. */
public record RuinsPlan17(List<Network> networks, int samples) {
public static final int MAX_SAMPLES = 250_000;
public static final RuinsPlan17 EMPTY = new RuinsPlan17(List.of(), 0);
public enum Family { MINE, STRANGE, INFRASTRUCTURE }
public enum Kind { ACCESS, CORRIDOR, MINE_GALLERY, WORKSHOP, STORE,
TILED_HALL, ARCHIVE, SERVICE_HALL, PUMP_HALL, CONTROL_ROOM }
/** One-block architectural pier. Its immediately lower block remains native solid rock. */
public record Support(int x, int minY, int z, int maxY) {
public Support {
if (minY < 9 || maxY >= 380 || minY > maxY || maxY - minY >= 12)
throw new IllegalArgumentException("Invalid generation17 ruins support");
}
}
/** Openings N=1,E=2,S=4,W=8: centered three wide, local y2..4. Floor is local y1.
* Adjacent bodies never overlap, including their opened boundary walls. */
public record Room(Kind kind, int minX, int minY, int minZ, int width, int height, int depth,
int openings, long seed, List<Support> supports) {
public Room {
supports = List.copyOf(supports);
if (kind == null || width < 5 || width > 17 || depth < 5 || depth > 17
|| (width & 1) == 0 || (depth & 1) == 0 || height < 6 || height > 40
|| minY < 10 || minY + height > 383 || openings < 0 || openings > 15 || supports.size() > 9)
throw new IllegalArgumentException("Invalid generation17 ruins room");
if (kind == Kind.ACCESS && (width != 5 || depth != 5))
throw new IllegalArgumentException("Ruins access must be five by five");
if (kind != Kind.ACCESS && kind != Kind.CORRIDOR && (width < 9 || depth < 9 || height < 7))
throw new IllegalArgumentException("Thematic ruins rooms need space for their central passage and furnishings");
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() != minY - 1
|| !positions.add(key(support.x(), support.z())))
throw new IllegalArgumentException("Ruins support does not meet its own room floor");
}
public int maxX() { return minX + width - 1; }
public int maxY() { return minY + height - 1; }
public int maxZ() { return minZ + depth - 1; }
public int centerX() { return minX + width / 2; }
public int centerZ() { return minZ + depth / 2; }
public int floorY() { return minY + 1; }
public int boundsMinY() { return supports.stream().mapToInt(Support::minY).min().orElse(minY); }
public int boundsMaxY() { return maxY(); }
public boolean contains(int x, int y, int z) {
if (x < minX || x > maxX() || z < minZ || z > maxZ()) return false;
if (y >= minY && y <= maxY()) return true;
for (var support : supports) if (support.x() == x && support.z() == z && y >= support.minY() && y <= support.maxY()) return true;
return false;
}
public boolean intersects(int x0, int y0, int z0, int x1, int y1, int z1) {
if (x0 > maxX() || x1 < minX || z0 > maxZ() || z1 < minZ) return false;
if (y0 <= maxY() && y1 >= minY) return true;
for (var support : supports) if (support.x() >= x0 && support.x() <= x1 && support.z() >= z0 && support.z() <= z1
&& support.minY() <= y1 && support.maxY() >= y0) return true;
return false;
}
public Room withOpening(int direction) {
if (direction < 0 || direction > 3) throw new IllegalArgumentException("Invalid opening direction");
return new Room(kind, minX, minY, minZ, width, height, depth, openings | 1 << direction, seed, supports);
}
public Room withSupports(List<Support> values) {
return new Room(kind, minX, minY, minZ, width, height, depth, openings, seed, values);
}
}
public record Network(String id, Family family, int startChunkX, int startChunkZ, List<Room> rooms) {
public Network {
rooms = List.copyOf(rooms);
if (id == null || !id.matches("[a-z][a-z0-9_]{0,31}") || family == null || rooms.size() < 3 || rooms.size() > 6
|| rooms.stream().filter(room -> room.kind() == Kind.ACCESS).count() != 1)
throw new IllegalArgumentException("Invalid generation17 ruins network");
for (var room : rooms) if (Math.abs((room.minX() >> 4) - startChunkX) > 7
|| Math.abs((room.maxX() >> 4) - startChunkX) > 7
|| Math.abs((room.minZ() >> 4) - startChunkZ) > 7 || Math.abs((room.maxZ() >> 4) - startChunkZ) > 7)
throw new IllegalArgumentException("Ruins network exceeds native reference range");
}
}
public RuinsPlan17 {
networks = List.copyOf(networks);
if (networks.size() > 3 || samples < 0 || samples > MAX_SAMPLES)
throw new IllegalArgumentException("Invalid generation17 ruins planning budget");
var ids = new HashSet<String>();
for (var network : networks) if (!ids.add(network.id())) throw new IllegalArgumentException("Duplicate ruins network id");
}
public boolean isEmpty() { return networks.isEmpty(); }
public boolean contains(int x, int y, int z) {
for (var network : networks) for (var room : network.rooms()) if (room.contains(x, y, z)) return true;
return false;
}
public boolean intersects(int x0, int y0, int z0, int x1, int y1, int z1) {
if (x0 > x1 || y0 > y1 || z0 > z1) throw new IllegalArgumentException("Reversed ruins bounds");
for (var network : networks) for (var room : network.rooms()) if (room.intersects(x0, y0, z0, x1, y1, z1)) return true;
return false;
}
/** Construction plus the unpainted native rock which carries its floors and short piers. */
public boolean protects(int x, int y, int z) {
if (contains(x, y, z)) return true;
for (var network : networks) for (var room : network.rooms()) {
if (x >= room.minX() && x <= room.maxX() && z >= room.minZ() && z <= room.maxZ()
&& y >= room.minY() - 2 && y < room.minY()) return true;
for (var support : room.supports()) if (x == support.x() && z == support.z() && y == support.minY() - 1) return true;
}
return false;
}
public boolean intersectsProtection(int x0, int y0, int z0, int x1, int y1, int z1) {
if (intersects(x0, y0, z0, x1, y1, z1)) return true;
for (var network : networks) for (var room : network.rooms()) {
if (x0 <= room.maxX() && x1 >= room.minX() && z0 <= room.maxZ() && z1 >= room.minZ()
&& y0 < room.minY() && y1 >= room.minY() - 2) return true;
for (var support : room.supports()) if (support.x() >= x0 && support.x() <= x1 && support.z() >= z0 && support.z() <= z1
&& support.minY() - 1 >= y0 && support.minY() - 1 <= y1) return true;
}
return false;
}
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
}
@@ -0,0 +1,217 @@
package fr.koka.sanctuary.worldgen.ruins;
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Family;
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Kind;
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Network;
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Room;
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Support;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/** Opt-in generation17 prototype. Three independently budgeted families have discoverable
* surface shafts and compact, connected interiors. No existing chunk or world state is read.
* Families remain separate in this prototype; no speculative cross-network door is published. */
public final class RuinsPlanner17 {
public static final int MAX_COLUMNS = 5_000;
public static final int MAX_SAMPLES = RuinsPlan17.MAX_SAMPLES;
private static final int INVALID = Integer.MIN_VALUE;
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
private RuinsPlanner17() {}
public interface Terrain {
boolean owns(int x, int z);
boolean solid(int x, int y, int z);
boolean protectedAt(int x, int y, int z);
/** Highest dry solid block; -1 when the column has no accessible surface. */
int surfaceY(int x, int z);
}
public static RuinsPlan17 plan(long seed, int centerX, int centerZ, int diameter, Terrain terrain) {
if (diameter < 64 || diameter > 1024) throw new IllegalArgumentException("Unsupported ruins island size");
var probe = new Probe(terrain, centerX, centerZ, diameter);
var networks = new ArrayList<Network>();
for (var family : Family.values()) {
int remaining = Family.values().length - family.ordinal() - 1;
probe.columnLimit = MAX_COLUMNS - remaining * (MAX_COLUMNS / 3);
probe.sampleLimit = MAX_SAMPLES - remaining * (MAX_SAMPLES / 3);
Network best = null;
try {
search: for (int attempt = 0; attempt < 6; attempt++) {
Site site = site(seed, family, attempt, centerX, centerZ, diameter, probe);
if (site == null) continue;
for (int drop : new int[] {14, 22, 30}) {
Network candidate = network(seed, family, site, drop, networks, probe);
if (candidate != null && (best == null || candidate.rooms().size() > best.rooms().size())) best = candidate;
if (best != null && best.rooms().size() == 5) break search;
}
}
} catch (BudgetExceeded exhausted) { probe.reject(family.name().toLowerCase(Locale.ROOT) + "-budget"); }
if (best != null) networks.add(best);
}
if (Boolean.getBoolean("sanctuary.test.ruins17") || Boolean.getBoolean("sanctuary.ruins.plannerDiagnostics"))
System.getLogger(RuinsPlanner17.class.getName()).log(System.Logger.Level.INFO,
"Ruins17 seed=" + seed + " diameter=" + diameter + " networks="
+ networks.stream().map(n -> n.family() + ":" + n.rooms().size()).toList()
+ " columns=" + probe.columns.size() + " samples=" + probe.samples + " rejected=" + probe.rejections);
return new RuinsPlan17(networks, probe.samples);
}
private static Site site(long seed, Family family, int attempt, int cx, int cz, int diameter, Probe probe) {
long salt = mix(seed ^ family.ordinal() * 0x9E3779B97F4A7C15L ^ attempt * 0xD1342543DE82EF95L ^ 0x1700L);
double base = unit(mix(seed ^ 0x17BA5EL)) * Math.PI * 2 + family.ordinal() * Math.PI * 2 / 3;
double angle = base + new double[] {0, .30, -.30, .48, -.48, .72}[attempt];
double radius = Math.min(diameter * .27, 216) * (attempt == 3 || attempt == 4 ? .72 : 1);
int x = cx + (int) Math.round(Math.cos(angle) * radius), z = cz + (int) Math.round(Math.sin(angle) * radius);
int low = 384, high = -1;
// Five coarse witnesses reject cliffs, water and unowned corners before a volume scan.
for (int[] offset : new int[][] {{-2, -2}, {-2, 2}, {2, -2}, {2, 2}, {0, 0}}) {
int px = x + offset[0], pz = z + offset[1], top = probe.surface(px, pz);
if (top < 54 || top > 376 || !probe.solid(px, top, pz) || probe.protectedAt(px, top, pz)) {
probe.reject("coarse-entrance"); return null;
}
low = Math.min(low, top); high = Math.max(high, top);
}
if (high - low > 4) { probe.reject("entrance-cliff"); return null; }
int direction = Math.abs(x - cx) >= Math.abs(z - cz) ? x < cx ? 1 : 3 : z < cz ? 2 : 0;
if ((salt & 1) != 0) direction = (direction + (attempt % 2 == 0 ? 1 : 3)) & 3;
return new Site(x, z, high, direction, salt);
}
private static Network network(long seed, Family family, Site site, int drop, List<Network> previous, Probe probe) {
int top = site.top();
for (int z = site.z() - 2; z <= site.z() + 2; z++) for (int x = site.x() - 2; x <= site.x() + 2; x++) {
int height = probe.surface(x, z);
if (height < site.top() - 4 || height > site.top() + 4) { probe.reject("entrance-surface"); return null; }
top = Math.max(top, height);
}
int floor = top - drop;
Room access = new Room(Kind.ACCESS, site.x() - 2, floor - 1, site.z() - 2,
5, drop + 4, 5, 0, mix(seed ^ site.seed() ^ drop), List.of());
var rooms = new ArrayList<Room>();
access = certify(access, rooms, previous, probe);
if (access == null) return null;
rooms.add(access);
if (!attach(rooms, 0, Kind.CORRIDOR, site.direction(), mix(site.seed() ^ drop ^ 0xC0L), previous, probe)) return null;
Kind[] kinds = switch (family) {
case MINE -> new Kind[] {Kind.MINE_GALLERY, Kind.WORKSHOP, Kind.STORE};
case STRANGE -> new Kind[] {Kind.TILED_HALL, Kind.ARCHIVE, Kind.STORE};
case INFRASTRUCTURE -> new Kind[] {Kind.SERVICE_HALL, Kind.PUMP_HALL, Kind.CONTROL_ROOM};
};
if (!attach(rooms, 1, kinds[0], site.direction(), mix(site.seed() ^ drop ^ 0xA1L), previous, probe)) return null;
// Keep already connected rooms when an optional branch encounters protected terrain.
int side = (site.direction() + ((site.seed() & 2) == 0 ? 1 : 3)) & 3;
try {
attach(rooms, 2, kinds[1], side, mix(site.seed() ^ drop ^ 0xB2L), previous, probe);
attach(rooms, 2, kinds[2], site.direction(), mix(site.seed() ^ drop ^ 0xC3L), previous, probe);
} catch (BudgetExceeded exhausted) { probe.reject("optional-branch-budget"); }
return new Network(family.name().toLowerCase(Locale.ROOT), family, site.x() >> 4, site.z() >> 4, rooms);
}
private static boolean attach(List<Room> rooms, int parentIndex, Kind kind, int direction, long seed,
List<Network> previous, Probe probe) {
Room parent = rooms.get(parentIndex);
int width = kind == Kind.CORRIDOR ? 5 : kind == Kind.MINE_GALLERY ? 13 : 9;
int depth = kind == Kind.CORRIDOR ? 5 : 9, height = kind == Kind.CORRIDOR ? 6 : 7;
int x = parent.centerX() - width / 2, z = parent.centerZ() - depth / 2;
if (direction == 0) z = parent.minZ() - depth;
if (direction == 1) x = parent.maxX() + 1;
if (direction == 2) z = parent.maxZ() + 1;
if (direction == 3) x = parent.minX() - width;
Room room = new Room(kind, x, parent.minY(), z, width, height, depth, 1 << ((direction + 2) & 3), seed, List.of());
room = certify(room, rooms, previous, probe);
if (room == null) return false;
rooms.set(parentIndex, parent.withOpening(direction)); rooms.add(room);
return true;
}
private static Room certify(Room draft, List<Room> siblings, List<Network> previous, Probe probe) {
for (Room room : siblings) if (conflicts(room, draft)) { probe.reject("sibling-overlap"); return null; }
for (Network network : previous) for (Room room : network.rooms()) if (conflicts(room, draft)) {
probe.reject("network-overlap"); return null;
}
for (int z = draft.minZ(); z <= draft.maxZ(); z++) for (int x = draft.minX(); x <= draft.maxX(); x++) {
int top = probe.surface(x, z);
if (top == INVALID || top < draft.minY() + 5 || draft.kind() != Kind.ACCESS && top < draft.maxY() + 1) {
probe.reject("room-exposed"); return null;
}
for (int y = draft.minY(); y <= draft.maxY(); y++) if (probe.protectedAt(x, y, z)) {
probe.reject("room-protected"); return null;
}
}
var supports = new ArrayList<Support>();
for (int z : new int[] {draft.minZ() + 1, draft.centerZ(), draft.maxZ() - 1})
for (int x : new int[] {draft.minX() + 1, draft.centerX(), draft.maxX() - 1}) {
int bottom = draft.minY() - 1;
if (probe.solid(x, bottom, z) && probe.solid(x, bottom - 1, z)) {
if (probe.protectedAt(x, bottom, z) || probe.protectedAt(x, bottom - 1, z)) { probe.reject("footing-protected"); return null; }
continue;
}
int rock = -1;
for (int y = bottom - 1; y >= Math.max(9, draft.minY() - 13); y--)
if (probe.solid(x, y, z) && probe.solid(x, y - 1, z)) { rock = y; break; }
if (rock < 0) { probe.reject("footing-void"); return null; }
for (int y = rock; y <= bottom; y++) if (probe.protectedAt(x, y, z)) { probe.reject("pier-protected"); return null; }
supports.add(new Support(x, rock + 1, z, bottom));
}
Room result = draft.withSupports(supports);
for (Room sibling : siblings) if (conflicts(sibling, result)) { probe.reject("pier-sibling"); return null; }
for (Network network : previous) for (Room room : network.rooms()) if (conflicts(room, result)) {
probe.reject("pier-network"); return null;
}
return result;
}
private static boolean conflicts(Room a, Room b) {
if (protectsBounds(a, b.minX(), b.minY(), b.minZ(), b.maxX(), b.maxY(), b.maxZ())
|| protectsBounds(b, a.minX(), a.minY(), a.minZ(), a.maxX(), a.maxY(), a.maxZ())) return true;
for (var support : b.supports()) if (protectsBounds(a, support.x(), support.minY(), support.z(), support.x(), support.maxY(), support.z())) return true;
for (var support : a.supports()) if (protectsBounds(b, support.x(), support.minY(), support.z(), support.x(), support.maxY(), support.z())) return true;
return false;
}
private static boolean protectsBounds(Room room, int x0, int y0, int z0, int x1, int y1, int z1) {
if (room.intersects(x0, y0, z0, x1, y1, z1)) return true;
if (x0 <= room.maxX() && x1 >= room.minX() && z0 <= room.maxZ() && z1 >= room.minZ()
&& y0 < room.minY() && y1 >= room.minY() - 2) return true;
for (var support : room.supports()) if (support.x() >= x0 && support.x() <= x1 && support.z() >= z0 && support.z() <= z1
&& support.minY() - 1 >= y0 && support.minY() - 1 <= y1) return true;
return false;
}
private record Site(int x, int z, int top, int direction, long seed) {}
private static final class BudgetExceeded extends RuntimeException {}
private static final class Probe {
final Terrain terrain; final int centerX, centerZ; final double radius;
final Map<Long, Integer> columns = new HashMap<>();
final Map<String, Integer> rejections = new java.util.TreeMap<>();
int samples, columnLimit, sampleLimit;
Probe(Terrain terrain, int centerX, int centerZ, int diameter) {
this.terrain = java.util.Objects.requireNonNull(terrain); this.centerX = centerX; this.centerZ = centerZ;
radius = diameter / 2.0 - 3;
}
int surface(int x, int z) {
if (Math.hypot((double) x - centerX, (double) z - centerZ) > radius || !terrain.owns(x, z)) return INVALID;
long key = (long) x << 32 ^ z & 0xffffffffL;
var cached = columns.get(key); if (cached != null) return cached;
if (columns.size() >= columnLimit) throw new BudgetExceeded();
count(); int top = terrain.surfaceY(x, z); columns.put(key, top); return top;
}
boolean solid(int x, int y, int z) {
if (y < 0 || y >= 384 || surface(x, z) == INVALID) return false;
count(); return terrain.solid(x, y, z);
}
boolean protectedAt(int x, int y, int z) {
if (y < 0 || y >= 384 || surface(x, z) == INVALID) return true;
count(); return terrain.protectedAt(x, y, z);
}
void count() { if (samples >= sampleLimit) throw new BudgetExceeded(); samples++; }
void reject(String reason) { rejections.merge(reason, 1, Integer::sum); }
}
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,277 @@
package fr.koka.sanctuary.worldgen.ruins;
import fr.koka.sanctuary.worldgen.city.LostCityPalette;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.core.Direction;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.AttachFace;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.storage.loot.BuiltInLootTables;
/** Generation17 experiments: original, physical rooms reconstructed solely from their saved inputs.
* Coordinates given to the writer are local to the saved room. There are no terrain reads, entities,
* active machines or free fluids. Openings are restored last so connected pieces share dry thresholds. */
public final class RuinsRenderer17 {
private static final Direction[] DIRECTIONS = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
private RuinsRenderer17() {}
/** Local standing positions for native path checks, including approaches to useful fixtures. */
public record Target(String name, int x, int y, int z) {}
public static List<Target> targets(RuinsPlan17.Room room) {
var targets = new ArrayList<Target>();
if (room.kind() == RuinsPlan17.Kind.ACCESS) {
targets.add(new Target("surface_hatch_approach", room.width() / 2, room.height() - 2, room.depth() / 2));
targets.add(new Target("shaft_bottom", room.width() / 2, 2, room.depth() / 2));
} else {
targets.add(new Target("central_crossing", room.width() / 2, 2, room.depth() / 2));
switch (room.kind()) {
case WORKSHOP -> {
targets.add(decorTarget(room, "workbench", 2, 2, 2));
targets.add(decorTarget(room, "unfinished_work_platform", room.width() - 3, 4, room.depth() - 3));
}
case STORE -> targets.add(decorTarget(room, "supply_chest", room.width() - 3, 2, room.depth() - 2));
case ARCHIVE -> targets.add(decorTarget(room, "reading_desk", 2, 2, 2));
case PUMP_HALL, CONTROL_ROOM -> targets.add(decorTarget(room, "controls", 2, 2, 2));
default -> { }
}
}
for (int direction = 0; direction < 4; direction++) if ((room.openings() & 1 << direction) != 0) {
int x = direction == 1 ? room.width() - 1 : direction == 3 ? 0 : room.width() / 2;
int z = direction == 2 ? room.depth() - 1 : direction == 0 ? 0 : room.depth() / 2;
targets.add(new Target("opening_" + DIRECTIONS[direction].getName(), x, 2, z));
}
return List.copyOf(targets);
}
public static void render(RuinsPlan17.Room room, LostCityRenderer.BlockWriter output) {
var b = new Builder(room, output);
b.shell();
switch (room.kind()) {
case ACCESS -> b.access();
case CORRIDOR -> b.corridor();
case MINE_GALLERY -> b.mine();
case WORKSHOP -> b.workshop();
case STORE -> b.store();
case TILED_HALL -> b.tiledHall();
case ARCHIVE -> b.archive();
case SERVICE_HALL -> b.service();
case PUMP_HALL -> b.pumps();
case CONTROL_ROOM -> b.controls();
}
if (room.kind() != RuinsPlan17.Kind.ACCESS) b.lights();
b.openings();
for (var support : room.supports()) for (int y = support.minY(); y <= support.maxY(); y++)
output.set(support.x() - room.minX(), y - room.minY(), support.z() - room.minZ(),
y == support.minY() ? Blocks.SMOOTH_STONE.defaultBlockState() : Blocks.STONE_BRICKS.defaultBlockState());
}
private static boolean turn(RuinsPlan17.Room room) { return (room.seed() & 1L) != 0; }
private static Target decorTarget(RuinsPlan17.Room room, String name, int x, int y, int z) {
return new Target(name, turn(room) ? room.width() - 1 - x : x, y, turn(room) ? room.depth() - 1 - z : z);
}
private static final class Builder {
final RuinsPlan17.Room room;
final LostCityRenderer.BlockWriter out;
final int w, h, d, cx, cz;
final boolean reverse;
Builder(RuinsPlan17.Room room, LostCityRenderer.BlockWriter out) {
this.room = room; this.out = out; w = room.width(); h = room.height(); d = room.depth();
cx = w / 2; cz = d / 2; reverse = turn(room);
}
void set(int x, int y, int z, BlockState state) { out.set(x, y, z, state); }
void set(int x, int y, int z, net.minecraft.world.level.block.Block block) { set(x, y, z, block.defaultBlockState()); }
void decor(int x, int y, int z, BlockState state) {
out.set(reverse ? w - 1 - x : x, y, reverse ? d - 1 - z : z,
reverse ? state.rotate(Rotation.CLOCKWISE_180) : state);
}
void decor(int x, int y, int z, net.minecraft.world.level.block.Block block) { decor(x, y, z, block.defaultBlockState()); }
boolean crossing(int x, int z) { return Math.abs(x - cx) <= 1 || Math.abs(z - cz) <= 1; }
void shell() {
for (int y = 0; y < h; y++) for (int z = 0; z < d; z++) for (int x = 0; x < w; x++) {
BlockState state;
boolean wall = x == 0 || x == w - 1 || z == 0 || z == d - 1;
if (y == 0) state = Blocks.STONE.defaultBlockState();
else if (y == 1) state = floor(x, z);
else if (wall || y == h - 1) state = LostCityPalette.masonry(room.seed(), x, y, z);
else state = Blocks.AIR.defaultBlockState();
set(x, y, z, state);
}
// A continuous ceiling cornice and stone thresholds remain intact despite the weathering.
for (int x = 0; x < w; x++) for (int z = 0; z < d; z++) if (x == 0 || x == w - 1 || z == 0 || z == d - 1)
set(x, h - 2, z, Blocks.SMOOTH_STONE);
}
BlockState floor(int x, int z) {
return switch (room.kind()) {
case MINE_GALLERY -> (x == cx ? Blocks.DARK_OAK_PLANKS : (x + z) % 5 == 0 ? Blocks.ANDESITE : Blocks.STONE).defaultBlockState();
case WORKSHOP, STORE -> (x == 1 || x == w - 2 || z == 1 || z == d - 2
? Blocks.DARK_OAK_PLANKS : Blocks.SMOOTH_STONE).defaultBlockState();
case TILED_HALL, ARCHIVE -> ((x / 2 + z / 2) % 2 == 0 ? Blocks.CALCITE : Blocks.POLISHED_ANDESITE).defaultBlockState();
case SERVICE_HALL, PUMP_HALL, CONTROL_ROOM -> (x == cx || z == cz ? Blocks.POLISHED_ANDESITE : Blocks.SMOOTH_STONE).defaultBlockState();
default -> LostCityPalette.pavement(room.seed(), x, z);
};
}
void access() {
int exit = Integer.numberOfTrailingZeros(room.openings() == 0 ? 1 : room.openings());
int wall = (exit + 2) & 3;
int x = wall == 1 ? w - 2 : wall == 3 ? 1 : cx;
int z = wall == 2 ? d - 2 : wall == 0 ? 1 : cz;
Direction facing = DIRECTIONS[wall].getOpposite();
if ((room.openings() & 1 << wall) != 0) {
// A branching shaft cannot attach its lowest ladder to another open doorway.
// This corner post leaves all three doorway cells and the central crossing usable.
x = 1; z = 2; facing = Direction.SOUTH;
for (int y = 2; y <= h - 3; y++) set(1, y, 1, Blocks.STONE_BRICKS);
}
BlockState ladder = Blocks.LADDER.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing);
for (int y = 2; y <= h - 3; y++) set(x, y, z, ladder);
for (int zz = 0; zz < d; zz++) for (int xx = 0; xx < w; xx++) {
set(xx, h - 3, zz, xx == x && zz == z ? ladder : Blocks.SMOOTH_STONE.defaultBlockState());
set(xx, h - 2, zz, Blocks.AIR); set(xx, h - 1, zz, Blocks.AIR);
}
set(x, h - 2, z, Blocks.DARK_OAK_TRAPDOOR.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, facing));
}
void corridor() {
boolean alongX = w > d;
int length = alongX ? w : d;
for (int n = 2; n < length - 1; n += 4) {
if (alongX) {
for (int y = 2; y <= h - 1; y++) { set(n, y, 0, Blocks.POLISHED_ANDESITE); set(n, y, d - 1, Blocks.POLISHED_ANDESITE); }
} else for (int y = 2; y <= h - 1; y++) { set(0, y, n, Blocks.POLISHED_ANDESITE); set(w - 1, y, n, Blocks.POLISHED_ANDESITE); }
}
}
void mine() {
for (int z : new int[] {2, d - 3}) {
for (int x : new int[] {2, w - 3}) for (int y = 2; y <= h - 2; y++) decor(x, y, z, Blocks.STRIPPED_OAK_LOG);
for (int x = 2; x <= w - 3; x++) decor(x, h - 2, z, Blocks.STRIPPED_OAK_LOG.defaultBlockState()
.setValue(BlockStateProperties.AXIS, Direction.Axis.X));
}
for (int z = 1; z < d - 1; z++) set(cx, 2, z, Blocks.RAIL);
// A short second siding ends at a buffer; the main track and cross passage remain open.
if (w >= 13) {
for (int z = 3; z < d - 2; z++) decor(w - 5, 2, z, Blocks.RAIL);
decor(w - 5, 2, d - 2, Blocks.DARK_OAK_FENCE);
}
decor(1, 2, 1, Blocks.BARREL);
decor(w - 2, 2, d - 2, Blocks.ANDESITE);
if (LostCityPalette.roll(room.seed(), 1, 0, 0, 2) == 0) decor(w - 2, 3, d - 2, Blocks.COBBLESTONE);
}
void workshop() {
decor(1, 2, 1, Blocks.CRAFTING_TABLE);
decor(2, 2, 1, Blocks.FURNACE.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
decor(1, 2, d - 3, Blocks.GRINDSTONE.defaultBlockState()
.setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR)
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST));
decor(w - 2, 2, 1, Blocks.STONECUTTER.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST));
decor(1, 2, d - 2, Blocks.CAULDRON);
decor(2, 2, d - 2, Blocks.ANVIL);
// Two grounded posts and a small planked deck preserve the trace of unfinished repairs.
for (int x = w - 3; x <= w - 2; x++) {
decor(x, 2, d - 3, Blocks.STRIPPED_OAK_LOG);
for (int z = d - 3; z <= d - 2; z++) decor(x, 3, z, Blocks.DARK_OAK_PLANKS);
}
for (int y = 2; y <= 3; y++) decor(w - 3, y, d - 4, Blocks.LADDER.defaultBlockState()
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
decor(w - 2, 2, d - 2, Blocks.BARREL);
}
void store() {
for (int z = 1; z < d - 1; z++) if (!crossing(1, z)) {
decor(1, 2, z, Blocks.BARREL);
if ((z & 1) == 0) decor(1, 3, z, Blocks.BARREL);
decor(1, 4, z, Blocks.DARK_OAK_SLAB);
}
for (int x = cx + 2; x <= w - 2; x++) decor(x, 2, 1, Blocks.DARK_OAK_PLANKS);
decor(w - 2, 3, 1, Blocks.BARREL);
loot(w - 2, 2, d - 2, BuiltInLootTables.VILLAGE_PLAINS_HOUSE);
}
void tiledHall() {
// Repeated columns are interrupted by complete, centered cross passages.
for (int x : new int[] {2, w - 3}) for (int z : new int[] {2, d - 3})
for (int y = 2; y < h - 1; y++) decor(x, y, z, y == 2 || y == h - 2 ? Blocks.SMOOTH_STONE : Blocks.CALCITE);
if (w >= 13) for (int x : new int[] {3, w - 4}) for (int z = 1; z <= d - 2; z++)
if (Math.abs(z - cz) > 1) for (int y = 2; y < h - 1; y++)
decor(x, y, z, y == h - 2 ? Blocks.POLISHED_ANDESITE : Blocks.SMOOTH_STONE);
for (int x = cx - 1; x <= cx + 1; x++) {
decor(x, h - 2, 1, Blocks.POLISHED_ANDESITE);
decor(x, h - 2, d - 2, Blocks.POLISHED_ANDESITE);
}
// Mismatched surviving benches give these physically reachable side niches a former use.
decor(1, 2, 1, seat(Direction.WEST));
decor(w - 2, 2, 1, seat(Direction.EAST));
}
void archive() {
for (int z = 1; z <= d - 2; z++) if (Math.abs(z - cz) > 1)
for (int y = 2; y <= 4; y++) decor(w - 2, y, z,
y == 4 && (z & 1) == 0 ? Blocks.DARK_OAK_PLANKS : Blocks.BOOKSHELF);
decor(1, 2, 1, Blocks.BOOKSHELF);
decor(1, 2, 2, Blocks.LECTERN.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST));
decor(1, 2, d - 2, Blocks.CARTOGRAPHY_TABLE);
decor(2, 2, d - 3, Blocks.BARREL);
loot(1, 2, d - 3, BuiltInLootTables.VILLAGE_CARTOGRAPHER);
}
void service() {
for (int z = 1; z < d - 1; z++) if (Math.abs(z - cz) > 1) {
decor(1, 2, z, Blocks.POLISHED_ANDESITE);
decor(1, 3, z, Blocks.IRON_BARS);
}
for (int z : new int[] {1, d - 2}) for (int x = 1; x < w - 1; x++)
decor(x, h - 2, z, Blocks.STRIPPED_OAK_LOG.defaultBlockState().setValue(BlockStateProperties.AXIS, Direction.Axis.X));
decor(w - 2, 2, 1, Blocks.BARREL);
decor(w - 2, 2, d - 2, Blocks.CRAFTING_TABLE);
decor(2, 2, d - 2, seat(Direction.SOUTH));
}
void pumps() {
for (int z : new int[] {1, d - 2}) {
decor(1, 2, z, Blocks.DISPENSER.defaultBlockState().setValue(BlockStateProperties.FACING, Direction.EAST));
decor(2, 2, z, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 3));
decor(1, 3, z, Blocks.POLISHED_ANDESITE);
decor(1, 4, z, Blocks.IRON_BARS);
decor(w - 2, 2, z, Blocks.CAULDRON);
}
for (int z = 1; z <= d - 2; z++) decor(1, h - 2, z, Blocks.POLISHED_ANDESITE);
decor(w - 2, 2, 2, Blocks.SMOOTH_STONE);
decor(w - 2, 3, 2, Blocks.LEVER.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR)
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST));
}
void controls() {
for (int x = 1; x <= cx - 2; x++) {
decor(x, 2, 1, Blocks.SMOOTH_STONE);
decor(x, 3, 1, Blocks.LEVER.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR)
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
decor(x, 3, 0, Blocks.REDSTONE_LAMP);
}
decor(1, 2, 2, seat(Direction.WEST));
decor(w - 2, 2, 1, Blocks.CRAFTING_TABLE);
decor(w - 2, 2, d - 2, Blocks.BARREL);
// An inspection window remains backed by a complete wall: there is no opening into unverified rock or water.
for (int z = cz + 2; z < d - 1; z++) decor(0, 3, z, Blocks.GLASS);
decor(1, 2, d - 2, Blocks.CARTOGRAPHY_TABLE);
}
BlockState seat(Direction back) { return Blocks.DARK_OAK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, back); }
void lights() {
// Every lantern hangs directly from the full masonry ceiling; never from a bottom slab.
for (int[] p : new int[][] {{1, 1}, {w - 2, d - 2}})
set(p[0], h - 2, p[1], Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
}
void openings() {
for (int direction = 0; direction < 4; direction++) if ((room.openings() & 1 << direction) != 0)
for (int side = -1; side <= 1; side++) {
int x = direction == 1 ? w - 1 : direction == 3 ? 0 : cx + side;
int z = direction == 2 ? d - 1 : direction == 0 ? 0 : cz + side;
set(x, 1, z, Blocks.SMOOTH_STONE);
for (int y = 2; y <= 4; y++) set(x, y, z, Blocks.AIR);
}
}
void loot(int x, int y, int z, net.minecraft.resources.ResourceKey<net.minecraft.world.level.storage.loot.LootTable> table) {
out.chest(reverse ? w - 1 - x : x, y, reverse ? d - 1 - z : z, table, room.seed() ^ 0x17A6C41L);
}
}
}
@@ -0,0 +1,39 @@
package fr.koka.sanctuary.worldgen.ruins;
import com.mojang.serialization.MapCodec;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.util.Optional;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureType;
/** One start contains every planned experimental network sharing its origin chunk. */
public final class RuinsStructure17 extends Structure {
public static final MapCodec<RuinsStructure17> CODEC = simpleCodec(RuinsStructure17::new);
public RuinsStructure17(StructureSettings settings) { super(settings); }
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 17 || !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();
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();
for (var network : networks) for (var room : network.rooms()) {
var bounds = RuinsPiece17.bounds(room);
if (Math.abs((long) Math.floorDiv(bounds.minX(), 16) - network.startChunkX()) > 8
|| Math.abs((long) Math.floorDiv(bounds.maxX(), 16) - network.startChunkX()) > 8
|| Math.abs((long) Math.floorDiv(bounds.minZ(), 16) - network.startChunkZ()) > 8
|| Math.abs((long) Math.floorDiv(bounds.maxZ(), 16) - network.startChunkZ()) > 8)
throw new IllegalStateException("Forgotten network exceeds native reference range: " + network.id());
}
return Optional.of(new GenerationStub(RuinsStructures17.locator(networks.getFirst()), pieces -> {
// Shafts come last so their surface hatches cannot be filled by a connected room.
networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() != RuinsPlan17.Kind.ACCESS)
.forEach(r -> pieces.addPiece(new RuinsPiece17(r))));
networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS)
.forEach(r -> pieces.addPiece(new RuinsPiece17(r))));
}));
}
@Override public StructureType<?> type() { return RuinsStructures17.TYPE; }
}
@@ -0,0 +1,111 @@
package fr.koka.sanctuary.worldgen.ruins;
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 generation17 uses independent native starts, references and saved room pieces. */
public final class RuinsStructures17 {
public static final ResourceKey<Structure> KEY = ResourceKey.create(Registries.STRUCTURE, SanctuaryMod.id("forgotten_networks"));
public static final StructureType<RuinsStructure17> TYPE = Registry.register(BuiltInRegistries.STRUCTURE_TYPE,
SanctuaryMod.id("forgotten_networks_v17"), () -> RuinsStructure17.CODEC);
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
SanctuaryMod.id("forgotten_room_v17"), RuinsPiece17::new);
private static final ThreadLocal<Integer> PLACEMENT_DEPTH = ThreadLocal.withInitial(() -> 0);
private RuinsStructures17() {}
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() != 17
|| !sanctuary.experimentalStructures()) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || island.generation() != 17) return;
var plan = sanctuary.ruinsPlan17();
if (plan.networks().stream().noneMatch(n -> n.startChunkX() == chunk.getPos().x()
&& n.startChunkZ() == chunk.getPos().z())) return;
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
Structure structure = holder.value();
StructureStart previous = manager.getStartForStructure(structure, chunk);
if (previous != null && previous.isValid()) return;
var random = generator.randomState(island);
var sampler = random.createClimateSampler(SamplerContext.builder().enableCaches().build());
var start = structure.generate(holder, level, registries, generator, generator.getBiomeSource(), sampler,
random, templates, island.seed(), chunk.getPos(), previous == null ? 0 : previous.getReferences(),
chunk, structure.biomes()::contains);
if (start.isValid()) manager.setStartForStructure(structure, start, chunk);
}
/** 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() != 17 || !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<RuinsPlan17.Network>();
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 (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 RuinsPiece17 room
&& room.room().kind() == RuinsPlan17.Kind.ACCESS).findFirst().orElse(start.getPieces().getFirst());
return Pair.of(entrance.getLocatorPosition(), holder);
}
return null;
}
static BlockPos locator(RuinsPlan17.Network network) {
var room = network.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS).findFirst()
.orElseGet(() -> network.rooms().getFirst());
return RuinsPiece17.locator(room);
}
}
@@ -0,0 +1,60 @@
package fr.koka.sanctuary.worldgen.ruins;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Predicate;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.RandomState;
/** Read-only, bounded native-noise cache. Unknown columns always reject construction. */
public final class RuinsTerrain17 implements RuinsPlanner17.Terrain {
public static final int MAX_COLUMNS = 5_000;
private final ExpansionIsland island;
private final NoiseBasedChunkGenerator generator;
private final RandomState random;
private final LevelHeightAccessor height;
private final Predicate<BlockPos> protection;
private final Map<Long, NoiseColumn> columns = new HashMap<>();
public RuinsTerrain17(ExpansionIsland island, NoiseBasedChunkGenerator generator, RandomState random,
LevelHeightAccessor height, Predicate<BlockPos> protection) {
this.island = island; this.generator = generator; this.random = random;
this.height = height; this.protection = protection;
}
private NoiseColumn column(int x, int z) {
if (!owns(x, z)) return null;
long key = ((long) x << 32) ^ (z & 0xffffffffL);
var found = columns.get(key);
if (found != null) return found;
if (columns.size() >= MAX_COLUMNS) return null;
var result = generator.getBaseColumn(x, z, height, random);
columns.put(key, result);
return result;
}
@Override public boolean owns(int x, int z) { return island.contains(x, z); }
@Override public boolean solid(int x, int y, int z) {
if (y < 0 || y >= 384) return false;
var column = column(x, z);
return column != null && column.getBlock(y).isSolid() && column.getBlock(y).getFluidState().isEmpty();
}
@Override public boolean protectedAt(int x, int y, int z) {
if (y < 0 || y >= 384) return true;
var column = column(x, z);
return column == null || !column.getBlock(y).getFluidState().isEmpty()
|| protection.test(new BlockPos(x, y, z));
}
@Override public int surfaceY(int x, int z) {
var column = column(x, z);
if (column == null) return -1;
for (int y = 380; y >= 1; y--) {
var state = column.getBlock(y);
if (!state.isAir()) return state.isSolid() && state.getFluidState().isEmpty() ? y : -1;
}
return -1;
}
public int columnsUsed() { return columns.size(); }
}
@@ -124,5 +124,8 @@
"sanctuary.expansion.error.city_generation_required": "This profile or size requires a generation 14 or 15 Sanctuary world. This save keeps its original generation.",
"sanctuary.expansion.reuse_reload": "Expansion %s reserved in previously generated void. Close and reopen the world to begin preparation.",
"sanctuary.expansion.error.reload_required": "This expansion is waiting for the world to reopen before reusing verified void chunks.",
"sanctuary.expansion.error.void_reuse_incomplete": "Void reuse is incomplete. The reservation is retained; check the server log."
"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"
}
@@ -124,5 +124,8 @@
"sanctuary.expansion.error.city_generation_required": "Ce profil ou cette taille demande un monde Sanctuary de génération 14 ou 15. Cette sauvegarde conserve sa génération.",
"sanctuary.expansion.reuse_reload": "Expansion %s réservée dans du vide déjà généré. Fermez puis rouvrez le monde pour démarrer sa préparation.",
"sanctuary.expansion.error.reload_required": "Cette expansion attend la réouverture du monde pour réutiliser les chunks de vide vérifiés.",
"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.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"
}
@@ -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:forgotten_networks_v17",
"biomes": "#sanctuary:has_structure/forgotten_networks",
"step": "underground_structures",
"terrain_adaptation": "none",
"spawn_overrides": {}
}
@@ -3,7 +3,7 @@
"minecraft:overworld": {
"type": "sanctuary:sanctuary_384",
"generator": {
"type": "sanctuary:island_v16",
"type": "sanctuary:island_v17",
"biome_source": {
"biomes": [
"minecraft:the_void",
@@ -0,0 +1,81 @@
{
"dimensions": {
"minecraft:overworld": {
"type": "sanctuary:sanctuary_384",
"generator": {
"type": "sanctuary:island_v16",
"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"
}
}
}
}
@@ -143,6 +143,21 @@ public final class ExpansionJournalSmoke {
require(Arrays.equals(ruinsBytes, Files.readAllBytes(ruinsDirectory.resolve(ExpansionJournal.FILE_NAME)))
&& Arrays.equals(detailBytes, Files.readAllBytes(detailDirectory.resolve(ExpansionJournal.FILE_NAME))),
"Rejected migrations preserve both published15 and16 bytes");
var experimentRoot = ExpansionIsland.origin(724, SEED, 17);
var experimentDirectory = scratch.resolve("experiment-17");
var experimentJournal = new ExpansionJournal(experimentDirectory, SEED, 724, 17);
var experimentChild = ExpansionPlacement.candidates(SEED, List.of(experimentRoot), "experiment", "sanctuary", "east", "lost_city", 128, "natural", 0).getFirst()
.withReuseToken("e".repeat(64));
experimentJournal.commit(List.of(experimentRoot, experimentChild));
require(new ExpansionJournal(experimentDirectory, SEED, 724, 17).islands().equals(List.of(experimentRoot, experimentChild)),
"Generation17 journal and child preserve their independent identity");
byte[] experimentBytes = Files.readAllBytes(experimentDirectory.resolve(ExpansionJournal.FILE_NAME));
reject(() -> new ExpansionJournal(experimentDirectory, SEED, 724, 16), "No implicit downgrade from17 to16");
reject(() -> new ExpansionJournal(ruinsDirectory, SEED, 724, 17), "No implicit upgrade from16 to17");
reject(() -> experimentJournal.commit(List.of(experimentRoot, ruinsChild)), "No generation16 child in17");
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");
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,55 @@
package fr.koka.sanctuary.worldgen.ruins;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
/** Generation17's independent saved payload: exact roundtrips and fail-closed malformed geometry. */
public final class RuinsPiece17Smoke {
public static void main(String[] args) {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); run();
System.out.println("RuinsPiece17Smoke: all room kinds, strict schema/types, supports, bounds and invalid geometry passed.");
}
public static void run() {
for (var kind : RuinsPlan17.Kind.values()) {
int size = kind == RuinsPlan17.Kind.ACCESS ? 5 : 17;
var room = new RuinsPlan17.Room(kind, -32, 120, 40, size, kind == RuinsPlan17.Kind.ACCESS ? 18 : 9, size,
10, Long.MIN_VALUE + kind.ordinal(), List.of(new RuinsPlan17.Support(-31, 114, 41, 119)));
var saved = RuinsPiece17.encodeRoom(room);
var decoded = RuinsPiece17.decodeRoom(saved);
require(room.equals(decoded) && RuinsPiece17.encodeRoom(decoded).equals(saved), "Saved room fields changed for " + kind);
var bounds = RuinsPiece17.bounds(room);
require(bounds.minX() == -32 && bounds.minZ() == 40 && bounds.minY() == 114
&& bounds.maxX() == room.maxX() && bounds.maxZ() == room.maxZ() && bounds.maxY() == room.maxY(), "Bounds excluded a saved footing");
if (kind == RuinsPlan17.Kind.ACCESS) require(RuinsPiece17.locator(room).getY() == room.maxY() - 1,
"Locate points beneath the surface hatch");
var missing = saved.copy(); missing.remove("Openings"); rejects(missing);
var version = saved.copy(); version.putInt("Schema", 2); rejects(version);
var coercion = saved.copy(); coercion.putDouble("MinY", 120.0); rejects(coercion);
var seedType = saved.copy(); seedType.putInt("RoomSeed", 1); rejects(seedType);
var unknown = saved.copy(); unknown.putInt("FutureField", 1); rejects(unknown);
var geometry = saved.copy(); geometry.putInt("Width", 18); rejects(geometry);
var opening = saved.copy(); opening.putInt("Openings", 16); rejects(opening);
var footings = new ListTag(); var bad = new CompoundTag();
bad.putInt("X", room.maxX() + 1); bad.putInt("MinY", 114); bad.putInt("Z", 41); bad.putInt("MaxY", 119);
footings.add(bad); var outside = saved.copy(); outside.put("Supports", footings); rejects(outside);
bad.putInt("X", -31); bad.putInt("MinY", 100); rejects(outside); // Overlong support.
bad.putInt("MinY", 114); bad.putInt("MaxY", 118); rejects(outside); // Detached support.
bad.putInt("MaxY", 119); footings.add(bad.copy()); rejects(outside); // Duplicate anchor column.
}
var supportList = new ArrayList<RuinsPlan17.Support>();
supportList.add(new RuinsPlan17.Support(1, 114, 1, 119));
var room = new RuinsPlan17.Room(RuinsPlan17.Kind.ACCESS, 0, 120, 0, 5, 18, 5, 2, 42, supportList);
supportList.clear();
require(RuinsPiece17.decodeRoom(RuinsPiece17.encodeRoom(room)).supports().size() == 1, "External list mutation changed saved support inputs");
}
private static void rejects(CompoundTag tag) {
try { RuinsPiece17.decodeRoom(tag); }
catch (IllegalArgumentException expected) { return; }
throw new AssertionError("Malformed forgotten room payload was accepted: " + tag);
}
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
}
@@ -0,0 +1,174 @@
package fr.koka.sanctuary.worldgen.ruins;
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Family;
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Kind;
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Room;
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 terrain-contract tests: entrances, exact doorway joins, independent budgets and short supports. */
public final class RuinsPlanner17Smoke {
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
public static void main(String[] args) {
Set<Kind> kinds = EnumSet.noneOf(Kind.class);
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 = RuinsPlanner17.plan(seed, 0, 0, diameter, terrain);
check(plan, terrain);
require(plan.networks().size() == 3, "Unobstructed starter does not receive all three families");
require(plan.networks().stream().map(RuinsPlan17.Network::family).distinct().count() == 3, "A family was duplicated");
require(plan.networks().stream().allMatch(n -> n.rooms().size() == 5), "Flat terrain loses a certified branch");
plan.networks().forEach(n -> n.rooms().forEach(r -> kinds.add(r.kind())));
require(plan.equals(RuinsPlanner17.plan(seed, 0, 0, diameter, new Fixture((x, z) -> 220))), "Ruins depend on planning order");
}
require(kinds.equals(EnumSet.allOf(Kind.class)), "A requested thematic kind is never generated");
var flat = RuinsPlanner17.plan(0, 0, 0, 724, new Fixture((x, z) -> 220));
require(!flat.equals(RuinsPlanner17.plan(1, 0, 0, 724, new Fixture((x, z) -> 220))), "Network locations ignore the seed");
Fixture shifted = new Fixture((x, z) -> 220);
var translated = RuinsPlanner17.plan(42, -1024, 512, 724, shifted);
require(translated.networks().size() == 3, "Negative world coordinates lose a network"); check(translated, shifted);
Fixture wet = new Fixture((x, z) -> 220); wet.protection = (x, y, z) -> true;
var refusedWater = RuinsPlanner17.plan(0, 0, 0, 724, wet);
check(refusedWater, wet); require(refusedWater.isEmpty(), "Ruins overwrite protected water or a city");
Fixture absent = new Fixture((x, z) -> -1);
var refusedVoid = RuinsPlanner17.plan(0, 0, 0, 724, absent);
check(refusedVoid, absent); require(refusedVoid.isEmpty(), "Void receives an artificial entry platform");
Fixture thin = new Fixture((x, z) -> 220); thin.rock = (x, y, z) -> y >= 218 && y <= 220;
var refusedThin = RuinsPlanner17.plan(0, 0, 0, 724, thin);
check(refusedThin, thin); require(refusedThin.isEmpty(), "A shaft descends from a shelf with no reachable bearing rock");
Fixture cavity = new Fixture((x, z) -> 220);
cavity.rock = (x, y, z) -> y >= 8 && y <= 220 && (y < 200 || y > 204);
var supported = RuinsPlanner17.plan(0, 0, 0, 724, cavity);
check(supported, cavity);
require(supported.networks().size() == 3 && supported.networks().stream().flatMap(n -> n.rooms().stream())
.anyMatch(r -> !r.supports().isEmpty()), "Short cave gaps cannot receive certified architectural supports");
// The first family's sector is obstructed. Its retries must not eliminate the other two.
var first = flat.networks().getFirst().rooms().getFirst();
double radiusSquared = (double) first.centerX() * first.centerX() + (double) first.centerZ() * first.centerZ();
Fixture obstructed = new Fixture((x, z) -> 220);
obstructed.protection = (x, y, z) -> (double) x * first.centerX() + (double) z * first.centerZ() > radiusSquared * .5;
var partial = RuinsPlanner17.plan(0, 0, 0, 724, obstructed);
check(partial, obstructed);
require(partial.networks().stream().noneMatch(n -> n.family() == Family.MINE), "Obstructed family escaped its own candidate sector");
require(partial.networks().stream().anyMatch(n -> n.family() == Family.STRANGE)
&& partial.networks().stream().anyMatch(n -> n.family() == Family.INFRASTRUCTURE), "One family starved all later families");
for (int diameter : new int[] {64, 128, 256}) {
Fixture small = new Fixture((x, z) -> 220);
check(RuinsPlanner17.plan(42, 0, 0, diameter, small), small);
}
boolean rejected = false;
try { new Room(Kind.WORKSHOP, 0, 100, 0, 5, 7, 5, 0, 0, List.of()); }
catch (IllegalArgumentException expected) { rejected = true; }
require(rejected, "Thematic room accepts dimensions too small for its central passage");
System.out.println("RuinsPlanner17Smoke passed: three independent families, discoverable shafts, exact connected doorways, protected-volume rejection, short native footings and bounded per-family searches");
}
private static void check(RuinsPlan17 plan, Fixture terrain) {
require(plan.samples() == terrain.samples && plan.samples() <= RuinsPlanner17.MAX_SAMPLES, "Probe budget drifted");
require(terrain.columns.size() <= RuinsPlanner17.MAX_COLUMNS, "Column budget exceeded");
Set<Long> starts = new HashSet<>(); Set<Point> written = new HashSet<>();
for (var network : plan.networks()) {
require(starts.add(key(network.startChunkX(), network.startChunkZ())), "Two networks collide at one native start");
require(network.rooms().size() >= 3 && network.rooms().size() <= 6, "Network size is unbounded");
require(network.rooms().stream().filter(r -> r.kind() == Kind.ACCESS).count() == 1, "Network has no unique surface shaft");
Map<Long, Integer> walk = new HashMap<>();
for (int index = 0; index < network.rooms().size(); index++) {
Room room = network.rooms().get(index);
require(Math.abs((room.minX() >> 4) - network.startChunkX()) <= 7
&& Math.abs((room.maxX() >> 4) - network.startChunkX()) <= 7
&& Math.abs((room.minZ() >> 4) - network.startChunkZ()) <= 7
&& Math.abs((room.maxZ() >> 4) - network.startChunkZ()) <= 7, "Native piece reference exceeds seven chunks");
int highest = -1;
for (int z = room.minZ(); z <= room.maxZ(); z++) for (int x = room.minX(); x <= room.maxX(); x++) {
highest = Math.max(highest, terrain.height.applyAsInt(x, z));
for (int y = room.minY(); y <= room.maxY(); y++) {
require(written.add(new Point(x, y, z)), "Independent rooms write the same voxel");
require(!terrain.protection.test(x, y, z), "Room overwrites protected terrain or water");
require(plan.contains(x, y, z) && plan.protects(x, y, z), "Room is absent from the construction mask");
}
require(plan.protects(x, room.minY() - 2, z), "Native floor bearing is exposed to decoration");
if (passage(room, x, z)) require(walk.put(key(x, z), index) == null, "Room walking planes overlap");
}
if (room.kind() == Kind.ACCESS) require(room.maxY() == highest + 2, "Entry is buried below the sampled surface");
for (int z : new int[] {room.minZ() + 1, room.centerZ(), room.maxZ() - 1})
for (int x : new int[] {room.minX() + 1, room.centerX(), room.maxX() - 1}) {
var support = room.supports().stream().filter(s -> s.x() == x && s.z() == z).findFirst();
if (support.isEmpty()) require(terrain.rock.test(x, room.minY() - 1, z) && terrain.rock.test(x, room.minY() - 2, z),
"Floor has neither a native rock footing nor a short pier");
}
for (var support : room.supports()) {
require(support.maxY() == room.minY() - 1 && support.maxY() - support.minY() < 12,
"Support misses the floor or descends too far");
require(terrain.rock.test(support.x(), support.minY() - 1, support.z())
&& terrain.rock.test(support.x(), support.minY() - 2, support.z()), "Support does not end on certified native rock");
require(plan.protects(support.x(), support.minY() - 1, support.z())
&& plan.intersectsProtection(support.x(), support.minY() - 1, support.z(), support.x(), support.minY() - 1, support.z()),
"Support bearing is missing from the decoration/structure protection masks");
for (int y = support.minY(); y <= support.maxY(); y++) {
require(written.add(new Point(support.x(), y, support.z())), "Support collides with another room or support");
require(!terrain.protection.test(support.x(), y, support.z()), "Support overwrites protected water");
require(plan.contains(support.x(), y, support.z()), "Support is missing from the exact volume mask");
}
}
for (int direction = 0; direction < 4; direction++) if ((room.openings() & 1 << direction) != 0) {
Room neighbor = neighbor(room, direction, network.rooms());
require(neighbor != null, "A published doorway opens into unplanned rock");
require(neighbor.floorY() == room.floorY() && (neighbor.openings() & 1 << ((direction + 2) & 3)) != 0,
"Doorways disagree on height or reciprocal opening");
}
}
require(!walk.isEmpty(), "Network has no walkable central passage");
Set<Long> reached = new HashSet<>(); var queue = new ArrayDeque<Long>();
Room access = network.rooms().stream().filter(r -> r.kind() == Kind.ACCESS).findFirst().orElseThrow();
long origin = key(access.centerX(), access.centerZ()); reached.add(origin); queue.add(origin);
while (!queue.isEmpty()) {
long position = queue.removeFirst(); int x = (int) (position >> 32), z = (int) position;
for (int direction = 0; direction < 4; direction++) {
long next = key(x + DX[direction], z + DZ[direction]);
if (walk.containsKey(next) && reached.add(next)) queue.addLast(next);
}
}
require(reached.equals(walk.keySet()), "A thematic room is disconnected from its discoverable entrance");
}
require(!plan.contains(100_000, 200, 100_000) && !plan.intersectsProtection(100_000, 0, 100_000, 100_005, 383, 100_005),
"Ruins protection escapes the accepted networks");
}
private static boolean passage(Room r, int x, int z) {
if (x == r.minX()) return (r.openings() & 8) != 0 && Math.abs(z - r.centerZ()) <= 1;
if (x == r.maxX()) return (r.openings() & 2) != 0 && Math.abs(z - r.centerZ()) <= 1;
if (z == r.minZ()) return (r.openings() & 1) != 0 && Math.abs(x - r.centerX()) <= 1;
if (z == r.maxZ()) return (r.openings() & 4) != 0 && Math.abs(x - r.centerX()) <= 1;
return Math.abs(x - r.centerX()) <= 1 || Math.abs(z - r.centerZ()) <= 1;
}
private static Room neighbor(Room room, int direction, List<Room> rooms) {
int x = direction == 1 ? room.maxX() + 1 : direction == 3 ? room.minX() - 1 : room.centerX();
int z = direction == 2 ? room.maxZ() + 1 : direction == 0 ? room.minZ() - 1 : room.centerZ();
return rooms.stream().filter(r -> r != room && x >= r.minX() && x <= r.maxX() && z >= r.minZ() && z <= r.maxZ()
&& (DX[direction] == 0 ? r.centerX() == room.centerX() : r.centerZ() == room.centerZ())).findFirst().orElse(null);
}
@FunctionalInterface private interface Volume { boolean test(int x, int y, int z); }
private static final class Fixture implements RuinsPlanner17.Terrain {
final IntBinaryOperator height; final Set<Long> columns = new HashSet<>(); int samples;
Volume protection = (x, y, z) -> false;
Volume rock;
Fixture(IntBinaryOperator height) { this.height = height; rock = (x, y, z) -> y >= 8 && y <= height.applyAsInt(x, z); }
@Override public boolean owns(int x, int z) { return true; }
@Override public boolean solid(int x, int y, int z) { samples++; return rock.test(x, y, z); }
@Override public boolean protectedAt(int x, int y, int z) { samples++; return protection.test(x, y, z); }
@Override public int surfaceY(int x, int z) {
samples++; require(columns.add(key(x, z)), "Column noise was sampled more than once"); return height.applyAsInt(x, z);
}
}
private record Point(int x, int y, int z) {}
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
private static void require(boolean valid, String message) { if (!valid) throw new AssertionError(message); }
}
@@ -0,0 +1,219 @@
package fr.koka.sanctuary.worldgen.ruins;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.RailBlock;
import net.minecraft.world.level.block.StairBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.storage.loot.LootTable;
/** Exercises the final native block states, saved bounds and meaningful circulation of all prototypes. */
public final class RuinsRenderer17Smoke {
private RuinsRenderer17Smoke() {}
public static void main(String[] args) {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); run();
System.out.println("RuinsRenderer17Smoke passed: ten physical room types, saved seed variation, dry connected openings, usable work platform, native fixture supports, deferred loot and chunk-order reconstruction");
}
public static void run() {
Set<Map<BlockPos, BlockState>> kinds = new HashSet<>();
for (var kind : RuinsPlan17.Kind.values()) {
for (int size = 0; size < 3; size++) for (int openings : new int[] {1, 2, 4, 8, 5, 10, 15})
for (long seed : new long[] {0, 1}) {
var room = room(kind, size, openings, seed);
var c = render(room); var repeated = render(room);
require(c.blocks.equals(repeated.blocks) && c.loot.equals(repeated.loot), label(room) + " is not deterministic");
if (size == 0 && openings == 15 && seed == 0) kinds.add(c.blocks);
var start = RuinsRenderer17.targets(room).getFirst();
Set<BlockPos> reached = walk(c, position(start));
for (var target : RuinsRenderer17.targets(room)) require(reached.contains(position(target)),
label(room) + " cannot reach " + target.name() + " at " + position(target));
openings(c, reached);
supports(c);
fixtures(c, reached);
identity(c);
for (var chest : c.loot.keySet()) require(approachable(chest, reached), label(room) + " chest is inaccessible at " + chest);
require(c.blocks.values().stream().noneMatch(b -> b.is(Blocks.SPAWNER) || !b.getFluidState().isEmpty()),
label(room) + " introduced a spawner or uncontrolled fluid");
}
var a = render(room(kind, 1, 15, 0)); var b = render(room(kind, 1, 15, 1));
require(!a.blocks.equals(b.blocks), kind + " ignores its saved variation seed");
if (kind != RuinsPlan17.Kind.ACCESS && kind != RuinsPlan17.Kind.CORRIDOR)
require(!furniture(a).equals(furniture(b)), kind + " varies only stone weathering, not physical use or furniture placement");
}
require(kinds.size() == RuinsPlan17.Kind.values().length, "Room names disguise identical rendered interiors");
nativeChunkPartition();
}
private static RuinsPlan17.Room room(RuinsPlan17.Kind kind, int size, int openings, long seed) {
int width = size == 0 ? 9 : size == 1 ? 13 : 17;
int depth = size < 2 ? 9 : 13, height = size < 2 ? 7 : 9;
if (kind == RuinsPlan17.Kind.ACCESS) { width = depth = 5; height = new int[] {14, 27, 40}[size]; }
if (kind == RuinsPlan17.Kind.CORRIDOR) { width = size == 2 ? 17 : 5; depth = size == 1 ? 13 : 5; height = 6; }
int x = -43, y = 150, z = 59;
return new RuinsPlan17.Room(kind, x, y, z, width, height, depth, openings, seed,
List.of(new RuinsPlan17.Support(x, y - 5, z, y - 1),
new RuinsPlan17.Support(x + width - 1, y - 12, z + depth - 1, y - 1)));
}
private static String label(RuinsPlan17.Room r) {
return r.kind() + " " + r.width() + "x" + r.height() + "x" + r.depth() + " openings=" + r.openings() + " seed=" + r.seed();
}
private static BlockPos position(RuinsRenderer17.Target target) { return new BlockPos(target.x(), target.y(), target.z()); }
private static Capture render(RuinsPlan17.Room room) {
var result = new Capture(room); RuinsRenderer17.render(room, result); return result;
}
private static void openings(Capture c, Set<BlockPos> reached) {
var r = c.room;
for (int direction = 0; direction < 4; direction++) for (int side = -1; side <= 1; side++) {
int x = direction == 1 ? r.width() - 1 : direction == 3 ? 0 : r.width() / 2 + side;
int z = direction == 2 ? r.depth() - 1 : direction == 0 ? 0 : r.depth() / 2 + side;
var threshold = new BlockPos(x, 1, z);
require(solid(c.block(threshold)), label(r) + " has no dry threshold at " + threshold);
if ((r.openings() & 1 << direction) == 0) {
require(!c.block(threshold.above()).isAir(), label(r) + " opens an unconnected wall");
continue;
}
for (int y = 2; y <= 4; y++) require(c.block(new BlockPos(x, y, z)).isAir(),
label(r) + " obstructs its three-wide opening at " + x + "/" + z);
require(reached.contains(threshold.above()), label(r) + " opening edge is not connected to the room");
}
}
private static void supports(Capture c) {
var r = c.room;
for (var support : r.supports()) {
for (int y = support.minY(); y <= support.maxY(); y++) require(solid(c.block(new BlockPos(
support.x() - r.minX(), y - r.minY(), support.z() - r.minZ()))), label(r) + " interrupts its saved footing");
require(solid(c.block(new BlockPos(support.x() - r.minX(), 0, support.z() - r.minZ()))),
label(r) + " footing does not meet the structural floor");
}
for (int x = 0; x < r.width(); x++) for (int z = 0; z < r.depth(); z++)
require(solid(c.block(new BlockPos(x, 0, z))), label(r) + " has a perforated structural floor");
}
private static void fixtures(Capture c, Set<BlockPos> reached) {
for (var entry : c.blocks.entrySet()) {
var p = entry.getKey(); var block = entry.getValue();
if (block.is(Blocks.LANTERN)) {
require(block.getValue(BlockStateProperties.HANGING), "Lantern must hang from the ceiling");
require(solid(c.block(p.above())), label(c.room) + " floating lantern at " + p);
}
if (block.is(Blocks.LADDER)) {
Direction facing = block.getValue(BlockStateProperties.HORIZONTAL_FACING);
require(solid(c.block(p.relative(facing.getOpposite()))), label(c.room) + " ladder lacks a back wall at " + p);
}
if (block.is(Blocks.LEVER) || block.is(Blocks.GRINDSTONE)) require(solid(c.block(p.below())),
label(c.room) + " unsupported usable fixture at " + p);
if (block.is(Blocks.DARK_OAK_STAIRS)) {
Direction back = block.getValue(BlockStateProperties.HORIZONTAL_FACING);
require(solid(c.block(p.below())), label(c.room) + " seat has no floor");
require(reached.contains(p.relative(back.getOpposite())), label(c.room) + " seat faces an obstruction at " + p);
}
if (block.is(Blocks.CRAFTING_TABLE) || block.is(Blocks.FURNACE) || block.is(Blocks.GRINDSTONE)
|| block.is(Blocks.STONECUTTER) || block.is(Blocks.CARTOGRAPHY_TABLE) || block.is(Blocks.LECTERN))
require(approachable(p, reached), label(c.room) + " useful furniture cannot be approached at " + p);
}
}
private static void identity(Capture c) {
int expectedLoot = c.room.kind() == RuinsPlan17.Kind.STORE || c.room.kind() == RuinsPlan17.Kind.ARCHIVE ? 1 : 0;
require(c.loot.size() == expectedLoot, label(c.room) + " has unbounded or missing deferred loot");
switch (c.room.kind()) {
case ACCESS -> { has(c, Blocks.LADDER); has(c, Blocks.DARK_OAK_TRAPDOOR); }
case CORRIDOR -> has(c, Blocks.POLISHED_ANDESITE);
case MINE_GALLERY -> { has(c, Blocks.RAIL); has(c, Blocks.STRIPPED_OAK_LOG); has(c, Blocks.BARREL); }
case WORKSHOP -> { has(c, Blocks.CRAFTING_TABLE); has(c, Blocks.FURNACE); has(c, Blocks.GRINDSTONE); has(c, Blocks.STONECUTTER); has(c, Blocks.ANVIL); has(c, Blocks.LADDER); }
case STORE -> { has(c, Blocks.BARREL); has(c, Blocks.DARK_OAK_SLAB); }
case TILED_HALL -> { has(c, Blocks.CALCITE); has(c, Blocks.POLISHED_ANDESITE); has(c, Blocks.DARK_OAK_STAIRS); }
case ARCHIVE -> { has(c, Blocks.BOOKSHELF); has(c, Blocks.LECTERN); has(c, Blocks.CARTOGRAPHY_TABLE); }
case SERVICE_HALL -> { has(c, Blocks.IRON_BARS); has(c, Blocks.CRAFTING_TABLE); }
case PUMP_HALL -> { has(c, Blocks.WATER_CAULDRON); has(c, Blocks.DISPENSER); has(c, Blocks.LEVER); }
case CONTROL_ROOM -> { has(c, Blocks.LEVER); has(c, Blocks.REDSTONE_LAMP); has(c, Blocks.CARTOGRAPHY_TABLE); }
}
}
private static void has(Capture c, Block block) {
require(c.blocks.values().stream().anyMatch(b -> b.is(block)), label(c.room) + " lost its recognizable " + block);
}
private static Map<BlockPos, BlockState> furniture(Capture c) {
var result = new HashMap<BlockPos, BlockState>();
c.blocks.forEach((p, b) -> {
if (p.getY() >= 2 && p.getY() < c.room.height() - 2 && !b.isAir()
&& !b.is(Blocks.STONE_BRICKS) && !b.is(Blocks.CRACKED_STONE_BRICKS)
&& !b.is(Blocks.MOSSY_STONE_BRICKS) && !b.is(Blocks.SMOOTH_STONE)) result.put(p, b);
});
return result;
}
private static boolean approachable(BlockPos p, Set<BlockPos> reached) {
for (Direction d : Direction.Plane.HORIZONTAL) if (reached.contains(p.relative(d))) return true;
return false;
}
private static Set<BlockPos> walk(Capture c, BlockPos start) {
require(stand(c, start), label(c.room) + " starts without two-block standing space at " + start);
var seen = new HashSet<BlockPos>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
while (!queue.isEmpty()) {
var p = queue.removeFirst();
for (Direction direction : Direction.Plane.HORIZONTAL) for (int dy : new int[] {0, 1, -1}) {
var q = p.relative(direction).offset(0, dy, 0);
if (stand(c, q) && seen.add(q)) queue.add(q);
}
for (int dy : new int[] {-1, 1}) {
var q = p.offset(0, dy, 0);
if ((climb(c.block(p)) || climb(c.block(q)) || climb(c.block(p.below())))
&& stand(c, q) && seen.add(q)) queue.add(q);
}
}
return seen;
}
private static boolean stand(Capture c, BlockPos p) {
if (p.getX() < 0 || p.getX() >= c.room.width() || p.getZ() < 0 || p.getZ() >= c.room.depth()
|| p.getY() < 2 || p.getY() >= c.room.height() - 1) return false;
return passable(c.block(p)) && passable(c.block(p.above()))
&& (solid(c.block(p.below())) || c.block(p.below()).getBlock() instanceof StairBlock
|| climb(c.block(p)) || climb(c.block(p.below())));
}
private static boolean climb(BlockState b) { return b.is(Blocks.LADDER) || b.is(Blocks.DARK_OAK_TRAPDOOR); }
private static boolean passable(BlockState b) { return b.isAir() || climb(b) || b.getBlock() instanceof RailBlock || b.is(Blocks.LEVER); }
private static boolean solid(BlockState b) { return b.canOcclude() && b.getFluidState().isEmpty(); }
private static void nativeChunkPartition() {
var r = room(RuinsPlan17.Kind.WORKSHOP, 2, 15, 1); var all = render(r); var chunks = new Capture(r);
for (int cz = r.maxZ() >> 4; cz >= r.minZ() >> 4; cz--) for (int cx = r.maxX() >> 4; cx >= r.minX() >> 4; cx--) {
final int chunkX = cx, chunkZ = cz;
RuinsRenderer17.render(r, new LostCityRenderer.BlockWriter() {
private boolean belongs(int x, int z) { return ((r.minX() + x) >> 4) == chunkX && ((r.minZ() + z) >> 4) == chunkZ; }
@Override public void set(int x, int y, int z, BlockState state) { if (belongs(x, z)) chunks.set(x, y, z, state); }
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { if (belongs(x, z)) chunks.chest(x, y, z, table, seed); }
});
}
require(chunks.blocks.equals(all.blocks) && chunks.loot.equals(all.loot), "Chunk order changes saved room rendering");
}
private record Loot(ResourceKey<LootTable> table, long seed) {}
private static final class Capture implements LostCityRenderer.BlockWriter {
final RuinsPlan17.Room room;
final Map<BlockPos, BlockState> blocks = new HashMap<>();
final Map<BlockPos, Loot> loot = new HashMap<>();
Capture(RuinsPlan17.Room room) { this.room = room; }
@Override public void set(int x, int y, int z, BlockState state) {
int wx = room.minX() + x, wy = room.minY() + y, wz = room.minZ() + z;
require(room.contains(wx, wy, wz), label(room) + " wrote outside its saved exact body/support mask: " + x + "/" + y + "/" + z);
require(wy >= room.boundsMinY() && wy <= room.boundsMaxY(), "Write escapes native vertical bounds");
blocks.put(new BlockPos(x, y, z), state);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
var pos = new BlockPos(x, y, z);
require(loot.put(pos, new Loot(table, seed)) == null, "Duplicate deferred chest");
set(x, y, z, Blocks.CHEST.defaultBlockState());
}
BlockState block(BlockPos p) { return blocks.getOrDefault(p, Blocks.AIR.defaultBlockState()); }
}
private static void require(boolean test, String message) { if (!test) throw new AssertionError(message); }
}
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "0.1.0-alpha.16"
version = "0.1.0-alpha.17"
pack-format = "packwiz:1.1.0"
[index]