Compare commits

...
Author SHA1 Message Date
koka a633d4cac9 docs: record verified alpha22 release and Prism update
Build Sanctuary / build (push) Canceled after 0s
2026-09-10 11:54:17 +02:00
koka d6e0c144d0 feat: selective surface placement and independent underground core for alpha22
Build Sanctuary / build (push) Canceled after 0s
2026-09-10 11:52:24 +02:00
koka d462f01e2b docs: record verified alpha21 release and Prism update
Build Sanctuary / build (push) Canceled after 0s
2026-09-10 10:08:01 +02:00
52 changed files with 2822 additions and 96 deletions
+31 -5
View File
@@ -15,11 +15,32 @@ 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.21 est construite et
vérifiée** : palettes sobres, couvertures portées, petits quartiers reliés et
premières galeries industrielles. Voir [le catalogue alpha.21](docs/architecture-alpha21.md),
[les essais alpha.21](docs/testing-alpha21.md)
et l’état du canal packwiz dans [Distribution](docs/packwiz.md).
décrits dans la vision ne sont pas encore implémentés. **Lalpha.22 est
publiée** : le placement des structures redevient sélectif. Les palettes,
intérieurs et filons de lalpha.21 sont conservés. Voir [les essais22](docs/testing-alpha22.md)
et [la distribution](docs/packwiz.md).
## Alpha.22 — trouver le bon terrain
La nouvelle génération préfère refuser une ville ou un bâtiment facultatif
qui ne trouve pas un emplacement adapté. Plus de ville forcée sur une réserve
artificielle ; moins de terrassement, des fondations proches de la roche et des
chemins qui suivent le relief. La grande traversée et la salle dexpansion sont
réservées en priorité et peuvent exister sans ville au-dessus. Les autres lieux
nont pas de quota garanti.
Créer un **nouveau monde Sanctuary** pour utiliser ces règles. Les tailles
512/724/1024 et le bouton natif **Personnaliser** restent inchangés ; les anciens
mondes conservent leurs générateurs. La vision retient désormais **Minecraft
comme archéologie dun autre Minecraft** : chaque ruine témoigne dune ancienne
solution, et de ceux qui lont construite puis ont disparu.
Les trois tailles natives, la réouverture, le cas expérimental OFF et la
compatibilité21 passent, ainsi que `check build assemblePack` et ses huit
GameTests généraux. Le [MRpack alpha.22](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.22/Sanctuary-0.1.0-alpha.22.mrpack)
est disponible, le canal packwiz et la même instance Prism sont synchronisés.
Les925 fichiers personnels et réglages suivis sont conservés. Voir
[la distribution](docs/packwiz.md).
## Alpha.21 — matériaux, quartiers et traversées
@@ -36,6 +57,11 @@ Créer un **nouveau monde Sanctuary** pour ce générateur21. Les anciens mondes
conservent leur génération. Les vérifications et coordonnées du
témoin sont consignées dans [les essais](docs/testing-alpha21.md).
Le [MRpack alpha.21](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.21/Sanctuary-0.1.0-alpha.21.mrpack)
est disponible. Le canal packwiz et la même instance Prism sont synchronisés ;
les 925 fichiers personnels et réglages suivis sont conservés. Les tests de
création/réouverture, le cas OFF et `check build assemblePack` passent.
## Alpha.20 — identité des bâtiments
La passe reprend les six bâtiments de ville, les quatorze infrastructures,
+27 -1
View File
@@ -809,7 +809,33 @@ option expérimentale enregistrée. Aucun monde personnel modifié. Vérifier le
pièces natives, accès, connexions, supports, création/réouverture, ancien monde20,
puis livrer le pack stable21 et synchroniser la même instance Prism.
**État : implémenté et vérifié ; MRpack fourni, publication et synchronisation en cours.**
**État : livré en alpha.21 ; MRpack, release et canal packwiz publiés, même instance Prism synchronisée deux fois. Tests, sauvegardes et limites documentés dans `docs/testing-alpha21.md`.**
### WG-25 — Placement sélectif et archéologie — alpha.22
**Branche :** `codex/placement-selectif-alpha22`.
Annule pour les nouveaux mondes lassouplissement de WG-24. Chercher un terrain
vraiment adapté et accepter labsence dune structure plutôt que de forcer son
implantation. La ville ne reçoit plus de réservation artificielle de secours.
Bâtiments et chemins limitent terrassement et remblai. Exception explicite :
la grande traversée et la salle dexpansion sont des éléments de base, à placer
en priorité indépendamment de la ville ; les égouts peuvent être ailleurs.
Les galeries gardent leur génie civil et la salle reçoit un accès autonome.
Les recherches restent bornées.
Conserver les palettes, les intérieurs, les filons et le rendu21. Nouveau codec22
pour les nouveaux mondes, sans migration ni retouche des mondes précédents.
Vérifier ladmission sur les bons terrains, les refus sur les mauvais, la création
native, la réouverture et la compatibilité21. Livrer le MRpack et mettre à jour
le canal et la même instance Prism en conservant les données personnelles.
Inscrire dans la vision « Minecraft comme archéologie dun autre Minecraft » :
chaque ruine garde la trace dune solution ancienne. Les neuf personnages sont
ceux nommés dans la vision ; la découverte et les rôles restent à définir.
Ce cadre narratif ne constitue pas une nouvelle mécanique implémentée.
**État : livré en alpha.22 ; MRpack, release et canal packwiz publiés, même instance Prism synchronisée deux fois.** Voir [les essais22](testing-alpha22.md).
## Réserve de thèmes futurs
+72 -1
View File
@@ -13,10 +13,81 @@ JAR Sanctuary sont des pièces jointes de releases Gitea. Aucun JAR de mod ou
dépendance téléchargée n'entre dans l'historique Git. Le canal Beta est distinct
de l'ancien pack 26.2.
## Alpha.22 publiée — 10 septembre 2026
[Sanctuary 0.1.0-alpha.22](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.22)
est publiée depuis le commit source `d6e0c14`. Le canal stable est au commit
`0019178`. Le JAR, le
[MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.22/Sanctuary-0.1.0-alpha.22.mrpack)
et le ZIP damorçage Prism ont été vérifiés par téléchargement public.
SHA-256 du JAR :
```text
10d7ef32ac2e81a977ea4854503bafa29e0bc10929984b76df01d6f5f61f9056
```
Les bâtiments de surface recherchent des terrains adaptés ; la grande traversée
et la salle dexpansion restent des éléments de base, indépendants dune ville.
Les galeries peuvent chercher un détour et de nouveaux accès. Le hall peut
accueillir des égouts ailleurs dans l’île. Les trois tailles natives, le cas OFF,
la réouverture et la comparaison21 passent. `check build assemblePack` passe
avec75 tâches et les huit GameTests généraux. Voir [les essais22](testing-alpha22.md).
Deux synchronisations isolées puis deux dans la même instance Prism réussissent.
Un seul JAR Sanctuary alpha.22 est actif ; les **925 fichiers personnels et
réglages suivis** gardent leurs hashes. Le dossier reste
`Sanctuary-0.1.0-alpha.1`, affiché « Sanctuary Beta ». Le JAR21 et le suivi
packwiz sont sauvegardés sous `sanctuary-backups/before-0.1.0-alpha.22/`.
Reçus locaux : `build/alpha22-isolated-validation.json` et
`build/alpha22-prism-validation.json`.
Pour tester : **nouveau monde Sanctuary, Moyen, graine0**. Les portiques de la
traversée sont `-170 253 -98` et `170 254 -98` ; la trappe du hall indépendant
est `2 252 0`. `/sanctuary structures` liste les lieux. Les villes et bâtiments
facultatifs peuvent être refusés. Les graines vérifiées ne prouvent pas la
présence des infrastructures sur toutes les graines ; les recherches restent
bornées. Aucun monde personnel na été ouvert, converti ou régénéré. Aucun
essai client visuel ni essai Windows natif nest revendiqué.
## Alpha.21 publiée — 10 septembre 2026
[Sanctuary 0.1.0-alpha.21](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.21)
est publiée depuis le commit source `bb63fab`. Le canal stable utilisait cette version au commit
`8002fce`. Le JAR, le
[MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.21/Sanctuary-0.1.0-alpha.21.mrpack)
et le ZIP damorçage Prism ont été vérifiés. SHA-256 du JAR :
```text
e528e42d99f9e1ea221f780ea089b49b42a1a4e9e24e4ddd001d045183202c21
```
Palettes sobres, toitures portées, quartiers reliés, placements patrimoniaux
assouplis et premières galeries industrielles avec fronts rocheux et filons.
Voir le [catalogue](architecture-alpha21.md) et les [essais](testing-alpha21.md).
`check build assemblePack`, les huit GameTests généraux, les créations et
réouvertures natives passent. Le cas expérimental OFF passe. Les fichiers
historiques et les témoins natifs de la génération20 sont conservés.
Deux synchronisations isolées puis deux dans la même instance Prism réussissent.
Un seul JAR Sanctuary alpha.21 est actif ; les **925 fichiers personnels et
réglages suivis** gardent leurs hashes. Le dossier reste
`Sanctuary-0.1.0-alpha.1`, affiché « Sanctuary Beta ». Le JAR20 et le suivi
packwiz sont sauvegardés sous `sanctuary-backups/before-0.1.0-alpha.21/`.
Reçus locaux : `build/alpha21-isolated-validation.json` et
`build/alpha21-prism-validation.json`.
Pour tester : **nouveau monde Sanctuary, Moyen, graine0**, **Structures
expérimentales** activées dans **Personnaliser**. Entrées des galeries :
`-135 253 -178` et `-135 249 162`. `/sanctuary structures` liste les lieux.
Les raccords dépendent du relief ; certains sites restent isolés et les réseaux
souterrains ne sont pas tous reliés. Aucun monde personnel na été ouvert,
converti ou régénéré. Aucun essai Windows natif ni essai visuel client nest
revendiqué. Une sauvegarde21 nécessite le binaire21 ou un futur binaire compatible.
## Alpha.20 publiée — 10 septembre 2026
[Sanctuary 0.1.0-alpha.20](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.20)
est publiée depuis le commit source `0edac29`. Le canal stable utilise cette
est publiée depuis le commit source `0edac29`. Le canal stable utilisait cette
version au commit `4e0967d`. Le JAR, le
[MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.20/Sanctuary-0.1.0-alpha.20.mrpack)
et le ZIP damorçage Prism ont été vérifiés par téléchargement public.
+27
View File
@@ -25,6 +25,33 @@ Sources locales : [planificateur17](../mods/sanctuary/src/main/java/fr/koka/sanc
[rendu17](../mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/ruins/RuinsRenderer17.java),
[preuves alpha.17](testing-alpha17.md), [vision](vision.md).
## Ligne de conception retenue après cet audit
**Minecraft comme archéologie dun autre Minecraft.** Steve, Ari, Sunny, Kai,
Zuri, Alex, Mefe, Makena, Noor sont les personnages disparus de la partie. Ils
ont découvert quelque chose de révolutionnaire et ont continué parce que ça
marchait, jusquau moment où ça na plus marché.
> Ils nont pas ouvert quelque chose quils nauraient pas dû ouvrir. Ils ont
> construit quelque chose quils ne savaient plus refermer.
Chaque ruine doit conserver la trace dune ancienne solution : une infrastructure
dont lauteur a progressivement disparu derrière son fonctionnement. Ce canon
ne définit pas la découverte, les rôles individuels ou une catastrophe confirmée.
Il guide les prochaines structures ; il najoute aucun personnage, récit jouable
ou système de progression aux versions déjà livrées. L’état alpha.17 décrit
ci-dessus reste le constat historique de cet audit.
Pour valider la conception dun lieu, relever concrètement :
- **Un usage ancien lisible** dans les pièces, les outils et les circulations.
- **Une implantation fonctionnelle** expliquée par le terrain, une ressource
ou une destination réellement présente.
- **Des traces dentretien, dextension ou de chantier** qui montrent comment
linstallation a été utilisée et adaptée.
- **Un lien futur à la progression**, explicitement décrit comme une intention
tant que son système et ses interactions ne sont pas implémentés.
## Faire comprendre les usages avant dintroduire leurs systèmes
Chaque lieu devrait répondre à trois questions : pourquoi était-il construit
+4 -2
View File
@@ -97,8 +97,10 @@ Les smokes des renderers, plans, pièces enregistrées et raccords passent.
La réouverture des lieux patrimoniaux conserve également le plan, les pièces,
les coffres et la modification témoin. Loption expérimentale désactivée passe :
elle ne publie aucun module de galerie. `check build assemblePack` passe, avec les huit GameTests généraux. Le
MRpack, son index et le JAR embarqué sont vérifiés ; la publication et la
synchronisation restent à consigner.
MRpack, son index et le JAR embarqué sont vérifiés. La release et le canal
packwiz sont publiés. Deux synchronisations isolées puis deux dans la même
instance Prism conservent les 925 fichiers personnels et réglages suivis.
Voir le [reçu de distribution](packwiz.md#alpha21-publiée--10-septembre-2026).
Les chemins de quartier regroupent leurs pentes à lintérieur de rectangles ;
leurs frontières et raccords sont des paliers plats. Les structures restent
+153
View File
@@ -0,0 +1,153 @@
# Alpha.22 — placement sélectif
Ticket WG-25, branche `codex/placement-selectif-alpha22`.
Minecraft 26.3-pre-2, Fabric Loader 0.19.5, Fabric API 0.160.0+26.3 et Java25,
sans changement de dépendances.
## Contrat
Les bâtiments de surface redeviennent sélectifs : une ville peut être absente
si aucun emplacement nest adéquat. Deux éléments de base sont prioritaires :
**la grande traversée souterraine et la salle dexpansion**, même sans ville.
Leurs emplacements sont réservés avant les petits réseaux et le catalogue
expérimental. Une recherche bornée ne prouve pas quaucun autre emplacement
possible nexiste dans toute l’île.
- Ville : aucune réserve artificielle de secours, aucun pilier de rattrapage,
terrassement/remblai limité à deux blocs par colonne. Les fondations sont
entièrement recertifiées dans la roche. Les quartiers supplémentaires exigent
un chemin complet vers les quartiers retenus. Au plus trois propositions dans
un budget partagé de20 000 colonnes et1 120 000 contrôles de volume.
- Infrastructures : relief maximal de quatre blocs sur lemprise17×17, coupe et
remblai limités à deux blocs, deux couches de roche intacte sous chaque colonne
de fondation, sans piliers de secours. Jusqu’à24 candidats par famille, avec
les mêmes plafonds globaux de16 000 colonnes et800 000 sondages.
- Chemins patrimoniaux : coupe/remblai limité à un bloc, accès indépendant et
raccord complet requis pour publier une liaison.
- Traversée : conserve le génie civil21, avec deux accès réels, une chaussée
de cinq blocs et deux trottoirs. Les piliers sont bornés à16 blocs ; les
travées sans grille complète sont limitées à34 blocs et reposent sur les
modules adjacents. La recherche22 intervient avant les structures facultatives
pour leur réserver lemprise. Si les lignes droites sont refusées, une seconde
passe cherche des accès différents et des détours. Chaque passe est limitée à
12 000 colonnes et3 millions de sondages (24 000 et6 millions au total au plus).
Ses parois peuvent traverser des cavités existantes sans écraser les volumes
deau et de lave protégés.
- Salle : recherche autonome dun accès de surface et dune salle25×25, sans
faux quartier ni socle de ville. Une salle déjà présente sous une ville évite
un doublon. Les égouts et annexes peuvent accompagner ce lieu indépendant.
Les palettes, les plans intérieurs et les petits filons minables restent ceux
de21. Les anciens petits réseaux expérimentaux17 et les sous-sols des villes gardent
leur géométrie ; leurs protections restent respectées.
Le nouveau principe narratif est documenté dans [la vision](vision.md) et
[laudit](structures-audit.md), sans activer de système de progression.
## Sauvegardes
Le seul preset public utilise désormais `sanctuary:island_v22`. Le preset caché
`sanctuary_v21` est la copie de lancien preset public. Les anciens codecs,
planificateurs et rendus restent disponibles. Les pièces21 sont réutilisées
avec leur format existant ; les nouveaux coudes possèdent une pièce22 distincte
pour fermer leurs murs sans modifier les jonctions21 sauvegardées.
La salle indépendante possède un type de structure22
et réutilise les pièces souterraines20.
Le journal identifie la génération22 et refuse une lecture croisée avec21.
Aucun monde personnel nest ouvert, converti ou régénéré.
Les nouvelles règles nécessitent un **nouveau monde Sanctuary**. Petit512,
Moyen724 par défaut et Grand1024 restent proposés. Loption **Structures
expérimentales** reste enregistrée à la création ; en22 elle retire le catalogue
facultatif, tandis que la traversée et la salle restent des éléments de base.
Le commutateur Minecraft général des structures demeure respecté.
## Témoin natif Moyen, graine0
La création et la réouverture dans deux JVM distinctes passent. La ville est
refusée ; la traversée comporte23 modules, dont deux accès et une salle de
maintenance. Son axe s’étend sur340 blocs. Les accès sont `-170 253 -100` et
`170 254 -100`. Le hall dexpansion indépendant se rejoint par la trappe
`2 252 0` ; son réseau comprend laccès, la salle, deux passages et un égout.
La piscine est une annexe conditionnelle, absente sur ce témoin.
Les14 bâtiments patrimoniaux trouvent un terrain adapté ; sept liaisons sont
retenues. Les115 chunks de lemprise sont générés en statut FULL. Les plans,
pièces natives et une modification témoin persistent après réouverture. Le
contrôle natif de circulation vérifie la vraie trappe, les échelles soutenues,
les ouvertures et le trajet jusquau socle central. Aucun déplacement de joueur
ou chargement dun monde personnel na été utilisé pour ces essais.
## Vérifications complémentaires
**Petit512, graine42 :**17 modules, dont un coude sauvegardé avec sa pièce22,
et une salle indépendante. Les anciens alignements droits sont refusés ; le
repli trouve une paire daccès naturels et conserve une chaussée continue.
Les portiques sont `-68 250 -93` et `68 250 43`, la trappe du hall `43 244 -10`.
La marche native relie les sorties en564 déplacements et atteint53 points de
contrôle. Huit bâtiments patrimoniaux et six liaisons sont admis ; aucune ville
nest forcée. Les88 chunks de lemprise sont vérifiés en statut FULL.
**Grand1024, graine42 :** un quartier naturel est admis avec son réseau de
sous-sols : salle dexpansion, piscine, égout et parking. Aucun hall autonome
supplémentaire nest créé. La traversée comporte31 modules et s’étend sur476
blocs ; portiques `-272 249 -8` et `204 250 -8`. La marche native les relie
en740 déplacements et atteint98 points de contrôle. Quatorze bâtiments
patrimoniaux et huit liaisons sont admis. Les179 chunks de lemprise sont
vérifiés en statut FULL ; la salle et ses17 ouvertures sont accessibles.
Les tests purs des trois tailles passent sur des terrains plats adaptés ;
falaises, eau/protections, vides et appuis insuffisants sont refusés pour les
bâtiments. Le hall autonome et ses annexes sont vérifiés sans fixture de ville.
Les suites historiques restent actives. Avec loption expérimentale désactivée,
le témoin Moyen conserve ses23 modules de traversée et sa salle indépendante,
sans bâtiments patrimoniaux ni petits réseaux expérimentaux. La marche native
relie les deux sorties en548 déplacements et atteint74 points de contrôle ;
les échelles et laccès au hall restent praticables.
Les créations et réouvertures de génération21 sont identiques aux rapports de
la release21, hors identifiants de processus et dossier de test. Les391 fichiers
de production historiques qui ne nécessitent pas de raccord22 restent identiques
octet pour octet ; le preset caché21 est la copie exacte du précédent preset
public. Les masques sont vérifiés à lintérieur et hors des structures.
`check build assemblePack` passe :75 tâches, dont les huit GameTests généraux.
Le JAR, lindex packwiz, le MRpack et le ZIP Prism sont vérifiés. Aucun code de
test nest embarqué dans le JAR. SHA-256 du JAR :
```text
10d7ef32ac2e81a977ea4854503bafa29e0bc10929984b76df01d6f5f61f9056
```
SHA-256 du MRpack :
```text
0b553fcdd8b6a2ec26f72059e428d2cc0ce526f36ef8580dd3e50c1dfa40ec78
```
Les reçus de publication et de synchronisation sont suivis dans
[la distribution](packwiz.md).
Ces témoins vérifient les trois tailles, avec et sans ville ; ils ne constituent
pas une preuve de présence pour toutes les graines. La recherche reste bornée
et refuse un volume protégé ou insuffisamment porté. Aucun essai visuel client
ou essai Windows natif nest revendiqué.
## Reproduire dans le dossier de développement
Avec Java25, création du témoin Moyen puis réouverture du même monde de test :
```sh
./gradlew :sanctuary:runGameTest -PsanctuarySelective22Tests=true -PsanctuaryDensityMaps=false
./gradlew :sanctuary:runGameTest -PsanctuarySelective22Tests=true -PsanctuaryDensityMaps=false -PsanctuaryExpansionReload=true
```
Pour une nouvelle création Petit ou Grand, ajouter `-PsanctuaryTestSeed=42`
et `-PsanctuaryTestDiameter=512` ou `1024`. Le cas OFF utilise
`-PsanctuaryExperimentalStructures=false`. Chaque réouverture doit suivre la
création correspondante, avec les mêmes paramètres. Les rapports sont dans
`mods/sanctuary/build/run/gameTest/diagnostics/selective22-<graine>-<diamètre>/`.
Le témoin historique utilise `-PsanctuaryHeritage21Tests=true`, puis le même
drapeau avec `-PsanctuaryExpansionReload=true`. La vérification de livraison
reste `./gradlew check build assemblePack`. Exécuter ces JVM séquentiellement.
+21
View File
@@ -22,6 +22,27 @@ L'objectif mythologique est de progresser d'une condition très limitée, proche
- **Humour, surprise et souvenirs.** Les noms d'entités, les événements, les animaux rares et les objets insolites comptent autant que les systèmes économiques.
- **Un développement par tickets.** Livrer une petite mécanique jouable et vérifiable, puis l'ajuster avec des tickets de fonctionnalités et de bugs. Ne pas construire tous les systèmes avant de pouvoir jouer.
## Minecraft comme archéologie dun autre Minecraft
**Canon et ligne de conception retenus.** Steve, Ari, Sunny, Kai, Zuri, Alex,
Mefe, Makena, Noor sont les personnages disparus de la partie. Ils ont découvert
quelque chose de révolutionnaire et ont continué parce que ça marchait. Jusquau
moment où ça na plus marché.
> Ils nont pas ouvert quelque chose quils nauraient pas dû ouvrir. Ils ont
> construit quelque chose quils ne savaient plus refermer.
Chaque ruine doit être une trace dune ancienne solution, pas seulement la trace
dun problème : une infrastructure dont lauteur a progressivement disparu
derrière son fonctionnement. Son ancien usage, ses circulations, ses réparations,
ses extensions et ses chantiers doivent permettre de comprendre ce qui avait
été résolu ici, même quand linstallation est abandonnée.
La nature de la découverte et les rôles individuels restent à définir. Aucune
catastrophe précise nest confirmée par ce principe. **La mise en scène de cette
histoire et ses liens avec la progression restent à implémenter** ; les versions
livrées et leurs tests établissent ce que chaque structure contient effectivement.
## Périmètre de départ
Le premier incrément porte sur le socle Fabric et la génération du monde : retrouver le générateur 26.2, produire une île principale dans un vrai vide, assurer une arrivée commune et sûre, puis poser une manière reproductible de générer des continents à une distance, une direction et une taille choisies.
+2 -2
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.21
pack_version=0.1.0-alpha.21
mod_version=0.1.0-alpha.22
pack_version=0.1.0-alpha.22
maven_group=fr.koka.sanctuary
+24 -2
View File
@@ -40,7 +40,9 @@ tasks.named('runGameTest') {
systemProperty('sanctuary.test.unified', providers.gradleProperty('sanctuaryUnifiedTests').getOrElse('true'))
systemProperty('sanctuary.test.diameter', providers.gradleProperty('sanctuaryTestDiameter').getOrElse('724'))
systemProperty('sanctuary.test.generation', providers.gradleProperty('sanctuaryTestGeneration').getOrElse(
providers.gradleProperty('sanctuaryLostCityTests').map { it.toBoolean() }.getOrElse(false) ? '14' : providers.gradleProperty('sanctuaryLostCity15Tests').map { it.toBoolean() }.getOrElse(false) ? '15' : providers.gradleProperty('sanctuaryLostCity16Tests').map { it.toBoolean() }.getOrElse(false) ? '16' : providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false) ? '17' : providers.gradleProperty('sanctuaryHeritage19Tests').map { it.toBoolean() }.getOrElse(false) ? '19' : (providers.gradleProperty('sanctuaryHeritage20Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryLostCity20Tests').map { it.toBoolean() }.getOrElse(false)) ? '20' : '21'))
providers.gradleProperty('sanctuaryLostCityTests').map { it.toBoolean() }.getOrElse(false) ? '14' : providers.gradleProperty('sanctuaryLostCity15Tests').map { it.toBoolean() }.getOrElse(false) ? '15' : providers.gradleProperty('sanctuaryLostCity16Tests').map { it.toBoolean() }.getOrElse(false) ? '16' : providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false) ? '17' : providers.gradleProperty('sanctuaryHeritage19Tests').map { it.toBoolean() }.getOrElse(false) ? '19' : (providers.gradleProperty('sanctuaryHeritage20Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryLostCity20Tests').map { it.toBoolean() }.getOrElse(false)) ? '20' : (providers.gradleProperty('sanctuaryHeritage21Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryLostCity21Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryTransit21Tests').map { it.toBoolean() }.getOrElse(false)) ? '21' : '22'))
systemProperty('sanctuary.test.selective22', providers.gradleProperty('sanctuarySelective22Tests').getOrElse('false'))
systemProperty('sanctuary.transit.plannerDiagnostics', providers.gradleProperty('sanctuaryTransitDiagnostics').getOrElse('false'))
systemProperty('sanctuary.test.heritage21', providers.gradleProperty('sanctuaryHeritage21Tests').getOrElse('false'))
systemProperty('sanctuary.test.lostCity21', providers.gradleProperty('sanctuaryLostCity21Tests').getOrElse('false'))
systemProperty('sanctuary.test.transit21', providers.gradleProperty('sanctuaryTransit21Tests').getOrElse('false'))
@@ -83,6 +85,8 @@ tasks.named('processGametestResources') {
inputs.property('heritage21', heritage21)
def lostCity21 = providers.gradleProperty('sanctuaryLostCity21Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('lostCity21', lostCity21)
def selective22 = providers.gradleProperty('sanctuarySelective22Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('selective22', selective22)
def transit21 = providers.gradleProperty('sanctuaryTransit21Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('transit21', transit21)
def heritage19 = providers.gradleProperty('sanctuaryHeritage19Tests').map { it.toBoolean() }.getOrElse(false)
@@ -90,7 +94,7 @@ tasks.named('processGametestResources') {
def voidReuse = providers.gradleProperty('sanctuaryVoidReuseTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('voidReuse', voidReuse)
filesMatching('fabric.mod.json') {
expand(gametest_entrypoint: transit21 ? 'fr.koka.sanctuary.gametest.Transit21WorldGameTests' : lostCity21 ? 'fr.koka.sanctuary.gametest.LostCity21WorldGameTests' : heritage21 ? 'fr.koka.sanctuary.gametest.Heritage21WorldGameTests' : lostCity20 ? 'fr.koka.sanctuary.gametest.LostCity20WorldGameTests' : heritage20 ? 'fr.koka.sanctuary.gametest.Heritage20WorldGameTests' : heritage19 ? 'fr.koka.sanctuary.gametest.Heritage19WorldGameTests' : ruins17 ? 'fr.koka.sanctuary.gametest.Ruins17WorldGameTests' : voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark
expand(gametest_entrypoint: selective22 ? 'fr.koka.sanctuary.gametest.Selective22WorldGameTests' : transit21 ? 'fr.koka.sanctuary.gametest.Transit21WorldGameTests' : lostCity21 ? 'fr.koka.sanctuary.gametest.LostCity21WorldGameTests' : heritage21 ? 'fr.koka.sanctuary.gametest.Heritage21WorldGameTests' : lostCity20 ? 'fr.koka.sanctuary.gametest.LostCity20WorldGameTests' : heritage20 ? 'fr.koka.sanctuary.gametest.Heritage20WorldGameTests' : heritage19 ? 'fr.koka.sanctuary.gametest.Heritage19WorldGameTests' : ruins17 ? 'fr.koka.sanctuary.gametest.Ruins17WorldGameTests' : voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark
? 'fr.koka.sanctuary.gametest.WorldgenPerformanceGameTests'
: unified ? 'fr.koka.sanctuary.gametest.UnifiedWorldGameTests'
: 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests')
@@ -508,3 +512,21 @@ tasks.named('lostCitySurface21Smoke') {
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
systemProperty('sanctuary.city21.exteriorPreview', rootProject.layout.buildDirectory.file('alpha21-preview/exterior.json').get().asFile.absolutePath)
}
// Generation22 filters surface placement and adds an independent core hall; historical tests still run.
[
lostCity22PlannerSmoke: 'city.LostCityPlanner22Smoke',
underground22Smoke: 'city.LostCityUnderground22Smoke',
heritage22PlannerSmoke: 'heritage.HeritagePlanner22Smoke',
transit22PlannerSmoke: 'transit.TransitPlanner22Smoke',
transit22RouteSmoke: 'transit.TransitRoute22Smoke',
transit22RendererSmoke: 'transit.TransitRenderer22Smoke'
].each { name, harness ->
def smoke = tasks.register(name, JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = "fr.koka.sanctuary.worldgen.${harness}"
}
tasks.named('check') { dependsOn(smoke) }
}
@@ -211,7 +211,7 @@ public final class PopulationDiagnostics {
void prepare() throws IOException {
boolean heritage = level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator sanctuary
&& (sanctuary.rootGeneration() == 19 || sanctuary.rootGeneration() == 20 || sanctuary.rootGeneration() == 21) && sanctuary.experimentalStructures();
&& (sanctuary.rootGeneration() == 22 || (sanctuary.rootGeneration() == 19 || sanctuary.rootGeneration() == 20 || sanctuary.rootGeneration() == 21) && sanctuary.experimentalStructures());
// Generation19 explicitly prospects destinations across the root island before publishing them.
// A consulted hydrology region is a bounded immutable plan, not a loaded/generated chunk.
// Historical generators retain their original near-spawn contract.
@@ -0,0 +1,156 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mojang.serialization.JsonOps;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import fr.koka.sanctuary.worldgen.PopulationGenerationContext;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import fr.koka.sanctuary.worldgen.city.*;
import fr.koka.sanctuary.worldgen.heritage.*;
import fr.koka.sanctuary.worldgen.transit.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.LevelResource;
/** Read-only admission audit, then native starts and reload in the disposable development world. */
public final class Selective22WorldGameTests {
@GameTest(maxTicks = 12000)
public void naturalSitesAndRefusalsPersist(GameTestHelper helper) throws Exception {
var level = helper.getLevel();
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
helper.assertTrue(generator.rootGeneration() == 22, "Use a fresh22 development world");
var root = ExpansionIsland.origin(generator.initialDiameter(), level.getSeed(), 22);
var city = generator.cityPlacement21(root);
var heritage = generator.heritagePlan19();
var transit = generator.transitPlan21();
helper.assertTrue(city != null && city.reservation() == null && city.supports().isEmpty(), "No forced city envelope or rescue piers");
var binding = Objects.requireNonNull(PopulationGenerationContext.of(level));
var terrain = new HeritageTerrain21(root, binding.generator(), binding.random(), level, p -> false);
int columns = 0;
for (var district : city.surface().districts()) for (var parcel : district.parcels()) {
for (int x = parcel.minX(); x <= parcel.maxX(); x++) for (int z = parcel.minZ(); z <= parcel.maxZ(); z++) {
int floor = parcel.surfaceY(x - parcel.minX(), z - parcel.minZ());
helper.assertTrue(Math.abs(floor - terrain.surfaceY(x, z)) <= 2, "City must fit natural relief"); columns++;
}
}
// The three samplers have independent bounded caches, just like production planning.
terrain = new HeritageTerrain21(root, binding.generator(), binding.random(), level, p -> false);
for (var site : heritage.sites()) {
helper.assertTrue(site.supports().isEmpty(), "No heritage rescue piers");
for (int x = site.minX(); x <= site.maxX(); x++) for (int z = site.minZ(); z <= site.maxZ(); z++) {
helper.assertTrue(Math.abs(site.floorY() - terrain.surfaceY(x, z)) <= 2, "Building cut/fill stays within two blocks");
helper.assertTrue(terrain.solid(x, site.minY() - 1, z) && terrain.solid(x, site.minY() - 2, z), "Every foundation column rests on original rock"); columns++;
}
}
helper.assertTrue(!transit.isEmpty(), "The reference island retains its core traversal");
var hall = generator.undergroundHall22();
helper.assertTrue(!hall.isEmpty() || generator.undergroundPlan16(root).networks().stream().flatMap(n -> n.modules().stream())
.anyMatch(m -> m.kind() == LostCityUnderground16.Kind.EXPANSION_HALL), "Core hall exists with or without a surface town");
for (var module : transit.modules()) for (var support : module.supports())
helper.assertTrue(support.maxY() - support.minY() + 1 <= 16, "Core traversal retains bounded engineered supports");
if (!generator.experimentalStructures()) helper.assertTrue(heritage.isEmpty() && generator.ruinsPlan17().isEmpty(), "OFF removes optional catalogue, not core hall and traversal");
var gson = new GsonBuilder().setPrettyPrinting().create();
var encoded = ChunkGenerator.CODEC.encodeStart(RegistryOps.create(JsonOps.INSTANCE, level.registryAccess()), generator).getOrThrow();
var geometry = gson.toJsonTree(Map.of("city", city.surface().districts(), "heritage_sites", heritage.sites(),
"heritage_links", heritage.links(), "heritage_segments", heritage.segments(), "transit", transit.modules(), "independent_hall", hall.networks()));
var report = new LinkedHashMap<String, Object>();
report.put("seed", level.getSeed()); report.put("size", generator.initialDiameter());
report.put("process_id", ProcessHandle.current().pid()); report.put("generator_codec", encoded); report.put("geometry", geometry);
report.put("experimental", generator.experimentalStructures()); report.put("city_districts", city.surface().districts().size());
report.put("heritage_sites", heritage.sites().size()); report.put("heritage_links", heritage.links().size()); report.put("transit_modules", transit.modules().size());
report.put("independent_hall_networks", hall.networks().size());
report.put("core_entries", transit.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.ACCESS).map(m -> List.of(m.centerX(), m.surfaceY() + 1, m.minZ() - 2)).toList());
report.put("hall_entries", hall.networks().stream().map(n -> { var at = LostCityUndergroundStructures22.locator(n); return List.of(at.getX(), at.getY(), at.getZ()); }).toList());
report.put("natural_columns_audited", columns); report.put("city_diagnostics", city.diagnostics());
report.put("heritage_diagnostics", heritage.diagnostics()); report.put("transit_diagnostics", transit.diagnostic());
var starts = new ArrayList<StructureStart>();
var origins = new LinkedHashMap<ResourceKey<Structure>, Set<ChunkPos>>();
origins.put(LostCityStructures.KEY, new HashSet<>()); origins.put(HeritageStructures19.KEY, new HashSet<>()); origins.put(TransitStructures21.KEY, new HashSet<>()); origins.put(LostCityUndergroundStructures22.KEY, new HashSet<>());
city.surface().districts().forEach(d -> origins.get(LostCityStructures.KEY).add(new ChunkPos(d.startChunkX(), d.startChunkZ())));
heritage.sites().forEach(s -> origins.get(HeritageStructures19.KEY).add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
heritage.segments().forEach(s -> origins.get(HeritageStructures19.KEY).add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
transit.modules().forEach(m -> origins.get(TransitStructures21.KEY).add(new ChunkPos(m.startChunkX(), m.startChunkZ())));
hall.networks().forEach(n -> origins.get(LostCityUndergroundStructures22.KEY).add(new ChunkPos(n.startChunkX(), n.startChunkZ())));
var chunks = new TreeSet<ChunkPos>(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z));
var nativeTags = new TreeMap<String, String>();
var context = StructurePieceSerializationContext.fromLevel(level);
for (var entry : origins.entrySet()) for (var origin : entry.getValue()) {
var type = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(entry.getKey()).value();
var start = level.getChunk(origin.x(), origin.z(), ChunkStatus.STRUCTURE_STARTS, true).getStartForStructure(type);
helper.assertTrue(start != null && start.isValid(), "Accepted geometry must have a native start");
for (var piece : start.getPieces()) {
helper.assertTrue(!(piece instanceof LostCityFoundationPiece16), "No artificial city foundation emitted");
if (entry.getKey().equals(HeritageStructures19.KEY)) helper.assertTrue(piece instanceof HeritagePiece21, "Use published21 architecture");
if (entry.getKey().equals(LostCityUndergroundStructures22.KEY)) helper.assertTrue(piece instanceof LostCityUndergroundPiece20, "Independent hall keeps published20 room geometry");
if (entry.getKey().equals(TransitStructures21.KEY)) helper.assertTrue(piece instanceof TransitPiece21 || piece instanceof TransitPiece22, "Published21 tunnel geometry with sealed22 corners");
piece.getBoundingBox().intersectingChunks().forEach(chunks::add);
}
var tag = start.createTag(context, origin);
var loaded = StructureStart.loadStaticStart(context, tag, level.getSeed());
helper.assertTrue(loaded != null && tag.equals(loaded.createTag(context, origin)), "Native start round trip");
nativeTags.put(entry.getKey().identifier() + "/" + origin.x() + "/" + origin.z(), tag.toString()); starts.add(start);
}
if (chunks.isEmpty()) chunks.add(new ChunkPos(0, 0));
report.put("native_starts", nativeTags); report.put("full_chunks", chunks.size());
Path world = level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize();
Path proof = world.resolve("selective22-create.json");
boolean reload = Boolean.getBoolean("sanctuary.test.expansionReload");
JsonObject previous = reload ? JsonParser.parseString(Files.readString(proof)).getAsJsonObject() : null;
if (reload) {
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload follows a successful separate JVM");
helper.assertTrue(previous.get("generator_codec").equals(encoded) && previous.get("geometry").equals(geometry), "Generation, options and accepted/refused placements survive reload");
helper.assertTrue(previous.get("native_starts").equals(gson.toJsonTree(nativeTags)), "Native saved starts survive reload");
}
var iterator = chunks.iterator();
Runnable finish = () -> {
try {
// Refresh already-generated transit chunks: the earlier short FULL tickets can expire
// while the catalogue loads. The walk checks real native collision shapes, including turns.
var transitChunks = new HashSet<ChunkPos>();
transit.modules().forEach(m -> TransitPiece21.bounds(m).intersectingChunks().forEach(transitChunks::add));
for (var pos : transitChunks) level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true);
report.put("transit_native_walk", Transit21WorldGameTests.verifyAccess(helper, transit));
report.put("hall_native_access", Underground22NativeChecks.verify(helper, hall.isEmpty() ? generator.undergroundPlan16(root) : hall));
if (!heritage.sites().isEmpty()) {
var site = heritage.sites().getFirst();
var witness = new BlockPos(site.centerX(), site.floorY() - 1, site.centerZ());
if (reload) helper.assertTrue(level.getBlockState(witness).is(Blocks.GOLD_BLOCK), "Personal edit in disposable test survives reload");
else {
helper.assertTrue(level.getBlockState(witness).isSolid(), "Accepted building foundation generated in real FULL chunks");
level.setBlock(witness, Blocks.GOLD_BLOCK.defaultBlockState(), 3);
}
report.put("saved_edit", List.of(witness.getX(), witness.getY(), witness.getZ()));
}
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush native world before shutdown");
report.put("passed", true);
var directory = Path.of("diagnostics", "selective22-" + level.getSeed() + "-" + generator.initialDiameter()); Files.createDirectories(directory);
var text = gson.toJson(report) + "\n"; Files.writeString(directory.resolve(reload ? "reload.json" : "create.json"), text);
if (!reload) Files.writeString(proof, text);
helper.succeed();
} catch (Exception failure) { throw new IllegalStateException(failure); }
};
Runnable[] load = new Runnable[1];
load[0] = () -> {
if (!iterator.hasNext()) { finish.run(); return; }
var pos = iterator.next(); level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true);
helper.runAtTickTime(helper.getTick() + 1, () -> load[0].run());
};
load[0].run();
}
}
@@ -339,7 +339,7 @@ public final class Transit21WorldGameTests {
masks.put(room, quarters);
return checked;
}
private static Map<String, Object> verifyAccess(GameTestHelper helper, TransitPlan21 plan) {
static Map<String, Object> verifyAccess(GameTestHelper helper, TransitPlan21 plan) {
var level = helper.getLevel();
var accesses = plan.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.ACCESS).toList();
helper.assertTrue(accesses.size() == 2, "Native transit requires exactly two surface gates");
@@ -0,0 +1,126 @@
package fr.koka.sanctuary.gametest;
import fr.koka.sanctuary.worldgen.city.LostCityUnderground16;
import fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Kind;
import fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Module;
import java.util.ArrayDeque;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.StairBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
/** Native circulation proof on the already generated piece footprint. */
public final class Underground22NativeChecks {
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
private Underground22NativeChecks() {}
public static Map<String, Integer> verify(GameTestHelper helper, LostCityUnderground16.Plan plan) {
var level = helper.getLevel(); var chunks = new HashSet<Long>();
int halls = 0, ladders = 0, apertures = 0, walked = 0;
helper.assertTrue(!plan.isEmpty(), "Independent underground hall plan must be present");
for (var network : plan.networks()) {
for (var m : network.modules()) {
for (int cx = m.minX() >> 4; cx <= m.maxX() >> 4; cx++) for (int cz = m.minZ() >> 4; cz <= m.maxZ() >> 4; cz++) {
long key = ((long) cx << 32) ^ (cz & 0xffffffffL);
// Short-lived read tickets can expire during the callers asynchronous footprint load.
// Refresh only these already generated chunks before inspecting their blocks.
if (chunks.add(key)) helper.assertTrue(level.getChunk(cx, cz, ChunkStatus.FULL, true) instanceof LevelChunk,
"Underground22 piece has no loaded FULL chunk at " + cx + "," + cz);
}
for (int d = 0; d < 4; d++) if ((m.openings() & 1 << d) != 0) {
for (int side = -1; side <= 1; side++) for (int dy = 1; dy <= 3; dy++) {
var at = mouth(m, d, side, dy);
helper.assertTrue(passable(level.getBlockState(at)), "Native utility doorway blocked at " + at + " in " + m.kind());
}
apertures++;
}
}
var access = network.modules().stream().filter(m -> m.kind() == Kind.ACCESS).findFirst().orElseThrow();
BlockPos hatch = null;
for (int x = access.minX(); x <= access.maxX(); x++) for (int z = access.minZ(); z <= access.maxZ(); z++) {
var at = new BlockPos(x, access.maxY() - 1, z);
if (level.getBlockState(at).is(Blocks.DARK_OAK_TRAPDOOR)) {
helper.assertTrue(hatch == null, "The access has more than one surface hatch"); hatch = at;
}
}
helper.assertTrue(hatch != null, "Independent access has no real wooden surface hatch");
final BlockPos entrance = hatch;
var closed = level.getBlockState(entrance);
for (int y = access.floorY() + 1; y < entrance.getY(); y++) {
var at = new BlockPos(entrance.getX(), y, entrance.getZ()); var ladder = level.getBlockState(at);
helper.assertTrue(ladder.is(Blocks.LADDER), "Access ladder is interrupted at " + at);
var wall = at.relative(ladder.getValue(BlockStateProperties.HORIZONTAL_FACING).getOpposite());
helper.assertTrue(level.getBlockState(wall).isCollisionShapeFullBlock(level, wall), "Ladder has no real backing wall at " + at);
ladders++;
}
level.setBlock(entrance, closed.setValue(BlockStateProperties.OPEN, true), 2);
try {
Predicate<BlockPos> inside = pos -> network.modules().stream().anyMatch(m -> m.contains(pos.getX(), pos.getY(), pos.getZ()));
BlockPos approach = java.util.Arrays.stream(HORIZONTAL).map(entrance::relative)
.filter(inside).filter(pos -> standable(level, pos)).findFirst().orElseThrow();
var reached = walk(helper, approach, inside); walked += reached.size();
helper.assertTrue(reached.contains(entrance), "Public deck cannot reach its hatch");
for (var m : network.modules()) {
helper.assertTrue(reached.stream().anyMatch(pos -> pos.getY() == m.floorY() + 1 && pos.getX() > m.minX()
&& pos.getX() < m.maxX() && pos.getZ() > m.minZ() && pos.getZ() < m.maxZ()),
"Surface access cannot reach emitted utility module " + m.kind() + " at " + m.centerX() + "," + m.centerZ());
if (m.kind() != Kind.EXPANSION_HALL) continue;
halls++;
var plinth = new BlockPos(m.centerX(), m.minY() + 2, m.centerZ());
helper.assertTrue(level.getBlockState(plinth).is(Blocks.SMOOTH_STONE) && reached.contains(plinth.above()),
"Reserved expansion plinth must be emitted and reachable");
for (int dx : new int[]{-6, 6}) for (int dz : new int[]{-6, 6})
helper.assertTrue(reached.contains(new BlockPos(m.centerX() + dx, m.floorY() + 1, m.centerZ() + dz)),
"Expansion hall has a blocked quarter");
}
} finally { level.setBlock(entrance, closed, 2); }
}
helper.assertTrue(halls > 0, "Native networks have no usable expansion hall");
var report = new LinkedHashMap<String, Integer>(); report.put("full_chunks", chunks.size()); report.put("usable_halls", halls);
report.put("ladder_blocks", ladders); report.put("clear_openings", apertures); report.put("reached_positions", walked);
return Map.copyOf(report);
}
private static BlockPos mouth(Module m, int direction, int across, int dy) {
return new BlockPos(direction == 1 ? m.maxX() : direction == 3 ? m.minX() : m.centerX() + across,
m.floorY() + dy, direction == 0 ? m.minZ() : direction == 2 ? m.maxZ() : m.centerZ() + across);
}
/** Same finite walking/ladder rule used by the native city16/20/21 access proofs. */
private static Set<BlockPos> walk(GameTestHelper helper, BlockPos start, Predicate<BlockPos> inside) {
var level = helper.getLevel(); var reached = new HashSet<BlockPos>(); var queue = new ArrayDeque<BlockPos>();
helper.assertTrue(inside.test(start) && standable(level, start), "Blocked utility approach"); reached.add(start); queue.add(start);
while (!queue.isEmpty()) {
var pos = queue.remove();
for (var direction : HORIZONTAL) for (int dy = -1; dy <= 1; dy++) {
var next = pos.relative(direction).offset(0, dy, 0);
if (!inside.test(next) || dy > 0 && !passable(level.getBlockState(pos.above(2)))) continue;
if (standable(level, next) && reached.add(next)) queue.add(next);
}
for (int dy : new int[]{-1, 1}) {
var next = pos.offset(0, dy, 0);
if (inside.test(next) && (level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(next).is(Blocks.LADDER)
|| level.getBlockState(pos.below()).is(Blocks.LADDER)) && standable(level, next) && reached.add(next)) queue.add(next);
}
helper.assertTrue(reached.size() < 100_000, "Utility traversal exceeded its bounded footprint");
}
return reached;
}
private static boolean standable(ServerLevel level, BlockPos pos) {
var below = level.getBlockState(pos.below()); var at = level.getBlockState(pos);
return passable(at) && passable(level.getBlockState(pos.above()))
&& (below.isSolid() || below.getBlock() instanceof StairBlock || at.is(Blocks.LADDER) || below.is(Blocks.LADDER));
}
private static boolean passable(BlockState state) {
return state.isAir() || state.is(Blocks.LADDER) || state.is(Blocks.REDSTONE_WALL_TORCH)
|| state.is(Blocks.DARK_OAK_TRAPDOOR) && state.getValue(BlockStateProperties.OPEN);
}
}
@@ -55,8 +55,12 @@ final class UnifiedWorldSettingsDiagnostics {
var historical20 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v20")));
helper.assertTrue(historical20.value().createWorldDimensions().overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 20
&& old.source().generation() == 20 && old.experimentalStructures(), "Historical20 keeps its original architecture");
helper.assertTrue(generator.rootGeneration() == 21 && generator.source().generation() == 21 && generator.experimentalStructures(), "The public preset uses generation21 with its new-world option");
var historical21 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v21")));
helper.assertTrue(historical21.value().createWorldDimensions().overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 21
&& old.source().generation() == 21 && old.experimentalStructures(), "Historical21 keeps its original placement rules");
helper.assertTrue(generator.rootGeneration() == 22 && generator.source().generation() == 22 && generator.experimentalStructures(), "The public preset uses generation22 with its new-world option");
helper.assertTrue(generator.initialDiameter() == 724, "The public preset defaults to Medium / 724 blocks");
verifyConstructionMasks(helper, (SanctuaryChunkGenerator) historical21.value().createWorldDimensions().overworld(), generator);
for (var tag : List.of(WorldPresetTags.NORMAL, WorldPresetTags.EXTENDED)) {
var visible = presets.getOrThrow(tag).stream().map(holder -> holder.getRegisteredName())
.filter(name -> name.startsWith("sanctuary:")).collect(Collectors.toSet());
@@ -65,11 +69,11 @@ final class UnifiedWorldSettingsDiagnostics {
}
var ops = RegistryOps.create(JsonOps.INSTANCE, access);
var activeCodec = generator.rootGeneration() == 21 ? SanctuaryChunkGenerator.NETWORK21_CODEC : generator.rootGeneration() == 20 ? SanctuaryChunkGenerator.ARCHITECTURE20_CODEC : generator.rootGeneration() == 19 ? SanctuaryChunkGenerator.HERITAGE19_CODEC : generator.rootGeneration() == 17 ? SanctuaryChunkGenerator.RUINS17_CODEC : generator.rootGeneration() == 16 ? SanctuaryChunkGenerator.CITY16_CODEC : generator.rootGeneration() == 15 ? SanctuaryChunkGenerator.CITY15_CODEC : generator.rootGeneration() == 14 ? SanctuaryChunkGenerator.CITY_CODEC : SanctuaryChunkGenerator.CODEC;
var activeCodec = generator.rootGeneration() == 22 ? SanctuaryChunkGenerator.SELECTIVE22_CODEC : generator.rootGeneration() == 21 ? SanctuaryChunkGenerator.NETWORK21_CODEC : generator.rootGeneration() == 20 ? SanctuaryChunkGenerator.ARCHITECTURE20_CODEC : generator.rootGeneration() == 19 ? SanctuaryChunkGenerator.HERITAGE19_CODEC : generator.rootGeneration() == 17 ? SanctuaryChunkGenerator.RUINS17_CODEC : generator.rootGeneration() == 16 ? SanctuaryChunkGenerator.CITY16_CODEC : generator.rootGeneration() == 15 ? SanctuaryChunkGenerator.CITY15_CODEC : generator.rootGeneration() == 14 ? SanctuaryChunkGenerator.CITY_CODEC : SanctuaryChunkGenerator.CODEC;
var serialized = activeCodec.codec().encodeStart(ops, generator).getOrThrow().getAsJsonObject();
serialized.remove("experimental_structures");
helper.assertTrue(activeCodec.codec().parse(ops, serialized).getOrThrow().experimentalStructures(),
"Generation21 omitted experimental option defaults to enabled");
"Generation22 omitted experimental option defaults to enabled");
helper.assertTrue(!((SanctuaryChunkGenerator) historical16.overworld()).withExperimentalStructures(true).experimentalStructures(),
"The new-world switch cannot activate experimental generation in a historical16 generator");
serialized.remove("initial_diameter");
@@ -112,4 +116,37 @@ final class UnifiedWorldSettingsDiagnostics {
"public_presets", List.of("sanctuary:sanctuary"), "sizes", results,
"invalid_diameters_rejected", true, "copied_biome_source", true, "passed", true)) + "\n");
}
private static void verifyConstructionMasks(GameTestHelper helper, SanctuaryChunkGenerator historical21,
SanctuaryChunkGenerator generation22) {
var outside = new net.minecraft.core.BlockPos(200_000, 192, 200_000);
// Decoding a generation must not reserve the world before any structures are planned.
// These checks also cover the old OFF contract without initializing another island.
for (var generator : List.of(historical21, historical21.withExperimentalStructures(false),
generation22, generation22.withExperimentalStructures(false))) {
helper.assertTrue(!generator.protectsCityDecoration(outside),
"Unplanned generation" + generator.rootGeneration() + " does not protect arbitrary blocks");
helper.assertTrue(!generator.intersectsExperimentalStructures(outside.getX(), outside.getY(), outside.getZ(),
outside.getX() + 1, outside.getY() + 1, outside.getZ() + 1),
"Unplanned generation" + generator.rootGeneration() + " does not reject arbitrary structure bounds");
}
// Reuse the attached world's real plans: no extra native-noise planning or chunk loads.
if (!(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator active)
|| active.rootGeneration() != 21 && active.rootGeneration() != 22) return;
helper.assertTrue(!active.protectsCityDecoration(outside)
&& !active.intersectsExperimentalStructures(outside.getX(), outside.getY(), outside.getZ(),
outside.getX() + 1, outside.getY() + 1, outside.getZ() + 1),
"Prepared construction masks remain local to their real structures");
var interior = new ArrayList<net.minecraft.core.BlockPos>();
for (var module : active.transitPlan21().modules())
interior.add(new net.minecraft.core.BlockPos(module.centerX(), module.floorY(), module.centerZ()));
for (var network : active.undergroundHall22().networks()) for (var module : network.modules())
interior.add(new net.minecraft.core.BlockPos(module.centerX(), module.floorY(), module.centerZ()));
if (active.rootGeneration() == 22 && helper.getLevel().structureManager().shouldGenerateStructures())
helper.assertTrue(!interior.isEmpty(), "The attached generation22 world has core construction to protect");
for (var position : interior) helper.assertTrue(active.protectsCityDecoration(position)
&& active.intersectsExperimentalStructures(position.getX(), position.getY(), position.getZ(),
position.getX(), position.getY(), position.getZ()),
"Real underground construction remains protected from decoration and overlapping native structures");
}
}
@@ -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.heritage20") || Boolean.getBoolean("sanctuary.test.heritage21") || Boolean.getBoolean("sanctuary.test.lostCity21") || Boolean.getBoolean("sanctuary.test.transit21")) || Boolean.getBoolean("sanctuary.test.lostCity20")
if (Boolean.getBoolean("sanctuary.test.selective22") || (Boolean.getBoolean("sanctuary.test.heritage20") || Boolean.getBoolean("sanctuary.test.heritage21") || Boolean.getBoolean("sanctuary.test.lostCity21") || Boolean.getBoolean("sanctuary.test.transit21")) || Boolean.getBoolean("sanctuary.test.lostCity20")
|| Boolean.getBoolean("sanctuary.test.heritage19") || Boolean.getBoolean("sanctuary.test.ruins17") || Boolean.getBoolean("sanctuary.test.expansion") || Boolean.getBoolean("sanctuary.test.lostCity")
|| Boolean.getBoolean("sanctuary.test.lostCity16") || Boolean.getBoolean("sanctuary.test.lostCity15") || Boolean.getBoolean("sanctuary.test.voidReuse")) {
long until = System.nanoTime() + 20_000_000L;
@@ -47,9 +47,9 @@ public abstract class SanctuaryGameTestServerMixin {
"Lnet/minecraft/world/level/levelgen/presets/WorldPresets;FLAT_ALL_DIMENSIONS:Lnet/minecraft/resources/ResourceKey;"))
private static ResourceKey<WorldPreset> sanctuary$selectActualPreset() {
if (Boolean.getBoolean("sanctuary.test.unified")) {
int generation = Integer.getInteger("sanctuary.test.generation", 21);
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21) throw new IllegalArgumentException("Unsupported test generation");
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 21 ? "sanctuary" : "sanctuary_v" + generation));
int generation = Integer.getInteger("sanctuary.test.generation", 22);
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22) throw new IllegalArgumentException("Unsupported test generation");
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 22 ? "sanctuary" : "sanctuary_v" + generation));
}
if (Boolean.getBoolean("sanctuary.test.expansion")) return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("expansion_lab"));
int players = Integer.getInteger("sanctuary.test.players", 10);
@@ -67,7 +67,10 @@ public final class SanctuaryMod implements ModInitializer {
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v20"), SanctuaryBiomeSource.ARCHITECTURE20_CODEC);
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v21"), SanctuaryChunkGenerator.NETWORK21_CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v21"), SanctuaryBiomeSource.NETWORK21_CODEC);
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v22"), SanctuaryChunkGenerator.SELECTIVE22_CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v22"), SanctuaryBiomeSource.SELECTIVE22_CODEC);
fr.koka.sanctuary.worldgen.transit.TransitStructures21.register();
fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.register();
fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.register();
fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.register();
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("population_island"), PopulationIslandDensity.CODEC);
@@ -51,11 +51,11 @@ public final class SanctuaryWorldOptionsScreen extends Screen {
contents.addChild(sizeButton);
diameterHint = contents.addChild(new MultiLineTextWidget(diameterHint(), font)
.setMaxWidth(310).setCentered(true));
if (generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21) {
if (generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22) {
contents.addChild(CycleButton.builder((Boolean enabled) -> enabled ? CommonComponents.OPTION_ON : CommonComponents.OPTION_OFF, experimental)
.withValues(true, false).create(0, 0, 280, 20, Component.translatable("sanctuary.world_options.experimental"),
(button, value) -> experimental = value));
contents.addChild(new MultiLineTextWidget(Component.translatable((generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21) ? "sanctuary.world_options.heritage_hint" : "sanctuary.world_options.experimental_hint"), font)
contents.addChild(new MultiLineTextWidget(Component.translatable(generator.rootGeneration() == 22 ? "sanctuary.world_options.heritage22_hint" : (generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22) ? "sanctuary.world_options.heritage_hint" : "sanctuary.world_options.experimental_hint"), font)
.setMaxWidth(310).setCentered(true));
}
layout.addToContents(contents);
@@ -124,6 +124,7 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.generate(this, registries, state, structures, chunk, templates, dimension);
fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.generate(this, registries, state, structures, chunk, templates, dimension);
fr.koka.sanctuary.worldgen.transit.TransitStructures21.generate(this, registries, state, structures, chunk, templates, dimension);
fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.generate(this, registries, state, structures, chunk, templates, dimension);
ExpansionStructures.generate(this, registries, state, structures, chunk, templates, dimension);
}
@Override public void spawnOriginalMobs(WorldGenRegion region) {
@@ -41,7 +41,7 @@ public record ExpansionIsland(String id, String parent, String direction, String
if (!((spatialGeneration(generation) ? DIAMETERS : LEGACY_DIAMETERS).contains(diameter) || (id.equals("sanctuary") && (diameter == 724 || (generation == 13 || spatialGeneration(generation)) && diameter == 1024)))) throw new IllegalArgumentException("Unsupported nominal diameter");
if (Math.abs((long) centerX) > 100_000 || Math.abs((long) centerZ) > 100_000 || (centerX & 15) != 0 || (centerZ & 15) != 0) throw new IllegalArgumentException("Invalid island centre");
if (!List.of("reserved", "ready").contains(status) || (generation != GENERATION && !spatialGeneration(generation) && !(generation == 13 && id.equals("sanctuary")))) throw new IllegalArgumentException("Unsupported island state or generation");
if (climate.equals("lost_city") && (!spatialGeneration(generation) || id.equals("sanctuary"))) throw new IllegalArgumentException("Lost City requires a generation 14, 15, 16, 17, 19, 20 or 21 expansion");
if (climate.equals("lost_city") && (!spatialGeneration(generation) || id.equals("sanctuary"))) throw new IllegalArgumentException("Lost City requires a generation 14, 15, 16, 17, 19, 20, 21 or 22 expansion");
if (reuseToken == null || !reuseToken.isEmpty() && (!spatialGeneration(generation) || id.equals("sanctuary") || !reuseToken.matches("[0-9a-f]{64}")))
throw new IllegalArgumentException("Invalid void reuse certificate token");
}
@@ -53,7 +53,7 @@ public record ExpansionIsland(String id, String parent, String direction, String
return new ExpansionIsland("sanctuary", "sanctuary", "north", "temperate", diameter, 0, 0, seed, "natural", "ready", generation);
}
public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21; }
public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22; }
public boolean origin() { return id.equals("sanctuary"); }
public double referenceDiameter() { return origin() && diameter == 724 ? Math.sqrt(2) * 512 : 512; }
public double horizontalScale() { return origin() ? 1.0 : diameter / referenceDiameter(); }
@@ -42,7 +42,7 @@ public abstract class UnifiedDecorationProtectionMixin {
if (state.is(Blocks.SUGAR_CANE)) return;
// Generation16 reserves its architectural volume before decoration. Only its own
// native pieces may replace water there; published generations retain the old rule.
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21)
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22)
&& fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
&& generator.protectsCityDecoration(pos)) return;
if (generator.protectsDecoration(pos) && !region.getBlockState(pos).equals(state)) {
@@ -54,6 +54,10 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 21)));
public static final MapCodec<SanctuaryBiomeSource> SELECTIVE22_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 22)));
private final List<Holder<Biome>> allBiomes;
private final PopulationIslandBiomeSource population;
private final boolean cities;
@@ -87,7 +91,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
cityPlans = java.util.Map.copyOf(plans);
}
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; }
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 22 ? SELECTIVE22_CODEC : generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; }
@Override public BiomeResolver createResolver(Climate.Sampler sampler) {
BiomeResolver woodlands = population.createResolver(sampler);
@@ -63,6 +63,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 21, experimental)));
public static final MapCodec<SanctuaryChunkGenerator> SELECTIVE22_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
SanctuaryBiomeSource.SELECTIVE22_CODEC.fieldOf("biome_source").forGetter(SanctuaryChunkGenerator::source),
DIAMETER_CODEC.optionalFieldOf("initial_diameter", 724).forGetter(SanctuaryChunkGenerator::initialDiameter),
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 22, experimental)));
private final boolean experimentalStructures;
private volatile fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17 = fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.EMPTY;
private boolean ruinsPrepared;
@@ -71,6 +77,10 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
private final java.util.Map<ExpansionIsland, fr.koka.sanctuary.worldgen.city.LostCityPlanner21.Result> cityPlacements21 = new java.util.concurrent.ConcurrentHashMap<>();
private volatile fr.koka.sanctuary.worldgen.transit.TransitPlan21 transitPlan21 = fr.koka.sanctuary.worldgen.transit.TransitPlan21.EMPTY;
private boolean transitPrepared;
private volatile fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan undergroundHall22 = new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
private volatile fr.koka.sanctuary.worldgen.city.LostCityProtection16 undergroundHallProtection22 = fr.koka.sanctuary.worldgen.city.LostCityProtection16.EMPTY;
private boolean undergroundHallPrepared22;
public fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan undergroundHall22() { return undergroundHall22; }
public fr.koka.sanctuary.worldgen.transit.TransitPlan21 transitPlan21() { return transitPlan21; }
public fr.koka.sanctuary.worldgen.city.LostCityPlanner21.Result cityPlacement21(ExpansionIsland island) {
lostCityPlan(island);
@@ -111,12 +121,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
}
private SanctuaryChunkGenerator(SanctuaryBiomeSource source, int initialDiameter, int generation, boolean experimental) {
super(source, initialDiameter, true);
this.experimentalStructures = (generation == 17 || generation == 19 || generation == 20 || generation == 21) && experimental;
this.experimentalStructures = (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) && experimental;
this.generation = generation;
if (generation != source.generation()) throw new IllegalArgumentException("Sanctuary generator and biome source versions must agree");
}
@Override public SanctuaryBiomeSource source() { return (SanctuaryBiomeSource) super.source(); }
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; }
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 22 ? SELECTIVE22_CODEC : generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; }
public SanctuaryChunkGenerator withInitialDiameter(int diameter) {
return new SanctuaryChunkGenerator(source().copy(), diameter, generation, experimentalStructures);
}
@@ -127,9 +137,10 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
public fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17() { return ruinsPlan17; }
public fr.koka.sanctuary.worldgen.heritage.HeritagePlan19 heritagePlan19() { return heritagePlan19; }
public boolean intersectsExperimentalStructures(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
return (generation == 17 || generation == 19 || generation == 20 || generation == 21) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 19 || generation == 20 || generation == 21) && heritagePlan19.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| generation == 21 && transitPlan21.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ);
return (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22) && heritagePlan19.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 21 || generation == 22) && transitPlan21.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| generation == 22 && undergroundHallProtection22.intersects(minX, minY, minZ, maxX, maxY, maxZ);
}
public IslandCapacity capacity() {
return switch (initialDiameter()) {
@@ -164,6 +175,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
// Publish one root context before any generation workers use it.
context(islands.getFirst());
prepareCities(islands);
if (generation == 22) { prepareTransit(islands); prepareUndergroundHall22(islands); }
prepareRuins(islands);
prepareHeritage(islands);
prepareTransit(islands);
@@ -171,6 +183,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
@Override public void setIslands(List<ExpansionIsland> islands) {
// Complete and publish city geometry before workers can observe the new owner list.
prepareCities(islands);
if (generation == 22) { prepareTransit(islands); prepareUndergroundHall22(islands); }
prepareRuins(islands);
prepareHeritage(islands);
prepareTransit(islands);
@@ -184,7 +197,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
source().setCityPlans(java.util.Map.of());
return;
}
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21) && !cityStructuresEnabled) {
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) && !cityStructuresEnabled) {
cityProtection16 = fr.koka.sanctuary.worldgen.city.LostCityProtection16.EMPTY;
activeCityPlans = java.util.Map.of();
source().setCityPlans(java.util.Map.of());
@@ -200,7 +213,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
}
cityProtection15 = protection.build();
}
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21) {
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) {
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
for (var island : islands) {
protection.add(published.get(island.id()), undergroundPlan16(island));
@@ -215,11 +228,11 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
source().setCityPlans(published);
}
private void prepareRuins(List<ExpansionIsland> islands) {
if ((generation != 17 && generation != 19 && generation != 20 && generation != 21) || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return;
if ((generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22) || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var terrain = context(root);
var sampled = new fr.koka.sanctuary.worldgen.ruins.RuinsTerrain17(root, terrain.generator(), terrain.random(),
heightAccessor, pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()));
heightAccessor, pos -> protectsCore22(pos) || protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()));
long start = System.nanoTime();
var plan = fr.koka.sanctuary.worldgen.ruins.RuinsPlanner17.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
ruinsPlan17 = plan;
@@ -228,25 +241,27 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
root.seed(), plan.networks().size(), plan.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000);
}
private void prepareHeritage(List<ExpansionIsland> islands) {
if ((generation != 19 && generation != 20 && generation != 21) || !experimentalStructures || !cityStructuresEnabled || heritagePrepared) return;
if ((generation != 19 && generation != 20 && generation != 21 && generation != 22) || !experimentalStructures || !cityStructuresEnabled || heritagePrepared) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var terrain = context(root);
if (generation == 21) {
if (generation == 21 || generation == 22) {
var sampled = new fr.koka.sanctuary.worldgen.heritage.HeritageTerrain21(root, terrain.generator(), terrain.random(),
heightAccessor, pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
heightAccessor, pos -> protectsCore22(pos) || protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()));
long start = System.nanoTime();
sampled.discoverLandmarks(lostCityPlan(root), ruinsPlan17);
var plan = fr.koka.sanctuary.worldgen.heritage.HeritagePlanner21.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
var plan = generation == 22
? fr.koka.sanctuary.worldgen.heritage.HeritagePlanner22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
: fr.koka.sanctuary.worldgen.heritage.HeritagePlanner21.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
heritagePlan19 = plan;
heritagePrepared = true;
SanctuaryMod.LOGGER.info("Heritage21 seed {}: {} sites, {} routes, {} segments, {} probes, {} native columns in {} ms; {}",
SanctuaryMod.LOGGER.info("Heritage" + generation + " seed {}: {} sites, {} routes, {} segments, {} probes, {} native columns in {} ms; {}",
root.seed(), plan.sites().size(), plan.links().size(), plan.segments().size(), plan.samples(), sampled.columnsUsed(),
(System.nanoTime() - start) / 1_000_000, plan.diagnostics());
return;
}
var sampled = new fr.koka.sanctuary.worldgen.heritage.HeritageTerrain19(root, terrain.generator(), terrain.random(),
heightAccessor, pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
heightAccessor, pos -> protectsCore22(pos) || protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()));
long start = System.nanoTime();
sampled.discoverLandmarks(lostCityPlan(root), ruinsPlan17);
@@ -258,17 +273,55 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
(System.nanoTime() - start) / 1_000_000, plan.diagnostics());
}
private void prepareTransit(List<ExpansionIsland> islands) {
if (generation != 21 || !experimentalStructures || !cityStructuresEnabled || transitPrepared) return;
if ((generation != 21 && generation != 22) || generation == 21 && !experimentalStructures || !cityStructuresEnabled || transitPrepared) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var terrain = context(root);
var sampled = new fr.koka.sanctuary.worldgen.transit.TransitTerrain21(root, terrain.generator(), terrain.random(),
heightAccessor, pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
heightAccessor, pos -> protectsCore22(pos) || protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()) || heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ()));
long start = System.nanoTime();
transitPlan21 = fr.koka.sanctuary.worldgen.transit.TransitPlanner21.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
transitPlan21 = generation == 22
? fr.koka.sanctuary.worldgen.transit.TransitPlanner22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
: fr.koka.sanctuary.worldgen.transit.TransitPlanner21.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
int directColumns = 0;
if (generation == 22 && transitPlan21.isEmpty()) {
directColumns = sampled.columnsUsed();
sampled = new fr.koka.sanctuary.worldgen.transit.TransitTerrain21(root, terrain.generator(), terrain.random(),
heightAccessor, pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()));
transitPlan21 = fr.koka.sanctuary.worldgen.transit.TransitRoute22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
}
transitPrepared = true;
SanctuaryMod.LOGGER.info("Transit21 seed {}: {} modules, {} probes, {} columns in {} ms; {}", root.seed(),
transitPlan21.modules().size(), transitPlan21.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000, transitPlan21.diagnostic());
SanctuaryMod.LOGGER.info("Transit" + generation + " seed {}: {} modules, {} probes, {} columns in {} ms; {}", root.seed(),
transitPlan21.modules().size(), transitPlan21.samples(), directColumns + sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000, transitPlan21.diagnostic());
}
private boolean protectsCore22(net.minecraft.core.BlockPos pos) {
return generation == 22 && (transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ())
|| undergroundHallProtection22.contains(pos.getX(), pos.getY(), pos.getZ()));
}
private void prepareUndergroundHall22(List<ExpansionIsland> islands) {
if (generation != 22 || !cityStructuresEnabled || undergroundHallPrepared22) return;
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
if (undergroundPlan16(root).networks().stream().flatMap(n -> n.modules().stream())
.anyMatch(m -> m.kind() == fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Kind.EXPANSION_HALL)) {
undergroundHallPrepared22 = true; return;
}
var terrain = context(root);
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain21(root, terrain.generator(), terrain.random(), heightAccessor,
pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()) || protectsCore22(pos));
long start = System.nanoTime();
undergroundHall22 = fr.koka.sanctuary.worldgen.city.LostCityUnderground22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(),
new fr.koka.sanctuary.worldgen.city.LostCityUnderground22.Terrain() {
@Override public int surfaceY(int x, int z) { var ground = sampled.ground(x, z); return ground.dry() ? ground.top() : -1; }
@Override public boolean owns(int x, int z) { return sampled.owns(x, z); }
@Override public boolean solid(int x, int y, int z) { return sampled.solid(x, y, z); }
@Override public boolean protectedAt(int x, int y, int z) { return sampled.protectedAt(x, y, z); }
@Override public boolean exteriorAir(int x, int y, int z, int dx, int dz) { return sampled.exteriorAir(x, y, z, dx, dz); }
});
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
protection.add(fr.koka.sanctuary.worldgen.city.LostCityPlan.EMPTY, undergroundHall22);
undergroundHallProtection22 = protection.build(); undergroundHallPrepared22 = true;
SanctuaryMod.LOGGER.info("Independent hall22 seed {}: {} networks, {} probes, {} columns in {} ms", root.seed(),
undergroundHall22.networks().size(), undergroundHall22.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000);
}
private static ExpansionIsland cityIdentity(ExpansionIsland island) {
// State and receipt do not change the terrain geometry used by a cached plan.
@@ -296,11 +349,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
return cityPlans.computeIfAbsent(cityIdentity(island), ignored -> {
long start = System.nanoTime();
var terrain = context(island);
if (generation == 21) {
if (generation == 21 || generation == 22) {
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain21(island, terrain.generator(), terrain.random(),
heightAccessor, pos -> island.origin() && protectsDecoration(pos));
var placement = fr.koka.sanctuary.worldgen.city.LostCityPlanner21.plan(island.seed(), island.centerX(), island.centerZ(),
island.diameter(), island.origin(), sampled);
var placement = generation == 22
? fr.koka.sanctuary.worldgen.city.LostCityPlanner22.plan(island.seed(), island.centerX(), island.centerZ(), island.diameter(), island.origin(), sampled)
: fr.koka.sanctuary.worldgen.city.LostCityPlanner21.plan(island.seed(), island.centerX(), island.centerZ(), island.diameter(), island.origin(), sampled);
var surface = placement.surface();
cityPlacements21.put(cityIdentity(island), placement);
citySupports16.put(cityIdentity(island), placement.supports());
@@ -323,10 +377,10 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
}
});
undergroundPlans16.put(cityIdentity(island), underground);
SanctuaryMod.LOGGER.info("Lost City21 {} seed {}: {} city districts, {} underground networks, {} noise columns in {} ms",
SanctuaryMod.LOGGER.info("Lost City" + generation + " {} seed {}: {} city districts, {} underground networks, {} noise columns in {} ms",
island.id(), island.seed(), surface.districts().size(), underground.networks().size(), sampled.columnsUsed(),
(System.nanoTime() - start) / 1_000_000);
SanctuaryMod.LOGGER.info("City21 extensions: {}", placement.diagnostics());
SanctuaryMod.LOGGER.info("City" + generation + " extensions: {}", placement.diagnostics());
return surface;
}
if (generation == 16 || generation == 17 || generation == 19 || generation == 20) {
@@ -400,10 +454,11 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
});
}
public boolean protectsCityDecoration(net.minecraft.core.BlockPos pos) {
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21) && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 19 || generation == 20 || generation == 21) && heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ())
|| generation == 21 && transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ());
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22) && heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 21 || generation == 22) && transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ())
|| generation == 22 && undergroundHallProtection22.contains(pos.getX(), pos.getY(), pos.getZ());
if (generation == 15) return cityProtection15.contains(pos.getX(), pos.getY(), pos.getZ());
if (generation != 14) return false;
var island = islandAt(pos.getX(), pos.getZ());
@@ -425,20 +480,20 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
return citySupports.getOrDefault(cityIdentity(island), List.of());
}
public boolean intersectsCityConstruction16(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
return (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
return (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
}
public fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan undergroundPlan16(ExpansionIsland island) {
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
lostCityPlan(island);
return undergroundPlans16.getOrDefault(cityIdentity(island), new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0));
}
public List<LostCityPlanner16.Support> citySupports16(ExpansionIsland island) {
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21) return List.of();
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22) return List.of();
lostCityPlan(island);
return citySupports16.getOrDefault(cityIdentity(island), List.of());
}
public LostCityPlanner16.Fallback cityReservation16(ExpansionIsland island) {
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21) return null;
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22) return null;
lostCityPlan(island);
return cityReservations16.get(cityIdentity(island));
}
@@ -448,13 +503,15 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
@Override public com.mojang.datafixers.util.Pair<net.minecraft.core.BlockPos, net.minecraft.core.Holder<net.minecraft.world.level.levelgen.structure.Structure>> findNearestMapStructure(
ServerLevel level, net.minecraft.core.HolderSet<net.minecraft.world.level.levelgen.structure.Structure> structures,
net.minecraft.core.BlockPos origin, int radius, boolean skipReferenced) {
if (generation == 22 && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.KEY))
return fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.findNearest(level, structures, origin, radius, skipReferenced);
if (structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.city.LostCityStructures.KEY))
return fr.koka.sanctuary.worldgen.city.LostCityStructures.findNearest(level, structures, origin, radius, skipReferenced);
if ((generation == 17 || generation == 19 || generation == 20 || generation == 21) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.KEY))
if ((generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.KEY))
return fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.findNearest(level, structures, origin, radius, skipReferenced);
if ((generation == 19 || generation == 20 || generation == 21) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.KEY))
if ((generation == 19 || generation == 20 || generation == 21 || generation == 22) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.KEY))
return fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.findNearest(level, structures, origin, radius, skipReferenced);
if (generation == 21 && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.transit.TransitStructures21.KEY))
if ((generation == 21 || generation == 22) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.transit.TransitStructures21.KEY))
return fr.koka.sanctuary.worldgen.transit.TransitStructures21.findNearest(level, structures, origin, radius, skipReferenced);
return super.findNearestMapStructure(level, structures, origin, radius, skipReferenced);
}
@@ -483,7 +540,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
@Override protected void afterTerrain(ExpansionIsland island, ChunkAccess chunk) {
// Root water/lava are already applied by the Population hook on the named delegate.
if (!island.origin()) super.afterTerrain(island, chunk);
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21) && cityStructuresEnabled) {
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) && cityStructuresEnabled) {
var reserved = cityReservations16.get(cityIdentity(island));
if (reserved != null) {
var mutable = new net.minecraft.core.BlockPos.MutableBlockPos();
@@ -0,0 +1,147 @@
package fr.koka.sanctuary.worldgen.city;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.District;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Kind;
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Parcel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/** Generation22 only: earlier planners propose geometry, but cannot authorize artificial terrain.
* Proposals and their forced reservations are read-only; only fully recertified natural towns
* reach the returned plan. The published generation21 solver and all renderers stay unchanged. */
public final class LostCityPlanner22 {
public static final int MAX_COLUMNS = LostCityPlanner21.MAX_COLUMNS;
public static final int MAX_VOLUME_CHECKS = LostCityPlanner21.MAX_VOLUME_CHECKS;
public static final int MAX_ATTEMPTS = 3, MAX_CUT = 2, MAX_FILL = 2;
private static final LostCityPlanner.Ground INVALID = new LostCityPlanner.Ground(0, 0, false, true);
private LostCityPlanner22() {}
public static LostCityPlanner21.Result plan(long seed, int cx, int cz, int diameter, boolean origin,
LostCityPlanner16.Terrain terrain) {
if (diameter < 64 || diameter > 1024 || origin && diameter < 512)
throw new IllegalArgumentException("Unsupported natural city island size");
var probe = new Probe(terrain);
for (int attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
probe.reject("proposal-attempt");
long proposalSeed = attempt == 0 ? seed : mix(seed ^ attempt * 0x22C175L);
try {
var proposal = LostCityPlanner21.plan(proposalSeed, cx, cz, diameter, origin, probe);
if (proposal.reservation() != null) { probe.reject("forced-reservation-rejected"); continue; }
if (proposal.core().surface().isEmpty()) { probe.reject("no-natural-core"); continue; }
if (!proposal.core().supports().isEmpty()) { probe.reject("core-pier-rejected"); continue; }
boolean valid = true;
for (var district : proposal.core().surface().districts())
if (!validate(district, probe)) { valid = false; break; }
if (!valid) continue;
var districts = new ArrayList<>(proposal.core().surface().districts());
var connections = new ArrayList<LostCityPlanner21.Connection>();
for (var connection : proposal.connections()) {
try {
var district = proposal.surface().districts().get(connection.districtIndex());
if (proposal.supports().stream().anyMatch(s -> s.startChunkX() == district.startChunkX()
&& s.startChunkZ() == district.startChunkZ())) {
probe.reject("satellite-pier-rejected"); continue;
}
if (!validate(district, probe)) continue;
var retained = new ArrayList<>(districts); retained.add(district);
if (!connected(connection.path(), retained)) {
probe.reject("satellite-depends-on-rejected-road"); continue;
}
connections.add(new LostCityPlanner21.Connection(districts.size(), connection.path()));
districts.add(district);
} catch (Exhausted exhausted) { probe.reject("satellite-budget"); break; }
}
probe.reject("natural-core-accepted");
return result(proposal.core(), new LostCityPlan(districts), connections, probe);
} catch (Exhausted exhausted) {
probe.reject("natural-search-budget"); break;
} catch (IllegalArgumentException rejectedReservation) {
// The immutable16 proposal solver requires an owned fallback envelope. In22 an
// island with no such envelope is simply unsuitable; unrelated errors still fail.
String message = rejectedReservation.getMessage();
if (!"Starter terrain does not own a central 49x49 city envelope".equals(message)
&& !"Starter city reservation crosses another island's ownership".equals(message)) throw rejectedReservation;
probe.reject("unowned-reservation-rejected");
}
}
return result(new LostCityPlanner16.Result(LostCityPlan.EMPTY, List.of(), 0, 0),
LostCityPlan.EMPTY, List.of(), probe);
}
private static LostCityPlanner21.Result result(LostCityPlanner16.Result core, LostCityPlan surface,
List<LostCityPlanner21.Connection> connections, Probe probe) {
return new LostCityPlanner21.Result(core, surface, List.of(), probe.columns.size(), probe.checks,
connections, probe.diagnostics);
}
/** Validate actual emitted footprint and bearing columns, not just a few terrain samples.
* Eight-block pool/garage basements remain possible only when entirely inside native rock. */
private static boolean validate(District district, Probe probe) {
for (Parcel parcel : district.parcels()) {
if (parcel.kind() == Kind.BRIDGE) { probe.reject("suspended-bridge-rejected"); return false; }
for (int z = parcel.minZ(); z <= parcel.maxZ(); z++) for (int x = parcel.minX(); x <= parcel.maxX(); x++) {
var ground = probe.ground(x, z);
if (!ground.dry() || ground.protectedTerrain() || ground.solidDepth() < 4) {
probe.reject("not-dry-natural-ground"); return false;
}
int floor = parcel.surfaceY(x - parcel.minX(), z - parcel.minZ());
if (ground.top() - floor > MAX_CUT || floor - ground.top() > MAX_FILL) {
probe.reject("excess-cut-or-fill"); return false;
}
int bottom = floor - parcel.foundationDepth();
// Two unpainted rock layers must carry every column. The entire native part of
// each footing stays solid: no borrowed corner certificate across a cavity.
for (int y = bottom - 2; y <= Math.min(floor - 1, ground.top()); y++) {
if (!probe.solid(x, y, z)) { probe.reject("incomplete-natural-bearing"); return false; }
}
for (int y = bottom - 2; y <= parcel.maxY(); y++) {
if (probe.protectedAt(x, y, z)) { probe.reject("protected-construction-volume"); return false; }
}
}
}
return true;
}
private static boolean connected(List<LostCityPlanner21.Point> path, List<District> retained) {
for (var point : path) {
boolean found = false;
for (var district : retained) for (var parcel : district.parcels()) {
if (parcel.kind() == Kind.STREET && point.x() >= parcel.minX() && point.x() <= parcel.maxX()
&& point.z() >= parcel.minZ() && point.z() <= parcel.maxZ()
&& parcel.surfaceY(point.x() - parcel.minX(), point.z() - parcel.minZ()) == point.floorY()) found = true;
}
if (!found) return false;
}
return true;
}
private static final class Probe implements LostCityPlanner16.Terrain {
final LostCityPlanner16.Terrain terrain;
final Map<Long, LostCityPlanner.Ground> columns = new HashMap<>();
final Map<String, Integer> diagnostics = new LinkedHashMap<>();
int checks;
Probe(LostCityPlanner16.Terrain terrain) { this.terrain = java.util.Objects.requireNonNull(terrain); }
@Override public boolean owns(int x, int z) { return terrain.owns(x, z); }
@Override public LostCityPlanner.Ground ground(int x, int z) {
if (!owns(x, z)) return INVALID;
long key = (long) x << 32 ^ z & 0xffffffffL;
var cached = columns.get(key); if (cached != null) return cached;
if (columns.size() >= MAX_COLUMNS) throw new Exhausted();
var result = java.util.Objects.requireNonNull(terrain.ground(x, z)); columns.put(key, result); return result;
}
@Override public boolean solid(int x, int y, int z) {
count(); return ground(x, z) != INVALID && terrain.solid(x, y, z);
}
@Override public boolean protectedAt(int x, int y, int z) {
count(); return ground(x, z) == INVALID || terrain.protectedAt(x, y, z);
}
void count() { if (checks >= MAX_VOLUME_CHECKS) throw new Exhausted(); checks++; }
void reject(String reason) { diagnostics.merge(reason, 1, Integer::sum); }
}
private static long mix(long v) { v = (v ^ v >>> 30) * 0xbf58476d1ce4e5b9L; v = (v ^ v >>> 27) * 0x94d049bb133111ebL; return v ^ v >>> 31; }
private static final class Exhausted extends RuntimeException { private static final long serialVersionUID = 1L; }
}
@@ -29,18 +29,18 @@ public final class LostCityStructure extends Structure {
throw new IllegalStateException("Lost City district exceeds native structure reference range");
}
return Optional.of(new GenerationStub(LostCityStructures.locator(district), pieces -> {
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21) && generator.cityReservation16(island) != null
&& (generator.rootGeneration() != 21 || generator.cityPlacement21(island).core().surface().districts().contains(district)))
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22) && generator.cityReservation16(island) != null
&& ((generator.rootGeneration() != 21 && generator.rootGeneration() != 22) || generator.cityPlacement21(island).core().surface().districts().contains(district)))
pieces.addPiece(new LostCityFoundationPiece16(generator.cityReservation16(island), district.seed()));
if (generator.rootGeneration() == 15) for (var support : generator.citySupports(island)) {
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
pieces.addPiece(new LostCitySupportPiece15(support));
}
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21)) for (var support : generator.citySupports16(island)) {
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22)) for (var support : generator.citySupports16(island)) {
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
pieces.addPiece(new LostCitySupportPiece16(support));
}
district.parcels().forEach(parcel -> pieces.addPiece(generator.rootGeneration() == 21 ? new LostCityPiece21(parcel) : generator.rootGeneration() == 20 ? new LostCityPiece20(parcel) : (generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21) ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
district.parcels().forEach(parcel -> pieces.addPiece((generator.rootGeneration() == 21 || generator.rootGeneration() == 22) ? new LostCityPiece21(parcel) : generator.rootGeneration() == 20 ? new LostCityPiece20(parcel) : (generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22) ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
? new LostCityPiece15(parcel) : new LostCityPiece(parcel)));
if (generator.rootGeneration() == 15) for (var network : generator.undergroundPlan(island).networks()) {
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
@@ -50,13 +50,13 @@ public final class LostCityStructure extends Structure {
network.modules().stream().filter(m -> m.kind() == LostCityUnderground15.Kind.ACCESS)
.forEach(m -> pieces.addPiece(new LostCityUndergroundPiece15(m)));
}
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21)) for (var network : generator.undergroundPlan16(island).networks()) {
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22)) for (var network : generator.undergroundPlan16(island).networks()) {
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
// Surface comes first, rooms/corridors next, shafts last so every entrance remains open.
network.modules().stream().filter(m -> m.kind() != LostCityUnderground16.Kind.ACCESS)
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
network.modules().stream().filter(m -> m.kind() == LostCityUnderground16.Kind.ACCESS)
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
}
}));
}
@@ -0,0 +1,187 @@
package fr.koka.sanctuary.worldgen.city;
import fr.koka.sanctuary.worldgen.city.LostCityUnderground16.*;
import fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Module;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/** An independent public undercroft. No surface district, fake parcel or city reservation is needed.
* Only complete hall/access pairs are published; optional utility wings cannot invalidate them. */
public final class LostCityUnderground22 {
public static final int MAX_COLUMNS = 6_000, MAX_SAMPLES = LostCityUnderground16.MAX_SAMPLES;
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
private LostCityUnderground22() {}
public interface Terrain extends LostCityUnderground16.Terrain { int surfaceY(int x, int z); }
public static Plan plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
if (!List.of(512, 724, 1024).contains(diameter)) throw new IllegalArgumentException("Unsupported undercroft island size");
var probe = new Probe(terrain, cx, cz, diameter);
List<Module> accepted = null;
long acceptedSeed = seed;
try {
search: for (int attempt = 0; attempt < 40; attempt++) {
double angle = (mix(seed ^ 0x22A11L) >>> 11) * 0x1.0p-53 * Math.PI * 2 + attempt * 2.399963229728653;
double radius = attempt == 0 ? 0 : diameter * (attempt < 9 ? .09 : attempt < 21 ? .18 : .28);
int x = cx + (int) Math.round(Math.cos(angle) * radius), z = cz + (int) Math.round(Math.sin(angle) * radius);
int top = accessHeight(x, z, probe);
if (top < 0) continue;
for (int drop : new int[]{22, 32, 46, 62, 86, 110}) {
int floor = top - drop;
if (floor < 24) continue;
long networkSeed = mix(seed ^ ((long) x << 32) ^ z ^ drop);
for (int turn = 0; turn < 4; turn++) {
int direction = (int) Math.floorMod(networkSeed + turn, 4);
var access = new Module(Kind.ACCESS, x - 3, floor - 1, z - 3, 7, drop + 4, 7,
1 << direction, networkSeed, List.of());
var hall = attached(access, Kind.EXPANSION_HALL, direction, mix(networkSeed + 1));
// Cheap hall witnesses come before repeatedly scanning a tall access shaft.
if (!coarse(hall, probe)) continue;
var modules = new ArrayList<Module>();
access = certify(access, probe);
if (access == null) continue;
modules.add(access);
if (!attach(modules, 0, hall, direction, probe)) continue;
accepted = modules; acceptedSeed = networkSeed;
break search;
}
}
}
} catch (BudgetExceeded exhausted) { probe.reject("required-search-budget"); }
if (accepted != null) {
// A sewer and a pool are additional destinations, never conditions for the public hall.
int forward = Integer.numberOfTrailingZeros(accepted.getFirst().openings());
try {
for (int side : new int[]{(forward + 1) & 3, (forward + 3) & 3})
if (attach(accepted, 1, attached(accepted.get(1), Kind.SEWER, side, mix(acceptedSeed + 2)), side, probe)) break;
if (probe.samples < MAX_SAMPLES - 70_000 && probe.columns.size() < MAX_COLUMNS - 1_900)
attach(accepted, 1, attached(accepted.get(1), Kind.POOLROOM, forward, mix(acceptedSeed + 3)), forward, probe);
} catch (BudgetExceeded exhausted) { probe.reject("optional-wing-budget"); }
}
var networks = accepted == null ? List.<Network>of() : List.of(new Network(
accepted.getFirst().centerX() >> 4, accepted.getFirst().centerZ() >> 4, acceptedSeed, accepted));
System.getLogger(LostCityUnderground22.class.getName()).log(System.Logger.Level.INFO,
"Independent undercroft22 seed=" + seed + " networks=" + networks.size() + " columns=" + probe.columns.size()
+ " samples=" + probe.samples + " kinds=" + (accepted == null ? List.of() : accepted.stream().map(Module::kind).toList())
+ " admission=" + probe.rejections);
return new Plan(networks, probe.samples);
}
/** The whole visible seven-square hatch deck stays within two blocks of existing dry ground. */
private static int accessHeight(int cx, int cz, Probe p) {
int low = 384, high = -1;
for (int z = cz - 3; z <= cz + 3; z++) for (int x = cx - 3; x <= cx + 3; x++) {
int top = p.surface(x, z);
if (top < 64 || top > 365 || !p.solid(x, top, z) || p.protectedAt(x, top, z)) return p.rejectHeight("dry-access");
low = Math.min(low, top); high = Math.max(high, top);
if (high - low > 2) return p.rejectHeight("access-relief");
}
return high;
}
private static Module attached(Module parent, Kind kind, int direction, long seed) {
int width = kind == Kind.POOLROOM ? 57 : kind == Kind.EXPANSION_HALL ? 25 : 9;
int depth = kind == Kind.POOLROOM ? 31 : width;
int height = kind == Kind.POOLROOM ? 13 : kind == Kind.EXPANSION_HALL ? 10 : 7;
int offset = (DX[direction] == 0 ? parent.depth() / 2 + depth / 2 : parent.width() / 2 + width / 2) + 8;
int x = parent.centerX() + DX[direction] * offset, z = parent.centerZ() + DZ[direction] * offset;
return new Module(kind, x - width / 2, parent.floorY() - (kind == Kind.POOLROOM ? 5 : 1),
z - depth / 2, width, height, depth, 1 << ((direction + 2) & 3), seed, List.of());
}
private static boolean attach(List<Module> modules, int parentIndex, Module room, int direction, Probe p) {
var parent = modules.get(parentIndex);
int x = DX[direction] == 0 ? parent.centerX() - 2 : direction == 1 ? parent.maxX() + 1 : room.maxX() + 1;
int z = DZ[direction] == 0 ? parent.centerZ() - 2 : direction == 2 ? parent.maxZ() + 1 : room.maxZ() + 1;
var passage = new Module(Kind.PASSAGE, x, parent.floorY() - 1, z, DX[direction] == 0 ? 5 : 7, 6,
DZ[direction] == 0 ? 5 : 7, (1 << direction) | (1 << ((direction + 2) & 3)), mix(room.seed()), List.of());
for (var old : modules) if (overlaps(room, old) || overlaps(passage, old)) { p.reject("wing-collision"); return false; }
if (!coarse(room, p) || !coarse(passage, p)) return false;
room = certify(room, p); if (room == null) return false;
passage = certify(passage, p); if (passage == null) return false;
if (LostCityUnderground16.supportConflict(room, passage)) return false;
for (var old : modules) if (LostCityUnderground16.supportConflict(room, old)
|| LostCityUnderground16.supportConflict(passage, old)) return false;
modules.set(parentIndex, new Module(parent.kind(), parent.minX(), parent.minY(), parent.minZ(), parent.width(),
parent.height(), parent.depth(), parent.openings() | 1 << direction, parent.seed(), parent.supports()));
modules.add(room); modules.add(passage);
return true;
}
private static boolean overlaps(Module a, Module b) {
return a.minX() <= b.maxX() && a.maxX() >= b.minX() && a.minZ() <= b.maxZ() && a.maxZ() >= b.minZ()
&& a.boundsMinY() <= b.boundsMaxY() && a.boundsMaxY() >= b.boundsMinY();
}
private static boolean coarse(Module m, Probe p) {
int solid = 0;
for (int x : new int[]{m.minX(), m.centerX(), m.maxX()}) for (int z : new int[]{m.minZ(), m.centerZ(), m.maxZ()}) {
if (!p.owns(x, z)) { p.reject("outside-island"); return false; }
for (int y : new int[]{m.minY(), m.floorY() + 2, m.maxY()}) {
if (p.protectedAt(x, y, z)) { p.reject("coarse-protected"); return false; }
if (p.solid(x, y, z)) solid++;
}
}
if (solid < 5) { p.reject("coarse-rock"); return false; }
return true;
}
private static Module certify(Module m, Probe p) {
var supports = new ArrayList<Support>();
int stride = m.kind() == Kind.EXPANSION_HALL || m.kind() == Kind.POOLROOM ? 8 : 3;
for (int z : lattice(m.minZ(), m.maxZ(), stride)) for (int x : lattice(m.minX(), m.maxX(), stride)) {
var support = support(m, x, z, p);
if (support == null) { p.reject("no-short-anchor"); return null; }
supports.add(support);
}
int solid = 0, count = 0;
for (int z = m.minZ(); z <= m.maxZ(); z++) for (int x = m.minX(); x <= m.maxX(); x++) {
if (!p.owns(x, z)) { p.reject("outside-island"); return null; }
for (int y = m.minY(); y <= m.maxY(); y++) {
if (p.protectedAt(x, y, z)) { p.reject("protected-volume"); return null; }
if (p.solid(x, y, z)) solid++; count++;
}
}
// The constructed shell can cross a native cavity; it must still be embedded in real terrain.
if (solid * 100L < count * (m.kind() == Kind.ACCESS ? 50 : 20)) { p.reject("insufficient-rock"); return null; }
return new Module(m.kind(), m.minX(), m.minY(), m.minZ(), m.width(), m.height(), m.depth(),
m.openings(), m.seed(), supports);
}
private static Support support(Module m, int x, int z, Probe p) {
// Save even direct bearings: the existing protection builder then retains their untouched rock.
for (int distance = 1; distance <= 8; distance++) {
int y = m.minY() - distance;
if (y < 2 || p.protectedAt(x, y, z)) break;
if (p.solid(x, y, z) && p.solid(x, y - 1, z) && !p.protectedAt(x, y - 1, z))
return new Support(Type.PILLAR, x, y, z, m.minY() - 1);
}
for (int distance = 1; distance <= 8; distance++) {
int y = m.maxY() + distance;
if (y > 381 || p.protectedAt(x, y, z)) break;
if (p.solid(x, y, z) && p.solid(x, y + 1, z) && !p.protectedAt(x, y + 1, z))
return new Support(Type.CHAIN, x, m.maxY() + 1, z, y);
}
return null;
}
private static List<Integer> lattice(int min, int max, int stride) {
var result = new ArrayList<Integer>(); for (int n = min; n <= max; n += stride) result.add(n);
if (result.getLast() != max) result.add(max); return result;
}
private static final class Probe {
final Terrain terrain; final int cx, cz; final double radius;
final Map<Long, Integer> columns = new HashMap<>(); final Map<String, Integer> rejections = new LinkedHashMap<>(); int samples;
Probe(Terrain terrain, int cx, int cz, int diameter) { this.terrain = terrain; this.cx = cx; this.cz = cz; radius = diameter / 2.0 - 4; }
void count() { if (samples >= MAX_SAMPLES) throw new BudgetExceeded(); samples++; }
boolean owns(int x, int z) { return Math.hypot((double) x - cx, (double) z - cz) <= radius && terrain.owns(x, z); }
int surface(int x, int z) {
if (!owns(x, z)) return -1;
long key = ((long) x << 32) ^ (z & 0xffffffffL); var value = columns.get(key); if (value != null) return value;
if (columns.size() >= MAX_COLUMNS) throw new BudgetExceeded();
count(); int y = terrain.surfaceY(x, z); columns.put(key, y); return y;
}
boolean solid(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) < 0) return false; count(); return terrain.solid(x, y, z); }
boolean protectedAt(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) < 0) return true; count(); return terrain.protectedAt(x, y, z); }
void reject(String reason) { rejections.merge(reason, 1, Integer::sum); }
int rejectHeight(String reason) { reject(reason); return -1; }
}
private static final class BudgetExceeded extends RuntimeException { private static final long serialVersionUID = 1L; }
private static long mix(long x) { x = (x ^ x >>> 30) * 0xbf58476d1ce4e5b9L; x = (x ^ x >>> 27) * 0x94d049bb133111ebL; return x ^ x >>> 31; }
}
@@ -0,0 +1,41 @@
package fr.koka.sanctuary.worldgen.city;
import com.mojang.serialization.MapCodec;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.util.Optional;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureType;
/** An independent native start keeps a certified underground hall available without a surface city. */
public final class LostCityUndergroundStructure22 extends Structure {
public static final MapCodec<LostCityUndergroundStructure22> CODEC = simpleCodec(LostCityUndergroundStructure22::new);
public LostCityUndergroundStructure22(StructureSettings settings) { super(settings); }
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 22) return Optional.empty();
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
if (island == null || !island.origin() || island.generation() != 22) return Optional.empty();
var networks = generator.undergroundHall22().networks().stream()
.filter(n -> n.startChunkX() == context.chunkPos().x() && n.startChunkZ() == context.chunkPos().z()).toList();
if (networks.isEmpty()) return Optional.empty();
for (var network : networks) for (var module : network.modules()) {
var bounds = LostCityUndergroundPiece20.bounds(module);
if (Math.abs((long) Math.floorDiv(bounds.minX(), 16) - network.startChunkX()) > 7
|| Math.abs((long) Math.floorDiv(bounds.maxX(), 16) - network.startChunkX()) > 7
|| Math.abs((long) Math.floorDiv(bounds.minZ(), 16) - network.startChunkZ()) > 7
|| Math.abs((long) Math.floorDiv(bounds.maxZ(), 16) - network.startChunkZ()) > 7)
throw new IllegalStateException("Underground hall exceeds native structure reference range");
}
return Optional.of(new GenerationStub(LostCityUndergroundStructures22.locator(networks.getFirst()), pieces -> {
// Shafts are last so connected rooms cannot refill their surface hatches.
networks.forEach(n -> n.modules().stream().filter(m -> m.kind() != LostCityUnderground16.Kind.ACCESS)
.forEach(m -> pieces.addPiece(new LostCityUndergroundPiece20(m))));
networks.forEach(n -> n.modules().stream().filter(m -> m.kind() == LostCityUnderground16.Kind.ACCESS)
.forEach(m -> pieces.addPiece(new LostCityUndergroundPiece20(m))));
}));
}
@Override public StructureType<?> type() { return LostCityUndergroundStructures22.TYPE; }
}
@@ -0,0 +1,108 @@
package fr.koka.sanctuary.worldgen.city;
import com.mojang.datafixers.util.Pair;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.StructureType;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
/** Native lifecycle for generation22's standalone hall; its saved modules reuse the generation20 pieces. */
public final class LostCityUndergroundStructures22 {
public static final ResourceKey<Structure> KEY = ResourceKey.create(Registries.STRUCTURE,
SanctuaryMod.id("underground_hall22"));
public static final StructureType<LostCityUndergroundStructure22> TYPE = Registry.register(
BuiltInRegistries.STRUCTURE_TYPE, SanctuaryMod.id("underground_hall_v22"), () -> LostCityUndergroundStructure22.CODEC);
private LostCityUndergroundStructures22() {}
public static void register() { /* Initialize the codec before reading the dynamic structure registry. */ }
public static void generate(ExpansionChunkGenerator generator, RegistryAccess registries,
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> level) {
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || generator.rootGeneration() != 22) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || island.generation() != 22) return;
if (sanctuary.undergroundHall22().networks().stream().noneMatch(n -> n.startChunkX() == chunk.getPos().x()
&& n.startChunkZ() == chunk.getPos().z())) return;
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
Structure structure = holder.value();
StructureStart previous = manager.getStartForStructure(structure, chunk);
if (previous != null && previous.isValid()) return;
var random = generator.randomState(island);
var sampler = random.createClimateSampler(SamplerContext.builder().enableCaches().build());
var start = structure.generate(holder, level, registries, generator, generator.getBiomeSource(), sampler,
random, templates, island.seed(), chunk.getPos(), previous == null ? 0 : previous.getReferences(),
chunk, structure.biomes()::contains);
if (start.isValid()) manager.setStartForStructure(structure, start, chunk);
}
/** Locate returns the persisted surface hatch, loading only the native structure start when necessary. */
public static Pair<BlockPos, Holder<Structure>> findNearest(ServerLevel level, HolderSet<Structure> structures,
BlockPos origin, int radius, boolean skipReferenced) {
if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 22 || !level.structureManager().shouldGenerateStructures()) return null;
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
if (!structures.contains(holder)) return null;
var candidates = new ArrayList<LostCityUnderground16.Network>();
int originChunkX = origin.getX() >> 4, originChunkZ = origin.getZ() >> 4;
for (var network : generator.undergroundHall22().networks()) {
var island = generator.islandForChunk(network.startChunkX(), network.startChunkZ());
if (island == null || !island.origin() || island.generation() != 22 || !island.status().equals("ready")) continue;
if (Math.abs((long) network.startChunkX() - originChunkX) <= radius
&& Math.abs((long) network.startChunkZ() - originChunkZ) <= radius) candidates.add(network);
}
candidates.sort(Comparator.comparingDouble(n -> origin.distSqr(locator(n))));
var visited = new HashSet<Long>();
for (var network : candidates) {
long key = ((long) network.startChunkX() << 32) ^ (network.startChunkZ() & 0xffffffffL);
if (!visited.add(key)) continue;
var chunk = level.getChunk(network.startChunkX(), network.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
StructureStart start = level.structureManager().getStartForStructure(holder.value(), chunk);
if (start == null || !start.isValid() || skipReferenced && !start.canBeReferenced()) continue;
var entrance = start.getPieces().stream()
.filter(p -> p instanceof LostCityUndergroundPiece20 piece && piece.module().kind() == LostCityUnderground16.Kind.ACCESS)
.map(p -> locator(((LostCityUndergroundPiece20) p).module()))
.min(Comparator.comparingDouble(origin::distSqr));
if (entrance.isEmpty()) continue;
if (skipReferenced) level.structureManager().addReference(start);
return Pair.of(entrance.get(), holder);
}
return null;
}
public static BlockPos locator(LostCityUnderground16.Network network) {
return locator(network.modules().stream().filter(m -> m.kind() == LostCityUnderground16.Kind.ACCESS)
.findFirst().orElseThrow(() -> new IllegalArgumentException("Underground hall has no surface access")));
}
private static BlockPos locator(LostCityUnderground16.Module access) {
int exit = Integer.numberOfTrailingZeros(access.openings() == 0 ? 1 : access.openings());
int wall = (exit + 2) & 3;
int x = wall == 1 ? access.width() - 2 : wall == 3 ? 1 : access.width() / 2;
int z = wall == 2 ? access.depth() - 2 : wall == 0 ? 1 : access.depth() / 2;
return new BlockPos(access.minX() + x, access.maxY() - 1, access.minZ() + z);
}
}
@@ -19,7 +19,7 @@ public final class HeritageCommands19 {
var level = source.getLevel();
if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21) || !generator.experimentalStructures()) {
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && generator.rootGeneration() != 22) || generator.rootGeneration() != 22 && !generator.experimentalStructures()) {
source.sendFailure(Component.translatable("sanctuary.heritage.unavailable"));
return 0;
}
@@ -28,12 +28,18 @@ public final class HeritageCommands19 {
for (var site : plan.sites()) source.sendSuccess(() -> Component.translatable("sanctuary.heritage.entry",
Component.translatable("sanctuary.heritage.kind." + site.kind().name().toLowerCase(Locale.ROOT)),
site.entry().x(), site.entry().y(), site.entry().z()), false);
if (generator.rootGeneration() == 21) for (var module : generator.transitPlan21().modules()) {
if (generator.rootGeneration() == 21 || generator.rootGeneration() == 22) for (var module : generator.transitPlan21().modules()) {
if (module.kind() != fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind.ACCESS) continue;
var at = fr.koka.sanctuary.worldgen.transit.TransitPiece21.locator(module);
source.sendSuccess(() -> Component.translatable("sanctuary.transit.entry", at.getX(), at.getY(), at.getZ()), false);
}
return plan.sites().size();
if (generator.rootGeneration() == 22) for (var network : generator.undergroundHall22().networks()) {
var at = fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.locator(network);
source.sendSuccess(() -> Component.translatable("sanctuary.underground_hall.entry", at.getX(), at.getY(), at.getZ()), false);
}
return plan.sites().size() + (generator.rootGeneration() == 22
? (int) generator.transitPlan21().modules().stream().filter(m -> m.kind() == fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind.ACCESS).count()
+ generator.undergroundHall22().networks().size() : 0);
}))));
}
}
@@ -0,0 +1,612 @@
package fr.koka.sanctuary.worldgen.heritage;
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.*;
import fr.koka.sanctuary.worldgen.heritage.HeritagePlanner21.Terrain;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/** Generation22 only. A finite catalogue is admitted before complete, repairable routes are published.
* Everything samples native columns; no chunk, player object or future continent is requested. */
public final class HeritagePlanner22 {
public static final int MAX_COLUMNS = HeritagePlan19.MAX_COLUMNS, MAX_SAMPLES = HeritagePlan19.MAX_SAMPLES;
// Prefer an existing platform: at most two blocks of cut/fill and four of total relief.
public static final int MAX_SITE_CUT = 2, MAX_SITE_FILL = 2, MAX_SITE_RELIEF = 4;
public static final int MAX_ROUTE_CUT = 1, MAX_ROUTE_FILL = 1;
private static final int SITE_COLUMNS = 11_000, SITE_SAMPLES = 540_000;
private static final int INVALID = Integer.MIN_VALUE;
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
private HeritagePlanner22() {}
public static HeritagePlan19 plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
if (!List.of(512, 724, 1024).contains(diameter)) throw new IllegalArgumentException("Heritage22 requires a starter size");
var probe = new Probe(terrain, cx, cz, diameter);
List<Landmark> landmarks = List.copyOf(terrain.landmarks()).stream().limit(16).toList();
var sites = new ArrayList<Site>(); var cells = new LinkedHashMap<Long, OwnedCell>(); var diagnostics = new EnumMap<Kind, String>(Kind.class);
for (var kind : Kind.values()) {
int remaining = Kind.values().length - kind.ordinal() - 1;
probe.columnLimit = SITE_COLUMNS - remaining * (SITE_COLUMNS / 14);
probe.sampleLimit = SITE_SAMPLES - remaining * (SITE_SAMPLES / 14);
SiteDraft admitted = null; int attempts = 0;
var reasons = new java.util.TreeMap<String, Integer>(); probe.reasons = reasons;
try {
for (var candidate : candidates(seed, kind, cx, cz, diameter, sites, landmarks, probe)) {
attempts++;
admitted = site(seed, kind, candidate, sites, landmarks, cells, probe);
if (admitted != null) break;
}
} catch (BudgetExceeded exhausted) { reasons.merge(exhausted.columns ? "column-budget" : "probe-budget", 1, Integer::sum); }
if (admitted != null) {
sites.add(admitted.site());
Request landing = new Request(admitted.site().id() + "_landing", admitted.site().id(), admitted.site().id(), RouteKind.PATH);
for (var cell : admitted.landings()) cells.put(key(cell.x(), cell.z()), new OwnedCell(landing, cell));
}
diagnostics.put(kind, (admitted == null ? "not admitted" : "admitted") + "; candidates=" + attempts + "; " + reasons);
}
probe.columnLimit = MAX_COLUMNS; probe.sampleLimit = MAX_SAMPLES;
var links = new ArrayList<Link>();
var routeResults = new LinkedHashMap<String, String>();
var routeReasons = new java.util.TreeMap<String, Integer>(); probe.reasons = routeReasons;
Map<String, Endpoint> endpoints = new LinkedHashMap<>();
for (var site : sites) endpoints.put(site.id(), new Endpoint(site.id(), site.entry(), false));
for (var landmark : landmarks) if (!endpoints.containsKey(landmark.id()))
endpoints.put(landmark.id(), new Endpoint(landmark.id(), landmark.entry(), true));
var requests = new ArrayList<>(requests(sites, landmarks).stream()
.filter(r -> endpoints.containsKey(r.from()) && endpoints.containsKey(r.to())).toList());
var components = new Components(endpoints.keySet());
// Multiple exterior entries into the certified connected city represent one already walkable destination.
String firstCity = null;
for (var landmark : landmarks) if (landmark.kind() == LandmarkKind.CITY) {
if (firstCity == null) firstCity = landmark.id(); else components.join(firstCity, landmark.id());
}
Request longFreight = requests.stream().filter(r -> r.kind() == RouteKind.FREIGHT
&& span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry()) >= diameter / 4)
.max(Comparator.comparingInt(r -> span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry()))).orElse(null);
var siteIds = new HashSet<String>(); sites.forEach(site -> siteIds.add(site.id()));
var addressed = new HashSet<String>();
var destinationAttempts = new HashMap<String, Integer>();
int attempted = 0;
while (!requests.isEmpty() && attempted < 36 && links.size() < 24) {
// Give each isolated site a first useful route before spending its columns on a cycle.
// Repeated city gates already belong to one walkable component; no redundant freight bypass is needed.
requests.removeIf(r -> components.same(r.from(), r.to()));
if (requests.isEmpty()) break;
requests.sort(Comparator.comparingInt((Request r) -> isolatedSites(r, siteIds, addressed) > 0
&& isolatedAttempts(r, siteIds, addressed, destinationAttempts) == 0 ? 0 : 1)
.thenComparingInt(r -> span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry()))
.thenComparingInt(r -> r.kind() == RouteKind.FREIGHT ? 0 : 1).thenComparing(Request::id));
var request = attempted == 0 && longFreight != null ? longFreight : requests.getFirst(); requests.remove(request);
attempted++;
int distance = Math.abs(endpoints.get(request.from()).entry().x() - endpoints.get(request.to()).entry().x())
+ Math.abs(endpoints.get(request.from()).entry().z() - endpoints.get(request.to()).entry().z());
boolean longAxis = span(endpoints.get(request.from()).entry(), endpoints.get(request.to()).entry()) >= diameter / 4;
int isolated = isolatedSites(request, siteIds, addressed);
var waiting = new HashSet<String>();
for (var next : requests) for (var id : List.of(next.from(), next.to()))
if (siteIds.contains(id) && !addressed.contains(id) && !id.equals(request.from()) && !id.equals(request.to())) waiting.add(id);
int reserve = Math.min(Math.min(waiting.size(), 8), 36 - attempted) * 100;
int allowance = longAxis && attempted == 1 ? 3_800 : isolated > 0 ? Math.clamp(distance * 5 + 350, 900, 2_400) : 900;
probe.columnLimit = Math.min(MAX_COLUMNS - reserve, probe.columns.size() + allowance);
probe.sampleLimit = Math.min(MAX_SAMPLES - reserve * 20, probe.samples + (longAxis ? 150_000 : 80_000));
for (var id : List.of(request.from(), request.to())) if (siteIds.contains(id) && !addressed.contains(id))
destinationAttempts.merge(id, 1, Integer::sum);
Endpoint from = endpoints.get(request.from()), to = endpoints.get(request.to());
int beforeRouteColumns = probe.columns.size();
var beforeReasons = new HashMap<>(routeReasons);
try {
RouteDraft route = route(seed, request, from, to, sites, landmarks, cells, probe);
if (route == null) { probe.reject("no-complete-route"); routeResults.put(request.id(), "refused; columns=" + (probe.columns.size() - beforeRouteColumns) + rejectionDelta(beforeReasons, routeReasons, probe.routeBlock)); continue; }
var groups = new HashSet<SegmentKey>();
cells.values().forEach(c -> groups.add(new SegmentKey(c.request(), c.cell().x() >> 4, c.cell().z() >> 4)));
route.cells().values().stream().filter(c -> !cells.containsKey(key(c.x(), c.z())))
.forEach(c -> groups.add(new SegmentKey(request, c.x() >> 4, c.z() >> 4)));
if (groups.size() > 512) { routeResults.put(request.id(), "refused; native-piece-budget"); continue; }
links.add(new Link(request.id(), request.from(), request.to(), request.kind(), route.path()));
components.join(request.from(), request.to());
addressed.add(request.from()); addressed.add(request.to());
routeResults.put(request.id(), "admitted; span=" + span(from.entry(), to.entry()) + "; length=" + route.path().size() + "; columns=" + (probe.columns.size() - beforeRouteColumns));
for (var cell : route.cells().values()) {
long key = key(cell.x(), cell.z());
OwnedCell old = cells.get(key);
if (old == null) cells.put(key, new OwnedCell(request, cell));
else if (cell.rail() != Rail.NONE && old.cell().rail() == Rail.NONE)
cells.put(key, new OwnedCell(old.request(), cell));
else if (cell.rail() != Rail.NONE && old.cell().rail() != Rail.NONE && cell.rail() != old.cell().rail()) {
// A missing switch is intentional; never emit a rail with contradictory connections.
var c = old.cell(); cells.put(key, new OwnedCell(old.request(),
new Cell(c.x(), c.floorY(), c.z(), c.foundationDepth(), c.rail(), false)));
}
}
} catch (BudgetExceeded exhausted) { probe.reject(exhausted.columns ? "route-column-budget" : "route-probe-budget"); routeResults.put(request.id(), "budget; columns=" + (probe.columns.size() - beforeRouteColumns) + rejectionDelta(beforeReasons, routeReasons, probe.routeBlock)); }
}
for (var site : sites) {
var incident = links.stream().filter(l -> l.fromId().equals(site.id()) || l.toId().equals(site.id())).map(Link::id).toList();
var attempts = new LinkedHashMap<String, String>();
routeResults.forEach((id, value) -> { if (id.contains(site.id())) attempts.put(id, value); });
diagnostics.put(site.kind(), diagnostics.get(site.kind()) + "; connected=" + !incident.isEmpty()
+ "; accepted-links=" + incident + "; route-attempts=" + attempts);
}
var segments = segments(seed, cells);
if (Boolean.getBoolean("sanctuary.test.heritage22") || Boolean.getBoolean("sanctuary.heritage.plannerDiagnostics"))
System.getLogger(HeritagePlanner22.class.getName()).log(System.Logger.Level.INFO,
"Heritage22 seed=" + seed + " diameter=" + diameter + " sites=" + sites.stream().map(Site::kind).toList()
+ " links=" + links.size() + " segments=" + segments.size() + " columns=" + probe.columns.size()
+ " samples=" + probe.samples + " catalogue=" + diagnostics + " routes=" + routeReasons + " routeResults=" + routeResults);
return new HeritagePlan19(sites, landmarks, links, segments, probe.samples, probe.columns.size(), diagnostics);
}
private static List<Candidate> candidates(long seed, Kind kind, int cx, int cz, int diameter,
List<Site> sites, List<Landmark> landmarks, Probe probe) {
double angle = unit(mix(seed ^ 0x19ACEL)) * Math.PI * 2 + kind.ordinal() * 2.399963229728653;
double radius = diameter * (kind == Kind.VOID_DOCK ? .40 : kind == Kind.OBSERVATORY || kind == Kind.RADIO_RELAY ? .31 : .22);
int x = cx + (int) Math.round(Math.cos(angle) * radius), z = cz + (int) Math.round(Math.sin(angle) * radius);
Kind parent = switch (kind) {
case POST_OFFICE, COMMUNAL_DEPOT, RAIL_WORKSHOP -> Kind.FREIGHT_STATION;
case EXPEDITION_OFFICE, PHOTO_CLUB -> Kind.POST_OFFICE;
case SUBSTATION, CONSTRUCTION_REFUGE -> Kind.RAIL_WORKSHOP;
case GREENHOUSE, IMPOSSIBLE_SERVICE -> Kind.PUMP_STATION;
default -> null;
};
Site neighbor = parent == null ? null : find(sites, parent);
if (neighbor != null) {
double local = unit(mix(seed ^ kind.ordinal() * 179L)) * Math.PI * 2;
int distance = kind == Kind.RAIL_WORKSHOP ? 65 : 40;
x = neighbor.centerX() + (int) Math.round(Math.cos(local) * distance);
z = neighbor.centerZ() + (int) Math.round(Math.sin(local) * distance);
}
var candidates = new ArrayList<Candidate>();
for (int i = 0; i < 24; i++) {
double turn = angle + i * Math.PI / 3;
int offset = i == 0 ? 0 : i < 5 ? 24 : i < 8 ? 48 : i < 12 ? 72 : i < 16 ? 96 : 128;
int px = x + (int) Math.round(Math.cos(turn) * offset), pz = z + (int) Math.round(Math.sin(turn) * offset);
if (Math.hypot((double) px - cx, (double) pz - cz) > diameter / 2.0 - 18) continue;
if (overlapsSites(px - 15, pz - 15, px + 15, pz + 15, sites)) continue;
int low = 384, high = -1; boolean dry = true;
for (int dz : new int[] {-8, 0, 8}) for (int dx : new int[] {-8, 0, 8}) {
int top = probe.surface(px + dx, pz + dz);
if (top < 48 || top > 330 || !probe.solid(px + dx, top, pz + dz)
|| probe.protectedAt(px + dx, top, pz + dz)) { dry = false; break; }
low = Math.min(low, top); high = Math.max(high, top);
}
if (!dry || high - low > MAX_SITE_RELIEF) { probe.reject("coarse-terrain"); continue; }
int facing = direction(px, pz, neighbor == null ? cx : neighbor.centerX(), neighbor == null ? cz : neighbor.centerZ());
if (kind == Kind.VOID_DOCK) facing = (direction(px, pz, cx, cz) + 2) & 3;
int score = (high - low) * 1_024 + i * 8 + (kind == Kind.OBSERVATORY || kind == Kind.RADIO_RELAY ? (300 - high) / 4 : 0);
candidates.add(new Candidate(px - 8, pz - 8, facing, score, mix(seed ^ kind.ordinal() * 463L ^ i)));
}
candidates.sort(Comparator.comparingInt(Candidate::score).thenComparingLong(Candidate::seed));
return candidates;
}
private static SiteDraft site(long seed, Kind kind, Candidate candidate, List<Site> previous,
List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
int x = candidate.x(), z = candidate.z(), low = 384, high = -1;
for (var landmark : landmarks) if (landmark.entry().x() >= x - 2 && landmark.entry().x() <= x + 18
&& landmark.entry().z() >= z - 2 && landmark.entry().z() <= z + 18) { probe.reject("existing-entry"); return null; }
var heights = new ArrayList<Integer>();
for (int dz = 0; dz < 17; dz++) for (int dx = 0; dx < 17; dx++) {
int top = probe.surface(x + dx, z + dz);
if (top < 48 || top > 330 || !probe.solid(x + dx, top, z + dz)) { probe.reject("dry-footprint"); return null; }
heights.add(top); low = Math.min(low, top); high = Math.max(high, top);
}
int minimum = Math.max(48, high - MAX_SITE_CUT), maximum = Math.min(330, low + MAX_SITE_FILL);
if (minimum > maximum || high - low > MAX_SITE_RELIEF) { probe.reject("site-slope"); return null; }
heights.sort(Integer::compareTo);
int floor = Math.clamp(heights.get(heights.size() / 2), minimum, maximum);
int foundation = Math.max(3, floor - low + 1), height = height(kind);
int base = floor - foundation;
for (int dz = 0; dz < 17; dz++) for (int dx = 0; dx < 17; dx++)
for (int y = base; y <= floor + height; y++) if (probe.protectedAt(x + dx, y, z + dz)) {
probe.reject("site-protected-volume"); return null;
}
var supports = new ArrayList<Support>();
// Every foundation column rests directly on intact rock. Do not catch a cliff
// or a cavity with long piers just to retain this candidate.
for (int dz = 0; dz < 17; dz++) for (int dx = 0; dx < 17; dx++) {
if (!probe.solid(x + dx, base - 1, z + dz) || !probe.solid(x + dx, base - 2, z + dz)) {
probe.reject("site-no-direct-bearing"); return null;
}
for (int y = base - 2; y < base; y++) if (probe.protectedAt(x + dx, y, z + dz)) {
probe.reject("support-protected"); return null;
}
}
// The square envelope and bearing grid have already been certified. A difficult front slope
// should try another real facade before rejecting the entire building; no terrain is weakened.
for (int turn : new int[]{0, 1, 3, 2}) {
int facing = (candidate.facing() + turn) & 3;
int ex = facing == 1 ? x + 17 : facing == 3 ? x - 1 : x + 8;
int ez = facing == 2 ? z + 17 : facing == 0 ? z - 1 : z + 8;
Entry entry = new Entry(ex, floor + 1, ez, facing);
Site result = new Site(id(kind), kind, x, floor, z, 17, 17, height, facing,
mix(seed ^ candidate.seed()), foundation, supports, entry);
var landings = landing(result.entry(), previous, probe);
if (landings == null) { probe.reject("entry-landing"); continue; }
boolean collision = false;
for (var cell : landings) {
OwnedCell old = existing.get(key(cell.x(), cell.z()));
if (old != null && old.cell().floorY() != cell.floorY()) { collision = true; break; }
}
if (collision) { probe.reject("landing-collision"); continue; }
if (turn != 0) probe.reject("admitted-other-facade");
return new SiteDraft(result, landings);
}
return null;
}
private static int height(Kind kind) {
return switch (kind) {
case FREIGHT_STATION, POST_OFFICE, RAIL_WORKSHOP, SUBSTATION, GREENHOUSE, PHOTO_CLUB -> 10;
case EXPEDITION_OFFICE, IMPOSSIBLE_SERVICE -> 12;
case COMMUNAL_DEPOT, VOID_DOCK, CONSTRUCTION_REFUGE -> 14;
case PUMP_STATION -> 18; case OBSERVATORY -> 20; case RADIO_RELAY -> 22;
};
}
private static List<Request> requests(List<Site> sites, List<Landmark> landmarks) {
var result = new LinkedHashMap<String, Request>();
var endpoints = new ArrayList<Endpoint>();
sites.forEach(s -> endpoints.add(new Endpoint(s.id(), s.entry(), false)));
landmarks.forEach(l -> endpoints.add(new Endpoint(l.id(), l.entry(), true)));
var freight = new HashSet<String>();
for (var kind : new Kind[] {Kind.FREIGHT_STATION, Kind.COMMUNAL_DEPOT, Kind.RAIL_WORKSHOP, Kind.PUMP_STATION}) {
var site = find(sites, kind); if (site != null) freight.add(site.id());
}
landmarks.stream().filter(l -> l.kind() == LandmarkKind.MINE).forEach(l -> freight.add(l.id()));
for (var a : endpoints) {
// Four alternatives per entrance tolerate one protected cut without a quadratic terrain search.
var nearest = endpoints.stream().filter(b -> !a.id().equals(b.id()))
.sorted(Comparator.comparingInt((Endpoint b) -> span(a.entry(), b.entry())).thenComparing(Endpoint::id)).limit(4).toList();
for (var b : nearest) addRequest(result, a, b, freight.contains(a.id()) && freight.contains(b.id()) ? RouteKind.FREIGHT : RouteKind.PATH);
}
for (var pair : List.of(new Kind[]{Kind.FREIGHT_STATION, Kind.COMMUNAL_DEPOT},
new Kind[]{Kind.FREIGHT_STATION, Kind.RAIL_WORKSHOP}, new Kind[]{Kind.COMMUNAL_DEPOT, Kind.PUMP_STATION})) {
var a = find(sites, pair[0]); var b = find(sites, pair[1]);
if (a != null && b != null) addRequest(result, new Endpoint(a.id(), a.entry(), false), new Endpoint(b.id(), b.entry(), false), RouteKind.FREIGHT);
}
return List.copyOf(result.values());
}
private static void addRequest(Map<String, Request> result, Endpoint a, Endpoint b, RouteKind kind) {
if (a.id().compareTo(b.id()) > 0) { var swap = a; a = b; b = swap; }
String key = a.id() + "__" + b.id(); var old = result.get(key);
if (old == null || kind == RouteKind.FREIGHT) result.put(key, new Request("graph_" + key, a.id(), b.id(), kind));
}
private static String rejectionDelta(Map<String, Integer> before, Map<String, Integer> after, XZ obstacle) {
var changes = new java.util.TreeMap<String, Integer>();
after.forEach((key, count) -> { int difference = count - before.getOrDefault(key, 0); if (difference > 0) changes.put(key, difference); });
return "; reasons=" + changes + (obstacle == null ? "" : "; last-column=" + obstacle.x() + "," + obstacle.z());
}
private static int isolatedSites(Request request, Set<String> siteIds, Set<String> addressed) {
return (siteIds.contains(request.from()) && !addressed.contains(request.from()) ? 1 : 0)
+ (siteIds.contains(request.to()) && !addressed.contains(request.to()) ? 1 : 0);
}
private static int isolatedAttempts(Request request, Set<String> siteIds, Set<String> addressed, Map<String, Integer> attempts) {
int result = Integer.MAX_VALUE;
for (var id : List.of(request.from(), request.to())) if (siteIds.contains(id) && !addressed.contains(id))
result = Math.min(result, attempts.getOrDefault(id, 0));
return result == Integer.MAX_VALUE ? 0 : result;
}
private static final class Components {
final Map<String, String> parents = new HashMap<>();
Components(Set<String> ids) { for (var id : ids) parents.put(id, id); }
String root(String id) { String parent = parents.get(id); while (!parent.equals(id)) { id = parent; parent = parents.get(id); } return parent; }
boolean same(String a, String b) { return root(a).equals(root(b)); }
void join(String a, String b) { parents.put(root(b), root(a)); }
}
/** Always persisted, including for an isolated destination: five steps return to natural terrain. */
private static List<Cell> landing(Entry entry, List<Site> sites, Probe probe) {
int facing = entry.facing(), floor = entry.y() - 1;
int endX = entry.x() + DX[facing] * 4, endZ = entry.z() + DZ[facing] * 4;
int target = probe.surface(endX, endZ);
if (target < 14 || target > 350 || Math.abs(target - floor) > 3) return null;
var result = new ArrayList<Cell>();
for (int step = 0; step < 5; step++) {
// Keep the first two rows flat for the facade and the first rail turn.
int y = floor + (int) Math.round((target - floor) * Math.max(0, step - 1) / 3.0);
for (int cross = -1; cross <= 1; cross++) {
int x = entry.x() + DX[facing] * step + DZ[facing] * cross;
int z = entry.z() + DZ[facing] * step - DX[facing] * cross;
if (overlapsSites(x, z, x, z, sites)) return null;
Cell cell = routeCell(x, y, z, Rail.NONE, false, probe);
if (cell == null) return null;
result.add(cell);
if (step == 4) {
int beyond = probe.surface(x + DX[facing], z + DZ[facing]);
if (Math.abs(beyond - y) > 1 || !probe.solid(x + DX[facing], beyond, z + DZ[facing])) return null;
}
}
}
return List.copyOf(result);
}
private static RouteDraft route(long seed, Request request, Endpoint from, Endpoint to, List<Site> sites,
List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
Entry a = from.entry(), b = to.entry();
XZ start = new XZ(a.x(), a.z()), end = new XZ(b.x(), b.z());
XZ outA = start.move(a.facing(), 6), outB = end.move(b.facing(), 6);
int middleX = (outA.x() + outB.x()) / 2, middleZ = (outA.z() + outB.z()) / 2;
var alternatives = List.of(
List.of(outA, new XZ(outB.x(), outA.z()), outB),
List.of(outA, new XZ(outA.x(), outB.z()), outB),
List.of(outA, new XZ(middleX, outA.z()), new XZ(middleX, outB.z()), outB),
List.of(outA, new XZ(outA.x(), middleZ), new XZ(outB.x(), middleZ), outB),
List.of(outA, new XZ(outA.x(), middleZ - 16), new XZ(outB.x(), middleZ - 16), outB),
List.of(outA, new XZ(middleX + 16, outA.z()), new XZ(middleX + 16, outB.z()), outB));
for (var waypoints : alternatives) {
var points = new ArrayList<XZ>(); points.add(start);
boolean okay = true;
for (var waypoint : waypoints) if (!line(points, waypoint)) { okay = false; break; }
if (!okay || !line(points, end) || points.size() > 1024 || new HashSet<>(points).size() != points.size()) continue;
RouteDraft result = profile(seed, request, from, to, points, sites, landmarks, existing, probe);
if (result != null) return result;
}
return adaptive(seed, request, from, to, start, end, outA, outB, sites, landmarks, existing, probe);
}
/** Adaptive fallback. The coarse graph only proposes an alignment; it grants no writing permission. */
private static RouteDraft adaptive(long seed, Request request, Endpoint from, Endpoint to, XZ start, XZ end,
XZ outA, XZ outB, List<Site> sites, List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
int stepSize = span(from.entry(), to.entry()) >= 160 ? 8 : 4;
int certificateReserve = Math.min(2_200, distance(outA, outB) * 3 + 80);
var excluded = new HashSet<XZ>();
for (int attempt = 0; attempt < 3; attempt++) {
var open = new java.util.PriorityQueue<SearchNode>(Comparator.comparingDouble(SearchNode::priority)
.thenComparingDouble(SearchNode::cost).thenComparingInt(n -> n.position().x()).thenComparingInt(n -> n.position().z()));
var best = new HashMap<XZ, Double>(); var parents = new HashMap<XZ, XZ>(); var closed = new HashSet<XZ>();
var terrainHeights = new HashMap<XZ, Integer>();
int begin = coarseRoute(outA, sites, terrainHeights, probe);
if (begin == INVALID) return null;
open.add(new SearchNode(outA, 0, distance(outA, outB))); best.put(outA, 0.0);
List<XZ> found = null;
int searchColumns = Math.max(probe.columns.size() + 150, probe.columnLimit - certificateReserve);
while (!open.isEmpty() && closed.size() < 500 && probe.columns.size() < searchColumns) {
var current = open.remove(); XZ p = current.position();
if (!closed.add(p) || current.cost() > best.getOrDefault(p, Double.POSITIVE_INFINITY)) continue;
if (distance(p, outB) <= stepSize * 2) {
var reversed = new ArrayList<XZ>(); XZ ancestor = p;
while (ancestor != null) { reversed.add(ancestor); ancestor = parents.get(ancestor); }
java.util.Collections.reverse(reversed);
for (int order = 0; order < 2 && found == null; order++) {
var candidate = new ArrayList<XZ>(); candidate.add(start); boolean valid = true;
for (XZ step : reversed) if (!line(candidate, step)) { valid = false; break; }
XZ turn = order == 0 ? new XZ(outB.x(), p.z()) : new XZ(p.x(), outB.z());
if (valid && line(candidate, turn) && line(candidate, outB) && line(candidate, end)
&& new HashSet<>(candidate).size() == candidate.size()) found = candidate;
}
if (found != null) break;
}
int top = terrainHeights.get(p); XZ parent = parents.get(p);
for (int d = 0; d < 4; d++) {
if (parent == null && d == ((from.entry().facing() + 2) & 3)) continue;
XZ next = p.move(d, stepSize);
if (closed.contains(next) || excluded.contains(next)
|| Math.min(distance(next, outA), distance(next, outB)) > distance(outA, outB) + 64) continue;
int height = coarseRoute(next, sites, terrainHeights, probe);
if (height == INVALID || Math.abs(height - top) > stepSize + 3) continue;
double turnCost = parent == null || parent.x() == next.x() || parent.z() == next.z() ? 0 : 1.5;
double cost = current.cost() + stepSize + Math.abs(height - top) * 1.1 + turnCost;
if (cost >= best.getOrDefault(next, Double.POSITIVE_INFINITY)) continue;
best.put(next, cost); parents.put(next, p);
open.add(new SearchNode(next, cost, cost + distance(next, outB) * 1.6));
}
}
if (found == null) { probe.reject("adaptive-no-alignment"); return null; }
RouteDraft certified = profile(seed, request, from, to, found, sites, landmarks, existing, probe);
if (certified != null) { probe.reject("adaptive-admitted"); return certified; }
if (probe.routeBlock == null) return null;
// A failed exact corridor removes only nearby coarse nodes on the next proposal.
XZ bad = probe.routeBlock;
int gx = outA.x() + (int) Math.round((bad.x() - outA.x()) / (double) stepSize) * stepSize;
int gz = outA.z() + (int) Math.round((bad.z() - outA.z()) / (double) stepSize) * stepSize;
excluded.add(new XZ(gx, gz));
for (int d = 0; d < 4; d++) excluded.add(new XZ(gx, gz).move(d, stepSize));
}
return null;
}
private record SearchNode(XZ position, double cost, double priority) {}
private static int distance(XZ a, XZ b) { return Math.abs(a.x() - b.x()) + Math.abs(a.z() - b.z()); }
private static int coarseRoute(XZ p, List<Site> sites, Map<XZ, Integer> cache, Probe probe) {
Integer old = cache.get(p); if (old != null) return old;
if (overlapsSites(p.x() - 2, p.z() - 2, p.x() + 2, p.z() + 2, sites)) { cache.put(p, INVALID); return INVALID; }
int top = probe.surface(p.x(), p.z());
if (top < 14 || top > 350 || !probe.solid(p.x(), top, p.z()) || probe.protectedAt(p.x(), top, p.z())) {
cache.put(p, INVALID); return INVALID;
}
for (int d = 0; d < 4; d++) {
XZ side = p.move(d, 1); int sideTop = probe.surface(side.x(), side.z());
if (sideTop < 14 || sideTop > 350 || Math.abs(sideTop - top) > MAX_ROUTE_CUT + 1 || probe.protectedAt(side.x(), sideTop, side.z())) {
cache.put(p, INVALID); return INVALID;
}
}
cache.put(p, top); return top;
}
/** Finite orthogonal alignments, each certified at full block resolution before publication. */
private static boolean line(List<XZ> points, XZ destination) {
XZ p = points.getLast();
if (p.x() != destination.x() && p.z() != destination.z()) return false;
int dx = Integer.signum(destination.x() - p.x()), dz = Integer.signum(destination.z() - p.z());
while (!p.equals(destination)) {
p = new XZ(p.x() + dx, p.z() + dz); points.add(p);
if (points.size() > 1024) return false;
}
return true;
}
private static RouteDraft profile(long seed, Request request, Endpoint from, Endpoint to, List<XZ> points,
List<Site> sites, List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
int size = points.size(); probe.routeBlock = null;
int[] lo = new int[size], hi = new int[size], desired = new int[size], group = new int[size];
boolean[] flat = new boolean[size - 1];
for (int i = 1; i < size - 1; i++) if (turn(points, i)) { flat[i - 1] = true; flat[i] = true; }
for (int i = 0; i < size; i++) {
XZ point = points.get(i); probe.routeBlock = point; lo[i] = 14; hi[i] = 350;
desired[i] = probe.surface(point.x(), point.z());
boolean readOnly = i == 0 && from.readOnly() || i == size - 1 && to.readOnly();
for (XZ column : strip(points, i)) {
if (overlapsSites(column.x(), column.z(), column.x(), column.z(), sites)) { probe.reject("route-building"); return null; }
int top = probe.surface(column.x(), column.z());
if (top < 14 || top > 350 || !probe.solid(column.x(), top, column.z())) { probe.reject("route-dry-ground"); return null; }
lo[i] = Math.max(lo[i], top - MAX_ROUTE_CUT); hi[i] = Math.min(hi[i], top + MAX_ROUTE_FILL);
OwnedCell old = existing.get(key(column.x(), column.z()));
if (old != null) { lo[i] = Math.max(lo[i], old.cell().floorY()); hi[i] = Math.min(hi[i], old.cell().floorY()); }
if (readOnly && top != (i == 0 ? from.entry().y() : to.entry().y()) - 1) return null;
}
if (i == 0 || i == size - 1) {
int exact = (i == 0 ? from.entry().y() : to.entry().y()) - 1;
lo[i] = Math.max(lo[i], exact); hi[i] = Math.min(hi[i], exact);
}
if (lo[i] > hi[i]) { probe.reject("route-grade-interval"); return null; }
}
// Curves and both adjacent rails share one elevation; a Minecraft curved rail cannot slope.
int groups = 1;
for (int i = 1; i < size; i++) { if (!flat[i - 1]) groups++; group[i] = groups - 1; }
int[] gl = new int[groups], gh = new int[groups], gd = new int[groups], y = new int[groups];
java.util.Arrays.fill(gl, 14); java.util.Arrays.fill(gh, 350);
for (int i = 0; i < size; i++) { int g = group[i]; gl[g] = Math.max(gl[g], lo[i]); gh[g] = Math.min(gh[g], hi[i]); gd[g] = desired[i]; }
for (int g = 1; g < groups; g++) { gl[g] = Math.max(gl[g], gl[g - 1] - 1); gh[g] = Math.min(gh[g], gh[g - 1] + 1); }
for (int g = groups - 2; g >= 0; g--) { gl[g] = Math.max(gl[g], gl[g + 1] - 1); gh[g] = Math.min(gh[g], gh[g + 1] + 1); }
for (int g = 0; g < groups; g++) {
if (gl[g] > gh[g]) { probe.reject("route-grade"); return null; }
int lower = g == 0 ? gl[g] : Math.max(gl[g], y[g - 1] - 1), upper = g == 0 ? gh[g] : Math.min(gh[g], y[g - 1] + 1);
y[g] = Math.clamp(gd[g], lower, upper);
}
var path = new ArrayList<Point>(); var floors = new LinkedHashMap<Long, Integer>();
for (int i = 0; i < size; i++) {
XZ point = points.get(i); int floor = y[group[i]]; path.add(new Point(point.x(), floor, point.z()));
if (i == 0 && from.readOnly() || i == size - 1 && to.readOnly()) continue;
for (XZ column : strip(points, i)) {
long key = key(column.x(), column.z()); Integer old = floors.putIfAbsent(key, floor);
if (old != null && old != floor) { probe.reject("route-self-grade"); return null; }
// Historical endpoints and their lateral landing are read-only, even at another nearby turn.
for (var landmark : landmarks) if (landingColumn(landmark.entry(), column)) {
boolean actualEnd = from.readOnly() && landmark.id().equals(from.id())
|| to.readOnly() && landmark.id().equals(to.id());
if (!actualEnd || floor != landmark.entry().y() - 1) { probe.reject("route-historical-entry"); return null; }
floors.remove(key); break;
}
}
}
var cells = new LinkedHashMap<Long, Cell>();
for (var entry : floors.entrySet()) {
int x = (int) (entry.getKey() >> 32), z = (int) (long) entry.getKey();
probe.routeBlock = new XZ(x, z);
Cell cell = routeCell(x, entry.getValue(), z, Rail.NONE, false, probe);
if (cell == null) { probe.reject("route-column-certificate"); return null; }
cells.put(entry.getKey(), cell);
}
if (request.kind() == RouteKind.FREIGHT) for (int i = 0; i < path.size(); i++) {
Point point = path.get(i); long key = key(point.x(), point.z()); Cell cell = cells.get(key);
if (cell == null) continue;
Rail rail = rail(path, i);
// A one-block valley cannot carry two ascending slopes on the same rail. Keep a repairable rail gap.
boolean intact = rail != Rail.NONE && Math.floorMod(mix(seed ^ key ^ request.id().hashCode()), 19) != 0;
if (rail == Rail.NONE) rail = points.get(Math.max(0, i - 1)).x() == points.get(Math.min(size - 1, i + 1)).x() ? Rail.NORTH_SOUTH : Rail.EAST_WEST;
cells.put(key, new Cell(cell.x(), cell.floorY(), cell.z(), cell.foundationDepth(), rail, intact));
}
return new RouteDraft(List.copyOf(path), cells);
}
private static boolean landingColumn(Entry entry, XZ column) {
int dx = column.x() - entry.x(), dz = column.z() - entry.z();
return DX[entry.facing()] * dx + DZ[entry.facing()] * dz == 0 && Math.abs(dx) + Math.abs(dz) <= 1;
}
private static List<XZ> strip(List<XZ> points, int i) {
XZ p = points.get(i); var result = new ArrayList<XZ>();
if (turn(points, i)) {
for (int dx = -1; dx <= 1; dx++) for (int dz = -1; dz <= 1; dz++) result.add(new XZ(p.x() + dx, p.z() + dz));
} else {
XZ other = points.get(i == points.size() - 1 ? i - 1 : i + 1);
int dx = Integer.signum(other.x() - p.x()), dz = Integer.signum(other.z() - p.z());
for (int cross = -1; cross <= 1; cross++) result.add(new XZ(p.x() + dz * cross, p.z() - dx * cross));
}
return result;
}
private static boolean turn(List<XZ> points, int i) {
if (i == 0 || i == points.size() - 1) return false;
XZ a = points.get(i - 1), b = points.get(i + 1);
return a.x() != b.x() && a.z() != b.z();
}
private static Rail rail(List<Point> path, int i) {
Point p = path.get(i), a = path.get(Math.max(0, i - 1)), b = path.get(Math.min(path.size() - 1, i + 1));
if (a.floorY() > p.floorY() && b.floorY() > p.floorY()) return Rail.NONE;
Point high = a.floorY() > p.floorY() ? a : b.floorY() > p.floorY() ? b : null;
if (high != null) return switch (direction(p.x(), p.z(), high.x(), high.z())) {
case 0 -> Rail.ASCENDING_NORTH; case 1 -> Rail.ASCENDING_EAST; case 2 -> Rail.ASCENDING_SOUTH; default -> Rail.ASCENDING_WEST;
};
if (a.x() == b.x()) return Rail.NORTH_SOUTH;
if (a.z() == b.z()) return Rail.EAST_WEST;
boolean n = a.z() < p.z() || b.z() < p.z(), s = a.z() > p.z() || b.z() > p.z();
boolean e = a.x() > p.x() || b.x() > p.x();
return n ? e ? Rail.NORTH_EAST : Rail.NORTH_WEST : s && e ? Rail.SOUTH_EAST : Rail.SOUTH_WEST;
}
private static Cell routeCell(int x, int floor, int z, Rail rail, boolean present, Probe probe) {
int top = probe.surface(x, z);
if (floor < 14 || floor > 350 || top < floor - MAX_ROUTE_FILL || top > floor + MAX_ROUTE_CUT) return null;
int foundation = Math.max(1, floor - top + 1), base = floor - foundation;
if (!probe.solid(x, base - 1, z) || !probe.solid(x, base, z)) return null;
for (int y = base - 1; y <= floor + 3; y++) if (probe.protectedAt(x, y, z)) return null;
return new Cell(x, floor, z, foundation, rail, present);
}
private record XZ(int x, int z) { XZ move(int facing, int distance) { return new XZ(x + DX[facing] * distance, z + DZ[facing] * distance); } }
private record SegmentKey(Request request, int cx, int cz) {}
private static List<Segment> segments(long seed, Map<Long, OwnedCell> cells) {
var groups = new LinkedHashMap<SegmentKey, List<Cell>>();
for (var owned : cells.values()) {
Cell c = owned.cell(); var key = new SegmentKey(owned.request(), c.x() >> 4, c.z() >> 4);
groups.computeIfAbsent(key, ignored -> new ArrayList<>()).add(c);
}
var result = new ArrayList<Segment>();
for (var entry : groups.entrySet()) {
var k = entry.getKey(); var r = k.request();
// IDs are stable, bounded and independent of native generation order.
String id = "piece_" + r.id() + "_" + k.cx() + "_" + k.cz();
result.add(new Segment(id, r.from(), r.to(), r.kind(), k.cx(), k.cz(), entry.getValue(), mix(seed ^ key(k.cx(), k.cz()) ^ r.id().hashCode())));
}
return List.copyOf(result);
}
private static int span(Entry a, Entry b) { return Math.max(Math.abs(a.x() - b.x()), Math.abs(a.z() - b.z())); }
private static Site find(List<Site> sites, Kind kind) { return sites.stream().filter(site -> site.kind() == kind).findFirst().orElse(null); }
private static String id(Kind kind) { return kind.name().toLowerCase(Locale.ROOT); }
private static int direction(int x, int z, int tx, int tz) { return Math.abs(tx - x) >= Math.abs(tz - z) ? tx > x ? 1 : 3 : tz > z ? 2 : 0; }
private static boolean overlapsSites(int x0, int z0, int x1, int z1, List<Site> sites) {
for (var site : sites) if (x0 <= site.maxX() && x1 >= site.minX() && z0 <= site.maxZ() && z1 >= site.minZ()) return true;
return false;
}
private record SiteDraft(Site site, List<Cell> landings) {}
private record Candidate(int x, int z, int facing, int score, long seed) {}
private record Endpoint(String id, Entry entry, boolean readOnly) {}
private record Request(String id, String from, String to, RouteKind kind) {}
private record OwnedCell(Request request, Cell cell) {}
private record RouteDraft(List<Point> path, Map<Long, Cell> cells) {}
private static final class BudgetExceeded extends RuntimeException {
final boolean columns; BudgetExceeded(boolean columns) { this.columns = columns; }
}
private static final class Probe {
final Terrain terrain; final int cx, cz; final double radius;
final Map<Long, Integer> columns = new HashMap<>();
XZ routeBlock; Map<String, Integer> reasons = new HashMap<>(); int samples, columnLimit, sampleLimit;
Probe(Terrain terrain, int cx, int cz, int diameter) { this.terrain = java.util.Objects.requireNonNull(terrain); this.cx = cx; this.cz = cz; radius = diameter / 2.0 - 2; }
int surface(int x, int z) {
if (Math.hypot((double) x - cx, (double) z - cz) > radius || !terrain.owns(x, z)) return INVALID;
long key = key(x, z); var cached = columns.get(key); if (cached != null) return cached;
if (columns.size() >= columnLimit) throw new BudgetExceeded(true);
count(); int top = terrain.surfaceY(x, z); columns.put(key, top); return top;
}
boolean solid(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) == INVALID) return false; count(); return terrain.solid(x, y, z); }
boolean protectedAt(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) == INVALID) return true; count(); return terrain.protectedAt(x, y, z); }
void count() { if (samples >= sampleLimit) throw new BudgetExceeded(false); samples++; }
void reject(String reason) { reasons.merge(reason, 1, Integer::sum); }
}
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
private static double unit(long value) { return (value >>> 11) * 0x1.0p-53; }
private static long mix(long value) { value = (value ^ value >>> 30) * 0xbf58476d1ce4e5b9L; value = (value ^ value >>> 27) * 0x94d049bb133111ebL; return value ^ value >>> 31; }
}
@@ -14,7 +14,7 @@ public final class HeritageStructure19 extends Structure {
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21) || !generator.experimentalStructures()) return Optional.empty();
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && generator.rootGeneration() != 22) || !generator.experimentalStructures()) return Optional.empty();
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
if (island == null || !island.origin() || (island.generation() != generator.rootGeneration())) return Optional.empty();
var plan = generator.heritagePlan19();
@@ -23,9 +23,9 @@ public final class HeritageStructure19 extends Structure {
// Local route segments render before buildings. No piece needs a distant link endpoint loaded.
for (var segment : plan.segments())
if (segment.startChunkX() == x && segment.startChunkZ() == z)
pieces.add(generator.rootGeneration() == 21 ? new HeritagePiece21(segment) : generator.rootGeneration() == 20 ? new HeritagePiece20(segment) : new HeritagePiece19(segment));
pieces.add((generator.rootGeneration() == 21 || generator.rootGeneration() == 22) ? new HeritagePiece21(segment) : generator.rootGeneration() == 20 ? new HeritagePiece20(segment) : new HeritagePiece19(segment));
for (var site : plan.sites()) if (site.startChunkX() == x && site.startChunkZ() == z)
pieces.add(generator.rootGeneration() == 21 ? new HeritagePiece21(site) : generator.rootGeneration() == 20 ? new HeritagePiece20(site) : new HeritagePiece19(site));
pieces.add((generator.rootGeneration() == 21 || generator.rootGeneration() == 22) ? new HeritagePiece21(site) : generator.rootGeneration() == 20 ? new HeritagePiece20(site) : new HeritagePiece19(site));
if (pieces.isEmpty()) return Optional.empty();
for (var piece : pieces) {
var box = piece.getBoundingBox();
@@ -57,7 +57,7 @@ public final class HeritageStructures19 {
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> level) {
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21)
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && generator.rootGeneration() != 22)
|| !sanctuary.experimentalStructures()) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
@@ -79,7 +79,7 @@ public final class HeritageStructures19 {
BlockPos origin, int radius, boolean skipReferenced) {
if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21) || !generator.experimentalStructures()
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && generator.rootGeneration() != 22) || !generator.experimentalStructures()
|| !level.structureManager().shouldGenerateStructures()) return null;
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
if (!structures.contains(holder)) return null;
@@ -13,7 +13,7 @@ public final class RuinsStructure17 extends Structure {
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21) || !generator.experimentalStructures()) return Optional.empty();
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && generator.rootGeneration() != 22) || !generator.experimentalStructures()) return Optional.empty();
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return Optional.empty();
var networks = generator.ruinsPlan17().networks().stream()
@@ -30,9 +30,9 @@ public final class RuinsStructure17 extends Structure {
return Optional.of(new GenerationStub(RuinsStructures17.locator(networks.getFirst()), pieces -> {
// Shafts come last so their surface hatches cannot be filled by a connected room.
networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() != RuinsPlan17.Kind.ACCESS)
.forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
.forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS)
.forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
.forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
}));
}
@Override public StructureType<?> type() { return RuinsStructures17.TYPE; }
@@ -55,7 +55,7 @@ public final class RuinsStructures17 {
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> level) {
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21)
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && generator.rootGeneration() != 22)
|| !sanctuary.experimentalStructures()) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
@@ -78,7 +78,7 @@ public final class RuinsStructures17 {
BlockPos origin, int radius, boolean skipReferenced) {
if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21) || !generator.experimentalStructures()
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && generator.rootGeneration() != 22) || !generator.experimentalStructures()
|| !level.structureManager().shouldGenerateStructures()) return null;
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
if (!structures.contains(holder)) return null;
@@ -0,0 +1,52 @@
package fr.koka.sanctuary.worldgen.transit;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceKey;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.loot.LootTable;
/** Distinct native piece ID selects the corrected22 corner without migrating any saved21 piece. */
public final class TransitPiece22 extends StructurePiece {
private final TransitPlan21.Module room;
public TransitPiece22(TransitPlan21.Module room) {
super(TransitStructures21.PIECE22, 0, TransitPiece21.bounds(room));
this.room = room; setOrientation(null);
}
public TransitPiece22(StructurePieceSerializationContext context, CompoundTag tag) {
super(TransitStructures21.PIECE22, tag);
room = TransitPiece21.decodeModule(tag.getCompound("SanctuaryModule")
.orElseThrow(() -> new IllegalArgumentException("Missing saved transit22 room")));
if (!getBoundingBox().equals(TransitPiece21.bounds(room)))
throw new IllegalArgumentException("Transit22 room disagrees with its saved native bounding box");
setOrientation(null);
}
public TransitPlan21.Module module() { return room; }
@Override public BlockPos getLocatorPosition() { return TransitPiece21.locator(room); }
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
tag.put("SanctuaryModule", TransitPiece21.encodeModule(room));
}
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
TransitStructures21.placing(() -> TransitRenderer22.render(room, new LostCityRenderer.BlockWriter() {
private BlockPos world(int x, int y, int z) { return new BlockPos(room.minX() + x, room.minY() + y, room.minZ() + z); }
@Override public void set(int x, int y, int z, BlockState block) {
var pos = world(x, y, z);
if (getBoundingBox().isInside(pos) && clip.isInside(pos)) placeBlock(level, block, pos.getX(), pos.getY(), pos.getZ(), clip);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
var pos = world(x, y, z);
if (getBoundingBox().isInside(pos) && clip.isInside(pos)) createChest(level, clip, RandomSource.create(seed), pos, loot, null);
}
}));
}
}
@@ -0,0 +1,13 @@
package fr.koka.sanctuary.worldgen.transit;
/** The main traversal is core infrastructure in22. Reserve it before optional networks and sites.
* Its established civil engineering may bridge a short cavity with certified bearings; the stricter
* surface-building policy must not remove this underground destination. No terrain is generated here. */
public final class TransitPlanner22 {
public static final int MAX_COLUMNS = TransitPlanner21.MAX_COLUMNS;
public static final int MAX_PIER = 16, MAX_WEAK_SPAN = 34;
private TransitPlanner22() {}
public static TransitPlan21 plan(long seed, int centerX, int centerZ, int diameter, TransitPlanner21.Terrain terrain) {
return TransitPlanner21.plan(seed, centerX, centerZ, diameter, terrain);
}
}
@@ -0,0 +1,24 @@
package fr.koka.sanctuary.worldgen.transit;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
import net.minecraft.world.level.block.Blocks;
/** New22 corners retain complete walls on sides that have no saved opening.
* Existing21 pieces keep their historical paint sequence and geometry. */
public final class TransitRenderer22 {
private TransitRenderer22() {}
public static void render(TransitPlan21.Module module, LostCityRenderer.BlockWriter output) {
if (module.kind() != TransitPlan21.Kind.JUNCTION) { TransitRenderer21.render(module, output); return; }
var buffer = ConnectedBlocks20.buffer(output);
TransitRenderer21.render(module, buffer);
// The roadway crossing and its two slab curbs must stop at an unopened wall.
for (int direction = 0; direction < 4; direction++) if ((module.openings() & 1 << direction) == 0)
for (int across = -3; across <= 3; across++) {
int x = direction == 1 ? module.width() - 1 : direction == 3 ? 0 : module.width() / 2 + across;
int z = direction == 0 ? 0 : direction == 2 ? module.depth() - 1 : module.depth() / 2 + across;
buffer.set(x, 2, z, Blocks.TUFF_BRICKS.defaultBlockState());
}
buffer.flush();
}
}
@@ -0,0 +1,236 @@
package fr.koka.sanctuary.worldgen.transit;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Module;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Support;
import fr.koka.sanctuary.worldgen.transit.TransitPlanner21.Terrain;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/** Generation22 fallback alignments around protected volumes. It is called with a fresh terrain
* sampler after the historical straight-route search fails. No partial route is ever published. */
public final class TransitRoute22 {
public static final int MAX_COLUMNS = 12_000;
private TransitRoute22() {}
public static TransitPlan21 plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
if (!List.of(512, 724, 1024).contains(diameter)) throw new IllegalArgumentException("Unsupported transit detour diameter");
var probe = new Probe(terrain);
var choices = new ArrayList<Choice>(); String last = "no supported protected-volume detour";
int farRadius = (int) Math.ceil(diameter * .21 / 17) * 17;
int minimumRadius = (int) Math.ceil(diameter / 8.0 / 17) * 17;
var radii = new java.util.LinkedHashSet<Integer>(List.of(Math.max(minimumRadius, farRadius - 34), minimumRadius, farRadius));
// Search actual entrance pairs before repeatedly testing their identical bad porticos at seven depths.
// Fine translations also remove the previous assumption that every entrance must lie on x/z=17n.
int[][] offsets = {{0, 0}, {-17, 0}, {17, 0}, {0, -17}, {0, 17}, {-8, -8}, {8, 8}, {-8, 8}, {8, -8},
{-34, 0}, {34, 0}, {0, -34}, {0, 34}, {-34, -34}, {34, 34}, {-34, 34}, {34, -34}};
probe.columnLimit = 5_500; probe.sampleLimit = 1_200_000;
int alignments = 0;
try {
search: for (var offset : offsets) for (int radius : radii) for (int shape = 0; shape < 8; shape++)
for (boolean alongX : new boolean[]{true, false}) {
var line = alignment(cx + offset[0], cz + offset[1], radius, 0, shape, alongX);
alignments++;
int firstTop = approachTop(line.getFirst(), probe), lastTop = approachTop(line.getLast(), probe);
String geometry = "shape=" + shape + " axis=" + (alongX ? "east-west" : "north-south")
+ " radius=" + radius + " translation=" + offset[0] + "," + offset[1];
if (firstTop < 0 || lastTop < 0) { last = "no suitable pair of natural porticos"; diagnostic("portico reject " + geometry); continue; }
int selectedAtGeometry = 0;
for (int floor : floors(firstTop, lastTop)) {
var route = candidate(seed, line, floor, probe);
String label = geometry + " floor=" + floor;
if (route == null) { last = probe.reason; diagnostic("reject " + label + " " + last + " columns=" + probe.columns.size()); continue; }
int cost = route.stream().mapToInt(m -> m.supports().stream().mapToInt(p -> p.maxY() - p.minY() + 1).sum()
+ (m.kind() == Kind.ACCESS ? m.surfaceY() - m.floorY() : 0)).sum();
choices.add(new Choice(route, cost + line.size() * 4, label));
diagnostic("preselected " + label + " modules=" + route.size());
if (choices.size() >= 8) break search;
// Keep alternative footprints available if exact hydrology rejects one alignment.
if (++selectedAtGeometry >= 2) break;
}
}
} catch (BudgetExceeded exhausted) { diagnostic("prescreen budget; retain " + choices.size()); }
probe.columnLimit = MAX_COLUMNS; probe.sampleLimit = TransitPlan21.MAX_SAMPLES;
choices.sort(java.util.Comparator.comparingInt(Choice::cost));
try {
for (var choice : choices) {
var modules = new ArrayList<Module>();
for (var rough : choice.modules()) {
var complete = certify(rough, probe, true);
if (complete == null) { last = probe.reason; break; }
modules.add(complete);
}
if (modules.size() != choice.modules().size()) { diagnostic("exact reject " + choice.label() + " " + last); continue; }
int span = Math.max(Math.abs(modules.getFirst().centerX() - modules.getLast().centerX()),
Math.abs(modules.getFirst().centerZ() - modules.getLast().centerZ()));
return new TransitPlan21(modules, probe.samples, "accepted detour " + choice.label() + " span=" + span
+ " columns=" + probe.columns.size() + " bends=" + modules.stream().filter(m -> m.kind() == Kind.JUNCTION).count());
}
} catch (BudgetExceeded exhausted) { last = "bounded detour search exhausted: " + probe.reason; }
return new TransitPlan21(List.of(), probe.samples, last + "; candidates=" + choices.size() + "; alignments=" + alignments + "; columns=" + probe.columns.size());
}
private static List<Integer> floors(int a, int b) {
int lowerTop = Math.min(a, b), upperTop = Math.max(a, b);
var result = new java.util.LinkedHashSet<Integer>();
for (int y : new int[]{lowerTop - 22, lowerTop - 38, lowerTop - 54, lowerTop - 70, 216, 200, 184, 160})
if (y >= 32 && y >= upperTop - 145 && y <= lowerTop - 18) result.add(y);
return List.copyOf(result);
}
/** Height-independent portion of the existing21 portico certificate. Failure is cached per entrance. */
private static int approachTop(Point point, Probe probe) {
Integer old = probe.approaches.get(point); if (old != null) return old;
int top = -1, low = 384;
for (int x = point.x() - 2; x <= point.x() + 2; x++) for (int z = point.z() - 11; z <= point.z() - 9; z++) {
int y = probe.surface(x, z); low = Math.min(low, y); top = Math.max(top, y);
}
boolean valid = low >= 0 && top <= 378 && top - low <= 1;
if (valid) for (int x = point.x() - 2; x <= point.x() + 2; x++) for (int z = point.z() - 11; z <= point.z() - 9; z++) {
int y = probe.surface(x, z);
if (probe.protectedAt(x, y, z) || probe.protectedAt(x, top + 1, z) || probe.protectedAt(x, top + 2, z)) valid = false;
}
if (valid) for (int x : new int[]{point.x() - 8, point.x(), point.x() + 8})
for (int z : new int[]{point.z() - 8, point.z(), point.z() + 8}) {
int y = probe.surface(x, z); if (y < top - 24 || y > top + 16) valid = false;
}
probe.approaches.put(point, valid ? top : -1);
return valid ? top : -1;
}
private record Choice(List<Module> modules, int cost, String label) {}
private record Point(int x, int z) {}
private static List<Point> alignment(int cx, int cz, int radius, int shift, int shape, boolean alongX) {
var points = new ArrayList<Point>();
if (shape < 6) {
int offset = (shape / 2 + 1) * 34 * ((shape & 1) == 0 ? -1 : 1);
points.add(new Point(-radius, 0)); line(points, -radius, offset); line(points, radius, offset); line(points, radius, 0);
} else {
int offset = 68;
points.add(new Point(-radius, -offset));
if (shape == 6) line(points, radius, -offset); else line(points, -radius, offset);
line(points, radius, offset);
}
return points.stream().map(p -> new Point(cx + (alongX ? p.x() : p.z() + shift),
cz + (alongX ? p.z() + shift : p.x()))).toList();
}
private static void line(List<Point> points, int x, int z) {
var p = points.getLast(); int dx = Integer.signum(x - p.x()) * 17, dz = Integer.signum(z - p.z()) * 17;
while (p.x() != x || p.z() != z) { p = new Point(p.x() + dx, p.z() + dz); points.add(p); }
}
private static int direction(Point from, Point to) {
return from.x() == to.x() ? to.z() > from.z() ? 2 : 0 : to.x() > from.x() ? 1 : 3;
}
private static List<Module> candidate(long seed, List<Point> points, int floor, Probe probe) {
// A sparse route-body test rejects a wrong alignment before full shafts and slabs are sampled.
for (var p : points) for (int dx : new int[]{-4, 0, 4}) for (int dz : new int[]{-4, 0, 4})
for (int y : new int[]{floor - 2, floor, floor + 4, floor + 8}) if (probe.protectedAt(p.x() + dx, y, p.z() + dz)) {
probe.reason = "protected alignment [" + p.x() + "," + y + "," + p.z() + "]"; return null;
}
var first = points.getFirst(); var last = points.getLast();
var entrance = access(first.x(), first.z(), floor, 1 << direction(first, points.get(1)), seed ^ 0x2201L, probe, false);
if (entrance == null) return null;
var exit = access(last.x(), last.z(), floor, 1 << direction(last, points.get(points.size() - 2)), seed ^ 0x2202L, probe, false);
if (exit == null) return null;
var result = new ArrayList<Module>(); result.add(entrance); int unanchored = 0;
for (int i = 1; i < points.size() - 1; i++) {
var p = points.get(i); int a = direction(p, points.get(i - 1)), b = direction(p, points.get(i + 1));
boolean bend = ((a + 2) & 3) != b, alongX = a == 1 || a == 3;
int width = bend || alongX ? 17 : 9, depth = bend || !alongX ? 17 : 9;
var module = certify(new Module(bend ? Kind.JUNCTION : Kind.GALLERY, p.x() - width / 2, floor - 1,
p.z() - depth / 2, width, 10, depth, alongX ? 1 : 0, (1 << a) | (1 << b),
seed ^ (i * 0x9E3779B97F4A7C15L), List.of()), probe, false);
if (module == null) return null;
unanchored = probe.bearings.get(module) == 15 ? 0 : unanchored + 1;
if (unanchored > 2) { probe.reason = "unsupported detour span exceeds 34 blocks"; return null; }
result.add(module);
}
result.add(exit);
return List.copyOf(result);
}
private static Module access(int cx, int cz, int floor, int opening, long seed, Probe probe, boolean full) {
int top = -1, low = 384;
for (int x = cx - 2; x <= cx + 2; x++) for (int z = cz - 11; z <= cz - 9; z++) {
int surface = probe.surface(x, z); top = Math.max(top, surface); low = Math.min(low, surface);
}
if (low < 0 || top - low > 1) { probe.reason = "access approach relief [" + cx + "," + cz + "] " + low + ".." + top; return null; }
if (top < floor + 18 || top > floor + 145) { probe.reason = "surface access height"; return null; }
// Its visible north portico meets real ground; its three-wide approach is never an unsupported threshold.
for (int x = cx - 2; x <= cx + 2; x++) for (int z = cz - 11; z <= cz - 9; z++) {
int y = probe.surface(x, z);
if (y < top - 1 || y > top || probe.protectedAt(x, y, z)
|| probe.protectedAt(x, top + 1, z) || probe.protectedAt(x, top + 2, z)) {
probe.reason = "access approach [" + x + "," + top + "," + z + "] terrain=" + y; return null;
}
}
for (int x : new int[]{cx - 8, cx, cx + 8}) for (int z : new int[]{cz - 8, cz, cz + 8}) {
int y = probe.surface(x, z);
// The portico alone needs level terrain. A buried shaft can follow a hillside;
// its local walls and checked footings carry the deck without flattening the hill.
if (y < Math.max(floor + 8, top - 24) || y > top + 16) { probe.reason = "access hillside [" + x + "," + top + "," + z + "] terrain=" + y; return null; }
}
return certify(new Module(Kind.ACCESS, cx - 8, floor - 1, cz - 8, 17,
top - (floor - 1) + 5, 17, 0, opening, seed, List.of()), probe, full);
}
private static Module certify(Module module, Probe probe, boolean full) {
var supports = new ArrayList<Support>();
int bearingQuadrants = 0;
for (int z = module.minZ();; z = Math.min(z + 4, module.maxZ())) {
for (int x = module.minX();; x = Math.min(x + 4, module.maxX())) {
if (!probe.owns(x, z)) { probe.reason = "outside island"; return null; }
if (!full) for (int y = module.minY(); y <= module.maxY(); y += 4)
if (probe.protectedAt(x, y, z)) {
probe.reason = "coarse " + module.kind() + " [" + x + "," + y + "," + z + "] " + probe.terrain.obstruction(x, y, z); return null;
}
int anchor = module.minY() - 1;
while (anchor >= module.minY() - 17 && !(probe.solid(x, anchor, z) && probe.solid(x, anchor - 1, z))) anchor--;
if (anchor >= module.minY() - 17) {
for (int y = anchor - 1; y < module.minY(); y++) if (probe.protectedAt(x, y, z)) {
probe.reason = "protected footing"; return null;
}
if (anchor < module.minY() - 1) supports.add(new Support(x, anchor + 1, z, module.minY() - 1));
bearingQuadrants |= 1 << ((x < module.centerX() ? 0 : 1) + (z < module.centerZ() ? 0 : 2));
}
if (x == module.maxX()) break;
}
if (z == module.maxZ()) break;
}
// The continuous two-block deck can span gaps between its bearings. Require
// untouched rock in each quarter, not a pier at every point above a cavity.
if (bearingQuadrants != 15 && module.kind() != Kind.GALLERY) { probe.reason = "unsupported quarters " + module.kind()
+ " [" + module.minX() + "," + module.minY() + "," + module.minZ() + "] mask=" + bearingQuadrants; return null; }
if (full) {
// Hydrology and all previous structures are checked at every edited voxel after the cheap footing test.
for (int z = module.minZ(); z <= module.maxZ(); z++) for (int x = module.minX(); x <= module.maxX(); x++) {
if (!probe.owns(x, z)) { probe.reason = "outside island"; return null; }
for (int y = module.minY() - 2; y <= module.maxY(); y++) if (probe.protectedAt(x, y, z)) {
probe.reason = "volume " + module.kind() + " [" + x + "," + y + "," + z + "] " + probe.terrain.obstruction(x, y, z); return null;
}
}
}
var certified = new Module(module.kind(), module.minX(), module.minY(), module.minZ(), module.width(), module.height(),
module.depth(), module.facing(), module.openings(), module.seed(), supports);
probe.bearings.put(certified, bearingQuadrants);
return certified;
}
private static void diagnostic(String message) {
if (Boolean.getBoolean("sanctuary.test.transit22") || Boolean.getBoolean("sanctuary.transit.plannerDiagnostics"))
System.getLogger(TransitRoute22.class.getName()).log(System.Logger.Level.INFO, "TransitRoute22 " + message);
}
private static final class Probe {
final Terrain terrain; final Set<Long> columns = new HashSet<>(); int samples; int sampleLimit = TransitPlan21.MAX_SAMPLES, columnLimit = MAX_COLUMNS; String reason = "budget";
final java.util.Map<Module, Integer> bearings = new java.util.HashMap<>();
final java.util.Map<Point, Integer> approaches = new java.util.HashMap<>();
Probe(Terrain terrain) { this.terrain = terrain; }
void visit(int x, int z) {
if (samples >= sampleLimit) throw new BudgetExceeded();
samples++;
long key = (long) x << 32 ^ z & 0xffffffffL;
if (!columns.contains(key) && columns.size() >= columnLimit) throw new BudgetExceeded();
columns.add(key);
}
boolean owns(int x, int z) { visit(x, z); return terrain.owns(x, z); }
boolean solid(int x, int y, int z) { visit(x, z); return terrain.solid(x, y, z); }
boolean protectedAt(int x, int y, int z) { visit(x, z); return terrain.protectedAt(x, y, z); }
int surface(int x, int z) { visit(x, z); return terrain.surfaceY(x, z); }
}
private static final class BudgetExceeded extends RuntimeException { private static final long serialVersionUID = 1L; }
}
@@ -12,9 +12,9 @@ public final class TransitStructure21 extends Structure {
public TransitStructure21(StructureSettings settings) { super(settings); }
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 21 || !generator.experimentalStructures()) return Optional.empty();
|| (generator.rootGeneration() != 21 && generator.rootGeneration() != 22) || generator.rootGeneration() != 22 && !generator.experimentalStructures()) return Optional.empty();
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
if (island == null || !island.origin() || island.generation() != 21) return Optional.empty();
if (island == null || !island.origin() || (island.generation() != 21 && island.generation() != 22)) return Optional.empty();
var modules = generator.transitPlan21().modulesForChunk(context.chunkPos().x(), context.chunkPos().z());
if (modules.isEmpty()) return Optional.empty();
for (var module : modules) {
@@ -25,7 +25,9 @@ public final class TransitStructure21 extends Structure {
throw new IllegalStateException("Transit module exceeds local native reference range");
}
return Optional.of(new GenerationStub(TransitPiece21.locator(modules.getFirst()), pieces ->
modules.forEach(module -> pieces.addPiece(new TransitPiece21(module)))));
modules.forEach(module -> pieces.addPiece(generator.rootGeneration() == 22
&& module.kind() == TransitPlan21.Kind.JUNCTION && Integer.bitCount(module.openings()) == 2
? new TransitPiece22(module) : new TransitPiece21(module)))));
}
@Override public StructureType<?> type() { return TransitStructures21.TYPE; }
}
@@ -32,6 +32,8 @@ public final class TransitStructures21 {
SanctuaryMod.id("industrial_transit_v21"), () -> TransitStructure21.CODEC);
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
SanctuaryMod.id("industrial_transit_module_v21"), TransitPiece21::new);
public static final StructurePieceType PIECE22 = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
SanctuaryMod.id("industrial_transit_module_v22"), TransitPiece22::new);
private static final ThreadLocal<Integer> PLACEMENT_DEPTH = ThreadLocal.withInitial(() -> 0);
private TransitStructures21() {}
public static void register() {}
@@ -44,10 +46,10 @@ public final class TransitStructures21 {
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> level) {
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || generator.rootGeneration() != 21
|| !sanctuary.experimentalStructures()) return;
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 21 && generator.rootGeneration() != 22)
|| generator.rootGeneration() != 22 && !sanctuary.experimentalStructures()) return;
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
if (island == null || !island.origin() || island.generation() != 21) return;
if (island == null || !island.origin() || (island.generation() != 21 && island.generation() != 22)) return;
if (sanctuary.transitPlan21().modulesForChunk(chunk.getPos().x(), chunk.getPos().z()).isEmpty()) return;
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
StructureStart previous = manager.getStartForStructure(holder.value(), chunk);
@@ -64,7 +66,7 @@ public final class TransitStructures21 {
BlockPos origin, int radius, boolean skipReferenced) {
if (!Level.OVERWORLD.equals(level.dimension())
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 21 || !generator.experimentalStructures()
|| (generator.rootGeneration() != 21 && generator.rootGeneration() != 22) || generator.rootGeneration() != 22 && !generator.experimentalStructures()
|| !level.structureManager().shouldGenerateStructures()) return null;
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
if (!structures.contains(holder)) return null;
@@ -128,6 +128,8 @@
"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",
"sanctuary.underground_hall.entry": "Underground hall entrance: %s, %s, %s",
"sanctuary.world_options.heritage22_hint": "Adds experimental buildings and networks. The main traversal and underground hall belong to the island and remain without this option.",
"sanctuary.world_options.heritage_hint": "Adds forgotten networks, buildings and abandoned routes to the new island. This choice is saved with the world.",
"structure.sanctuary.heritage_networks": "Abandoned infrastructure",
"sanctuary.heritage.kind.freight_station": "Freight station",
@@ -128,6 +128,8 @@
"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",
"sanctuary.underground_hall.entry": "Accès à la salle souterraine : %s, %s, %s",
"sanctuary.world_options.heritage22_hint": "Ajoute les bâtiments et réseaux expérimentaux. La grande traversée et la salle souterraine font partie de l’île et restent présentes sans cette option.",
"sanctuary.world_options.heritage_hint": "Ajoute les anciens réseaux, bâtiments et voies abandonnées sur la nouvelle île. Ce choix est enregistré avec le monde.",
"structure.sanctuary.heritage_networks": "Anciennes infrastructures",
"sanctuary.heritage.kind.freight_station": "Gare de marchandises",
@@ -0,0 +1,7 @@
{
"type": "sanctuary:underground_hall_v22",
"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_v21",
"type": "sanctuary:island_v22",
"biome_source": {
"biomes": [
"minecraft:the_void",
@@ -0,0 +1,81 @@
{
"dimensions": {
"minecraft:overworld": {
"type": "sanctuary:sanctuary_384",
"generator": {
"type": "sanctuary:island_v21",
"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"
}
}
}
}
@@ -72,7 +72,7 @@ public final class ExpansionJournalSmoke {
require(Arrays.equals(unifiedMediumBytes, Files.readAllBytes(unifiedMediumDirectory.resolve(ExpansionJournal.FILE_NAME))),
"Rejected cross-generation read changed its journal");
for (int generation : List.of(20, 21)) {
for (int generation : List.of(20, 21, 22)) {
var saved = scratch.resolve("architecture-cross-version-" + generation);
var root21 = ExpansionIsland.origin(724, SEED, generation);
var journal21 = new ExpansionJournal(saved, SEED, 724, generation);
@@ -80,7 +80,7 @@ public final class ExpansionJournalSmoke {
journal21.commit(List.of(root21, child));
byte[] bytes = Files.readAllBytes(saved.resolve(ExpansionJournal.FILE_NAME));
require(new ExpansionJournal(saved, SEED, 724, generation).islands().equals(List.of(root21, child)), "Architecture journal round trip");
reject(() -> new ExpansionJournal(saved, SEED, 724, generation == 20 ? 21 : 20), "Cross-generation architecture journal");
reject(() -> new ExpansionJournal(saved, SEED, 724, generation == 22 ? 21 : 22), "Cross-generation architecture journal");
require(Arrays.equals(bytes, Files.readAllBytes(saved.resolve(ExpansionJournal.FILE_NAME))), "Rejected upgrade altered saved journal");
}
@@ -0,0 +1,109 @@
package fr.koka.sanctuary.worldgen.city;
import java.util.HashMap;
import java.util.Map;
import java.util.function.IntBinaryOperator;
/** Strict admission is exercised against the final footprint, including misleading coarse bearings. */
public final class LostCityPlanner22Smoke {
public static void main(String[] args) {
int satellites = 0;
for (int size : new int[]{512, 724, 1024}) for (long seed : new long[]{0, 42}) {
Terrain good = new Terrain((x, z) -> 220);
var plan = LostCityPlanner22.plan(seed, 0, 0, size, true, good);
require(!plan.surface().isEmpty(), "Strict admission rejected flat, fully supported terrain");
verify(plan, good); satellites += plan.connections().size();
var repeated = LostCityPlanner22.plan(seed, 0, 0, size, true, new Terrain((x, z) -> 220));
require(signature(plan).equals(signature(repeated)), "Natural placement depends on invocation order");
}
require(satellites > 0, "No strict witness retains a complete supported satellite approach");
for (int size : new int[]{512, 724, 1024}) {
Terrain empty = new Terrain((x, z) -> 0);
var result = LostCityPlanner22.plan(0, 0, 0, size, true, empty);
verify(result, empty);
require(result.surface().isEmpty(), "Void received the former forced town or a large foundation");
}
Terrain cliff = new Terrain((x, z) -> Math.floorMod(x, 10) < 5 ? 198 : 250);
var broken = LostCityPlanner22.plan(0, 0, 0, 724, true, cliff);
verify(broken, cliff);
require(broken.surface().isEmpty(), "Sharp repeated cliffs were replaced by an urban embankment");
Terrain wet = new Terrain((x, z) -> 220); wet.wet = true;
var water = LostCityPlanner22.plan(0, 0, 0, 512, true, wet);
verify(water, wet);
require(water.surface().isEmpty(), "A protected wet island received a forced city");
Terrain hollow = new Terrain((x, z) -> 220); hollow.hollow = true;
var unsupported = LostCityPlanner22.plan(42, 0, 0, 512, true, hollow);
verify(unsupported, hollow);
require(unsupported.surface().isEmpty(), "Coarse depth claims hid missing rock beneath the city foundations");
// Refusing a satellite must preserve the valid core and never leave an orphaned approach.
var baseline = LostCityPlanner22.plan(0, 0, 0, 724, true, new Terrain((x, z) -> 220));
var core = baseline.core().surface().districts().getFirst();
Terrain limited = new Terrain((x, z) -> 220);
limited.protectedVolume = (x, y, z) -> x < core.minX() - 2 || x > core.maxX() + 2
|| z < core.minZ() - 2 || z > core.maxZ() + 2;
var onlyCore = LostCityPlanner22.plan(0, 0, 0, 724, true, limited);
verify(onlyCore, limited);
require(!onlyCore.surface().isEmpty() && onlyCore.connections().isEmpty(),
"Rejecting protected outskirts removed the sound core or retained a fictional satellite");
var continent = LostCityPlanner22.plan(0, 0, 0, 256, false, new Terrain((x, z) -> 220));
require(!continent.surface().isEmpty() && continent.connections().isEmpty(),
"A flat continent lost its conditional city or inherited the root satellite policy");
verify(continent, new Terrain((x, z) -> 220));
System.out.println("LostCityPlanner22Smoke: naturally supported towns, bounded search, strict cut/fill, complete satellite routes and refusal of void/water/cliffs/false bearings passed.");
}
private static void verify(LostCityPlanner21.Result plan, Terrain terrain) {
require(plan.reservation() == null && plan.core().reservation() == null,
"Generation22 exposed a terrain-clearing fallback reservation");
require(plan.supports().isEmpty(), "Generation22 used a long catch-up pier");
require(plan.columns() <= LostCityPlanner22.MAX_COLUMNS && plan.volumeChecks() <= LostCityPlanner22.MAX_VOLUME_CHECKS,
"Strict search exceeded its global budget across retries");
require(plan.diagnostics().getOrDefault("proposal-attempt", 0) <= LostCityPlanner22.MAX_ATTEMPTS, "Unbounded proposal search");
Map<Long, Integer> road = new HashMap<>();
for (var district : plan.surface().districts()) for (var parcel : district.parcels()) {
require(parcel.kind() != LostCityPlan.Kind.BRIDGE, "A suspended crossing survived strict admission");
for (int z = parcel.minZ(); z <= parcel.maxZ(); z++) for (int x = parcel.minX(); x <= parcel.maxX(); x++) {
int floor = parcel.surfaceY(x - parcel.minX(), z - parcel.minZ()), top = terrain.height.applyAsInt(x, z);
require(top - floor <= LostCityPlanner22.MAX_CUT && floor - top <= LostCityPlanner22.MAX_FILL,
"Accepted geometry exceeds the two-block cut/fill limit");
for (int y = floor - parcel.foundationDepth() - 2; y <= Math.min(floor - 1, top); y++)
require(terrain.solid(x, y, z), "A final foundation borrows another column's bearing");
for (int y = floor - parcel.foundationDepth() - 2; y <= parcel.maxY(); y++)
require(!terrain.protectedAt(x, y, z), "Accepted construction edits a protected volume");
if (parcel.kind() == LostCityPlan.Kind.STREET) road.put(key(x, z), floor);
}
}
for (var link : plan.connections()) {
require(link.districtIndex() > 0 && link.districtIndex() < plan.surface().districts().size(),
"A connection references a rejected satellite");
for (var point : link.path()) require(road.get(key(point.x(), point.z())) != null
&& road.get(key(point.x(), point.z())) == point.floorY(),
"A retained satellite depends on a road that was discarded");
}
}
private static String signature(LostCityPlanner21.Result result) {
var text = new StringBuilder().append(result.columns()).append(result.volumeChecks()).append(result.connections())
.append(new java.util.TreeMap<>(result.diagnostics()));
for (var d : result.surface().districts()) for (var p : d.parcels()) text.append(p.kind()).append(p.minX()).append(p.minZ())
.append(p.width()).append(p.depth()).append(p.floorY()).append(p.seed()).append(java.util.Arrays.toString(p.surfaceHeights()));
return text.toString();
}
private interface Protection { boolean test(int x, int y, int z); }
private static final class Terrain implements LostCityPlanner16.Terrain {
final IntBinaryOperator height; boolean wet, hollow;
Protection protectedVolume = (x, y, z) -> false;
Terrain(IntBinaryOperator height) { this.height = height; }
@Override public boolean owns(int x, int z) { return Math.abs(x) < 600 && Math.abs(z) < 600; }
@Override public LostCityPlanner.Ground ground(int x, int z) {
int top = height.applyAsInt(x, z);
return new LostCityPlanner.Ground(top, top > 0 ? 24 : 0, !wet && top > 0, wet || protectedVolume.test(x, top, z));
}
@Override public boolean solid(int x, int y, int z) {
int top = height.applyAsInt(x, z); return y > 0 && y <= top && (!hollow || y >= top - 1);
}
@Override public boolean protectedAt(int x, int y, int z) { return wet || protectedVolume.test(x, y, z); }
}
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
private static void require(boolean ok, String message) { if (!ok) throw new AssertionError(message); }
}
@@ -0,0 +1,94 @@
package fr.koka.sanctuary.worldgen.city;
import fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Kind;
import fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Module;
import fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan;
import fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Type;
import java.util.ArrayDeque;
import java.util.HashSet;
/** No city fixture is supplied: the hall, its native start and its surface access stand alone. */
public final class LostCityUnderground22Smoke {
public static void main(String[] args) {
boolean pool = false, sewer = false;
for (int size : new int[]{512, 724, 1024}) for (long seed : new long[]{0, 42}) {
var terrain = new Plateau(); var plan = LostCityUnderground22.plan(seed, 0, 0, size, terrain);
require(!plan.isEmpty(), "Natural rock must admit an independent hall");
require(plan.equals(LostCityUnderground22.plan(seed, 0, 0, size, new Plateau())), "Stable complete plan");
verify(plan, terrain);
pool |= plan.networks().getFirst().modules().stream().anyMatch(m -> m.kind() == Kind.POOLROOM);
sewer |= plan.networks().getFirst().modules().stream().anyMatch(m -> m.kind() == Kind.SEWER);
}
require(pool && sewer, "Optional utility wings must be reachable on suitable rock");
var protectedCenter = new Plateau() {
@Override public boolean protectedAt(int x, int y, int z) { return Math.abs(x) <= 15 && Math.abs(z) <= 15; }
};
var shifted = LostCityUnderground22.plan(0, 0, 0, 724, protectedCenter);
require(!shifted.isEmpty(), "Search another real entrance when the central site is occupied"); verify(shifted, protectedCenter);
for (var bad : new Plateau[]{
new Plateau() { @Override public boolean owns(int x, int z) { return false; } },
new Plateau() { @Override public boolean solid(int x, int y, int z) { return false; } },
new Plateau() { @Override public boolean solid(int x, int y, int z) { return y >= 250 && y <= 252 || y <= 80; } },
new Plateau() { @Override public boolean protectedAt(int x, int y, int z) { return true; } }}) {
var refused = LostCityUnderground22.plan(0, 0, 0, 724, bad);
require(refused.isEmpty(), "No hanging hall or forced protected excavation");
require(refused.samples() <= LostCityUnderground22.MAX_SAMPLES, "Failed search has a hard budget");
}
System.out.println("Independent undercroft22: six city-free hall/access pairs, connected utility wings, occupied-site search and safe refusals.");
}
private static void verify(Plan plan, Plateau terrain) {
require(plan.networks().size() == 1 && plan.samples() <= LostCityUnderground22.MAX_SAMPLES, "One bounded native network");
var network = plan.networks().getFirst(); var modules = network.modules();
require(modules.stream().filter(m -> m.kind() == Kind.EXPANSION_HALL).count() == 1, "One independent expansion hall");
require(modules.stream().filter(m -> m.kind() == Kind.ACCESS).count() == 1, "A real public entrance");
var columns = new HashSet<Long>();
for (var m : modules) {
require(Math.abs((m.minX() >> 4) - network.startChunkX()) <= 7 && Math.abs((m.maxX() >> 4) - network.startChunkX()) <= 7
&& Math.abs((m.minZ() >> 4) - network.startChunkZ()) <= 7 && Math.abs((m.maxZ() >> 4) - network.startChunkZ()) <= 7, "Native reference range");
for (int x = m.minX(); x <= m.maxX(); x++) for (int z = m.minZ(); z <= m.maxZ(); z++) {
require(terrain.owns(x, z), "Owned footprint"); columns.add(((long) x << 32) ^ (z & 0xffffffffL));
if (m.kind() == Kind.ACCESS) require(Math.abs(m.maxY() - 2 - terrain.surfaceY(x, z)) <= 2, "Entrance deck follows natural surface");
for (int y = m.minY(); y <= m.maxY(); y++) require(!terrain.protectedAt(x, y, z), "Every written voxel respects protection");
}
for (var s : m.supports()) {
require(s.maxY() - s.minY() + 1 <= 8, "Short support only");
int anchor = s.type() == Type.PILLAR ? s.minY() : s.maxY(), neighbor = anchor + (s.type() == Type.PILLAR ? -1 : 1);
require(terrain.solid(s.x(), anchor, s.z()) && terrain.solid(s.x(), neighbor, s.z()), "Native paired anchor");
for (int y = Math.min(neighbor, s.minY()); y <= Math.max(neighbor, s.maxY()); y++)
require(!terrain.protectedAt(s.x(), y, s.z()), "Written support and untouched anchor protected");
}
}
require(columns.size() <= LostCityUnderground22.MAX_COLUMNS, "Published footprint fits the column budget");
// Native pieces are disjoint; adjacent opened walls meet at the same walking floor.
for (int a = 0; a < modules.size(); a++) for (int b = a + 1; b < modules.size(); b++) {
var x = modules.get(a); var y = modules.get(b);
require(x.maxX() < y.minX() || y.maxX() < x.minX() || x.maxZ() < y.minZ() || y.maxZ() < x.minZ(), "No conflicting piece writes");
}
var reached = new HashSet<Integer>(); var todo = new ArrayDeque<Integer>(); reached.add(0); todo.add(0);
while (!todo.isEmpty()) {
var a = modules.get(todo.remove());
for (int i = 0; i < modules.size(); i++) if (!reached.contains(i) && connected(a, modules.get(i))) { reached.add(i); todo.add(i); }
}
require(reached.size() == modules.size(), "All rooms have an open route from the surface access");
}
private static boolean connected(Module a, Module b) {
if (a.floorY() != b.floorY()) return false;
if (a.centerZ() == b.centerZ()) {
if (a.maxX() + 1 == b.minX()) return (a.openings() & 2) != 0 && (b.openings() & 8) != 0;
if (b.maxX() + 1 == a.minX()) return (a.openings() & 8) != 0 && (b.openings() & 2) != 0;
}
if (a.centerX() == b.centerX()) {
if (a.maxZ() + 1 == b.minZ()) return (a.openings() & 4) != 0 && (b.openings() & 1) != 0;
if (b.maxZ() + 1 == a.minZ()) return (a.openings() & 1) != 0 && (b.openings() & 4) != 0;
}
return false;
}
private static class Plateau implements LostCityUnderground22.Terrain {
public boolean owns(int x, int z) { return Math.abs(x) < 600 && Math.abs(z) < 600; }
public int surfaceY(int x, int z) { return 252; }
public boolean solid(int x, int y, int z) { return y >= 64 && y <= 252; }
public boolean protectedAt(int x, int y, int z) { return false; }
public boolean exteriorAir(int x, int y, int z, int dx, int dz) { return false; }
}
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
}
@@ -0,0 +1,47 @@
package fr.koka.sanctuary.worldgen.heritage;
/** Admission fixtures keep a good plateau usable while rejecting artificial terrain repairs. */
public final class HeritagePlanner22Smoke {
public static void main(String[] args) {
for (int size : new int[]{512, 724, 1024}) {
var terrain = new Plateau();
var plan = HeritagePlanner22.plan(0, 0, 0, size, terrain);
require(!plan.isEmpty() && !plan.links().isEmpty(), "Natural flat land must retain useful structures and routes");
var repeat = HeritagePlanner22.plan(0, 0, 0, size, new Plateau());
require(plan.sites().equals(repeat.sites()) && plan.links().equals(repeat.links()) && plan.segments().equals(repeat.segments())
&& plan.samples() == repeat.samples() && plan.diagnostics().equals(repeat.diagnostics()), "Placement must be deterministic");
require(plan.samples() <= HeritagePlanner22.MAX_SAMPLES && plan.columns() <= HeritagePlanner22.MAX_COLUMNS, "Bounded search");
for (var site : plan.sites()) {
require(site.supports().isEmpty(), "No rescue piers");
for (int x = site.minX(); x <= site.maxX(); x++) for (int z = site.minZ(); z <= site.maxZ(); z++) {
require(Math.abs(site.floorY() - terrain.surfaceY(x, z)) <= 2, "Minimal cut/fill");
require(terrain.solid(x, site.minY() - 1, z) && terrain.solid(x, site.minY() - 2, z), "Full footprint carries foundation");
}
}
for (var segment : plan.segments()) for (var cell : segment.cells())
require(Math.abs(cell.floorY() - terrain.surfaceY(cell.x(), cell.z())) <= 1, "Paths follow terrain");
}
var stepped = new Plateau() {
@Override public int surfaceY(int x, int z) { return 220 + (Math.floorMod(x, 8) < 4 ? 0 : 8); }
};
require(!HeritagePlanner21.plan(0, 0, 0, 724, stepped).isEmpty(), "Stepped witness exercised21's permissive placement");
require(HeritagePlanner22.plan(0, 0, 0, 724, stepped).isEmpty(), "Repeated cliffs are rejected, not filled");
require(HeritagePlanner22.plan(0, 0, 0, 724, new Plateau() {
@Override public boolean solid(int x, int y, int z) { return y == 220 || y <= 209; }
}).isEmpty(), "Hollow shelf cannot receive long piers");
require(HeritagePlanner22.plan(0, 0, 0, 724, new Plateau() {
@Override public boolean protectedAt(int x, int y, int z) { return true; }
}).isEmpty(), "Protected hydrology cannot be waived");
require(HeritagePlanner22.plan(0, 0, 0, 724, new Plateau() {
@Override public boolean owns(int x, int z) { return false; }
}).isEmpty(), "Void has no structures");
System.out.println("Heritage22: three sizes admitted on natural land; relief, hollow shelves and protected land refused.");
}
private static class Plateau implements HeritagePlanner21.Terrain {
public boolean owns(int x, int z) { return Math.abs(x) < 600 && Math.abs(z) < 600; }
public int surfaceY(int x, int z) { return 220; }
public boolean solid(int x, int y, int z) { return y >= 80 && y <= surfaceY(x, z); }
public boolean protectedAt(int x, int y, int z) { return false; }
}
private static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); }
}
@@ -0,0 +1,34 @@
package fr.koka.sanctuary.worldgen.transit;
/** Core22 retains the supported21 traversal independently of optional surface placement. */
public final class TransitPlanner22Smoke {
public static void main(String[] args) {
for (int size : new int[]{512, 724, 1024}) {
var plan = TransitPlanner22.plan(0, 0, 0, size, new Plateau());
require(!plan.isEmpty(), "Solid plateau retains a complete buried route: " + plan.diagnostic());
require(plan.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.ACCESS).count() == 2, "Two real exits");
require(plan.equals(TransitPlanner22.plan(0, 0, 0, size, new Plateau())), "Deterministic bounded search");
require(plan.samples() <= TransitPlan21.MAX_SAMPLES, "Probe budget");
for (var m : plan.modules()) {
for (var support : m.supports()) require(support.maxY() - support.minY() + 1 <= TransitPlanner22.MAX_PIER, "Bounded engineered pier");
}
}
var hollow = new Plateau() { @Override public boolean solid(int x, int y, int z) { return y == 252 || y < 140; } };
require(TransitPlanner22.plan(0, 0, 0, 724, hollow).isEmpty(), "No hanging route in huge cavern");
require(TransitPlanner22.plan(0, 0, 0, 724, new Plateau() {
@Override public boolean protectedAt(int x, int y, int z) { return true; }
}).isEmpty(), "No protected volume overwritten");
require(TransitPlanner22.plan(0, 0, 0, 724, new Plateau() {
@Override public boolean owns(int x, int z) { return false; }
}).isEmpty(), "No route beyond island ownership");
System.out.println("Transit22: three sizes admit solid plateaus; void and protected volumes still refuse unsafe placement.");
}
private static class Plateau implements TransitPlanner21.Terrain {
public boolean owns(int x, int z) { return Math.abs(x) < 600 && Math.abs(z) < 600; }
public int surfaceY(int x, int z) { return 252; }
public boolean solid(int x, int y, int z) { return y >= 80 && y <= 252; }
public boolean protectedAt(int x, int y, int z) { return false; }
}
private static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); }
}
@@ -0,0 +1,76 @@
package fr.koka.sanctuary.worldgen.transit;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Module;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.EmptyBlockGetter;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.loot.LootTable;
/** All four L bends keep closed walls while retaining their full five-wide connection. */
public final class TransitRenderer22Smoke {
public static void main(String[] args) {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap();
for (int openings : new int[]{3, 6, 12, 9}) for (int facing : new int[]{0, 1}) {
var m = new Module(Kind.JUNCTION, 0, 199, 0, 17, 10, 17, facing, openings, 42, List.of());
var old = new Capture(); TransitRenderer21.render(m, old);
var actual = new Capture(); TransitRenderer22.render(m, actual);
require(TransitPiece21.decodeModule(TransitPiece21.encodeModule(m)).equals(m), "Saved geometry schema remains identical");
int changes = 0;
for (var entry : actual.blocks.entrySet()) {
var at = entry.getKey(); require(at.getX() >= 0 && at.getX() < 17 && at.getZ() >= 0 && at.getZ() < 17
&& at.getY() >= 0 && at.getY() < 10, "No expanded piece bounds");
if (!entry.getValue().equals(old.block(at))) {
changes++;
require(at.getY() == 2 && entry.getValue().is(Blocks.TUFF_BRICKS)
&& (old.block(at).isAir() || old.block(at).is(Blocks.SMOOTH_STONE_SLAB)), "Only unintended low wall gaps are restored");
}
}
require(changes == 12 && actual.blocks.keySet().equals(old.blocks.keySet()), "Exactly ten holes and two curb gaps change");
var targets = new HashSet<BlockPos>();
for (int d = 0; d < 4; d++) {
boolean open = (openings & 1 << d) != 0;
for (int across = -3; across <= 3; across++) for (int y = 2; y <= 8; y++) {
var at = edge(d, across, y);
require(open ? actual.block(at).isAir() : actual.block(at).isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, at),
"Openings and complete closed walls must match saved direction " + d + " at " + at);
}
if (open) for (int across = -2; across <= 2; across++) targets.add(edge(d, across, 2));
}
var reached = new HashSet<BlockPos>(); var queue = new ArrayDeque<BlockPos>(); var center = new BlockPos(8, 2, 8);
reached.add(center); queue.add(center);
while (!queue.isEmpty()) {
var at = queue.remove();
for (int[] delta : new int[][]{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}) {
var next = at.offset(delta[0], 0, delta[1]);
if (next.getX() < 0 || next.getX() > 16 || next.getZ() < 0 || next.getZ() > 16) continue;
if (actual.block(next).isAir() && actual.block(next.above()).isAir()
&& actual.block(next.below()).isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, next.below()) && reached.add(next)) queue.add(next);
}
}
require(reached.containsAll(targets), "Both five-wide road mouths remain connected through the corner");
}
System.out.println("Transit22 corners: eight orientations retain sealed unopened walls, five-wide circulation and the unchanged module schema.");
}
private static BlockPos edge(int direction, int across, int y) {
return new BlockPos(direction == 1 ? 16 : direction == 3 ? 0 : 8 + across, y,
direction == 0 ? 0 : direction == 2 ? 16 : 8 + across);
}
private static final class Capture implements LostCityRenderer.BlockWriter {
final Map<BlockPos, BlockState> blocks = new HashMap<>();
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(new BlockPos(x, y, z), state); }
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { set(x, y, z, Blocks.CHEST.defaultBlockState()); }
BlockState block(BlockPos pos) { return blocks.getOrDefault(pos, Blocks.AIR.defaultBlockState()); }
}
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
}
@@ -0,0 +1,112 @@
package fr.koka.sanctuary.worldgen.transit;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Module;
import java.util.ArrayDeque;
import java.util.HashSet;
/** A dry plateau with a protected central obstacle must retain a genuine connected detour. */
public final class TransitRoute22Smoke {
public static void main(String[] args) {
for (int size : new int[]{512, 724, 1024}) {
var terrain = new Plateau(); var plan = TransitRoute22.plan(42, 0, 0, size, terrain);
require(!plan.isEmpty(), "Detour around protected center must exist: " + plan.diagnostic());
require(plan.equals(TransitRoute22.plan(42, 0, 0, size, new Plateau())), "Deterministic detour");
verify(plan, size, terrain);
}
var rigidPorticos = new Plateau() {
@Override public int surfaceY(int x, int z) {
if (Math.abs(x) >= 105 && Math.abs(x) <= 130) return 252 + Math.floorMod(z, 5);
if (Math.abs(z) >= 105 && Math.abs(z) <= 140) return 252 + Math.floorMod(x, 5);
return 252;
}
@Override public boolean solid(int x, int y, int z) { return y >= 64 && y <= surfaceY(x, z); }
};
var moved = TransitRoute22.plan(42, 0, 0, 512, rigidPorticos);
require(!moved.isEmpty(), "Reject the old119-radius porticos and search another complete pair");
verify(moved, 512, rigidPorticos);
require(Math.max(Math.abs(moved.modules().getFirst().centerX() - moved.modules().getLast().centerX()),
Math.abs(moved.modules().getFirst().centerZ() - moved.modules().getLast().centerZ())) < 238,
"A closer well-supported pair replaces the failed fixed endpoints");
for (var terrain : new Plateau[]{
new Plateau() { @Override public boolean protectedAt(int x, int y, int z) { return true; } },
new Plateau() { @Override public boolean owns(int x, int z) { return false; } },
new Plateau() { @Override public int surfaceY(int x, int z) { return 380; } },
new Plateau() { @Override public boolean solid(int x, int y, int z) { return y == 252; } }}) {
var plan = TransitRoute22.plan(42, 0, 0, 512, terrain);
require(plan.isEmpty(), "No incomplete, protected or unanchored fallback route");
require(plan.samples() <= TransitPlan21.MAX_SAMPLES, "Failed detour is bounded");
}
System.out.println("Transit22 detours: three sizes, protected obstacle bypass, connected broad corners and bounded safe refusals.");
}
private static void verify(TransitPlan21 plan, int diameter, Plateau terrain) {
require(plan.samples() <= TransitPlan21.MAX_SAMPLES, "Sampling budget");
var modules = plan.modules();
require(modules.getFirst().kind() == Kind.ACCESS && modules.getLast().kind() == Kind.ACCESS
&& modules.stream().filter(m -> m.kind() == Kind.ACCESS).count() == 2, "Two indispensable surface exits");
require(Math.max(Math.abs(modules.getFirst().centerX() - modules.getLast().centerX()),
Math.abs(modules.getFirst().centerZ() - modules.getLast().centerZ())) >= diameter / 4, "Island-scale destinations");
require(modules.stream().anyMatch(m -> m.kind() == Kind.JUNCTION), "A real turn bypasses the direct alignment");
var columns = new HashSet<Long>(); int floating = 0;
for (var m : modules) {
require(m.floorY() == modules.getFirst().floorY(), "Constant roadway height");
require(m.kind() != Kind.JUNCTION || m.width() == 17 && m.depth() == 17, "Broad corner with the full five-wide crossing");
if (m.kind() == Kind.ACCESS) {
int low = 384, high = -1;
for (int x = m.centerX() - 2; x <= m.centerX() + 2; x++) for (int z = m.minZ() - 3; z < m.minZ(); z++) {
int y = terrain.surfaceY(x, z); low = Math.min(low, y); high = Math.max(high, y);
}
require(high - low <= 1 && high == m.surfaceY(), "Changing endpoints never weakens the natural portico certificate");
}
for (int x = m.minX(); x <= m.maxX(); x++) for (int z = m.minZ(); z <= m.maxZ(); z++) {
require(terrain.owns(x, z), "Owned footprint"); columns.add(((long) x << 32) ^ (z & 0xffffffffL));
for (int y = m.minY() - 2; y <= m.maxY(); y++) require(!terrain.protectedAt(x, y, z), "Every edited voxel avoids the obstacle");
}
int quadrants = 0;
for (int z = m.minZ();; z = Math.min(z + 4, m.maxZ())) {
for (int x = m.minX();; x = Math.min(x + 4, m.maxX())) {
boolean carried = false;
for (int y = m.minY() - 1; y >= m.minY() - 17; y--)
if (terrain.solid(x, y, z) && terrain.solid(x, y - 1, z)) { carried = true; break; }
if (carried) quadrants |= 1 << ((x < m.centerX() ? 0 : 1) + (z < m.centerZ() ? 0 : 2));
if (x == m.maxX()) break;
}
if (z == m.maxZ()) break;
}
if (m.kind() != Kind.GALLERY) require(quadrants == 15, "Every access and corner is carried in four quarters");
floating = quadrants == 15 ? 0 : floating + 1; require(floating <= 2, "At most34 blocks between complete bearings");
for (var support : m.supports()) {
require(support.maxY() - support.minY() + 1 <= 16, "Bounded civil-engineering support");
require(terrain.solid(support.x(), support.minY() - 1, support.z())
&& terrain.solid(support.x(), support.minY() - 2, support.z()), "Two native anchor layers");
}
}
require(columns.size() <= TransitRoute22.MAX_COLUMNS, "Published footprint fits column budget");
for (int i = 1; i < modules.size(); i++) require(connected(modules.get(i - 1), modules.get(i)), "Saved adjacent mouths meet at a turn");
var seen = new HashSet<Integer>(); var todo = new ArrayDeque<Integer>(); seen.add(0); todo.add(0);
while (!todo.isEmpty()) {
var a = modules.get(todo.remove());
for (int i = 0; i < modules.size(); i++) if (!seen.contains(i) && connected(a, modules.get(i))) { seen.add(i); todo.add(i); }
}
require(seen.size() == modules.size(), "Every emitted module belongs to the route between both accesses");
}
private static boolean connected(Module a, Module b) {
if (a.floorY() != b.floorY()) return false;
if (a.centerZ() == b.centerZ()) {
if (a.maxX() + 1 == b.minX()) return (a.openings() & 2) != 0 && (b.openings() & 8) != 0;
if (b.maxX() + 1 == a.minX()) return (a.openings() & 8) != 0 && (b.openings() & 2) != 0;
}
if (a.centerX() == b.centerX()) {
if (a.maxZ() + 1 == b.minZ()) return (a.openings() & 4) != 0 && (b.openings() & 1) != 0;
if (b.maxZ() + 1 == a.minZ()) return (a.openings() & 1) != 0 && (b.openings() & 4) != 0;
}
return false;
}
private static class Plateau implements TransitPlanner21.Terrain {
public boolean owns(int x, int z) { return Math.abs(x) < 600 && Math.abs(z) < 600; }
public int surfaceY(int x, int z) { return 252; }
public boolean solid(int x, int y, int z) { return y >= 64 && y <= 252; }
public boolean protectedAt(int x, int y, int z) { return Math.abs(x) <= 10 && Math.abs(z) <= 45; }
}
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
}
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "0.1.0-alpha.21"
version = "0.1.0-alpha.22"
pack-format = "packwiz:1.1.0"
[index]