worldgen: sober palettes and connected industrial galleries for alpha21
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -15,11 +15,26 @@ infrastructures qui permettront progressivement d'ouvrir de nouveaux continents.
|
|||||||
Ce dépôt démarre la nouvelle base Fabric. La [vision complète](docs/vision.md)
|
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
|
conserve les intentions ; le [backlog](docs/backlog.md) suit les
|
||||||
tickets. Les systèmes d’économie, de progression collective et les dimensions
|
tickets. Les systèmes d’économie, de progression collective et les dimensions
|
||||||
décrits dans la vision ne sont pas encore implémentés. **L’alpha.20 est publiée** : elle reprend
|
décrits dans la vision ne sont pas encore implémentés. **L’alpha.21 est construite et
|
||||||
l’architecture des bâtiments, leurs palettes, les blocs sculptés et les raccords
|
vérifiée** : palettes sobres, couvertures portées, petits quartiers reliés et
|
||||||
des vitres. Les villes, infrastructures et réseaux gardent leurs implantations.
|
premières galeries industrielles. Voir [le catalogue alpha.21](docs/architecture-alpha21.md),
|
||||||
Voir le [catalogue illustré](docs/architecture-alpha20.md) et l’état du canal
|
[les essais alpha.21](docs/testing-alpha21.md)
|
||||||
packwiz dans [Distribution](docs/packwiz.md).
|
et l’état du canal packwiz dans [Distribution](docs/packwiz.md).
|
||||||
|
|
||||||
|
## Alpha.21 — matériaux, quartiers et traversées
|
||||||
|
|
||||||
|
La ville revient à la pierre lisse, pierre, briques de pierre, andésite et chêne.
|
||||||
|
Le tuf accompagne surtout les chemins et les sous-sols. Les toits et ornements
|
||||||
|
sont raccordés à leurs appuis ; les pièces gardent leurs aménagements. La ville
|
||||||
|
recherche des petits quartiers séparés par du terrain naturel, avec des chemins
|
||||||
|
vers le cœur. Les lieux patrimoniaux recherchent davantage de raccords.
|
||||||
|
|
||||||
|
L’option expérimentale ajoute une recherche de galeries industrielles : chaussée
|
||||||
|
cinq blocs, deux trottoirs, salles et escaliers de surface. Les placements sur
|
||||||
|
pente sont assouplis et les destinations isolées sont prioritaires pour les chemins.
|
||||||
|
Créer un **nouveau monde Sanctuary** pour ce générateur21. Les anciens mondes
|
||||||
|
conservent leur génération. Les vérifications et coordonnées du
|
||||||
|
témoin sont consignées dans [les essais](docs/testing-alpha21.md).
|
||||||
|
|
||||||
## Alpha.20 — identité des bâtiments
|
## Alpha.20 — identité des bâtiments
|
||||||
|
|
||||||
@@ -443,7 +458,7 @@ sera une mise à jour explicite, avec vérification des API et des sauvegardes.
|
|||||||
| Fabric API | 0.160.0+26.3 |
|
| Fabric API | 0.160.0+26.3 |
|
||||||
| Fabric Loom | 1.17.20 |
|
| Fabric Loom | 1.17.20 |
|
||||||
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
|
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
|
||||||
| Sanctuary / pack | 0.1.0-alpha.19 |
|
| Sanctuary / pack | 0.1.0-alpha.21 |
|
||||||
|
|
||||||
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
|
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
|
||||||
uniquement la bibliothèque standard et repère aussi une installation Python
|
uniquement la bibliothèque standard et repère aussi une installation Python
|
||||||
@@ -472,7 +487,7 @@ décrits dans [Validation](docs/testing.md).
|
|||||||
|
|
||||||
Résultats :
|
Résultats :
|
||||||
|
|
||||||
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.19.jar` : mod à installer avec
|
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.21.jar` : mod à installer avec
|
||||||
Fabric API sur la version Minecraft indiquée.
|
Fabric API sur la version Minecraft indiquée.
|
||||||
- `build/packwiz/` : pack de développement complet, avec le mod construit et
|
- `build/packwiz/` : pack de développement complet, avec le mod construit et
|
||||||
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
|
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Alpha.21 — palettes et couvertures
|
||||||
|
|
||||||
|
La ville revient à une base commune : pierre lisse, pierre, briques de pierre,
|
||||||
|
andésite et chêne. Les façades, seuils, corniches et verrières gardent leurs
|
||||||
|
formes ; les pièces conservent leurs fonctions et leur circulation. La serre
|
||||||
|
emploie le bouleau comme bois propre. Le tuf accompagne les routes et les
|
||||||
|
parties souterraines.
|
||||||
|
|
||||||
|

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

|
||||||
|
|
||||||
|
La seconde planche coupe volontairement les murs pour voir les dessous des toits.
|
||||||
|
Les détails de validation et la graine d’essai sont dans [le protocole](testing-alpha21.md).
|
||||||
@@ -779,6 +779,38 @@ en briques de pierre. Ajouter des blocs sculptés et du relief architectural.
|
|||||||
**État : livré en alpha.20 ; code, artefacts et même instance Prism vérifiés.** Voir
|
**État : livré en alpha.20 ; code, artefacts et même instance Prism vérifiés.** Voir
|
||||||
[le contrat et les essais alpha.20](testing-alpha20.md).
|
[le contrat et les essais alpha.20](testing-alpha20.md).
|
||||||
|
|
||||||
|
### WG-24 — Palettes sobres, toits portés et galeries — alpha.21
|
||||||
|
|
||||||
|
**Branche :** `codex/palettes-reseaux-alpha21`.
|
||||||
|
|
||||||
|
Reprendre les extérieurs sans changer les plans intérieurs approuvés : smooth
|
||||||
|
stone, stone brick, stone, andésite et chêne dominants en ville ; un bois cohérent
|
||||||
|
par bâtiment unique et tuf surtout au sol et sous terre. Corriger les toits
|
||||||
|
suspendus et les anciens plafonds superposés, avec appuis vérifiables.
|
||||||
|
|
||||||
|
Étendre la ville par au plus deux petits quartiers séparés de terrain naturel,
|
||||||
|
reliés par des chemins praticables ; rapprocher les destinations patrimoniales
|
||||||
|
par un graphe de chemins. Ajouter des galeries industrielles abandonnées :
|
||||||
|
chaussée de cinq blocs, deux trottoirs, quelques grandes salles et deux accès
|
||||||
|
par escaliers depuis la surface, en respectant les eaux et structures existantes.
|
||||||
|
|
||||||
|
Ajouter des fronts rocheux et de petits filons intacts dans les galeries et
|
||||||
|
leurs salles de maintenance : ressources vanilla réellement minables, sans
|
||||||
|
régénération de butin ni remplacement des blocs déjà extraits. Fournir un MRpack
|
||||||
|
pour l’essai séparé demandé.
|
||||||
|
|
||||||
|
Assouplir les refus de placement : admettre les pentes, des volumes partiellement
|
||||||
|
enfouis et des appuis adaptés ; rechercher des détours et prioriser les lieux
|
||||||
|
isolés pour les chemins. Garder une ville fragmentée sans socle géant.
|
||||||
|
|
||||||
|
Nouvelle génération21 réservée aux nouveaux mondes ; tous les codecs et rendus
|
||||||
|
20 et antérieurs restent disponibles. Tailles512/724/1024, Moyen par défaut ;
|
||||||
|
option expérimentale enregistrée. Aucun monde personnel modifié. Vérifier les
|
||||||
|
pièces natives, accès, connexions, supports, création/réouverture, ancien monde20,
|
||||||
|
puis livrer le pack stable21 et synchroniser la même instance Prism.
|
||||||
|
|
||||||
|
**État : implémenté et vérifié ; MRpack fourni, publication et synchronisation en cours.**
|
||||||
|
|
||||||
## Réserve de thèmes futurs
|
## Réserve de thèmes futurs
|
||||||
|
|
||||||
Ces thèmes servent à retrouver la vision, pas à demander leur implémentation immédiate. On en extrait un ticket seulement lorsqu'il devient utile au prochain incrément jouable.
|
Ces thèmes servent à retrouver la vision, pas à demander leur implémentation immédiate. On en extrait un ticket seulement lorsqu'il devient utile au prochain incrément jouable.
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 306 KiB |
@@ -0,0 +1,138 @@
|
|||||||
|
# Alpha.21 — contrat et vérification
|
||||||
|
|
||||||
|
Ticket WG-24, branche `codex/palettes-reseaux-alpha21`.
|
||||||
|
Minecraft 26.3-pre-2, Fabric Loader 0.19.5, Fabric API 0.160.0+26.3,
|
||||||
|
Java 25. Les dépendances restent celles de l’alpha.20.
|
||||||
|
|
||||||
|
## Résultat recherché
|
||||||
|
|
||||||
|
La ville reprend une palette de pierre lisse, pierre, briques de pierre,
|
||||||
|
andésite et chêne. Les plans intérieurs sont conservés ; les corrections
|
||||||
|
portent sur les matériaux, les appuis et les couvertures. Chaque infrastructure
|
||||||
|
emploie un bois cohérent. Le tuf reste surtout au sol et dans les sous-sols.
|
||||||
|
|
||||||
|
La ville peut accueillir deux petits quartiers supplémentaires reliés à son
|
||||||
|
cœur, avec du terrain naturel entre eux. La réserve centrale existante n’est
|
||||||
|
pas agrandie. Les infrastructures reçoivent davantage de chemins entre des
|
||||||
|
entrées réelles. Les recherches respectent des budgets de colonnes et refusent
|
||||||
|
un quartier si sa liaison entière ne peut pas être construite.
|
||||||
|
|
||||||
|
Les galeries industrielles expérimentales ont cinq blocs de chaussée et un
|
||||||
|
trottoir de chaque côté, des salles de maintenance et deux accès par escaliers.
|
||||||
|
Le plan est recherché après les structures précédentes et évite leurs volumes,
|
||||||
|
l’hydrologie. Un tablier continu peut franchir une cavité entre ses appuis ;
|
||||||
|
les travées sans appuis complets sont limitées à 34 blocs, avec des piliers
|
||||||
|
locaux d’au plus 16 blocs et deux couches de roche intacte sous les appuis. Chaque tronçon a une pièce native
|
||||||
|
locale ; aucun démarrage de structure ne charge toute l’île.
|
||||||
|
|
||||||
|
## Sauvegardes et comparaison
|
||||||
|
|
||||||
|
Créer un **nouveau monde Sanctuary**, taille **Moyen**, graine **0**, avec
|
||||||
|
**Structures expérimentales** activées. Petit 512, Moyen 724 et Grand 1024 restent
|
||||||
|
les tailles proposées par le bouton natif **Personnaliser**.
|
||||||
|
|
||||||
|
Le nouveau codec est `sanctuary:island_v21`. Les codecs et pièces 20 et
|
||||||
|
antérieurs restent disponibles, sans modifier leurs rendus. Le journal 21
|
||||||
|
réutilise le schéma de persistance existant avec une génération distincte ;
|
||||||
|
les ouvertures entre versions incompatibles sont refusées. Aucune conversion
|
||||||
|
ni rénovation automatique des mondes précédents n’est effectuée.
|
||||||
|
|
||||||
|
Pour comparer sans les infrastructures, créer un autre monde avec l’option
|
||||||
|
expérimentale désactivée. Une sauvegarde 21 nécessite le binaire 21 ou un futur
|
||||||
|
binaire compatible ; elle ne se rétrograde pas en remplaçant simplement le JAR.
|
||||||
|
|
||||||
|
## Compatibilité vérifiée
|
||||||
|
|
||||||
|
373 fichiers de production historiques sont identiques au commit de publication
|
||||||
|
20 (`0edac29`). Le preset caché `sanctuary_v20` est une copie exacte de son
|
||||||
|
ancien preset public. Création et réouverture natives de la génération 20 donnent
|
||||||
|
les mêmes plans, structures, appuis, accès et coffres que les rapports publiés.
|
||||||
|
La comparaison ignore seulement l’identifiant du processus, le chemin du monde
|
||||||
|
et deux nouveaux compteurs de détail ajoutés à l’oracle de connexion des tests.
|
||||||
|
|
||||||
|
## Galeries vérifiées dans Minecraft
|
||||||
|
|
||||||
|
Moyen, graine 0 : 23 modules dont deux accès et une salle, sur un axe de 340 blocs.
|
||||||
|
Les entrées sont `-135 253 -178` et `-135 249 162` ; chaussée à Y 216. La portée
|
||||||
|
sans appuis complets la plus longue du témoin est 17 blocs. Le plan emploie
|
||||||
|
8 686 colonnes de bruit, avec un budget maximal de 12 000 ; cette recherche prend
|
||||||
|
environ 11 secondes sur la machine de développement, hors reste du monde.
|
||||||
|
|
||||||
|
La création et la réouverture dans deux JVM distinctes passent : 34 chunks
|
||||||
|
FULL, 57 635 blocs vérifiés, mêmes pièces natives, mêmes coffres différés et
|
||||||
|
modification témoin conservée. L’oracle de collision de Minecraft vérifie un
|
||||||
|
corps de 0,6×1,8 bloc, 74 cibles et 520 pas entre les deux portiques ; il utilise
|
||||||
|
les vrais escaliers et les demi-marches des trottoirs.
|
||||||
|
|
||||||
|
Une alcôve d’excavation inachevée ouvre le parement de la salle sur une paroi
|
||||||
|
irrégulière de tuf, pierre et deepslate. Certains tronçons exposent de petits
|
||||||
|
filons ordinaires. Le témoin contient cinq minerais de charbon, dix-neuf de
|
||||||
|
fer, cinq de redstone, un de diamant et un d’émeraude ajoutés dans ces fronts.
|
||||||
|
Cuivre, or et lapis font aussi partie des variantes possibles. Ce sont des
|
||||||
|
blocs finis, sans mécanisme de réapparition après minage. Ces quantités excluent
|
||||||
|
les minerais naturels de l’île et ne constituent pas une garantie par graine.
|
||||||
|
|
||||||
|
## Autres vérifications
|
||||||
|
|
||||||
|
La création et la réouverture natives de la ville passent. Le témoin possède
|
||||||
|
un cœur et deux quartiers reliés, 65 pièces de rue, trois immeubles de logements,
|
||||||
|
un hôtel, une bibliothèque, un jardin et une place. La réserve centrale n’est
|
||||||
|
pas étendue. Les deux nouveaux chemins sont longs de 51 et 38 cellules. Une
|
||||||
|
expansion de test distincte est également vérifiée ; aucune expansion personnelle
|
||||||
|
n’est activée.
|
||||||
|
|
||||||
|
La piscine couverte, le garage, la salle d’expansion, leurs accès et les égouts
|
||||||
|
restent présents. Les contrôles natifs vérifient les bassins et sorties après
|
||||||
|
exécution observée de la simulation des fluides.
|
||||||
|
|
||||||
|
Les quatorze types de lieux patrimoniaux apparaissent sur Moyen, graine 0,
|
||||||
|
avec douze liaisons et 127 segments. Leur création native passe : 141 pièces,
|
||||||
|
91 271 blocs, 140 chunks FULL et quatorze coffres différés. Les trois réseaux
|
||||||
|
oubliés sont également parcourus. Les chemins rejoignent deux entrées de ville.
|
||||||
|
L’observatoire et le quai du vide restent isolés sur ce témoin : leurs chemins
|
||||||
|
n’ont pas trouvé de tracé admissible dans le budget. La commande `/sanctuary structures` indique leurs entrées ; aucun raccord
|
||||||
|
construit complet n’est revendiqué.
|
||||||
|
|
||||||
|
Les smokes des renderers, plans, pièces enregistrées et raccords passent.
|
||||||
|
La réouverture des lieux patrimoniaux conserve également le plan, les pièces,
|
||||||
|
les coffres et la modification témoin. L’option expérimentale désactivée passe :
|
||||||
|
elle ne publie aucun module de galerie. `check build assemblePack` passe, avec les huit GameTests généraux. Le
|
||||||
|
MRpack, son index et le JAR embarqué sont vérifiés ; la publication et la
|
||||||
|
synchronisation restent à consigner.
|
||||||
|
|
||||||
|
Les chemins de quartier regroupent leurs pentes à l’intérieur de rectangles ;
|
||||||
|
leurs frontières et raccords sont des paliers plats. Les structures restent
|
||||||
|
conditionnées par le terrain admissible, dans un budget borné. La traversée
|
||||||
|
industrielle ne relie pas encore tous les réseaux souterrains historiques.
|
||||||
|
|
||||||
|
Les vues de modèles éventuelles ne remplacent pas une inspection dans le client.
|
||||||
|
Aucune validation Windows native ni charge multijoueur n’est revendiquée.
|
||||||
|
|
||||||
|
|
||||||
|
## Reproduire les contrôles
|
||||||
|
|
||||||
|
Depuis ce dépôt, avec Java 25, les mondes de test restent sous `mods/sanctuary/build/`.
|
||||||
|
Pour chaque scénario, exécuter la création puis sa réouverture avant tout autre
|
||||||
|
scénario qui nettoie ce monde de développement :
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./gradlew :sanctuary:runGameTest -PsanctuaryTransit21Tests=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --max-workers=1
|
||||||
|
./gradlew :sanctuary:runGameTest -PsanctuaryTransit21Tests=true -PsanctuaryExpansionReload=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --max-workers=1
|
||||||
|
```
|
||||||
|
|
||||||
|
Remplacer `sanctuaryTransit21Tests` par `sanctuaryLostCity21Tests` pour la ville
|
||||||
|
ou `sanctuaryHeritage21Tests` pour les infrastructures. Le cas OFF ajoute
|
||||||
|
`-PsanctuaryExperimentalStructures=false` à une création du scénario transit.
|
||||||
|
Les rapports de cette livraison sont conservés localement dans
|
||||||
|
`build/alpha21-evidence/`. `./gradlew check build assemblePack` constitue la
|
||||||
|
vérification complète avant export.
|
||||||
|
|
||||||
|
## Artefacts vérifiés
|
||||||
|
|
||||||
|
MRpack : `Sanctuary-0.1.0-alpha.21.mrpack`. Son JAR est identique au JAR du build.
|
||||||
|
Les versions et hashes de l’index packwiz sont contrôlés, ainsi que le ZIP
|
||||||
|
d’amorçage Prism. SHA-256 du JAR :
|
||||||
|
|
||||||
|
```text
|
||||||
|
e528e42d99f9e1ea221f780ea089b49b42a1a4e9e24e4ddd001d045183202c21
|
||||||
|
```
|
||||||
+2
-2
@@ -8,6 +8,6 @@ loader_version=0.19.5
|
|||||||
loom_version=1.17.20
|
loom_version=1.17.20
|
||||||
fabric_api_version=0.160.0+26.3
|
fabric_api_version=0.160.0+26.3
|
||||||
|
|
||||||
mod_version=0.1.0-alpha.20
|
mod_version=0.1.0-alpha.21
|
||||||
pack_version=0.1.0-alpha.20
|
pack_version=0.1.0-alpha.21
|
||||||
maven_group=fr.koka.sanctuary
|
maven_group=fr.koka.sanctuary
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ tasks.named('runGameTest') {
|
|||||||
systemProperty('sanctuary.test.unified', providers.gradleProperty('sanctuaryUnifiedTests').getOrElse('true'))
|
systemProperty('sanctuary.test.unified', providers.gradleProperty('sanctuaryUnifiedTests').getOrElse('true'))
|
||||||
systemProperty('sanctuary.test.diameter', providers.gradleProperty('sanctuaryTestDiameter').getOrElse('724'))
|
systemProperty('sanctuary.test.diameter', providers.gradleProperty('sanctuaryTestDiameter').getOrElse('724'))
|
||||||
systemProperty('sanctuary.test.generation', providers.gradleProperty('sanctuaryTestGeneration').getOrElse(
|
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' : '20'))
|
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'))
|
||||||
|
systemProperty('sanctuary.test.heritage21', providers.gradleProperty('sanctuaryHeritage21Tests').getOrElse('false'))
|
||||||
|
systemProperty('sanctuary.test.lostCity21', providers.gradleProperty('sanctuaryLostCity21Tests').getOrElse('false'))
|
||||||
|
systemProperty('sanctuary.test.transit21', providers.gradleProperty('sanctuaryTransit21Tests').getOrElse('false'))
|
||||||
systemProperty('sanctuary.test.expansion', providers.gradleProperty('sanctuaryExpansionTests').getOrElse('false'))
|
systemProperty('sanctuary.test.expansion', providers.gradleProperty('sanctuaryExpansionTests').getOrElse('false'))
|
||||||
systemProperty('sanctuary.test.expansionReload', providers.gradleProperty('sanctuaryExpansionReload').getOrElse('false'))
|
systemProperty('sanctuary.test.expansionReload', providers.gradleProperty('sanctuaryExpansionReload').getOrElse('false'))
|
||||||
systemProperty('sanctuary.test.lostCity', providers.gradleProperty('sanctuaryLostCityTests').getOrElse('false'))
|
systemProperty('sanctuary.test.lostCity', providers.gradleProperty('sanctuaryLostCityTests').getOrElse('false'))
|
||||||
@@ -76,12 +79,18 @@ tasks.named('processGametestResources') {
|
|||||||
inputs.property('lostCity20', lostCity20)
|
inputs.property('lostCity20', lostCity20)
|
||||||
def heritage20 = providers.gradleProperty('sanctuaryHeritage20Tests').map { it.toBoolean() }.getOrElse(false)
|
def heritage20 = providers.gradleProperty('sanctuaryHeritage20Tests').map { it.toBoolean() }.getOrElse(false)
|
||||||
inputs.property('heritage20', heritage20)
|
inputs.property('heritage20', heritage20)
|
||||||
|
def heritage21 = providers.gradleProperty('sanctuaryHeritage21Tests').map { it.toBoolean() }.getOrElse(false)
|
||||||
|
inputs.property('heritage21', heritage21)
|
||||||
|
def lostCity21 = providers.gradleProperty('sanctuaryLostCity21Tests').map { it.toBoolean() }.getOrElse(false)
|
||||||
|
inputs.property('lostCity21', lostCity21)
|
||||||
|
def transit21 = providers.gradleProperty('sanctuaryTransit21Tests').map { it.toBoolean() }.getOrElse(false)
|
||||||
|
inputs.property('transit21', transit21)
|
||||||
def heritage19 = providers.gradleProperty('sanctuaryHeritage19Tests').map { it.toBoolean() }.getOrElse(false)
|
def heritage19 = providers.gradleProperty('sanctuaryHeritage19Tests').map { it.toBoolean() }.getOrElse(false)
|
||||||
inputs.property('heritage19', heritage19)
|
inputs.property('heritage19', heritage19)
|
||||||
def voidReuse = providers.gradleProperty('sanctuaryVoidReuseTests').map { it.toBoolean() }.getOrElse(false)
|
def voidReuse = providers.gradleProperty('sanctuaryVoidReuseTests').map { it.toBoolean() }.getOrElse(false)
|
||||||
inputs.property('voidReuse', voidReuse)
|
inputs.property('voidReuse', voidReuse)
|
||||||
filesMatching('fabric.mod.json') {
|
filesMatching('fabric.mod.json') {
|
||||||
expand(gametest_entrypoint: lostCity20 ? 'fr.koka.sanctuary.gametest.LostCity20WorldGameTests' : heritage20 ? 'fr.koka.sanctuary.gametest.Heritage20WorldGameTests' : heritage19 ? 'fr.koka.sanctuary.gametest.Heritage19WorldGameTests' : ruins17 ? 'fr.koka.sanctuary.gametest.Ruins17WorldGameTests' : voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark
|
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
|
||||||
? 'fr.koka.sanctuary.gametest.WorldgenPerformanceGameTests'
|
? 'fr.koka.sanctuary.gametest.WorldgenPerformanceGameTests'
|
||||||
: unified ? 'fr.koka.sanctuary.gametest.UnifiedWorldGameTests'
|
: unified ? 'fr.koka.sanctuary.gametest.UnifiedWorldGameTests'
|
||||||
: 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests')
|
: 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests')
|
||||||
@@ -468,3 +477,34 @@ tasks.named('lostCitySurface20Smoke') {
|
|||||||
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
|
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
|
||||||
systemProperty('sanctuary.city20.exteriorPreview', rootProject.layout.buildDirectory.file('alpha20-preview/exterior.json').get().asFile.absolutePath)
|
systemProperty('sanctuary.city20.exteriorPreview', rootProject.layout.buildDirectory.file('alpha20-preview/exterior.json').get().asFile.absolutePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generation21 keeps published20 renderers immutable.
|
||||||
|
[
|
||||||
|
heritage21RendererSmoke: 'heritage.HeritageRenderer21Smoke',
|
||||||
|
heritage21PieceSmoke: 'heritage.HeritagePiece21Smoke',
|
||||||
|
heritage21PlannerSmoke: 'heritage.HeritagePlanner21Smoke',
|
||||||
|
lostCity21PlannerSmoke: 'city.LostCityPlanner21Smoke',
|
||||||
|
lostCitySurface21Smoke: 'city.LostCitySurface21Smoke',
|
||||||
|
lostCityInteriors21Smoke: 'city.LostCityInteriors21Smoke',
|
||||||
|
transit21Smoke: 'transit.Transit21Smoke'
|
||||||
|
].each { name, harness ->
|
||||||
|
def smoke = tasks.register(name, JavaExec) {
|
||||||
|
group = 'verification'
|
||||||
|
dependsOn('testClasses')
|
||||||
|
classpath = sourceSets.test.runtimeClasspath
|
||||||
|
mainClass = "fr.koka.sanctuary.worldgen.${harness}"
|
||||||
|
}
|
||||||
|
tasks.named('check') { dependsOn(smoke) }
|
||||||
|
}
|
||||||
|
tasks.named('heritage21RendererSmoke') {
|
||||||
|
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
|
||||||
|
systemProperty('sanctuary.test.heritage21.export', rootProject.layout.buildDirectory.dir('alpha21-preview/heritage').get().asFile.absolutePath)
|
||||||
|
}
|
||||||
|
tasks.named('lostCityInteriors21Smoke') {
|
||||||
|
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
|
||||||
|
systemProperty('sanctuary.city21.preview', rootProject.layout.buildDirectory.file('alpha21-preview/city.json').get().asFile.absolutePath)
|
||||||
|
}
|
||||||
|
tasks.named('lostCitySurface21Smoke') {
|
||||||
|
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
|
||||||
|
systemProperty('sanctuary.city21.exteriorPreview', rootProject.layout.buildDirectory.file('alpha21-preview/exterior.json').get().asFile.absolutePath)
|
||||||
|
}
|
||||||
|
|||||||
+554
@@ -0,0 +1,554 @@
|
|||||||
|
package fr.koka.sanctuary.gametest;
|
||||||
|
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import com.mojang.serialization.JsonOps;
|
||||||
|
import fr.koka.sanctuary.SanctuaryMod;
|
||||||
|
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||||
|
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||||
|
import fr.koka.sanctuary.worldgen.heritage.HeritagePiece21;
|
||||||
|
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19;
|
||||||
|
import fr.koka.sanctuary.worldgen.heritage.HeritageRenderer21;
|
||||||
|
import fr.koka.sanctuary.worldgen.heritage.HeritageStructures19;
|
||||||
|
import fr.koka.sanctuary.worldgen.ruins.RuinsPiece20;
|
||||||
|
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17;
|
||||||
|
import fr.koka.sanctuary.worldgen.ruins.RuinsRenderer20;
|
||||||
|
import fr.koka.sanctuary.worldgen.ruins.RuinsStructures17;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.HexFormat;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.gametest.framework.GameTestHelper;
|
||||||
|
import net.minecraft.resources.RegistryOps;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.item.Item;
|
||||||
|
import net.minecraft.world.item.Items;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.DoorBlock;
|
||||||
|
import net.minecraft.world.level.block.TrapDoorBlock;
|
||||||
|
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
import net.minecraft.world.level.chunk.LevelChunk;
|
||||||
|
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||||
|
import net.minecraft.world.level.storage.LevelResource;
|
||||||
|
import net.minecraft.world.level.storage.loot.LootParams;
|
||||||
|
import net.minecraft.world.level.storage.loot.LootTable;
|
||||||
|
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
|
||||||
|
import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets;
|
||||||
|
import net.minecraft.world.phys.AABB;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
|
||||||
|
/** Native creation/reload proof confined to a disposable generation21 starter island. */
|
||||||
|
public final class Heritage21WorldGameTests {
|
||||||
|
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
|
||||||
|
@GameTest(maxTicks = 12000)
|
||||||
|
public void heritagePersistsWithRealDestinations(GameTestHelper helper) {
|
||||||
|
helper.assertTrue(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator,
|
||||||
|
"The heritage proof requires a Sanctuary generator");
|
||||||
|
var generator = (SanctuaryChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||||
|
helper.assertTrue(generator.rootGeneration() == 21, "Use a fresh generation21 test world; do not convert an old save");
|
||||||
|
new Scenario(helper, generator, Boolean.getBoolean("sanctuary.test.expansionReload")).advance();
|
||||||
|
}
|
||||||
|
private static final class Scenario {
|
||||||
|
final GameTestHelper helper;
|
||||||
|
final ServerLevel level;
|
||||||
|
final SanctuaryChunkGenerator generator;
|
||||||
|
final HeritagePlan19 plan;
|
||||||
|
final boolean reload;
|
||||||
|
final Map<String, Object> report = new LinkedHashMap<>();
|
||||||
|
final List<ChunkPos> chunks = new ArrayList<>();
|
||||||
|
final List<StructureStart> starts = new ArrayList<>();
|
||||||
|
final List<StructureStart> ruinsStarts = new ArrayList<>();
|
||||||
|
JsonObject previous;
|
||||||
|
BlockPos gold;
|
||||||
|
int nextChunk;
|
||||||
|
boolean prepared;
|
||||||
|
Scenario(GameTestHelper helper, SanctuaryChunkGenerator generator, boolean reload) {
|
||||||
|
this.helper = helper; this.level = helper.getLevel(); this.generator = generator; this.reload = reload;
|
||||||
|
this.plan = generator.heritagePlan19();
|
||||||
|
report.put("seed", level.getSeed()); report.put("size", generator.initialDiameter());
|
||||||
|
report.put("process_id", ProcessHandle.current().pid()); report.put("world_directory", world(level).toString());
|
||||||
|
report.put("experimental_structures", generator.experimentalStructures());
|
||||||
|
report.put("structures_enabled", level.structureManager().shouldGenerateStructures());
|
||||||
|
report.put("scope", "Native local starts, saved sites and routes, player-sized entrances, supports and unopened chests on the starter island. "
|
||||||
|
+ "All14 tool tables are sampled separately without consuming a saved chest. Door states are restored after traversal. "
|
||||||
|
+ "No personal save or expansion is opened; one disposable outer-wall edit is checked in a second JVM. "
|
||||||
|
+ "Missing rails are intentional; this proof checks walking and saved rail shapes, not an uninterrupted minecart journey.");
|
||||||
|
}
|
||||||
|
void advance() {
|
||||||
|
try {
|
||||||
|
if (!prepared) { report.put("stage", "prepare"); prepare(); prepared = true; if (plan.isEmpty()) { finishDisabled(); return; } }
|
||||||
|
if (nextChunk < chunks.size()) {
|
||||||
|
report.put("stage", "load_full_chunks"); var pos = chunks.get(nextChunk++);
|
||||||
|
helper.assertTrue(level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true) instanceof LevelChunk,
|
||||||
|
"Heritage footprint must reach native FULL: " + pos);
|
||||||
|
if (nextChunk % 20 == 0) SanctuaryMod.LOGGER.info("Heritage21 loaded {}/{} footprint chunks", nextChunk, chunks.size());
|
||||||
|
helper.runAtTickTime(helper.getTick() + 1, this::advance); return;
|
||||||
|
}
|
||||||
|
report.put("stage", "verify_native_geometry"); verifyPlaced(); report.put("passed", true);
|
||||||
|
if (!reload) {
|
||||||
|
helper.assertTrue(gold != null && level.getBlockState(gold).isCollisionShapeFullBlock(level, gold), "Gold witness must replace a solid wall away from circulation");
|
||||||
|
level.setBlock(gold, Blocks.GOLD_BLOCK.defaultBlockState(), 3); report.put("gold_witness", coords(gold));
|
||||||
|
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush native starts, unopened loot and the wall edit");
|
||||||
|
report.put("world_flushed_before_shutdown", true);
|
||||||
|
} else {
|
||||||
|
helper.assertTrue(level.getBlockState(gold).is(Blocks.GOLD_BLOCK), "Reload replaced the player's wall edit");
|
||||||
|
report.put("gold_witness", coords(gold)); report.put("gold_witness_preserved", true);
|
||||||
|
}
|
||||||
|
write(level, report, reload); helper.succeed();
|
||||||
|
} catch (Exception failure) {
|
||||||
|
report.put("passed", false); report.put("failure", failure.toString());
|
||||||
|
try { write(level, report, reload); } catch (IOException ignored) { }
|
||||||
|
throw new IllegalStateException("Heritage21 native " + (reload ? "reload" : "creation") + " failed", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void prepare() throws IOException {
|
||||||
|
var ops = RegistryOps.create(JsonOps.INSTANCE, level.registryAccess());
|
||||||
|
var encoded = ChunkGenerator.CODEC.encodeStart(ops, generator).getOrThrow();
|
||||||
|
var decoded = ChunkGenerator.CODEC.parse(ops, encoded).getOrThrow();
|
||||||
|
helper.assertTrue(decoded instanceof SanctuaryChunkGenerator copy && copy.rootGeneration() == 21
|
||||||
|
&& copy.experimentalStructures() == generator.experimentalStructures(), "Generation21 codec lost its experimental switch");
|
||||||
|
report.put("generator_codec", encoded); report.put("plan_sha256", planHash(plan));
|
||||||
|
report.put("planned_sites", plan.sites().stream().map(site -> Map.of("id", site.id(), "kind", site.kind().name(),
|
||||||
|
"entry", coords(HeritagePiece21.locator(site)), "floor_y", site.floorY(), "foundation_depth", site.foundationDepth(), "footings", site.supports().size())).toList());
|
||||||
|
report.put("planned_links", plan.links().stream().map(link -> Map.of("id", link.id(), "from", link.fromId(), "to", link.toId(), "kind", link.kind().name(), "length", link.path().size(), "path", pathCoords(link))).toList());
|
||||||
|
report.put("planning_samples", plan.samples()); report.put("planning_columns", plan.columns()); report.put("planning_diagnostics", plan.diagnostics());
|
||||||
|
helper.assertTrue(plan.samples() <= HeritagePlan19.MAX_SAMPLES && plan.columns() <= HeritagePlan19.MAX_COLUMNS, "Heritage planning exceeded its budget");
|
||||||
|
verifyLootTables(helper, report);
|
||||||
|
if (reload) {
|
||||||
|
previous = JsonParser.parseString(Files.readString(proof(level))).getAsJsonObject();
|
||||||
|
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("world_flushed_before_shutdown").getAsBoolean(), "Reload needs a successful flushed creation proof");
|
||||||
|
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload must use a second JVM");
|
||||||
|
helper.assertTrue(previous.get("seed").getAsLong() == level.getSeed() && previous.get("size").getAsInt() == generator.initialDiameter()
|
||||||
|
&& previous.get("world_directory").getAsString().equals(world(level).toString()), "Reload selected another world");
|
||||||
|
helper.assertTrue(previous.get("generator_codec").equals(encoded) && previous.get("plan_sha256").getAsString().equals(planHash(plan)), "Saved settings or planned identities changed on reload");
|
||||||
|
if (previous.has("gold_witness")) gold = readPos(previous.getAsJsonArray("gold_witness"));
|
||||||
|
}
|
||||||
|
if (!generator.experimentalStructures() || !level.structureManager().shouldGenerateStructures()) {
|
||||||
|
helper.assertTrue(plan.isEmpty() && plan.segments().isEmpty() && plan.links().isEmpty(), "Disabled generation published heritage geometry"); return;
|
||||||
|
}
|
||||||
|
helper.assertTrue(!plan.isEmpty(), "Enabled reference island has no heritage site");
|
||||||
|
report.put("present_site_kinds", plan.sites().stream().map(s -> s.kind().name()).distinct().sorted().toList());
|
||||||
|
report.put("missing_site_kinds", java.util.Arrays.stream(HeritagePlan19.Kind.values()).filter(k -> plan.sites().stream().noneMatch(s -> s.kind() == k)).map(Enum::name).toList());
|
||||||
|
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
|
||||||
|
var origins = new HashSet<ChunkPos>(); var uniqueChunks = new HashSet<ChunkPos>();
|
||||||
|
plan.sites().forEach(s -> origins.add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
|
||||||
|
plan.segments().forEach(s -> origins.add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
|
||||||
|
for (var origin : origins.stream().sorted(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList()) {
|
||||||
|
var chunk = level.getChunk(origin.x(), origin.z(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||||
|
var start = chunk.getStartForStructure(structure.value());
|
||||||
|
helper.assertTrue(start != null && start.isValid(), "Accepted site/route has no native start at " + origin); starts.add(start);
|
||||||
|
for (var piece : start.getPieces()) {
|
||||||
|
helper.assertTrue(piece instanceof HeritagePiece21, "Heritage loaded a foreign piece type");
|
||||||
|
var box = piece.getBoundingBox();
|
||||||
|
helper.assertTrue(Math.abs((long) (box.minX() >> 4) - origin.x()) <= 7 && Math.abs((long) (box.maxX() >> 4) - origin.x()) <= 7
|
||||||
|
&& Math.abs((long) (box.minZ() >> 4) - origin.z()) <= 7 && Math.abs((long) (box.maxZ() >> 4) - origin.z()) <= 7, "Native start references a distant route endpoint");
|
||||||
|
box.intersectingChunks().forEach(uniqueChunks::add);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var site : plan.sites()) uniqueChunks.add(ChunkPos.containing(HeritagePiece21.locator(site)));
|
||||||
|
for (var link : plan.links()) for (var point : link.path()) uniqueChunks.add(new ChunkPos(point.x() >> 4, point.z() >> 4));
|
||||||
|
helper.assertTrue(uniqueChunks.size() <= 1024, "Bounded proof footprint exceeds1024 chunks");
|
||||||
|
var ruins = generator.ruinsPlan17();
|
||||||
|
report.put("ruins_plan_sha256", hash(ruins.networks().toString()));
|
||||||
|
if (reload) helper.assertTrue(previous.get("ruins_plan_sha256").getAsString().equals(hash(ruins.networks().toString())), "Reload changed forgotten room planning");
|
||||||
|
helper.assertTrue(!ruins.isEmpty(), "Reference20 must demonstrate native experimental room architecture");
|
||||||
|
var ruinsType = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(RuinsStructures17.KEY);
|
||||||
|
var ruinsOrigins = new HashSet<ChunkPos>();
|
||||||
|
for (var network : ruins.networks()) {
|
||||||
|
var origin = new ChunkPos(network.startChunkX(), network.startChunkZ()); if (!ruinsOrigins.add(origin)) continue;
|
||||||
|
var nativeStart = level.getChunk(origin.x(), origin.z(), ChunkStatus.STRUCTURE_STARTS, true).getStartForStructure(ruinsType.value());
|
||||||
|
helper.assertTrue(nativeStart != null && nativeStart.isValid(), "Missing native20 forgotten network: " + network.id());
|
||||||
|
ruinsStarts.add(nativeStart);
|
||||||
|
for (var piece : nativeStart.getPieces()) {
|
||||||
|
helper.assertTrue(piece instanceof RuinsPiece20, "Generation21 loaded a historical forgotten piece");
|
||||||
|
piece.getBoundingBox().intersectingChunks().forEach(uniqueChunks::add);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chunks.addAll(uniqueChunks.stream().sorted(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList());
|
||||||
|
}
|
||||||
|
void finishDisabled() throws IOException {
|
||||||
|
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
|
||||||
|
var start = level.getChunk(0, 0, ChunkStatus.STRUCTURE_STARTS, true).getStartForStructure(structure.value());
|
||||||
|
helper.assertTrue(start == null || !start.isValid(), "Disabled heritage emitted a native start");
|
||||||
|
helper.assertTrue(generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false) == null, "Disabled heritage is locatable");
|
||||||
|
report.put("empty_plan_verified", true); report.put("passed", true);
|
||||||
|
if (!reload) { helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save disabled experiment setting"); report.put("world_flushed_before_shutdown", true); }
|
||||||
|
write(level, report, reload); helper.succeed();
|
||||||
|
}
|
||||||
|
void verifyPlaced() {
|
||||||
|
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
|
||||||
|
var context = StructurePieceSerializationContext.fromLevel(level); var summaries = new ArrayList<Map<String, Object>>();
|
||||||
|
int blocks = 0, chests = 0, pieces = 0, supports = 0;
|
||||||
|
var connections = NativeConnections20.Stats.empty();
|
||||||
|
for (var start : starts) {
|
||||||
|
var origin = start.getChunkPos(); var saved = start.createTag(context, origin); String nativeHash = hash(saved.toString());
|
||||||
|
var copy = StructureStart.loadStaticStart(context, saved, level.getSeed());
|
||||||
|
helper.assertTrue(copy != null && copy.isValid() && copy.createTag(context, origin).equals(saved), "Native start failed an exact NBT roundtrip");
|
||||||
|
var expectedPayloads = new HashSet<net.minecraft.nbt.CompoundTag>();
|
||||||
|
plan.sites().stream().filter(s -> s.startChunkX() == origin.x() && s.startChunkZ() == origin.z()).forEach(s -> expectedPayloads.add(HeritagePiece21.encodeSite(s)));
|
||||||
|
plan.segments().stream().filter(s -> s.startChunkX() == origin.x() && s.startChunkZ() == origin.z()).forEach(s -> expectedPayloads.add(HeritagePiece21.encodeSegment(s)));
|
||||||
|
var actualPayloads = new HashSet<net.minecraft.nbt.CompoundTag>();
|
||||||
|
if (reload) {
|
||||||
|
JsonObject prior = null;
|
||||||
|
for (var value : previous.getAsJsonArray("starts")) { var item = value.getAsJsonObject(); var at = item.getAsJsonArray("start_chunk"); if (at.get(0).getAsInt() == origin.x() && at.get(1).getAsInt() == origin.z()) prior = item; }
|
||||||
|
helper.assertTrue(prior != null && prior.get("native_start_sha256").getAsString().equals(nativeHash), "Reload changed saved geometry or native reference count");
|
||||||
|
}
|
||||||
|
var loot = new ArrayList<Map<String, Object>>();
|
||||||
|
for (var value : start.getPieces()) {
|
||||||
|
var piece = (HeritagePiece21) value; pieces++;
|
||||||
|
var tag = piece.createTag(context); var restored = new HeritagePiece21(context, tag);
|
||||||
|
helper.assertTrue(restored.createTag(context).equals(tag), "Saved piece parameters changed on native load");
|
||||||
|
actualPayloads.add(tag.getCompound("SanctuaryHeritage").orElseThrow());
|
||||||
|
var corrupt = tag.copy(); corrupt.getCompound("SanctuaryHeritage").orElseThrow().putInt("Schema", 22);
|
||||||
|
boolean rejected = false; try { new HeritagePiece21(context, corrupt); } catch (IllegalArgumentException expected) { rejected = true; }
|
||||||
|
helper.assertTrue(rejected, "Unknown schema must fail closed");
|
||||||
|
piece.getBoundingBox().intersectingChunks().forEach(at -> helper.assertTrue(level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true)
|
||||||
|
.getReferencesForStructure(structure.value()).contains(origin.pack()), "FULL chunk lost its native structure reference"));
|
||||||
|
var expected = rendered(piece); blocks += expected.blocks().size();
|
||||||
|
for (var entry : expected.blocks().entrySet()) {
|
||||||
|
var at = entry.getKey(); var wanted = entry.getValue(); helper.assertTrue(piece.getBoundingBox().isInside(at), "Renderer escaped saved bounds");
|
||||||
|
if (reload && at.equals(gold)) continue;
|
||||||
|
helper.assertTrue(level.getBlockState(at).is(wanted.getBlock()), "Clipping or decoration changed heritage at " + at + ": expected " + wanted + " got " + level.getBlockState(at));
|
||||||
|
helper.assertTrue(!wanted.is(Blocks.SPAWNER), "Heritage must not create spawners");
|
||||||
|
}
|
||||||
|
connections = connections.add(NativeConnections20.verify(helper, expected.blocks(), reload ? Set.of(gold) : Set.of()));
|
||||||
|
if (piece.isSite()) {
|
||||||
|
supports += verifySiteBearing(helper, generator, plan, piece.site());
|
||||||
|
if (!reload && gold == null) {
|
||||||
|
gold = expected.blocks().entrySet().stream().filter(e -> isOuterWall(piece.site(), e.getKey()) && e.getValue().isCollisionShapeFullBlock(level, e.getKey()))
|
||||||
|
.map(Map.Entry::getKey).sorted(Comparator.<BlockPos>comparingInt(p -> p.getY()).thenComparingInt(p -> p.getX()).thenComparingInt(p -> p.getZ())).findFirst().orElse(null);
|
||||||
|
}
|
||||||
|
} else for (var cell : piece.segment().cells()) { supports++; assertRock(helper, new BlockPos(cell.x(), cell.minY() - 1, cell.z())); }
|
||||||
|
for (var chest : expected.chests()) {
|
||||||
|
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Deferred chest is missing");
|
||||||
|
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
|
||||||
|
helper.assertTrue(entity.getLootTable() != null && entity.getLootTable().equals(chest.table()) && entity.getLootTableSeed() == chest.seed(), "Deferred loot table/seed was lost or consumed");
|
||||||
|
if (piece.isSite()) helper.assertTrue(chest.table().equals(lootKey(piece.site().kind())), "Site uses another kind's tool table");
|
||||||
|
loot.add(Map.of("position", coords(chest.position()), "table", chest.table().identifier().toString(), "seed", chest.seed())); chests++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
helper.assertTrue(actualPayloads.equals(expectedPayloads) && start.getPieces().size() == expectedPayloads.size(), "Native start lost or duplicated planned pieces");
|
||||||
|
summaries.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", nativeHash, "pieces", start.getPieces().size(), "unopened_chests", loot));
|
||||||
|
}
|
||||||
|
report.put("connected_blocks", connections);
|
||||||
|
helper.assertTrue(connections.panes() > 0 && connections.stairs() > 0 && connections.nativeOracles() > 0,
|
||||||
|
"Reference20 needs real panes/stairs validated by vanilla neighbor rules");
|
||||||
|
if (level.getSeed() == 0 && generator.initialDiameter() == 724)
|
||||||
|
helper.assertTrue(connections.crossChunkPaneLinks() > 0, "Reference20 must demonstrate a real pane join across a chunk boundary");
|
||||||
|
verifyRuins();
|
||||||
|
report.put("starts", summaries); report.put("native_pieces", pieces); report.put("native_blocks_checked", blocks);
|
||||||
|
report.put("full_chunks_checked", chunks.size()); report.put("rock_supports_checked", supports); report.put("unopened_deferred_chests", chests);
|
||||||
|
report.put("stage", "verify_site_routes"); var sites = new ArrayList<Map<String, Object>>(); for (var site : plan.sites()) sites.add(verifyAccess(helper, site)); report.put("native_sites", sites);
|
||||||
|
report.put("stage", "verify_read_only_landings");
|
||||||
|
report.put("read_only_landmarks", verifyReadOnlyLandings(helper, generator, plan));
|
||||||
|
report.put("stage", "verify_island_routes"); report.put("native_links", verifyLinks(helper, plan));
|
||||||
|
int greatestSpan = plan.links().stream().mapToInt(Heritage21WorldGameTests::routeSpan).max().orElse(0);
|
||||||
|
report.put("greatest_single_route_span", greatestSpan);
|
||||||
|
var reachedIds = new HashSet<String>(); plan.links().forEach(l -> { reachedIds.add(l.fromId()); reachedIds.add(l.toId()); });
|
||||||
|
report.put("connected_destination_ids", reachedIds.stream().sorted().toList());
|
||||||
|
report.put("isolated_destination_ids", plan.sites().stream().map(HeritagePlan19.Site::id).filter(id -> !reachedIds.contains(id)).toList());
|
||||||
|
report.put("connected_city_arrivals", plan.landmarks().stream().filter(l -> l.kind() == HeritagePlan19.LandmarkKind.CITY && reachedIds.contains(l.id())).map(HeritagePlan19.Landmark::id).toList());
|
||||||
|
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
|
||||||
|
helper.assertTrue(located != null && located.getSecond().is(HeritageStructures19.KEY) && plan.sites().stream().anyMatch(s -> HeritagePiece21.locator(s).equals(located.getFirst())), "Locate must lead to a real exterior site entry");
|
||||||
|
report.put("locate", coords(located.getFirst()));
|
||||||
|
}
|
||||||
|
void verifyRuins() {
|
||||||
|
var context = StructurePieceSerializationContext.fromLevel(level);
|
||||||
|
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(RuinsStructures17.KEY);
|
||||||
|
var summaries = new ArrayList<Map<String, Object>>(); int count = 0, blockCount = 0, lootCount = 0;
|
||||||
|
var connections = NativeConnections20.Stats.empty();
|
||||||
|
for (var start : ruinsStarts) {
|
||||||
|
var origin = start.getChunkPos(); var saved = start.createTag(context, origin); var digest = hash(saved.toString());
|
||||||
|
var loaded = StructureStart.loadStaticStart(context, saved, level.getSeed());
|
||||||
|
helper.assertTrue(loaded != null && loaded.createTag(context, origin).equals(saved), "Forgotten20 native start NBT changed");
|
||||||
|
var expectedRooms = generator.ruinsPlan17().networks().stream().filter(n -> n.startChunkX() == origin.x() && n.startChunkZ() == origin.z()).flatMap(n -> n.rooms().stream()).toList();
|
||||||
|
var actualRooms = start.getPieces().stream().map(RuinsPiece20.class::cast).map(RuinsPiece20::room).toList();
|
||||||
|
helper.assertTrue(expectedRooms.size() == actualRooms.size() && new HashSet<>(expectedRooms).equals(new HashSet<>(actualRooms)), "Forgotten20 native rooms differ from plan");
|
||||||
|
if (reload) {
|
||||||
|
JsonObject prior = null;
|
||||||
|
for (var value : previous.getAsJsonArray("ruins_starts")) { var item = value.getAsJsonObject(); var at = item.getAsJsonArray("start_chunk"); if (at.get(0).getAsInt() == origin.x() && at.get(1).getAsInt() == origin.z()) prior = item; }
|
||||||
|
helper.assertTrue(prior != null && prior.get("native_start_sha256").getAsString().equals(digest), "Forgotten20 native start changed on a second JVM load");
|
||||||
|
}
|
||||||
|
for (var raw : start.getPieces()) {
|
||||||
|
var piece = (RuinsPiece20) raw; var room = piece.room(); count++;
|
||||||
|
var tag = piece.createTag(context); var roundtrip = new RuinsPiece20(context, tag);
|
||||||
|
helper.assertTrue(roundtrip.room().equals(room) && roundtrip.createTag(context).equals(tag), "Forgotten20 piece lost saved parameters");
|
||||||
|
piece.getBoundingBox().intersectingChunks().forEach(at -> helper.assertTrue(level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true).getReferencesForStructure(registry.value()).contains(origin.pack()), "Forgotten20 chunk lost native references"));
|
||||||
|
var expected = rendered(room); blockCount += expected.blocks().size();
|
||||||
|
for (var entry : expected.blocks().entrySet()) {
|
||||||
|
helper.assertTrue(piece.getBoundingBox().isInside(entry.getKey()), "Forgotten20 renderer escaped saved bounds");
|
||||||
|
helper.assertTrue(level.getBlockState(entry.getKey()).is(entry.getValue().getBlock()), "Forgotten20 native geometry changed at " + entry.getKey());
|
||||||
|
}
|
||||||
|
connections = connections.add(NativeConnections20.verify(helper, expected.blocks(), Set.of()));
|
||||||
|
for (var support : room.supports()) assertRock(helper, new BlockPos(support.x(), support.minY() - 1, support.z()));
|
||||||
|
for (var chest : expected.chests()) {
|
||||||
|
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Forgotten20 missing deferred chest");
|
||||||
|
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
|
||||||
|
helper.assertTrue(chest.table().equals(entity.getLootTable()) && chest.seed() == entity.getLootTableSeed(), "Forgotten20 consumed/changed saved loot"); lootCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
summaries.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", digest, "pieces", start.getPieces().size()));
|
||||||
|
}
|
||||||
|
report.put("ruins_starts", summaries); report.put("ruins_native_pieces", count); report.put("ruins_native_blocks_checked", blockCount);
|
||||||
|
report.put("ruins_unopened_deferred_chests", lootCount); report.put("ruins_connections", connections);
|
||||||
|
report.put("ruins_access", generator.ruinsPlan17().networks().stream().map(n -> verifyAccess(helper, n)).toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private record Chest(BlockPos position, ResourceKey<LootTable> table, long seed) {}
|
||||||
|
private record Rendered(Map<BlockPos, BlockState> blocks, List<Chest> chests) {}
|
||||||
|
private static Rendered rendered(HeritagePiece21 piece) {
|
||||||
|
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
|
||||||
|
int x0 = piece.isSite() ? piece.site().minX() : piece.segment().minX();
|
||||||
|
int y0 = piece.isSite() ? piece.site().floorY() : piece.segment().minY();
|
||||||
|
int z0 = piece.isSite() ? piece.site().minZ() : piece.segment().minZ();
|
||||||
|
var writer = new LostCityRenderer.BlockWriter() {
|
||||||
|
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(new BlockPos(x0 + x, y0 + y, z0 + z), state); }
|
||||||
|
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
|
||||||
|
var at = new BlockPos(x0 + x, y0 + y, z0 + z); blocks.put(at, Blocks.CHEST.defaultBlockState()); chests.add(new Chest(at, table, RandomSource.create(seed).nextLong()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (piece.isSite()) HeritageRenderer21.render(piece.site(), writer); else HeritageRenderer21.renderSegment(piece.segment(), writer);
|
||||||
|
return new Rendered(blocks, chests);
|
||||||
|
}
|
||||||
|
private static Rendered rendered(RuinsPlan17.Room room) {
|
||||||
|
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
|
||||||
|
RuinsRenderer20.render(room, new LostCityRenderer.BlockWriter() {
|
||||||
|
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(at(room, x, y, z), state); }
|
||||||
|
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
|
||||||
|
var position = at(room, x, y, z); blocks.put(position, Blocks.CHEST.defaultBlockState()); chests.add(new Chest(position, table, RandomSource.create(seed).nextLong()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return new Rendered(blocks, chests);
|
||||||
|
}
|
||||||
|
private static Map<String, Object> verifyAccess(GameTestHelper helper, RuinsPlan17.Network network) {
|
||||||
|
var level = helper.getLevel(); var access = network.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS).findFirst().orElseThrow();
|
||||||
|
var hatches = new HashMap<BlockPos, BlockState>();
|
||||||
|
rendered(access).blocks().forEach((at, state) -> { if (state.is(Blocks.DARK_OAK_TRAPDOOR)) hatches.put(at, level.getBlockState(at)); });
|
||||||
|
helper.assertTrue(hatches.size() == 1, "Each network must have one usable wooden surface hatch");
|
||||||
|
hatches.forEach((at, state) -> {
|
||||||
|
helper.assertTrue(state.is(Blocks.DARK_OAK_TRAPDOOR) && state.canSurvive(level, at), "Surface hatch is missing its native support");
|
||||||
|
level.setBlock(at, state.setValue(BlockStateProperties.OPEN, true), 2);
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
Predicate<BlockPos> inside = pos -> network.rooms().stream().anyMatch(r -> r.contains(pos.getX(), pos.getY(), pos.getZ()));
|
||||||
|
var start = RuinsPiece20.locator(access); var reached = walk(helper, start, inside);
|
||||||
|
int targets = 0;
|
||||||
|
for (var room : network.rooms()) for (var target : RuinsRenderer20.targets(room)) {
|
||||||
|
var point = at(room, target.x(), target.y(), target.z());
|
||||||
|
helper.assertTrue(reached.contains(point), "Native network " + network.id() + " cannot reach " + room.kind() + "/" + target.name() + " at " + point);
|
||||||
|
targets++;
|
||||||
|
}
|
||||||
|
for (var hatch : hatches.keySet()) helper.assertTrue(reached.contains(hatch), "Surface approach cannot enter its ladder shaft");
|
||||||
|
return Map.of("id", network.id(), "family", network.family().name(), "rooms", network.rooms().size(),
|
||||||
|
"entrance", coords(start), "walkable_targets", targets, "standing_cells", reached.size());
|
||||||
|
} finally { hatches.forEach((at, state) -> level.setBlock(at, state, 2)); }
|
||||||
|
}
|
||||||
|
private static BlockPos at(RuinsPlan17.Room room, int x, int y, int z) { return new BlockPos(room.minX() + x, room.minY() + y, room.minZ() + z); }
|
||||||
|
private static boolean isOuterWall(HeritagePlan19.Site site, BlockPos pos) {
|
||||||
|
return pos.getY() >= site.floorY() + 2 && pos.getY() <= site.floorY() + 3
|
||||||
|
&& (pos.getX() == site.minX() + 2 || pos.getX() == site.maxX() - 2 || pos.getZ() == site.minZ() + 2 || pos.getZ() == site.maxZ() - 2)
|
||||||
|
&& Math.abs(pos.getX() - site.entry().x()) + Math.abs(pos.getZ() - site.entry().z()) > 6;
|
||||||
|
}
|
||||||
|
private static Map<String, Object> verifyAccess(GameTestHelper helper, HeritagePlan19.Site site) {
|
||||||
|
var level = helper.getLevel(); var expected = rendered(new HeritagePiece21(site)); var doors = new HashMap<BlockPos, BlockState>();
|
||||||
|
expected.blocks().forEach((at, state) -> {
|
||||||
|
if ((state.getBlock() instanceof DoorBlock || state.getBlock() instanceof TrapDoorBlock) && state.hasProperty(BlockStateProperties.OPEN)) doors.put(at, level.getBlockState(at));
|
||||||
|
});
|
||||||
|
doors.forEach((at, state) -> level.setBlock(at, state.setValue(BlockStateProperties.OPEN, true), 2));
|
||||||
|
try {
|
||||||
|
var entry = HeritagePiece21.locator(site);
|
||||||
|
Predicate<BlockPos> inside = p -> p.equals(entry) || p.getX() >= site.minX() && p.getX() <= site.maxX()
|
||||||
|
&& p.getZ() >= site.minZ() && p.getZ() <= site.maxZ() && p.getY() >= site.floorY() - 4 && p.getY() < site.maxY();
|
||||||
|
var reached = walk(helper, entry, inside); int targets = 0;
|
||||||
|
for (var target : HeritageRenderer21.targets(site)) {
|
||||||
|
var at = new BlockPos(site.minX() + target.x(), site.floorY() + target.y(), site.minZ() + target.z());
|
||||||
|
helper.assertTrue(reached.contains(at), "Site " + site.id() + " cannot reach " + target.name() + " at " + at); targets++;
|
||||||
|
}
|
||||||
|
return Map.of("id", site.id(), "kind", site.kind().name(), "entry", coords(entry), "standing_cells", reached.size(), "walkable_targets", targets);
|
||||||
|
} finally { doors.forEach((at, state) -> level.setBlock(at, state, 2)); }
|
||||||
|
}
|
||||||
|
private static List<Map<String, Object>> verifyLinks(GameTestHelper helper, HeritagePlan19 plan) {
|
||||||
|
helper.assertTrue(!plan.links().isEmpty(), "Reference island needs a real route between accepted destinations");
|
||||||
|
var entries = new HashMap<String, HeritagePlan19.Entry>(); plan.sites().forEach(s -> entries.put(s.id(), s.entry())); plan.landmarks().forEach(s -> entries.put(s.id(), s.entry()));
|
||||||
|
var cells = new HashMap<Long, HeritagePlan19.Cell>(); plan.segments().forEach(s -> s.cells().forEach(c -> cells.put(column(c.x(), c.z()), c)));
|
||||||
|
var result = new ArrayList<Map<String, Object>>();
|
||||||
|
for (var link : plan.links()) {
|
||||||
|
var from = entries.get(link.fromId()); var to = entries.get(link.toId());
|
||||||
|
helper.assertTrue(from != null && to != null, "Route points to an absent destination");
|
||||||
|
var first = link.path().getFirst(); var last = link.path().getLast();
|
||||||
|
helper.assertTrue(first.x() == from.x() && first.z() == from.z() && first.floorY() + 1 == from.y()
|
||||||
|
&& last.x() == to.x() && last.z() == to.z() && last.floorY() + 1 == to.y(), "Route does not meet its real entrances");
|
||||||
|
int present = 0, missing = 0;
|
||||||
|
for (int i = 0; i < link.path().size(); i++) {
|
||||||
|
var point = link.path().get(i); var at = new BlockPos(point.x(), point.floorY() + 1, point.z());
|
||||||
|
helper.assertTrue(standable(helper.getLevel(), at), "Ballast route " + link.id() + " is blocked at " + at);
|
||||||
|
if (i > 0) { var prior = link.path().get(i - 1); if (point.floorY() > prior.floorY()) helper.assertTrue(clearBody(helper.getLevel(), new BlockPos(prior.x(), prior.floorY() + 2, prior.z())), "Route slope cannot be climbed by a player"); }
|
||||||
|
var cell = cells.get(column(point.x(), point.z()));
|
||||||
|
if (cell != null && cell.rail() != HeritagePlan19.Rail.NONE) {
|
||||||
|
var actual = helper.getLevel().getBlockState(at);
|
||||||
|
if (cell.railPresent()) {
|
||||||
|
helper.assertTrue(actual.is(Blocks.RAIL), "Saved existing rail disappeared");
|
||||||
|
helper.assertTrue(actual.getValue(BlockStateProperties.RAIL_SHAPE).name().equals(cell.rail().name()), "Native rail update changed the intended slope or curve at " + at); present++;
|
||||||
|
} else { helper.assertTrue(!actual.is(Blocks.RAIL), "An intentionally missing rail regenerated"); missing++; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int spanX = link.path().stream().mapToInt(HeritagePlan19.Point::x).max().orElseThrow() - link.path().stream().mapToInt(HeritagePlan19.Point::x).min().orElseThrow();
|
||||||
|
int spanZ = link.path().stream().mapToInt(HeritagePlan19.Point::z).max().orElseThrow() - link.path().stream().mapToInt(HeritagePlan19.Point::z).min().orElseThrow();
|
||||||
|
result.add(Map.of("id", link.id(), "from", link.fromId(), "to", link.toId(), "kind", link.kind().name(), "length", link.path().size(),
|
||||||
|
"span_x", spanX, "span_z", spanZ, "existing_rails", present, "deliberately_missing_rails", missing, "path", pathCoords(link)));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
private static int routeSpan(HeritagePlan19.Link link) {
|
||||||
|
int spanX = link.path().stream().mapToInt(HeritagePlan19.Point::x).max().orElseThrow()
|
||||||
|
- link.path().stream().mapToInt(HeritagePlan19.Point::x).min().orElseThrow();
|
||||||
|
int spanZ = link.path().stream().mapToInt(HeritagePlan19.Point::z).max().orElseThrow()
|
||||||
|
- link.path().stream().mapToInt(HeritagePlan19.Point::z).min().orElseThrow();
|
||||||
|
return Math.max(spanX, spanZ);
|
||||||
|
}
|
||||||
|
private static List<List<Integer>> pathCoords(HeritagePlan19.Link link) {
|
||||||
|
return link.path().stream().map(point -> List.of(point.x(), point.floorY(), point.z())).toList();
|
||||||
|
}
|
||||||
|
private static int verifySiteBearing(GameTestHelper helper, SanctuaryChunkGenerator generator, HeritagePlan19 plan, HeritagePlan19.Site site) {
|
||||||
|
int checked = 0;
|
||||||
|
// The planner certifies a nine-point bearing grid, either directly below its slab or below a saved short footing.
|
||||||
|
for (int dx : new int[] {1, 8, 15}) for (int dz : new int[] {1, 8, 15}) {
|
||||||
|
int x = site.minX() + dx, z = site.minZ() + dz;
|
||||||
|
var support = site.supports().stream().filter(s -> s.x() == x && s.z() == z).findFirst();
|
||||||
|
int bottom = support.map(HeritagePlan19.Support::minY).orElse(site.minY());
|
||||||
|
for (int below = 1; below <= 2; below++) {
|
||||||
|
var at = new BlockPos(x, bottom - below, z); assertRock(helper, at);
|
||||||
|
helper.assertTrue(plan.protects(at.getX(), at.getY(), at.getZ()) && generator.protectsCityDecoration(at),
|
||||||
|
"Site's certified bearing layer is not published in the decoration mask: " + site.id() + " at " + at);
|
||||||
|
helper.assertTrue(!plan.contains(at.getX(), at.getY(), at.getZ()), "Original bearing rock became a writable heritage volume: " + at);
|
||||||
|
checked++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return checked;
|
||||||
|
}
|
||||||
|
private static List<Map<String, Object>> verifyReadOnlyLandings(GameTestHelper helper, SanctuaryChunkGenerator generator, HeritagePlan19 plan) {
|
||||||
|
var used = new HashSet<String>(); plan.links().forEach(link -> { used.add(link.fromId()); used.add(link.toId()); });
|
||||||
|
var result = new ArrayList<Map<String, Object>>();
|
||||||
|
for (var landmark : plan.landmarks()) if (used.contains(landmark.id())) {
|
||||||
|
var e = landmark.entry(); int checked = 0;
|
||||||
|
for (int side = -1; side <= 1; side++) {
|
||||||
|
int x = e.x() + ((e.facing() & 1) == 0 ? side : 0), z = e.z() + ((e.facing() & 1) == 1 ? side : 0);
|
||||||
|
for (int y = e.y() - 2; y <= e.y() + 2; y++) {
|
||||||
|
var at = new BlockPos(x, y, z);
|
||||||
|
helper.assertTrue(plan.protects(x, y, z) && generator.protectsCityDecoration(at), "Read-only arrival lost its decoration mask: " + landmark.id() + " at " + at);
|
||||||
|
helper.assertTrue(!plan.contains(x, y, z), "Heritage gained write ownership of a historical arrival: " + landmark.id() + " at " + at);
|
||||||
|
if (y == e.y() - 1) assertRock(helper, at);
|
||||||
|
if (y >= e.y()) helper.assertTrue(helper.getLevel().getBlockState(at).isAir(), "Decoration obstructed a certified historical arrival: " + at);
|
||||||
|
checked++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.add(Map.of("id", landmark.id(), "entry", List.of(e.x(), e.y(), e.z()), "read_only_cells", checked));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
private static void assertRock(GameTestHelper helper, BlockPos at) {
|
||||||
|
helper.assertTrue(helper.getLevel().getBlockState(at).isSolid() && helper.getLevel().getFluidState(at).isEmpty(), "Lost original rock bearing at " + at);
|
||||||
|
}
|
||||||
|
private static void verifyLootTables(GameTestHelper helper, Map<String, Object> report) {
|
||||||
|
var result = new ArrayList<Map<String, Object>>(); var level = helper.getLevel();
|
||||||
|
var params = new LootParams.Builder(level).withParameter(LootContextParams.ORIGIN, new Vec3(0, 160, 0)).create(LootContextParamSets.CHEST);
|
||||||
|
for (var kind : HeritagePlan19.Kind.values()) {
|
||||||
|
var table = level.getServer().reloadableRegistries().getLootTable(lootKey(kind));
|
||||||
|
helper.assertTrue(table != LootTable.EMPTY, "Heritage tool table did not load: " + kind);
|
||||||
|
for (long seed : new long[] {0, 42, -1}) helper.assertTrue(table.getRandomItems(params, seed).stream().anyMatch(stack -> stack.is(tool(kind))), "Tool table is empty or lacks its expected primary tool: " + kind);
|
||||||
|
result.add(Map.of("kind", kind.name(), "table", lootKey(kind).identifier().toString(), "sampled_seeds", List.of(0, 42, -1)));
|
||||||
|
}
|
||||||
|
report.put("tool_tables_sampled_separately", result);
|
||||||
|
}
|
||||||
|
private static ResourceKey<LootTable> lootKey(HeritagePlan19.Kind kind) { return ResourceKey.create(Registries.LOOT_TABLE, SanctuaryMod.id("chests/heritage19/" + kind.name().toLowerCase(Locale.ROOT))); }
|
||||||
|
private static Item tool(HeritagePlan19.Kind kind) {
|
||||||
|
return switch (kind) {
|
||||||
|
case OBSERVATORY -> Items.SPYGLASS;
|
||||||
|
case POST_OFFICE, EXPEDITION_OFFICE, RADIO_RELAY -> Items.COMPASS;
|
||||||
|
case SUBSTATION, IMPOSSIBLE_SERVICE -> Items.CLOCK;
|
||||||
|
case FREIGHT_STATION -> Items.MINECART;
|
||||||
|
case COMMUNAL_DEPOT -> Items.IRON_AXE;
|
||||||
|
case RAIL_WORKSHOP -> Items.IRON_PICKAXE;
|
||||||
|
case PUMP_STATION -> Items.BUCKET;
|
||||||
|
case GREENHOUSE -> Items.IRON_HOE;
|
||||||
|
case VOID_DOCK -> Items.LEAD;
|
||||||
|
case PHOTO_CLUB -> Items.BOOK;
|
||||||
|
case CONSTRUCTION_REFUGE -> Items.IRON_SHOVEL;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
private static Set<BlockPos> walk(GameTestHelper helper, BlockPos start, Predicate<BlockPos> inside) {
|
||||||
|
var level = helper.getLevel(); helper.assertTrue(inside.test(start) && standable(level, start), "Blocked or unsupported exterior entrance: " + start);
|
||||||
|
var seen = new HashSet<BlockPos>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
var pos = queue.removeFirst();
|
||||||
|
for (var direction : HORIZONTAL) for (int dy = -1; dy <= 1; dy++) {
|
||||||
|
var next = pos.relative(direction).offset(0, dy, 0);
|
||||||
|
if (!inside.test(next) || dy > 0 && !clearBody(level, pos.above())) continue;
|
||||||
|
if (standable(level, next) && seen.add(next)) queue.add(next);
|
||||||
|
}
|
||||||
|
for (int dy : new int[] {-1, 1}) {
|
||||||
|
var next = pos.offset(0, dy, 0); if (!inside.test(next)) continue;
|
||||||
|
if ((level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(next).is(Blocks.LADDER) || level.getBlockState(pos.below()).is(Blocks.LADDER))
|
||||||
|
&& standable(level, next) && seen.add(next)) queue.add(next);
|
||||||
|
}
|
||||||
|
helper.assertTrue(seen.size() <= 50_000, "Native site traversal escaped its bounds");
|
||||||
|
}
|
||||||
|
return seen;
|
||||||
|
}
|
||||||
|
private static boolean standable(ServerLevel level, BlockPos pos) {
|
||||||
|
if (!clearBody(level, pos)) return false;
|
||||||
|
if (level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(pos.below()).is(Blocks.LADDER)) return true;
|
||||||
|
var below = pos.below(); return level.getBlockState(below).getCollisionShape(level, below).toAabbs().stream()
|
||||||
|
.anyMatch(b -> b.maxY >= .5 && b.minX <= .5 && b.maxX >= .5 && b.minZ <= .5 && b.maxZ >= .5);
|
||||||
|
}
|
||||||
|
private static boolean clearBody(ServerLevel level, BlockPos pos) {
|
||||||
|
var body = new AABB(pos.getX() + .2, pos.getY(), pos.getZ() + .2, pos.getX() + .8, pos.getY() + 1.8, pos.getZ() + .8);
|
||||||
|
for (var at : new BlockPos[] {pos, pos.above()}) {
|
||||||
|
if (!level.getFluidState(at).isEmpty()) return false;
|
||||||
|
for (var shape : level.getBlockState(at).getCollisionShape(level, at).toAabbs()) if (shape.move(at.getX(), at.getY(), at.getZ()).intersects(body)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
private static long column(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
|
||||||
|
private static List<Integer> coords(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
|
||||||
|
private static BlockPos readPos(com.google.gson.JsonArray value) { return new BlockPos(value.get(0).getAsInt(), value.get(1).getAsInt(), value.get(2).getAsInt()); }
|
||||||
|
private static Path world(ServerLevel level) { return level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize(); }
|
||||||
|
private static Path proof(ServerLevel level) { return world(level).resolve("heritage21-create-proof.json"); }
|
||||||
|
private static String planHash(HeritagePlan19 plan) { return hash(plan.sites().toString() + plan.landmarks() + plan.links() + plan.segments()); }
|
||||||
|
private static void write(ServerLevel level, Map<String, Object> report, boolean reload) throws IOException {
|
||||||
|
var path = Path.of("diagnostics", "heritage21-" + (reload ? "reload" : "create") + ".json"); Files.createDirectories(path.getParent());
|
||||||
|
var value = new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n"; Files.writeString(path, value); if (!reload) Files.writeString(proof(level), value);
|
||||||
|
}
|
||||||
|
private static String hash(String value) {
|
||||||
|
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8))); }
|
||||||
|
catch (NoSuchAlgorithmException impossible) { throw new AssertionError(impossible); }
|
||||||
|
}
|
||||||
|
}
|
||||||
+1083
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -211,7 +211,7 @@ public final class PopulationDiagnostics {
|
|||||||
|
|
||||||
void prepare() throws IOException {
|
void prepare() throws IOException {
|
||||||
boolean heritage = level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator sanctuary
|
boolean heritage = level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator sanctuary
|
||||||
&& (sanctuary.rootGeneration() == 19 || sanctuary.rootGeneration() == 20) && sanctuary.experimentalStructures();
|
&& (sanctuary.rootGeneration() == 19 || sanctuary.rootGeneration() == 20 || sanctuary.rootGeneration() == 21) && sanctuary.experimentalStructures();
|
||||||
// Generation19 explicitly prospects destinations across the root island before publishing them.
|
// 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.
|
// A consulted hydrology region is a bounded immutable plan, not a loaded/generated chunk.
|
||||||
// Historical generators retain their original near-spawn contract.
|
// Historical generators retain their original near-spawn contract.
|
||||||
|
|||||||
+453
@@ -0,0 +1,453 @@
|
|||||||
|
package fr.koka.sanctuary.gametest;
|
||||||
|
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import com.mojang.serialization.JsonOps;
|
||||||
|
import fr.koka.sanctuary.SanctuaryMod;
|
||||||
|
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||||
|
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||||
|
import fr.koka.sanctuary.worldgen.transit.TransitPiece21;
|
||||||
|
import fr.koka.sanctuary.worldgen.transit.TransitPlan21;
|
||||||
|
import fr.koka.sanctuary.worldgen.transit.TransitRenderer21;
|
||||||
|
import fr.koka.sanctuary.worldgen.transit.TransitStructures21;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.HexFormat;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.gametest.framework.GameTestHelper;
|
||||||
|
import net.minecraft.resources.RegistryOps;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.StairBlock;
|
||||||
|
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
import net.minecraft.world.level.chunk.LevelChunk;
|
||||||
|
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||||
|
import net.minecraft.world.level.storage.LevelResource;
|
||||||
|
import net.minecraft.world.level.storage.loot.LootTable;
|
||||||
|
import net.minecraft.world.phys.AABB;
|
||||||
|
|
||||||
|
/** Opt-in native proof confined to the new generation21 starter island in a disposable development world. */
|
||||||
|
public final class Transit21WorldGameTests {
|
||||||
|
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
|
||||||
|
|
||||||
|
@GameTest(maxTicks = 12000)
|
||||||
|
public void industrialTransitPersistsNatively(GameTestHelper helper) {
|
||||||
|
helper.assertTrue(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator,
|
||||||
|
"The industrial-transits proof requires a Sanctuary generator");
|
||||||
|
var generator = (SanctuaryChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||||
|
helper.assertTrue(generator.rootGeneration() == 21, "Use a fresh generation21 test world; do not convert old saves");
|
||||||
|
new Scenario(helper, generator, Boolean.getBoolean("sanctuary.test.expansionReload")).advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Scenario {
|
||||||
|
final GameTestHelper helper;
|
||||||
|
final ServerLevel level;
|
||||||
|
final SanctuaryChunkGenerator generator;
|
||||||
|
final TransitPlan21 plan;
|
||||||
|
final boolean reload;
|
||||||
|
final Map<String, Object> report = new LinkedHashMap<>();
|
||||||
|
final List<ChunkPos> chunks = new ArrayList<>();
|
||||||
|
final List<StructureStart> starts = new ArrayList<>();
|
||||||
|
final Map<BlockPos, BlockState> allGeometry = new HashMap<>();
|
||||||
|
JsonObject previous;
|
||||||
|
BlockPos gold;
|
||||||
|
int nextChunk;
|
||||||
|
boolean prepared;
|
||||||
|
|
||||||
|
Scenario(GameTestHelper helper, SanctuaryChunkGenerator generator, boolean reload) {
|
||||||
|
this.helper = helper; this.level = helper.getLevel(); this.generator = generator; this.reload = reload;
|
||||||
|
this.plan = generator.transitPlan21();
|
||||||
|
report.put("seed", level.getSeed()); report.put("size", generator.initialDiameter());
|
||||||
|
report.put("process_id", ProcessHandle.current().pid()); report.put("world_directory", world(level).toString());
|
||||||
|
report.put("experimental_structures", generator.experimentalStructures());
|
||||||
|
report.put("structures_enabled", level.structureManager().shouldGenerateStructures());
|
||||||
|
report.put("planning_diagnostic", plan.diagnostic());
|
||||||
|
report.put("scope", "Native starts, references, geometry, rock bearings, unopened loot and a player-sized stair walk between two surface gates. "
|
||||||
|
+ "No expansion is created and no personal save is opened. A single outer-wall gold edit in this disposable world "
|
||||||
|
+ "is saved and checked in a second JVM. Traversal remains inside the modules and certified portico approaches; underground one-block rises need real correctly oriented stairs. Only the certified natural portico approaches permit an ordinary one-block jump. No flight, ladder or external terrain shortcut is allowed. No fluid simulation is claimed.");
|
||||||
|
}
|
||||||
|
void advance() {
|
||||||
|
try {
|
||||||
|
if (!prepared) {
|
||||||
|
prepare(); prepared = true;
|
||||||
|
if (plan.isEmpty()) { finishDisabled(); return; }
|
||||||
|
}
|
||||||
|
if (nextChunk < chunks.size()) {
|
||||||
|
var pos = chunks.get(nextChunk++);
|
||||||
|
var loaded = level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true);
|
||||||
|
helper.assertTrue(loaded instanceof LevelChunk, "Every room footprint must reach native FULL: " + pos);
|
||||||
|
if (nextChunk % 10 == 0) SanctuaryMod.LOGGER.info("Industrial transit21: loaded {}/{} footprint chunks", nextChunk, chunks.size());
|
||||||
|
helper.runAtTickTime(helper.getTick() + 1, this::advance); return;
|
||||||
|
}
|
||||||
|
verifyPlaced();
|
||||||
|
report.put("passed", true);
|
||||||
|
if (!reload) {
|
||||||
|
helper.assertTrue(gold != null && level.getBlockState(gold).isCollisionShapeFullBlock(level, gold),
|
||||||
|
"Gameplay witness must replace a solid outer wall away from every doorway");
|
||||||
|
level.setBlock(gold, Blocks.GOLD_BLOCK.defaultBlockState(), 3);
|
||||||
|
report.put("gold_witness", coords(gold));
|
||||||
|
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush native structures, deferred loot and the gold edit");
|
||||||
|
report.put("world_flushed_before_shutdown", true);
|
||||||
|
} else {
|
||||||
|
helper.assertTrue(level.getBlockState(gold).is(Blocks.GOLD_BLOCK), "Reload replaced the existing wall edit");
|
||||||
|
report.put("gold_witness", coords(gold)); report.put("gold_witness_preserved", true);
|
||||||
|
}
|
||||||
|
write(level, report, reload); helper.succeed();
|
||||||
|
} catch (Exception failure) {
|
||||||
|
report.put("passed", false); report.put("failure", failure.toString());
|
||||||
|
try { write(level, report, reload); } catch (IOException ignored) { }
|
||||||
|
throw new IllegalStateException("Industrial transit21 native " + (reload ? "reload" : "creation") + " failed", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void prepare() throws IOException {
|
||||||
|
var ops = RegistryOps.create(JsonOps.INSTANCE, level.registryAccess());
|
||||||
|
var encoded = ChunkGenerator.CODEC.encodeStart(ops, generator).getOrThrow();
|
||||||
|
var decoded = ChunkGenerator.CODEC.parse(ops, encoded).getOrThrow();
|
||||||
|
helper.assertTrue(decoded instanceof SanctuaryChunkGenerator copy && copy.rootGeneration() == 21
|
||||||
|
&& copy.experimentalStructures() == generator.experimentalStructures(), "The generation21 codec must preserve its experimental switch");
|
||||||
|
report.put("generator_codec", encoded); report.put("plan_sha256", planHash(plan)); report.put("planning_samples", plan.samples());
|
||||||
|
helper.assertTrue(plan.samples() <= TransitPlan21.MAX_SAMPLES, "Transit planning exceeded its budget");
|
||||||
|
if (reload) {
|
||||||
|
previous = JsonParser.parseString(Files.readString(proof(level))).getAsJsonObject();
|
||||||
|
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("world_flushed_before_shutdown").getAsBoolean(),
|
||||||
|
"Reload requires the successful, flushed creation proof");
|
||||||
|
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload must run in a separate JVM");
|
||||||
|
helper.assertTrue(previous.get("seed").getAsLong() == level.getSeed()
|
||||||
|
&& previous.get("size").getAsInt() == generator.initialDiameter()
|
||||||
|
&& previous.get("world_directory").getAsString().equals(world(level).toString()), "Reload used another world, seed or size");
|
||||||
|
helper.assertTrue(previous.get("generator_codec").equals(encoded) && previous.get("plan_sha256").getAsString().equals(planHash(plan)),
|
||||||
|
"Saved switch or planned generation21 room identities changed on reload");
|
||||||
|
if (previous.has("gold_witness")) gold = readPos(previous.getAsJsonArray("gold_witness"));
|
||||||
|
}
|
||||||
|
boolean enabled = generator.experimentalStructures() && level.structureManager().shouldGenerateStructures();
|
||||||
|
if (!enabled) {
|
||||||
|
helper.assertTrue(plan.isEmpty(), "Experimental=false or generateStructures=false must publish no industrial transit");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
helper.assertTrue(!plan.isEmpty(), "Enabled reference island has no experimental network");
|
||||||
|
helper.assertTrue(plan.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.ACCESS).count() == 2,
|
||||||
|
"A through-route needs two independent saved surface stairs");
|
||||||
|
int spanX = plan.modules().stream().mapToInt(TransitPlan21.Module::maxX).max().orElseThrow()
|
||||||
|
- plan.modules().stream().mapToInt(TransitPlan21.Module::minX).min().orElseThrow() + 1;
|
||||||
|
int spanZ = plan.modules().stream().mapToInt(TransitPlan21.Module::maxZ).max().orElseThrow()
|
||||||
|
- plan.modules().stream().mapToInt(TransitPlan21.Module::minZ).min().orElseThrow() + 1;
|
||||||
|
report.put("span_x", spanX); report.put("span_z", spanZ);
|
||||||
|
helper.assertTrue(Math.max(spanX, spanZ) >= generator.initialDiameter() * .4,
|
||||||
|
"The native transit reference must cross at least40% of the island's nominal diameter");
|
||||||
|
report.put("modules", plan.modules().size());
|
||||||
|
report.put("halls", plan.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.HALL).count());
|
||||||
|
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(TransitStructures21.KEY);
|
||||||
|
var origins = new HashSet<Long>(); var uniqueChunks = new HashSet<ChunkPos>();
|
||||||
|
for (var network : plan.modules()) {
|
||||||
|
long origin = new ChunkPos(network.startChunkX(), network.startChunkZ()).pack();
|
||||||
|
if (!origins.add(origin)) continue;
|
||||||
|
var chunk = level.getChunk(network.startChunkX(), network.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||||
|
var start = chunk.getStartForStructure(structure.value());
|
||||||
|
helper.assertTrue(start != null && start.isValid(), "Accepted experimental network has no native start: " + network.kind() + " at " + network.centerX() + "," + network.centerZ());
|
||||||
|
starts.add(start);
|
||||||
|
for (var piece : start.getPieces()) {
|
||||||
|
helper.assertTrue(piece instanceof TransitPiece21, "Industrial transit loaded a foreign or historical piece type");
|
||||||
|
piece.getBoundingBox().intersectingChunks().forEach(at -> {
|
||||||
|
helper.assertTrue(Math.abs(at.x() - network.startChunkX()) <= 7 && Math.abs(at.z() - network.startChunkZ()) <= 7,
|
||||||
|
"Transit native start exceeds its local reference range");
|
||||||
|
uniqueChunks.add(at);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
helper.assertTrue(uniqueChunks.size() <= 256, "The bounded root-only proof unexpectedly covers more than256 chunks");
|
||||||
|
chunks.addAll(uniqueChunks.stream().sorted(java.util.Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList());
|
||||||
|
}
|
||||||
|
void finishDisabled() throws IOException {
|
||||||
|
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(TransitStructures21.KEY);
|
||||||
|
var chunk = level.getChunk(0, 0, ChunkStatus.STRUCTURE_STARTS, true);
|
||||||
|
var start = chunk.getStartForStructure(structure.value());
|
||||||
|
helper.assertTrue(start == null || !start.isValid(), "Disabled experimental generation emitted a native start");
|
||||||
|
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
|
||||||
|
helper.assertTrue(located == null, "Disabled experimental structures must not be located or prepared");
|
||||||
|
report.put("empty_plan_verified", true); report.put("passed", true);
|
||||||
|
if (!reload) {
|
||||||
|
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save the disabled experiment setting");
|
||||||
|
report.put("world_flushed_before_shutdown", true);
|
||||||
|
}
|
||||||
|
write(level, report, reload); helper.succeed();
|
||||||
|
}
|
||||||
|
void verifyPlaced() {
|
||||||
|
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(TransitStructures21.KEY);
|
||||||
|
var context = StructurePieceSerializationContext.fromLevel(level);
|
||||||
|
var sites = new ArrayList<Map<String, Object>>();
|
||||||
|
var modules = new ArrayList<Map<String, Object>>();
|
||||||
|
int blocks = 0, supports = 0, chests = 0, pieces = 0;
|
||||||
|
var bearingMasks = new HashMap<TransitPlan21.Module, Integer>();
|
||||||
|
for (var start : starts) {
|
||||||
|
var origin = start.getChunkPos(); var saved = start.createTag(context, origin);
|
||||||
|
var copy = StructureStart.loadStaticStart(context, saved, level.getSeed());
|
||||||
|
helper.assertTrue(copy != null && copy.isValid() && copy.createTag(context, origin).equals(saved), "The native start changed on an NBT roundtrip");
|
||||||
|
var expectedRooms = plan.modulesForChunk(origin.x(), origin.z());
|
||||||
|
var actualRooms = start.getPieces().stream().map(TransitPiece21.class::cast).map(TransitPiece21::module).toList();
|
||||||
|
helper.assertTrue(actualRooms.size() == expectedRooms.size() && new HashSet<>(actualRooms).equals(new HashSet<>(expectedRooms)),
|
||||||
|
"Native start did not persist every planned room exactly once");
|
||||||
|
String nativeHash = hash(saved.toString());
|
||||||
|
if (reload) {
|
||||||
|
JsonObject prior = null;
|
||||||
|
for (var value : previous.getAsJsonArray("starts")) {
|
||||||
|
var site = value.getAsJsonObject(); var at = site.getAsJsonArray("start_chunk");
|
||||||
|
if (at.get(0).getAsInt() == origin.x() && at.get(1).getAsInt() == origin.z()) prior = site;
|
||||||
|
}
|
||||||
|
helper.assertTrue(prior != null && prior.get("native_start_sha256").getAsString().equals(nativeHash),
|
||||||
|
"Reload altered native room parameters, supports or reference count");
|
||||||
|
}
|
||||||
|
var loot = new ArrayList<Map<String, Object>>();
|
||||||
|
for (var piece : start.getPieces()) {
|
||||||
|
var roomPiece = (TransitPiece21) piece; var room = roomPiece.module(); pieces++;
|
||||||
|
piece.getBoundingBox().intersectingChunks().forEach(at -> {
|
||||||
|
var full = level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true);
|
||||||
|
helper.assertTrue(full.getReferencesForStructure(structure.value()).contains(origin.pack()), "A saved FULL chunk lost the native structure reference");
|
||||||
|
});
|
||||||
|
var roomTag = roomPiece.createTag(context);
|
||||||
|
var roundtrip = new TransitPiece21(context, roomTag);
|
||||||
|
helper.assertTrue(roundtrip.module().equals(room) && roundtrip.createTag(context).equals(roomTag), "The native room failed its exact saved-field roundtrip");
|
||||||
|
var corrupt = roomTag.copy(); var payload = corrupt.getCompound("SanctuaryModule").orElseThrow(); payload.putInt("Schema", 99);
|
||||||
|
boolean rejected = false;
|
||||||
|
try { new TransitPiece21(context, corrupt); } catch (IllegalArgumentException expected) { rejected = true; }
|
||||||
|
helper.assertTrue(rejected, "An unknown generation21 piece schema must be refused");
|
||||||
|
var expected = rendered(room); blocks += expected.blocks().size();
|
||||||
|
expected.blocks().forEach((at, state) -> helper.assertTrue(allGeometry.putIfAbsent(at, state) == null,
|
||||||
|
"Two saved transit modules paint the same position: " + at));
|
||||||
|
modules.add(Map.of("kind", room.kind().name(), "min", List.of(room.minX(), room.minY(), room.minZ()),
|
||||||
|
"max", List.of(room.maxX(), room.maxY(), room.maxZ()), "floor_y", room.floorY(),
|
||||||
|
"surface_y", room.surfaceY(), "openings", room.openings(), "supports", room.supports().size(),
|
||||||
|
"start_chunk", List.of(room.startChunkX(), room.startChunkZ())));
|
||||||
|
for (var entry : expected.blocks().entrySet()) {
|
||||||
|
var at = entry.getKey(); var wanted = entry.getValue();
|
||||||
|
helper.assertTrue(TransitPiece21.bounds(room).isInside(at), "Renderer escaped its saved room/support bounds");
|
||||||
|
helper.assertTrue(plan.protects(at.getX(), at.getY(), at.getZ()) && generator.protectsCityDecoration(at),
|
||||||
|
"Transit construction/cleared air is missing its published decoration protection at " + at);
|
||||||
|
if (reload && at.equals(gold)) continue;
|
||||||
|
helper.assertTrue(level.getBlockState(at).is(wanted.getBlock()),
|
||||||
|
"Native clipping or later decoration changed " + room.kind() + " at " + at + ": expected " + wanted + " got " + level.getBlockState(at));
|
||||||
|
helper.assertTrue(!level.getBlockState(at).is(Blocks.SPAWNER), "Experimental architecture must not create a spawner");
|
||||||
|
}
|
||||||
|
supports += verifyBearings(helper, generator, room, bearingMasks);
|
||||||
|
for (var entry : expected.blocks().entrySet()) if (entry.getValue().is(Blocks.LANTERN)) {
|
||||||
|
var at = entry.getKey(); var state = level.getBlockState(at);
|
||||||
|
helper.assertTrue(state.canSurvive(level, at) && level.getBlockState(at.above()).isFaceSturdy(level, at.above(), Direction.DOWN),
|
||||||
|
"Transit lamp lost its complete ceiling bearing at " + at);
|
||||||
|
}
|
||||||
|
for (var chest : expected.chests()) {
|
||||||
|
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Deferred native chest block entity is missing");
|
||||||
|
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
|
||||||
|
helper.assertTrue(chest.table().equals(entity.getLootTable()) && entity.getLootTableSeed() == chest.seed(),
|
||||||
|
"Native chest loot must remain deferred with its exact table and seed");
|
||||||
|
loot.add(Map.of("position", coords(chest.position()), "table", chest.table().identifier().toString(), "seed", chest.seed())); chests++;
|
||||||
|
}
|
||||||
|
if (!reload && gold == null && room.kind() != TransitPlan21.Kind.ACCESS && room.kind() != TransitPlan21.Kind.JUNCTION)
|
||||||
|
gold = new BlockPos(room.minX(), room.floorY() + 1, room.minZ() + 1);
|
||||||
|
}
|
||||||
|
sites.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", nativeHash,
|
||||||
|
"pieces", start.getPieces().size(), "unopened_chests", loot));
|
||||||
|
}
|
||||||
|
report.put("module_coordinates", modules);
|
||||||
|
report.put("starts", sites);
|
||||||
|
int weakSpan = 0, longestSpan = 0;
|
||||||
|
for (var room : plan.modules()) {
|
||||||
|
weakSpan = bearingMasks.get(room) == 15 ? 0 : weakSpan + 1;
|
||||||
|
longestSpan = Math.max(longestSpan, weakSpan);
|
||||||
|
helper.assertTrue(weakSpan <= 2, "Native deck spans more than 34 blocks without distributed rock bearings");
|
||||||
|
}
|
||||||
|
report.put("longest_span_without_full_bearings", longestSpan * 17);
|
||||||
|
var ores = new java.util.TreeMap<String, Integer>();
|
||||||
|
allGeometry.values().forEach(state -> {
|
||||||
|
var id = net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString();
|
||||||
|
if (id.endsWith("_ore")) ores.merge(id, 1, Integer::sum);
|
||||||
|
});
|
||||||
|
report.put("finite_exposed_ores", ores);
|
||||||
|
var traversal = verifyAccess(helper, plan);
|
||||||
|
report.put("native_connections", NativeConnections20.verify(helper, allGeometry,
|
||||||
|
reload && gold != null ? Set.of(gold) : Set.of()));
|
||||||
|
report.put("module_coordinates", modules);
|
||||||
|
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
|
||||||
|
helper.assertTrue(located != null && located.getSecond().is(TransitStructures21.KEY), "Native locate cannot find a saved industrial transit");
|
||||||
|
helper.assertTrue(plan.modules().stream().filter(r -> r.kind() == TransitPlan21.Kind.ACCESS)
|
||||||
|
.anyMatch(r -> TransitPiece21.locator(r).equals(located.getFirst())), "Locate must lead to a surface entrance rather than an underground floor");
|
||||||
|
report.put("starts", sites); report.put("native_pieces", pieces); report.put("native_blocks_checked", blocks);
|
||||||
|
report.put("full_chunks_checked", chunks.size()); report.put("rock_supports_checked", supports);
|
||||||
|
report.put("unopened_deferred_chests", chests); report.put("native_routes", traversal); report.put("locate", coords(located.getFirst()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private record Chest(BlockPos position, ResourceKey<LootTable> table, long seed) {}
|
||||||
|
private record Rendered(Map<BlockPos, BlockState> blocks, List<Chest> chests) {}
|
||||||
|
private static Rendered rendered(TransitPlan21.Module room) {
|
||||||
|
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
|
||||||
|
TransitRenderer21.render(room, new LostCityRenderer.BlockWriter() {
|
||||||
|
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(at(room, x, y, z), state); }
|
||||||
|
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
|
||||||
|
var position = at(room, x, y, z); blocks.put(position, Blocks.CHEST.defaultBlockState()); chests.add(new Chest(position, table, RandomSource.create(seed).nextLong()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return new Rendered(blocks, chests);
|
||||||
|
}
|
||||||
|
private static int verifyBearings(GameTestHelper helper, SanctuaryChunkGenerator generator, TransitPlan21.Module room, Map<TransitPlan21.Module, Integer> masks) {
|
||||||
|
var level = helper.getLevel(); int checked = 0, quarters = 0;
|
||||||
|
for (int z = room.minZ();; z = Math.min(z + 4, room.maxZ())) {
|
||||||
|
for (int x = room.minX();; x = Math.min(x + 4, room.maxX())) {
|
||||||
|
int anchor = room.minY() - 1;
|
||||||
|
for (var support : room.supports()) if (support.x() == x && support.z() == z) anchor = support.minY() - 1;
|
||||||
|
var bearing = new BlockPos(x, anchor, z);
|
||||||
|
boolean supported = level.getBlockState(bearing).isSolid() && level.getFluidState(bearing).isEmpty()
|
||||||
|
&& level.getBlockState(bearing.below()).isSolid() && level.getFluidState(bearing.below()).isEmpty();
|
||||||
|
if (supported) {
|
||||||
|
quarters |= 1 << ((x < room.centerX() ? 0 : 1) + (z < room.centerZ() ? 0 : 2));
|
||||||
|
for (int dy = 0; dy >= -1; dy--) {
|
||||||
|
var at = new BlockPos(x, anchor + dy, z);
|
||||||
|
helper.assertTrue(level.getBlockState(at).isSolid() && level.getFluidState(at).isEmpty(),
|
||||||
|
"Transit bearing grid lost one of its two untouched rock layers at " + at);
|
||||||
|
helper.assertTrue(generator.protectsCityDecoration(at), "A certified transit rock dependency is unprotected at " + at);
|
||||||
|
checked++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (x == room.maxX()) break;
|
||||||
|
}
|
||||||
|
if (z == room.maxZ()) break;
|
||||||
|
}
|
||||||
|
helper.assertTrue(room.kind() == TransitPlan21.Kind.GALLERY || room.kind() == TransitPlan21.Kind.JUNCTION || quarters == 15, "The continuous deck lost its distributed rock bearings");
|
||||||
|
masks.put(room, quarters);
|
||||||
|
return checked;
|
||||||
|
}
|
||||||
|
private static Map<String, Object> verifyAccess(GameTestHelper helper, TransitPlan21 plan) {
|
||||||
|
var level = helper.getLevel();
|
||||||
|
var accesses = plan.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.ACCESS).toList();
|
||||||
|
helper.assertTrue(accesses.size() == 2, "Native transit requires exactly two surface gates");
|
||||||
|
Predicate<BlockPos> inside = pos -> plan.contains(pos.getX(), pos.getY(), pos.getZ())
|
||||||
|
|| accesses.stream().anyMatch(m -> pos.getX() >= m.centerX() - 2 && pos.getX() <= m.centerX() + 2
|
||||||
|
&& pos.getZ() >= m.minZ() - 3 && pos.getZ() < m.minZ()
|
||||||
|
&& pos.getY() >= m.surfaceY() && pos.getY() <= m.surfaceY() + 2);
|
||||||
|
var start = TransitPiece21.locator(accesses.getFirst());
|
||||||
|
Predicate<BlockPos> naturalApproach = pos -> accesses.stream().anyMatch(m ->
|
||||||
|
pos.getX() >= m.centerX() - 2 && pos.getX() <= m.centerX() + 2
|
||||||
|
&& pos.getZ() >= m.minZ() - 3 && pos.getZ() < m.minZ()
|
||||||
|
&& pos.getY() >= m.surfaceY() && pos.getY() <= m.surfaceY() + 2);
|
||||||
|
var reached = walk(helper, start, inside, naturalApproach); int targets = 0;
|
||||||
|
var arrivals = new ArrayList<Map<String, Object>>();
|
||||||
|
for (var module : plan.modules()) for (var target : TransitRenderer21.targets(module)) {
|
||||||
|
var point = at(module, target.x(), target.y(), target.z());
|
||||||
|
helper.assertTrue(reached.containsKey(Foot.at(point)), "Native transit cannot walk from its first portico to "
|
||||||
|
+ module.kind() + "/" + target.name() + " at " + point);
|
||||||
|
targets++;
|
||||||
|
}
|
||||||
|
for (var access : accesses) {
|
||||||
|
var gate = TransitPiece21.locator(access);
|
||||||
|
helper.assertTrue(reached.containsKey(Foot.at(gate)), "The second independent surface gate is not connected by real stairs");
|
||||||
|
int approaches = 0;
|
||||||
|
for (int x = access.centerX() - 2; x <= access.centerX() + 2; x++) for (int z = access.minZ() - 3; z < access.minZ(); z++) {
|
||||||
|
var high = new BlockPos(x, access.surfaceY() + 1, z);
|
||||||
|
var low = high.below(); var position = standable(level, high) ? high : low;
|
||||||
|
helper.assertTrue(standable(level, position) && reached.containsKey(Foot.at(position)),
|
||||||
|
"The certified natural approach no longer joins its portico at " + position);
|
||||||
|
helper.assertTrue(level.getBlockState(position.below()).isSolid()
|
||||||
|
&& level.getFluidState(position.below()).isEmpty(), "A surface arrival is suspended above air or water");
|
||||||
|
approaches++;
|
||||||
|
}
|
||||||
|
arrivals.add(Map.of("gate", coords(gate), "roadway", coords(at(access, 8, 2, 8)),
|
||||||
|
"rise", access.surfaceY() - access.floorY(), "steps_from_first_gate", reached.get(Foot.at(gate)),
|
||||||
|
"natural_approach_cells", approaches));
|
||||||
|
}
|
||||||
|
return Map.of("surface_accesses", arrivals, "walkable_targets", targets, "standing_cells", reached.size(),
|
||||||
|
"surface_to_surface_steps", reached.get(Foot.at(TransitPiece21.locator(accesses.getLast()))),
|
||||||
|
"traversal_policy", "0.6 by1.8 native collision volume, half-block sidewalk steps; underground rises require a real forward-facing stair; natural portico approaches alone permit a normal one-block jump; no vertical moves");
|
||||||
|
}
|
||||||
|
/** Half-block standing elevations retain real sidewalk slabs without pretending an integer cell can hover over them. */
|
||||||
|
private record Foot(int x, int halfY, int z) {
|
||||||
|
static Foot at(BlockPos pos) { return new Foot(pos.getX(), pos.getY() * 2, pos.getZ()); }
|
||||||
|
double y() { return halfY / 2.0; }
|
||||||
|
BlockPos block() { return new BlockPos(x, Math.floorDiv(halfY, 2), z); }
|
||||||
|
BlockPos bearing() { return new BlockPos(x, Math.floorDiv(halfY - 1, 2), z); }
|
||||||
|
Foot rise(int halves) { return new Foot(x, halfY + halves, z); }
|
||||||
|
Foot move(Direction direction, int halves) { return new Foot(x + direction.getStepX(), halfY + halves, z + direction.getStepZ()); }
|
||||||
|
}
|
||||||
|
private static Map<Foot, Integer> walk(GameTestHelper helper, BlockPos start, Predicate<BlockPos> inside, Predicate<BlockPos> naturalApproach) {
|
||||||
|
var level = helper.getLevel(); var first = Foot.at(start);
|
||||||
|
helper.assertTrue(inside.test(start) && standable(level, first), "Blocked native surface portico: " + start);
|
||||||
|
var seen = new HashMap<Foot, Integer>(); var queue = new ArrayDeque<Foot>(); seen.put(first, 0); queue.add(first);
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
var pos = queue.removeFirst();
|
||||||
|
for (var direction : HORIZONTAL) for (int dy = -2; dy <= 2; dy++) {
|
||||||
|
var next = pos.move(direction, dy);
|
||||||
|
if (!inside.test(next.block()) || seen.containsKey(next)) continue;
|
||||||
|
boolean ordinaryThreshold = naturalApproach.test(pos.block()) || naturalApproach.test(next.block());
|
||||||
|
// Native step height is enough for a half slab; a full block needs the two treads of a stair.
|
||||||
|
if (dy > 0 && (!clearBody(level, pos.rise(dy))
|
||||||
|
|| dy == 2 && !ordinaryThreshold && !ascendingStair(level, next.bearing(), direction))) continue;
|
||||||
|
if (dy < 0 && (!clearBody(level, next.rise(-dy))
|
||||||
|
|| dy == -2 && !ordinaryThreshold && !ascendingStair(level, pos.bearing(), direction.getOpposite()))) continue;
|
||||||
|
if (standable(level, next)) { seen.put(next, seen.get(pos) + 1); queue.add(next); }
|
||||||
|
}
|
||||||
|
helper.assertTrue(seen.size() <= 120_000, "Native transit traversal exceeded its bounded room volumes");
|
||||||
|
}
|
||||||
|
return seen;
|
||||||
|
}
|
||||||
|
private static boolean ascendingStair(ServerLevel level, BlockPos pos, Direction direction) {
|
||||||
|
var state = level.getBlockState(pos);
|
||||||
|
return state.getBlock() instanceof StairBlock
|
||||||
|
&& state.getValue(BlockStateProperties.HORIZONTAL_FACING) == direction
|
||||||
|
&& state.getValue(BlockStateProperties.HALF) == net.minecraft.world.level.block.state.properties.Half.BOTTOM;
|
||||||
|
}
|
||||||
|
private static boolean standable(ServerLevel level, BlockPos pos) { return standable(level, Foot.at(pos)); }
|
||||||
|
private static boolean standable(ServerLevel level, Foot pos) {
|
||||||
|
if (!clearBody(level, pos)) return false;
|
||||||
|
var below = pos.bearing(); double top = pos.y() - below.getY();
|
||||||
|
return level.getBlockState(below).getCollisionShape(level, below).toAabbs().stream()
|
||||||
|
.anyMatch(b -> Math.abs(b.maxY - top) < .0001 && b.minX <= .5 && b.maxX >= .5 && b.minZ <= .5 && b.maxZ >= .5);
|
||||||
|
}
|
||||||
|
private static boolean clearBody(ServerLevel level, Foot pos) {
|
||||||
|
var body = new AABB(pos.x() + .2, pos.y(), pos.z() + .2, pos.x() + .8, pos.y() + 1.8, pos.z() + .8);
|
||||||
|
for (int y = (int) Math.floor(pos.y()); y <= (int) Math.floor(pos.y() + 1.8 - .0001); y++) {
|
||||||
|
var at = new BlockPos(pos.x(), y, pos.z());
|
||||||
|
if (!level.getFluidState(at).isEmpty()) return false;
|
||||||
|
for (var shape : level.getBlockState(at).getCollisionShape(level, at).toAabbs())
|
||||||
|
if (shape.move(at.getX(), at.getY(), at.getZ()).intersects(body)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
private static BlockPos at(TransitPlan21.Module room, int x, int y, int z) { return new BlockPos(room.minX() + x, room.minY() + y, room.minZ() + z); }
|
||||||
|
private static List<Integer> coords(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
|
||||||
|
private static BlockPos readPos(com.google.gson.JsonArray value) { return new BlockPos(value.get(0).getAsInt(), value.get(1).getAsInt(), value.get(2).getAsInt()); }
|
||||||
|
private static Path world(ServerLevel level) { return level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize(); }
|
||||||
|
private static Path proof(ServerLevel level) { return world(level).resolve("transit21-create-proof.json"); }
|
||||||
|
private static String planHash(TransitPlan21 plan) { return hash(plan.modules().toString()); }
|
||||||
|
private static void write(ServerLevel level, Map<String, Object> report, boolean reload) throws IOException {
|
||||||
|
var path = Path.of("diagnostics", "transit21-" + (reload ? "reload" : "create") + ".json"); Files.createDirectories(path.getParent());
|
||||||
|
var value = new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n";
|
||||||
|
Files.writeString(path, value);
|
||||||
|
if (!reload) Files.writeString(proof(level), value);
|
||||||
|
}
|
||||||
|
private static String hash(String value) {
|
||||||
|
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8))); }
|
||||||
|
catch (NoSuchAlgorithmException impossible) { throw new AssertionError(impossible); }
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-3
@@ -52,7 +52,10 @@ final class UnifiedWorldSettingsDiagnostics {
|
|||||||
helper.assertTrue(historical19.overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 19
|
helper.assertTrue(historical19.overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 19
|
||||||
&& old.source().generation() == 19 && old.experimentalStructures(), "Historical19 keeps its original renderer and world settings");
|
&& old.source().generation() == 19 && old.experimentalStructures(), "Historical19 keeps its original renderer and world settings");
|
||||||
var generator = (SanctuaryChunkGenerator) standard.overworld();
|
var generator = (SanctuaryChunkGenerator) standard.overworld();
|
||||||
helper.assertTrue(generator.rootGeneration() == 20 && generator.source().generation() == 20 && generator.experimentalStructures(), "The public preset uses generation20 with its new-world option");
|
var historical20 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v20")));
|
||||||
|
helper.assertTrue(historical20.value().createWorldDimensions().overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 20
|
||||||
|
&& old.source().generation() == 20 && old.experimentalStructures(), "Historical20 keeps its original architecture");
|
||||||
|
helper.assertTrue(generator.rootGeneration() == 21 && generator.source().generation() == 21 && generator.experimentalStructures(), "The public preset uses generation21 with its new-world option");
|
||||||
helper.assertTrue(generator.initialDiameter() == 724, "The public preset defaults to Medium / 724 blocks");
|
helper.assertTrue(generator.initialDiameter() == 724, "The public preset defaults to Medium / 724 blocks");
|
||||||
for (var tag : List.of(WorldPresetTags.NORMAL, WorldPresetTags.EXTENDED)) {
|
for (var tag : List.of(WorldPresetTags.NORMAL, WorldPresetTags.EXTENDED)) {
|
||||||
var visible = presets.getOrThrow(tag).stream().map(holder -> holder.getRegisteredName())
|
var visible = presets.getOrThrow(tag).stream().map(holder -> holder.getRegisteredName())
|
||||||
@@ -62,11 +65,11 @@ final class UnifiedWorldSettingsDiagnostics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ops = RegistryOps.create(JsonOps.INSTANCE, access);
|
var ops = RegistryOps.create(JsonOps.INSTANCE, access);
|
||||||
var activeCodec = generator.rootGeneration() == 20 ? SanctuaryChunkGenerator.ARCHITECTURE20_CODEC : generator.rootGeneration() == 19 ? SanctuaryChunkGenerator.HERITAGE19_CODEC : generator.rootGeneration() == 17 ? SanctuaryChunkGenerator.RUINS17_CODEC : generator.rootGeneration() == 16 ? SanctuaryChunkGenerator.CITY16_CODEC : generator.rootGeneration() == 15 ? SanctuaryChunkGenerator.CITY15_CODEC : generator.rootGeneration() == 14 ? SanctuaryChunkGenerator.CITY_CODEC : SanctuaryChunkGenerator.CODEC;
|
var 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 serialized = activeCodec.codec().encodeStart(ops, generator).getOrThrow().getAsJsonObject();
|
var serialized = activeCodec.codec().encodeStart(ops, generator).getOrThrow().getAsJsonObject();
|
||||||
serialized.remove("experimental_structures");
|
serialized.remove("experimental_structures");
|
||||||
helper.assertTrue(activeCodec.codec().parse(ops, serialized).getOrThrow().experimentalStructures(),
|
helper.assertTrue(activeCodec.codec().parse(ops, serialized).getOrThrow().experimentalStructures(),
|
||||||
"Generation20 omitted experimental option defaults to enabled");
|
"Generation21 omitted experimental option defaults to enabled");
|
||||||
helper.assertTrue(!((SanctuaryChunkGenerator) historical16.overworld()).withExperimentalStructures(true).experimentalStructures(),
|
helper.assertTrue(!((SanctuaryChunkGenerator) historical16.overworld()).withExperimentalStructures(true).experimentalStructures(),
|
||||||
"The new-world switch cannot activate experimental generation in a historical16 generator");
|
"The new-world switch cannot activate experimental generation in a historical16 generator");
|
||||||
serialized.remove("initial_diameter");
|
serialized.remove("initial_diameter");
|
||||||
|
|||||||
+4
-4
@@ -21,7 +21,7 @@ public abstract class SanctuaryGameTestServerMixin {
|
|||||||
// generation test's deadline before its IO/workers can complete. This is test-only pacing.
|
// generation test's deadline before its IO/workers can complete. This is test-only pacing.
|
||||||
@Inject(method = "waitUntilNextTick", at = @At("RETURN"))
|
@Inject(method = "waitUntilNextTick", at = @At("RETURN"))
|
||||||
private void sanctuary$paceExpansionWorkers(CallbackInfo ci) {
|
private void sanctuary$paceExpansionWorkers(CallbackInfo ci) {
|
||||||
if (Boolean.getBoolean("sanctuary.test.heritage20") || Boolean.getBoolean("sanctuary.test.lostCity20")
|
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")
|
||||||
|| Boolean.getBoolean("sanctuary.test.heritage19") || Boolean.getBoolean("sanctuary.test.ruins17") || Boolean.getBoolean("sanctuary.test.expansion") || Boolean.getBoolean("sanctuary.test.lostCity")
|
|| 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")) {
|
|| Boolean.getBoolean("sanctuary.test.lostCity16") || Boolean.getBoolean("sanctuary.test.lostCity15") || Boolean.getBoolean("sanctuary.test.voidReuse")) {
|
||||||
long until = System.nanoTime() + 20_000_000L;
|
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;"))
|
"Lnet/minecraft/world/level/levelgen/presets/WorldPresets;FLAT_ALL_DIMENSIONS:Lnet/minecraft/resources/ResourceKey;"))
|
||||||
private static ResourceKey<WorldPreset> sanctuary$selectActualPreset() {
|
private static ResourceKey<WorldPreset> sanctuary$selectActualPreset() {
|
||||||
if (Boolean.getBoolean("sanctuary.test.unified")) {
|
if (Boolean.getBoolean("sanctuary.test.unified")) {
|
||||||
int generation = Integer.getInteger("sanctuary.test.generation", 20);
|
int generation = Integer.getInteger("sanctuary.test.generation", 21);
|
||||||
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19 && generation != 20) throw new IllegalArgumentException("Unsupported test generation");
|
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 == 20 ? "sanctuary" : "sanctuary_v" + generation));
|
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 21 ? "sanctuary" : "sanctuary_v" + generation));
|
||||||
}
|
}
|
||||||
if (Boolean.getBoolean("sanctuary.test.expansion")) return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("expansion_lab"));
|
if (Boolean.getBoolean("sanctuary.test.expansion")) return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("expansion_lab"));
|
||||||
int players = Integer.getInteger("sanctuary.test.players", 10);
|
int players = Integer.getInteger("sanctuary.test.players", 10);
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ public final class SanctuaryMod implements ModInitializer {
|
|||||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v19"), SanctuaryBiomeSource.HERITAGE19_CODEC);
|
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v19"), SanctuaryBiomeSource.HERITAGE19_CODEC);
|
||||||
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v20"), SanctuaryChunkGenerator.ARCHITECTURE20_CODEC);
|
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v20"), SanctuaryChunkGenerator.ARCHITECTURE20_CODEC);
|
||||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v20"), SanctuaryBiomeSource.ARCHITECTURE20_CODEC);
|
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v20"), SanctuaryBiomeSource.ARCHITECTURE20_CODEC);
|
||||||
|
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v21"), SanctuaryChunkGenerator.NETWORK21_CODEC);
|
||||||
|
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v21"), SanctuaryBiomeSource.NETWORK21_CODEC);
|
||||||
|
fr.koka.sanctuary.worldgen.transit.TransitStructures21.register();
|
||||||
fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.register();
|
fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.register();
|
||||||
fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.register();
|
fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.register();
|
||||||
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("population_island"), PopulationIslandDensity.CODEC);
|
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("population_island"), PopulationIslandDensity.CODEC);
|
||||||
|
|||||||
+2
-2
@@ -51,11 +51,11 @@ public final class SanctuaryWorldOptionsScreen extends Screen {
|
|||||||
contents.addChild(sizeButton);
|
contents.addChild(sizeButton);
|
||||||
diameterHint = contents.addChild(new MultiLineTextWidget(diameterHint(), font)
|
diameterHint = contents.addChild(new MultiLineTextWidget(diameterHint(), font)
|
||||||
.setMaxWidth(310).setCentered(true));
|
.setMaxWidth(310).setCentered(true));
|
||||||
if (generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20) {
|
if (generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21) {
|
||||||
contents.addChild(CycleButton.builder((Boolean enabled) -> enabled ? CommonComponents.OPTION_ON : CommonComponents.OPTION_OFF, experimental)
|
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"),
|
.withValues(true, false).create(0, 0, 280, 20, Component.translatable("sanctuary.world_options.experimental"),
|
||||||
(button, value) -> experimental = value));
|
(button, value) -> experimental = value));
|
||||||
contents.addChild(new MultiLineTextWidget(Component.translatable((generator.rootGeneration() == 19 || generator.rootGeneration() == 20) ? "sanctuary.world_options.heritage_hint" : "sanctuary.world_options.experimental_hint"), font)
|
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)
|
||||||
.setMaxWidth(310).setCentered(true));
|
.setMaxWidth(310).setCentered(true));
|
||||||
}
|
}
|
||||||
layout.addToContents(contents);
|
layout.addToContents(contents);
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
|
|||||||
fr.koka.sanctuary.worldgen.city.LostCityStructures.generate(this, registries, state, structures, chunk, templates, dimension);
|
fr.koka.sanctuary.worldgen.city.LostCityStructures.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||||
fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.generate(this, registries, state, structures, chunk, templates, dimension);
|
fr.koka.sanctuary.worldgen.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.heritage.HeritageStructures19.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||||
|
fr.koka.sanctuary.worldgen.transit.TransitStructures21.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||||
ExpansionStructures.generate(this, registries, state, structures, chunk, templates, dimension);
|
ExpansionStructures.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||||
}
|
}
|
||||||
@Override public void spawnOriginalMobs(WorldGenRegion region) {
|
@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 (!((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 (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 (!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 or 20 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 or 21 expansion");
|
||||||
if (reuseToken == null || !reuseToken.isEmpty() && (!spatialGeneration(generation) || id.equals("sanctuary") || !reuseToken.matches("[0-9a-f]{64}")))
|
if (reuseToken == null || !reuseToken.isEmpty() && (!spatialGeneration(generation) || id.equals("sanctuary") || !reuseToken.matches("[0-9a-f]{64}")))
|
||||||
throw new IllegalArgumentException("Invalid void reuse certificate token");
|
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);
|
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; }
|
public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21; }
|
||||||
public boolean origin() { return id.equals("sanctuary"); }
|
public boolean origin() { return id.equals("sanctuary"); }
|
||||||
public double referenceDiameter() { return origin() && diameter == 724 ? Math.sqrt(2) * 512 : 512; }
|
public double referenceDiameter() { return origin() && diameter == 724 ? Math.sqrt(2) * 512 : 512; }
|
||||||
public double horizontalScale() { return origin() ? 1.0 : diameter / referenceDiameter(); }
|
public double horizontalScale() { return origin() ? 1.0 : diameter / referenceDiameter(); }
|
||||||
|
|||||||
+3
-2
@@ -26,7 +26,8 @@ public abstract class UnifiedDecorationProtectionMixin {
|
|||||||
if (owner == null || destination == null || !owner.id().equals(destination.id())
|
if (owner == null || destination == null || !owner.id().equals(destination.id())
|
||||||
|| !fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
|
|| !fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
|
||||||
&& !fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.isPlacing()
|
&& !fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.isPlacing()
|
||||||
&& !fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.isPlacing() && generator.protectsCityDecoration(pos))
|
&& !fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.isPlacing()
|
||||||
|
&& !fr.koka.sanctuary.worldgen.transit.TransitStructures21.isPlacing() && generator.protectsCityDecoration(pos))
|
||||||
callback.setReturnValue(false);
|
callback.setReturnValue(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +42,7 @@ public abstract class UnifiedDecorationProtectionMixin {
|
|||||||
if (state.is(Blocks.SUGAR_CANE)) return;
|
if (state.is(Blocks.SUGAR_CANE)) return;
|
||||||
// Generation16 reserves its architectural volume before decoration. Only its own
|
// Generation16 reserves its architectural volume before decoration. Only its own
|
||||||
// native pieces may replace water there; published generations retain the old rule.
|
// 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)
|
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21)
|
||||||
&& fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
|
&& fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
|
||||||
&& generator.protectsCityDecoration(pos)) return;
|
&& generator.protectsCityDecoration(pos)) return;
|
||||||
if (generator.protectsDecoration(pos) && !region.getBlockState(pos).equals(state)) {
|
if (generator.protectsDecoration(pos) && !region.getBlockState(pos).equals(state)) {
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
|
|||||||
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
|
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
|
||||||
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 20)));
|
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 20)));
|
||||||
|
|
||||||
|
public static final MapCodec<SanctuaryBiomeSource> NETWORK21_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
|
||||||
|
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
|
||||||
|
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 21)));
|
||||||
|
|
||||||
private final List<Holder<Biome>> allBiomes;
|
private final List<Holder<Biome>> allBiomes;
|
||||||
private final PopulationIslandBiomeSource population;
|
private final PopulationIslandBiomeSource population;
|
||||||
private final boolean cities;
|
private final boolean cities;
|
||||||
@@ -83,7 +87,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
|
|||||||
cityPlans = java.util.Map.copyOf(plans);
|
cityPlans = java.util.Map.copyOf(plans);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; }
|
@Override 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 public BiomeResolver createResolver(Climate.Sampler sampler) {
|
@Override public BiomeResolver createResolver(Climate.Sampler sampler) {
|
||||||
BiomeResolver woodlands = population.createResolver(sampler);
|
BiomeResolver woodlands = population.createResolver(sampler);
|
||||||
|
|||||||
+99
-18
@@ -57,11 +57,25 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
|
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
|
||||||
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 20, experimental)));
|
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 20, experimental)));
|
||||||
|
|
||||||
|
public static final MapCodec<SanctuaryChunkGenerator> NETWORK21_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
|
||||||
|
SanctuaryBiomeSource.NETWORK21_CODEC.fieldOf("biome_source").forGetter(SanctuaryChunkGenerator::source),
|
||||||
|
DIAMETER_CODEC.optionalFieldOf("initial_diameter", 724).forGetter(SanctuaryChunkGenerator::initialDiameter),
|
||||||
|
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
|
||||||
|
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 21, experimental)));
|
||||||
|
|
||||||
private final boolean experimentalStructures;
|
private final boolean experimentalStructures;
|
||||||
private volatile fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17 = fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.EMPTY;
|
private volatile fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17 = fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.EMPTY;
|
||||||
private boolean ruinsPrepared;
|
private boolean ruinsPrepared;
|
||||||
private volatile fr.koka.sanctuary.worldgen.heritage.HeritagePlan19 heritagePlan19 = fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.EMPTY;
|
private volatile fr.koka.sanctuary.worldgen.heritage.HeritagePlan19 heritagePlan19 = fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.EMPTY;
|
||||||
private boolean heritagePrepared;
|
private boolean heritagePrepared;
|
||||||
|
private final java.util.Map<ExpansionIsland, fr.koka.sanctuary.worldgen.city.LostCityPlanner21.Result> cityPlacements21 = new java.util.concurrent.ConcurrentHashMap<>();
|
||||||
|
private volatile fr.koka.sanctuary.worldgen.transit.TransitPlan21 transitPlan21 = fr.koka.sanctuary.worldgen.transit.TransitPlan21.EMPTY;
|
||||||
|
private boolean transitPrepared;
|
||||||
|
public fr.koka.sanctuary.worldgen.transit.TransitPlan21 transitPlan21() { return transitPlan21; }
|
||||||
|
public fr.koka.sanctuary.worldgen.city.LostCityPlanner21.Result cityPlacement21(ExpansionIsland island) {
|
||||||
|
lostCityPlan(island);
|
||||||
|
return cityPlacements21.get(cityIdentity(island));
|
||||||
|
}
|
||||||
private volatile Context originContext;
|
private volatile Context originContext;
|
||||||
private final int generation;
|
private final int generation;
|
||||||
private net.minecraft.world.level.LevelHeightAccessor heightAccessor;
|
private net.minecraft.world.level.LevelHeightAccessor heightAccessor;
|
||||||
@@ -97,12 +111,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
}
|
}
|
||||||
private SanctuaryChunkGenerator(SanctuaryBiomeSource source, int initialDiameter, int generation, boolean experimental) {
|
private SanctuaryChunkGenerator(SanctuaryBiomeSource source, int initialDiameter, int generation, boolean experimental) {
|
||||||
super(source, initialDiameter, true);
|
super(source, initialDiameter, true);
|
||||||
this.experimentalStructures = (generation == 17 || generation == 19 || generation == 20) && experimental;
|
this.experimentalStructures = (generation == 17 || generation == 19 || generation == 20 || generation == 21) && experimental;
|
||||||
this.generation = generation;
|
this.generation = generation;
|
||||||
if (generation != source.generation()) throw new IllegalArgumentException("Sanctuary generator and biome source versions must agree");
|
if (generation != source.generation()) throw new IllegalArgumentException("Sanctuary generator and biome source versions must agree");
|
||||||
}
|
}
|
||||||
@Override public SanctuaryBiomeSource source() { return (SanctuaryBiomeSource) super.source(); }
|
@Override public SanctuaryBiomeSource source() { return (SanctuaryBiomeSource) super.source(); }
|
||||||
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; }
|
@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; }
|
||||||
public SanctuaryChunkGenerator withInitialDiameter(int diameter) {
|
public SanctuaryChunkGenerator withInitialDiameter(int diameter) {
|
||||||
return new SanctuaryChunkGenerator(source().copy(), diameter, generation, experimentalStructures);
|
return new SanctuaryChunkGenerator(source().copy(), diameter, generation, experimentalStructures);
|
||||||
}
|
}
|
||||||
@@ -113,8 +127,9 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
public fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17() { return ruinsPlan17; }
|
public fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17() { return ruinsPlan17; }
|
||||||
public fr.koka.sanctuary.worldgen.heritage.HeritagePlan19 heritagePlan19() { return heritagePlan19; }
|
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) {
|
public boolean intersectsExperimentalStructures(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||||
return (generation == 17 || generation == 19 || generation == 20) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
return (generation == 17 || generation == 19 || generation == 20 || generation == 21) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||||
|| (generation == 19 || generation == 20) && heritagePlan19.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);
|
||||||
}
|
}
|
||||||
public IslandCapacity capacity() {
|
public IslandCapacity capacity() {
|
||||||
return switch (initialDiameter()) {
|
return switch (initialDiameter()) {
|
||||||
@@ -151,12 +166,14 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
prepareCities(islands);
|
prepareCities(islands);
|
||||||
prepareRuins(islands);
|
prepareRuins(islands);
|
||||||
prepareHeritage(islands);
|
prepareHeritage(islands);
|
||||||
|
prepareTransit(islands);
|
||||||
}
|
}
|
||||||
@Override public void setIslands(List<ExpansionIsland> islands) {
|
@Override public void setIslands(List<ExpansionIsland> islands) {
|
||||||
// Complete and publish city geometry before workers can observe the new owner list.
|
// Complete and publish city geometry before workers can observe the new owner list.
|
||||||
prepareCities(islands);
|
prepareCities(islands);
|
||||||
prepareRuins(islands);
|
prepareRuins(islands);
|
||||||
prepareHeritage(islands);
|
prepareHeritage(islands);
|
||||||
|
prepareTransit(islands);
|
||||||
super.setIslands(islands);
|
super.setIslands(islands);
|
||||||
}
|
}
|
||||||
private void prepareCities(List<ExpansionIsland> islands) {
|
private void prepareCities(List<ExpansionIsland> islands) {
|
||||||
@@ -167,7 +184,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
source().setCityPlans(java.util.Map.of());
|
source().setCityPlans(java.util.Map.of());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20) && !cityStructuresEnabled) {
|
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21) && !cityStructuresEnabled) {
|
||||||
cityProtection16 = fr.koka.sanctuary.worldgen.city.LostCityProtection16.EMPTY;
|
cityProtection16 = fr.koka.sanctuary.worldgen.city.LostCityProtection16.EMPTY;
|
||||||
activeCityPlans = java.util.Map.of();
|
activeCityPlans = java.util.Map.of();
|
||||||
source().setCityPlans(java.util.Map.of());
|
source().setCityPlans(java.util.Map.of());
|
||||||
@@ -183,7 +200,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
}
|
}
|
||||||
cityProtection15 = protection.build();
|
cityProtection15 = protection.build();
|
||||||
}
|
}
|
||||||
if (generation == 16 || generation == 17 || generation == 19 || generation == 20) {
|
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21) {
|
||||||
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
|
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
|
||||||
for (var island : islands) {
|
for (var island : islands) {
|
||||||
protection.add(published.get(island.id()), undergroundPlan16(island));
|
protection.add(published.get(island.id()), undergroundPlan16(island));
|
||||||
@@ -198,7 +215,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
source().setCityPlans(published);
|
source().setCityPlans(published);
|
||||||
}
|
}
|
||||||
private void prepareRuins(List<ExpansionIsland> islands) {
|
private void prepareRuins(List<ExpansionIsland> islands) {
|
||||||
if ((generation != 17 && generation != 19 && generation != 20) || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return;
|
if ((generation != 17 && generation != 19 && generation != 20 && generation != 21) || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return;
|
||||||
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
||||||
var terrain = context(root);
|
var terrain = context(root);
|
||||||
var sampled = new fr.koka.sanctuary.worldgen.ruins.RuinsTerrain17(root, terrain.generator(), terrain.random(),
|
var sampled = new fr.koka.sanctuary.worldgen.ruins.RuinsTerrain17(root, terrain.generator(), terrain.random(),
|
||||||
@@ -211,9 +228,23 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
root.seed(), plan.networks().size(), plan.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000);
|
root.seed(), plan.networks().size(), plan.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000);
|
||||||
}
|
}
|
||||||
private void prepareHeritage(List<ExpansionIsland> islands) {
|
private void prepareHeritage(List<ExpansionIsland> islands) {
|
||||||
if ((generation != 19 && generation != 20) || !experimentalStructures || !cityStructuresEnabled || heritagePrepared) return;
|
if ((generation != 19 && generation != 20 && generation != 21) || !experimentalStructures || !cityStructuresEnabled || heritagePrepared) return;
|
||||||
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
||||||
var terrain = context(root);
|
var terrain = context(root);
|
||||||
|
if (generation == 21) {
|
||||||
|
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())
|
||||||
|
|| 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);
|
||||||
|
heritagePlan19 = plan;
|
||||||
|
heritagePrepared = true;
|
||||||
|
SanctuaryMod.LOGGER.info("Heritage21 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(),
|
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 -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|
||||||
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()));
|
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()));
|
||||||
@@ -226,6 +257,19 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
root.seed(), plan.sites().size(), plan.links().size(), plan.segments().size(), plan.samples(), sampled.columnsUsed(),
|
root.seed(), plan.sites().size(), plan.links().size(), plan.segments().size(), plan.samples(), sampled.columnsUsed(),
|
||||||
(System.nanoTime() - start) / 1_000_000, plan.diagnostics());
|
(System.nanoTime() - start) / 1_000_000, plan.diagnostics());
|
||||||
}
|
}
|
||||||
|
private void prepareTransit(List<ExpansionIsland> islands) {
|
||||||
|
if (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())
|
||||||
|
|| 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);
|
||||||
|
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());
|
||||||
|
}
|
||||||
private static ExpansionIsland cityIdentity(ExpansionIsland island) {
|
private static ExpansionIsland cityIdentity(ExpansionIsland island) {
|
||||||
// State and receipt do not change the terrain geometry used by a cached plan.
|
// State and receipt do not change the terrain geometry used by a cached plan.
|
||||||
var ready = island.status().equals("ready") ? island : island.ready();
|
var ready = island.status().equals("ready") ? island : island.ready();
|
||||||
@@ -236,6 +280,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
var key = cityIdentity(island);
|
var key = cityIdentity(island);
|
||||||
if (source().islands().stream().noneMatch(saved -> cityIdentity(saved).equals(key))) {
|
if (source().islands().stream().noneMatch(saved -> cityIdentity(saved).equals(key))) {
|
||||||
cityPlans.remove(key);
|
cityPlans.remove(key);
|
||||||
|
cityPlacements21.remove(key);
|
||||||
undergroundPlans.remove(key);
|
undergroundPlans.remove(key);
|
||||||
undergroundPlans16.remove(key);
|
undergroundPlans16.remove(key);
|
||||||
citySupports.remove(key);
|
citySupports.remove(key);
|
||||||
@@ -251,6 +296,39 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
return cityPlans.computeIfAbsent(cityIdentity(island), ignored -> {
|
return cityPlans.computeIfAbsent(cityIdentity(island), ignored -> {
|
||||||
long start = System.nanoTime();
|
long start = System.nanoTime();
|
||||||
var terrain = context(island);
|
var terrain = context(island);
|
||||||
|
if (generation == 21) {
|
||||||
|
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 surface = placement.surface();
|
||||||
|
cityPlacements21.put(cityIdentity(island), placement);
|
||||||
|
citySupports16.put(cityIdentity(island), placement.supports());
|
||||||
|
if (placement.reservation() != null) cityReservations16.put(cityIdentity(island), placement.reservation());
|
||||||
|
var supports = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
|
||||||
|
for (var support : placement.supports()) supports.addViaductSupport(support);
|
||||||
|
if (placement.reservation() != null) supports.addReservation(placement.reservation());
|
||||||
|
var extraDistricts = surface.districts().stream()
|
||||||
|
.filter(d -> !placement.core().surface().districts().contains(d)).toList();
|
||||||
|
supports.add(new fr.koka.sanctuary.worldgen.city.LostCityPlan(extraDistricts),
|
||||||
|
new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0));
|
||||||
|
var supportMask = supports.build();
|
||||||
|
var underground = fr.koka.sanctuary.worldgen.city.LostCityUnderground16.plan(island.seed(), placement.core().surface(),
|
||||||
|
new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Terrain() {
|
||||||
|
@Override public boolean solid(int x, int y, int z) { return sampled.solid(x, y, z); }
|
||||||
|
@Override public boolean owns(int x, int z) { return sampled.owns(x, z); }
|
||||||
|
@Override public boolean exteriorAir(int x, int y, int z, int dx, int dz) { return sampled.exteriorAir(x, y, z, dx, dz); }
|
||||||
|
@Override public boolean protectedAt(int x, int y, int z) {
|
||||||
|
return supportMask.contains(x, y, z) || sampled.protectedAt(x, y, z);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
undergroundPlans16.put(cityIdentity(island), underground);
|
||||||
|
SanctuaryMod.LOGGER.info("Lost City21 {} 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());
|
||||||
|
return surface;
|
||||||
|
}
|
||||||
if (generation == 16 || generation == 17 || generation == 19 || generation == 20) {
|
if (generation == 16 || generation == 17 || generation == 19 || generation == 20) {
|
||||||
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain16(island, terrain.generator(), terrain.random(),
|
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain16(island, terrain.generator(), terrain.random(),
|
||||||
heightAccessor, pos -> island.origin() && protectsDecoration(pos));
|
heightAccessor, pos -> island.origin() && protectsDecoration(pos));
|
||||||
@@ -322,9 +400,10 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
public boolean protectsCityDecoration(net.minecraft.core.BlockPos pos) {
|
public boolean protectsCityDecoration(net.minecraft.core.BlockPos pos) {
|
||||||
if (generation == 16 || generation == 17 || generation == 19 || generation == 20) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|
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) && ruinsPlan17.protects(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) && heritagePlan19.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 == 15) return cityProtection15.contains(pos.getX(), pos.getY(), pos.getZ());
|
if (generation == 15) return cityProtection15.contains(pos.getX(), pos.getY(), pos.getZ());
|
||||||
if (generation != 14) return false;
|
if (generation != 14) return false;
|
||||||
var island = islandAt(pos.getX(), pos.getZ());
|
var island = islandAt(pos.getX(), pos.getZ());
|
||||||
@@ -346,20 +425,20 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
return citySupports.getOrDefault(cityIdentity(island), List.of());
|
return citySupports.getOrDefault(cityIdentity(island), List.of());
|
||||||
}
|
}
|
||||||
public boolean intersectsCityConstruction16(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
public boolean intersectsCityConstruction16(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||||
return (generation == 16 || generation == 17 || generation == 19 || generation == 20) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
|
return (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
|
||||||
}
|
}
|
||||||
public fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan undergroundPlan16(ExpansionIsland island) {
|
public fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan undergroundPlan16(ExpansionIsland island) {
|
||||||
if (generation != 16 && generation != 17 && generation != 19 && generation != 20) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
|
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
|
||||||
lostCityPlan(island);
|
lostCityPlan(island);
|
||||||
return undergroundPlans16.getOrDefault(cityIdentity(island), new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0));
|
return undergroundPlans16.getOrDefault(cityIdentity(island), new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0));
|
||||||
}
|
}
|
||||||
public List<LostCityPlanner16.Support> citySupports16(ExpansionIsland island) {
|
public List<LostCityPlanner16.Support> citySupports16(ExpansionIsland island) {
|
||||||
if (generation != 16 && generation != 17 && generation != 19 && generation != 20) return List.of();
|
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21) return List.of();
|
||||||
lostCityPlan(island);
|
lostCityPlan(island);
|
||||||
return citySupports16.getOrDefault(cityIdentity(island), List.of());
|
return citySupports16.getOrDefault(cityIdentity(island), List.of());
|
||||||
}
|
}
|
||||||
public LostCityPlanner16.Fallback cityReservation16(ExpansionIsland island) {
|
public LostCityPlanner16.Fallback cityReservation16(ExpansionIsland island) {
|
||||||
if (generation != 16 && generation != 17 && generation != 19 && generation != 20) return null;
|
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21) return null;
|
||||||
lostCityPlan(island);
|
lostCityPlan(island);
|
||||||
return cityReservations16.get(cityIdentity(island));
|
return cityReservations16.get(cityIdentity(island));
|
||||||
}
|
}
|
||||||
@@ -371,10 +450,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
net.minecraft.core.BlockPos origin, int radius, boolean skipReferenced) {
|
net.minecraft.core.BlockPos origin, int radius, boolean skipReferenced) {
|
||||||
if (structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.city.LostCityStructures.KEY))
|
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);
|
return fr.koka.sanctuary.worldgen.city.LostCityStructures.findNearest(level, structures, origin, radius, skipReferenced);
|
||||||
if ((generation == 17 || generation == 19 || generation == 20) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.KEY))
|
if ((generation == 17 || generation == 19 || generation == 20 || generation == 21) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.KEY))
|
||||||
return fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.findNearest(level, structures, origin, radius, skipReferenced);
|
return fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.findNearest(level, structures, origin, radius, skipReferenced);
|
||||||
if ((generation == 19 || generation == 20) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.KEY))
|
if ((generation == 19 || generation == 20 || generation == 21) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.KEY))
|
||||||
return fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.findNearest(level, structures, origin, radius, skipReferenced);
|
return 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))
|
||||||
|
return fr.koka.sanctuary.worldgen.transit.TransitStructures21.findNearest(level, structures, origin, radius, skipReferenced);
|
||||||
return super.findNearestMapStructure(level, structures, origin, radius, skipReferenced);
|
return super.findNearestMapStructure(level, structures, origin, radius, skipReferenced);
|
||||||
}
|
}
|
||||||
@Override protected Context context(ExpansionIsland island) {
|
@Override protected Context context(ExpansionIsland island) {
|
||||||
@@ -402,7 +483,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
@Override protected void afterTerrain(ExpansionIsland island, ChunkAccess chunk) {
|
@Override protected void afterTerrain(ExpansionIsland island, ChunkAccess chunk) {
|
||||||
// Root water/lava are already applied by the Population hook on the named delegate.
|
// Root water/lava are already applied by the Population hook on the named delegate.
|
||||||
if (!island.origin()) super.afterTerrain(island, chunk);
|
if (!island.origin()) super.afterTerrain(island, chunk);
|
||||||
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20) && cityStructuresEnabled) {
|
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21) && cityStructuresEnabled) {
|
||||||
var reserved = cityReservations16.get(cityIdentity(island));
|
var reserved = cityReservations16.get(cityIdentity(island));
|
||||||
if (reserved != null) {
|
if (reserved != null) {
|
||||||
var mutable = new net.minecraft.core.BlockPos.MutableBlockPos();
|
var mutable = new net.minecraft.core.BlockPos.MutableBlockPos();
|
||||||
|
|||||||
+767
@@ -0,0 +1,767 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.city;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Kind;
|
||||||
|
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Parcel;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
|
||||||
|
import java.util.List;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.Rotation;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.block.state.properties.BedPart;
|
||||||
|
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||||
|
import net.minecraft.world.level.block.state.properties.DoorHingeSide;
|
||||||
|
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
|
||||||
|
import net.minecraft.world.level.block.state.properties.SlabType;
|
||||||
|
import net.minecraft.world.level.storage.loot.BuiltInLootTables;
|
||||||
|
import net.minecraft.world.level.storage.loot.LootTable;
|
||||||
|
|
||||||
|
/** Complete generation-21 buildings. Published renderers deliberately never call this renderer.
|
||||||
|
* A north-facing plan is rotated once; all positions remain relative to the saved parcel. */
|
||||||
|
public final class LostCityInteriors21 {
|
||||||
|
private static final BlockState AIR = Blocks.AIR.defaultBlockState();
|
||||||
|
private static final BlockState STONE = Blocks.STONE.defaultBlockState();
|
||||||
|
private static final BlockState SMOOTH = Blocks.SMOOTH_STONE.defaultBlockState();
|
||||||
|
private static final BlockState WOOD = Blocks.OAK_PLANKS.defaultBlockState();
|
||||||
|
private static final BlockState TILE = Blocks.POLISHED_ANDESITE.defaultBlockState();
|
||||||
|
private static final int STOREY = 5;
|
||||||
|
|
||||||
|
private LostCityInteriors21() {}
|
||||||
|
|
||||||
|
/** Reachable standing positions, in the same rotated local coordinates as BlockWriter. */
|
||||||
|
public record Room(String name, int x, int y, int z) {}
|
||||||
|
|
||||||
|
/** Local, already rotated enclosure including the floor, full ceiling, four walls and door. */
|
||||||
|
public record Bathroom(String name, int minX, int minY, int minZ, int maxX, int maxY, int maxZ,
|
||||||
|
int doorX, int doorY, int doorZ) {}
|
||||||
|
|
||||||
|
private record Area(int left, int front, int right, int back) {}
|
||||||
|
|
||||||
|
/** Adjacent storeys always change plan; the saved parcel seed chooses which plan comes first. */
|
||||||
|
public static int layout(Parcel parcel, int floor) {
|
||||||
|
validate(parcel);
|
||||||
|
if (floor < 0 || floor >= parcel.floors()) throw new IllegalArgumentException("Invalid interior floor");
|
||||||
|
return (int) ((parcel.seed() ^ (parcel.seed() >>> 32)) & 1L) ^ (floor & 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Area rear(Parcel p, int floor, boolean east) {
|
||||||
|
boolean wide = layout(p, floor) == 1;
|
||||||
|
return east ? new Area(wide ? 10 : 11, wide ? 10 : 11, 14, 14)
|
||||||
|
: new Area(2, wide ? 10 : 11, wide ? 6 : 5, 14);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int frontWall(Parcel p, int floor) { return layout(p, floor) == 0 ? 7 : 8; }
|
||||||
|
|
||||||
|
private static String frontUse(Parcel p, int floor) {
|
||||||
|
int variant = layout(p, floor);
|
||||||
|
return switch (p.kind()) {
|
||||||
|
case APARTMENT -> variant == 0 ? "kitchen_dining" : "studio_bedroom";
|
||||||
|
case HOTEL -> floor == 0 ? "reception" : "guest_suite";
|
||||||
|
case LIBRARY -> variant == 0 ? "book_stacks" : "periodicals_reading";
|
||||||
|
case RESTAURANT -> variant == 0 ? "dining_room" : "kitchen";
|
||||||
|
case BARRACKS -> variant == 0 ? "dormitory" : "duty_office";
|
||||||
|
case HALL -> floor == 0 ? "ticket_office" : "administration";
|
||||||
|
default -> throw new IllegalArgumentException("Not an interior kind");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String rearUse(Parcel p, int floor, boolean east) {
|
||||||
|
int variant = layout(p, floor);
|
||||||
|
return switch (p.kind()) {
|
||||||
|
case APARTMENT -> east ? "bathroom" : variant == 0 ? "bedroom" : "kitchen_dining";
|
||||||
|
case HOTEL -> floor == 0 ? east ? "laundry" : "lounge"
|
||||||
|
: variant == 0 ? east ? "guest_room" : "bedroom" : east ? "bathroom" : "guest_lounge";
|
||||||
|
case LIBRARY -> variant == 0 ? east ? "archive" : "reading_room" : east ? "bathroom" : "catalogue_office";
|
||||||
|
case RESTAURANT -> variant == 0 ? east ? "kitchen" : "washrooms" : east ? "pantry" : "dining_room";
|
||||||
|
case BARRACKS -> east ? "equipment_store" : variant == 0 ? "washrooms" : "officer_bedroom";
|
||||||
|
case HALL -> variant == 0 ? east ? "bathroom" : "meeting_room" : east ? "meeting_room" : "archive";
|
||||||
|
default -> throw new IllegalArgumentException("Not an interior kind");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void render(Parcel parcel, LostCityRenderer.BlockWriter output) {
|
||||||
|
validate(parcel);
|
||||||
|
var connected = ConnectedBlocks20.buffer(output);
|
||||||
|
new Building(parcel, connected).render();
|
||||||
|
connected.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** These describe actual rooms and services, not decorative labels placed in the world. */
|
||||||
|
public static List<Room> rooms(Parcel parcel) {
|
||||||
|
validate(parcel);
|
||||||
|
List<Room> result = new ArrayList<>();
|
||||||
|
add(result, parcel, "street_entrance", 8, 1, 0);
|
||||||
|
add(result, parcel, "vestibule", 8, 1, 3);
|
||||||
|
for (int floor = 0; floor < parcel.floors(); floor++) {
|
||||||
|
int y = floor * STOREY + 1;
|
||||||
|
String prefix = "floor_" + floor + "/";
|
||||||
|
add(result, parcel, prefix + "corridor", 8, y, 8);
|
||||||
|
add(result, parcel, prefix + "stair_landing", 4, y, 8);
|
||||||
|
add(result, parcel, prefix + frontUse(parcel, floor), 11, y, 6);
|
||||||
|
if (parcel.kind() == Kind.HALL && floor < 2) {
|
||||||
|
if (floor == 0) {
|
||||||
|
add(result, parcel, prefix + "auditorium", 8, y, 11);
|
||||||
|
add(result, parcel, prefix + "stage", 9, y + 1, 13);
|
||||||
|
} else add(result, parcel, prefix + "gallery", 8, y, 10);
|
||||||
|
} else {
|
||||||
|
Area west = rear(parcel, floor, false), east = rear(parcel, floor, true);
|
||||||
|
add(result, parcel, prefix + rearUse(parcel, floor, false), west.right(), y, 11);
|
||||||
|
add(result, parcel, prefix + rearUse(parcel, floor, true), east.left(), y, 11);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var bath : bathroomPlans(parcel)) {
|
||||||
|
int targetZ = bath.southDoor() ? bath.z1() - 1 : bath.z0() + 1;
|
||||||
|
add(result, parcel, bath.name(), bath.doorX(), bath.floor() * STOREY + 1, targetZ);
|
||||||
|
}
|
||||||
|
add(result, parcel, "roof", 8, parcel.floors() * STOREY + 1, 8);
|
||||||
|
switch (parcel.kind()) {
|
||||||
|
case HOTEL -> add(result, parcel, "roof_laundry_pavilion", 10, parcel.floors() * STOREY + 1, 5);
|
||||||
|
case APARTMENT -> add(result, parcel, "roof_residents_terrace", 8, parcel.floors() * STOREY + 1, 10);
|
||||||
|
case LIBRARY -> add(result, parcel, "roof_reading_terrace", 11, parcel.floors() * STOREY + 1, 10);
|
||||||
|
case RESTAURANT -> add(result, parcel, "roof_dining_terrace", 11, parcel.floors() * STOREY + 1, 8);
|
||||||
|
case BARRACKS -> add(result, parcel, "roof_service_stair", 4, parcel.floors() * STOREY + 1, 8);
|
||||||
|
default -> { }
|
||||||
|
}
|
||||||
|
return List.copyOf(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record BathPlan(String name, int floor, int x0, int z0, int x1, int z1, int doorX, boolean southDoor) {}
|
||||||
|
|
||||||
|
private static List<BathPlan> bathroomPlans(Parcel p) {
|
||||||
|
List<BathPlan> result = new ArrayList<>();
|
||||||
|
for (int floor = 0; floor < p.floors(); floor++) {
|
||||||
|
if (p.kind() == Kind.HOTEL && floor > 0)
|
||||||
|
result.add(new BathPlan("floor_" + floor + "/suite_bathroom", floor, 10, 1, 15, 4, 12, true));
|
||||||
|
if (p.kind() == Kind.HALL && floor < 2) continue;
|
||||||
|
for (boolean east : new boolean[]{false, true}) {
|
||||||
|
String use = rearUse(p, floor, east);
|
||||||
|
if (!use.equals("bathroom") && !use.equals("washrooms")) continue;
|
||||||
|
Area room = rear(p, floor, east);
|
||||||
|
result.add(new BathPlan("floor_" + floor + "/" + (east ? "east" : "west") + "_bathroom_interior",
|
||||||
|
floor, room.left() - 1, 12, room.right() + 1, 15, east ? 12 : 4, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Bathroom> bathrooms(Parcel p) {
|
||||||
|
validate(p);
|
||||||
|
List<Bathroom> result = new ArrayList<>();
|
||||||
|
for (var b : bathroomPlans(p)) {
|
||||||
|
int x0 = rx(b.x0(), b.z0(), p.facing()), z0 = rz(b.x0(), b.z0(), p.facing());
|
||||||
|
int x1 = rx(b.x1(), b.z1(), p.facing()), z1 = rz(b.x1(), b.z1(), p.facing());
|
||||||
|
int doorZ = b.southDoor() ? b.z1() : b.z0();
|
||||||
|
result.add(new Bathroom(b.name(), Math.min(x0, x1), b.floor() * STOREY, Math.min(z0, z1),
|
||||||
|
Math.max(x0, x1), b.floor() * STOREY + 4, Math.max(z0, z1), rx(b.doorX(), doorZ, p.facing()),
|
||||||
|
b.floor() * STOREY + 1, rz(b.doorX(), doorZ, p.facing())));
|
||||||
|
}
|
||||||
|
return List.copyOf(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validate(Parcel p) {
|
||||||
|
if (!p.kind().building() || p.width() != 17 || p.depth() != 17 || p.floors() < 2)
|
||||||
|
throw new IllegalArgumentException("Generation-21 interiors require a 17x17 building with 2..5 floors");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void add(List<Room> result, Parcel p, String name, int x, int y, int z) {
|
||||||
|
result.add(new Room(name, rx(x, z, p.facing()), y, rz(x, z, p.facing())));
|
||||||
|
}
|
||||||
|
private static int rx(int x, int z, int facing) {
|
||||||
|
return switch (facing) { case 0 -> x; case 1 -> 16 - z; case 2 -> 16 - x; default -> z; };
|
||||||
|
}
|
||||||
|
private static int rz(int x, int z, int facing) {
|
||||||
|
return switch (facing) { case 0 -> z; case 1 -> x; case 2 -> 16 - z; default -> 16 - x; };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Building {
|
||||||
|
final Parcel p;
|
||||||
|
final LostCityRenderer.BlockWriter output;
|
||||||
|
final Rotation rotation;
|
||||||
|
Building(Parcel p, LostCityRenderer.BlockWriter output) {
|
||||||
|
this.p = p;
|
||||||
|
this.output = output;
|
||||||
|
rotation = switch (p.facing()) {
|
||||||
|
case 0 -> Rotation.NONE; case 1 -> Rotation.CLOCKWISE_90;
|
||||||
|
case 2 -> Rotation.CLOCKWISE_180; default -> Rotation.COUNTERCLOCKWISE_90;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
void set(int x, int y, int z, BlockState state) {
|
||||||
|
output.set(rx(x, z, p.facing()), y, rz(x, z, p.facing()), state.rotate(rotation));
|
||||||
|
}
|
||||||
|
void set(int x, int y, int z, Block block) { set(x, y, z, block.defaultBlockState()); }
|
||||||
|
void box(int x0, int y0, int z0, int x1, int y1, int z1, BlockState state) {
|
||||||
|
for (int x = x0; x <= x1; x++) for (int z = z0; z <= z1; z++)
|
||||||
|
for (int y = y0; y <= y1; y++) set(x, y, z, state);
|
||||||
|
}
|
||||||
|
BlockState facing(Block block, Direction direction) {
|
||||||
|
return block.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, direction);
|
||||||
|
}
|
||||||
|
BlockState brick(int x, int y, int z) { return LostCityPalette.masonry(p.seed(), x, y, z); }
|
||||||
|
BlockState weatheredBrick(int x, int y, int z) {
|
||||||
|
// Weathering follows a few masonry joints, never private room partitions.
|
||||||
|
int weather = roll(x, y, z, 12);
|
||||||
|
return (weather == 0 ? Blocks.MOSSY_STONE_BRICKS : weather < 3
|
||||||
|
? Blocks.CRACKED_STONE_BRICKS : Blocks.STONE_BRICKS).defaultBlockState();
|
||||||
|
}
|
||||||
|
int roll(int x, int y, int z, int bound) { return LostCityPalette.roll(p.seed(), x, y, z, bound); }
|
||||||
|
|
||||||
|
void render() {
|
||||||
|
int roof = p.floors() * STOREY;
|
||||||
|
box(0, -p.foundationDepth(), 0, 16, -1, 16, Blocks.STONE_BRICKS.defaultBlockState());
|
||||||
|
box(0, 0, 0, 16, 0, 16, SMOOTH);
|
||||||
|
box(0, 1, 0, 16, roof + 4, 16, AIR);
|
||||||
|
shell(roof);
|
||||||
|
architecturalEnvelope(roof);
|
||||||
|
for (int floor = 0; floor < p.floors(); floor++) {
|
||||||
|
partitions(floor);
|
||||||
|
furnishings(floor);
|
||||||
|
}
|
||||||
|
stairs();
|
||||||
|
entrance();
|
||||||
|
roof(roof);
|
||||||
|
// Only one deferred loot table per building; empty furniture is not a resource vault.
|
||||||
|
int lootX = 6;
|
||||||
|
int lootZ = 6;
|
||||||
|
box(lootX, 2, lootZ, lootX, 3, lootZ, AIR);
|
||||||
|
ResourceKey<LootTable> table = switch (p.kind()) {
|
||||||
|
case LIBRARY, HALL -> BuiltInLootTables.VILLAGE_CARTOGRAPHER;
|
||||||
|
case RESTAURANT -> BuiltInLootTables.VILLAGE_BUTCHER;
|
||||||
|
case BARRACKS -> BuiltInLootTables.VILLAGE_FLETCHER;
|
||||||
|
default -> BuiltInLootTables.VILLAGE_PLAINS_HOUSE;
|
||||||
|
};
|
||||||
|
output.chest(rx(lootX, lootZ, p.facing()), 1, rz(lootX, lootZ, p.facing()), table,
|
||||||
|
p.seed() ^ 0x21c17a7eL ^ p.kind().ordinal());
|
||||||
|
}
|
||||||
|
|
||||||
|
void shell(int roof) {
|
||||||
|
for (int floor = 0; floor <= p.floors(); floor++) {
|
||||||
|
int base = floor * STOREY;
|
||||||
|
for (int x = 1; x <= 15; x++) for (int z = 1; z <= 15; z++)
|
||||||
|
set(x, base, z, floor == p.floors() ? SMOOTH : floorMaterial(floor, x, z));
|
||||||
|
if (floor == p.floors()) continue;
|
||||||
|
// A pale ceiling and contrasting structural floor are separate layers.
|
||||||
|
box(2, base + 4, 2, 14, base + 4, 14,
|
||||||
|
Blocks.SMOOTH_STONE_SLAB.defaultBlockState().setValue(BlockStateProperties.SLAB_TYPE, SlabType.TOP));
|
||||||
|
for (int y = 1; y <= 4; y++) for (int horizontal = 1; horizontal <= 15; horizontal++) {
|
||||||
|
facade(horizontal, base + y, 1, horizontal, y, true);
|
||||||
|
facade(horizontal, base + y, 15, horizontal, y, true);
|
||||||
|
facade(1, base + y, horizontal, horizontal, y, false);
|
||||||
|
facade(15, base + y, horizontal, horizontal, y, false);
|
||||||
|
}
|
||||||
|
// Floor bands remain intact even where windows have fallen out.
|
||||||
|
for (int n = 1; n <= 15; n++) {
|
||||||
|
set(n, base, 1, SMOOTH); set(n, base, 15, SMOOTH);
|
||||||
|
set(1, base, n, SMOOTH); set(15, base, n, SMOOTH);
|
||||||
|
}
|
||||||
|
if (p.kind() == Kind.HOTEL || p.kind() == Kind.LIBRARY) {
|
||||||
|
for (int x : new int[]{3, 13}) for (int y = 0; y <= 4; y++) set(x, base + y, 0, SMOOTH);
|
||||||
|
}
|
||||||
|
if (p.kind() == Kind.APARTMENT) for (int x = 11; x <= 14; x++)
|
||||||
|
set(x, base + 1, 0, Blocks.SMOOTH_STONE_SLAB);
|
||||||
|
if (p.kind() == Kind.RESTAURANT) box(10, base + 4, 0, 14, base + 4, 0,
|
||||||
|
Blocks.OAK_SLAB.defaultBlockState());
|
||||||
|
}
|
||||||
|
if (p.kind() == Kind.HALL) {
|
||||||
|
// The auditorium spans two storeys; its upper level is a guarded gallery.
|
||||||
|
box(3, 4, 11, 13, 5, 14, AIR);
|
||||||
|
for (int x = 3; x <= 13; x++) set(x, 6, 11, rail(true));
|
||||||
|
for (int z = 12; z <= 14; z++) { set(3, 6, z, rail(false)); set(13, 6, z, rail(false)); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockState floorMaterial(int floor, int x, int z) {
|
||||||
|
if (x == 1 || z == 1 || x == 15 || z == 15) return SMOOTH;
|
||||||
|
return switch (p.kind()) {
|
||||||
|
case LIBRARY -> (x <= 5 || z >= 10 ? Blocks.OAK_PLANKS : Blocks.SMOOTH_STONE).defaultBlockState();
|
||||||
|
case APARTMENT, HOTEL -> WOOD;
|
||||||
|
case RESTAURANT -> ((x / 3 + z / 3) & 1) == 0 ? SMOOTH : TILE;
|
||||||
|
case BARRACKS -> x <= 5 ? TILE : WOOD;
|
||||||
|
default -> x == 7 || x == 9 ? TILE : SMOOTH;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockState pier() {
|
||||||
|
return switch (p.kind()) {
|
||||||
|
case APARTMENT, LIBRARY -> Blocks.STRIPPED_OAK_LOG.defaultBlockState();
|
||||||
|
case HOTEL, RESTAURANT -> SMOOTH;
|
||||||
|
case BARRACKS -> Blocks.STONE_BRICKS.defaultBlockState();
|
||||||
|
default -> Blocks.CHISELED_STONE_BRICKS.defaultBlockState();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void facade(int x, int y, int z, int horizontal, int withinFloor, boolean eastWest) {
|
||||||
|
boolean column = horizontal == 1 || horizontal == 6 || horizontal == 10 || horizontal == 15;
|
||||||
|
boolean window = !column && (withinFloor == 2 || withinFloor == 3
|
||||||
|
|| withinFloor == 1 && (p.kind() == Kind.LIBRARY || p.kind() == Kind.RESTAURANT || p.kind() == Kind.HALL));
|
||||||
|
if (column) set(x, y, z, pier());
|
||||||
|
else if (window) {
|
||||||
|
if (roll(x, y / 5, z, 17) != 0) set(x, y, z,
|
||||||
|
p.kind() == Kind.BARRACKS ? rail(eastWest) : pane(eastWest));
|
||||||
|
} else set(x, y, z, withinFloor == 4 && horizontal % 5 == 0
|
||||||
|
? weatheredBrick(x, y, z) : SMOOTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Volume belongs to its use: projected bays, clipped corners and distinct roof profiles. */
|
||||||
|
void architecturalEnvelope(int roof) {
|
||||||
|
for (int floor = 0; floor < p.floors(); floor++) {
|
||||||
|
int y = floor * STOREY;
|
||||||
|
// Project the floor plates as a continuous, one-block cornice inside the parcel.
|
||||||
|
for (int n = 1; n <= 15; n++) {
|
||||||
|
set(n, y, 0, stairTrim(Direction.NORTH)); set(n, y, 16, stairTrim(Direction.SOUTH));
|
||||||
|
set(0, y, n, stairTrim(Direction.WEST)); set(16, y, n, stairTrim(Direction.EAST));
|
||||||
|
}
|
||||||
|
switch (p.kind()) {
|
||||||
|
case APARTMENT -> {
|
||||||
|
// Bay windows increase the living room rather than drawing flat stripes on it.
|
||||||
|
for (int z : new int[]{4, 5, 6, 11, 12, 13}) {
|
||||||
|
set(16, y, z, Blocks.OAK_PLANKS);
|
||||||
|
set(16, y + 1, z, Blocks.OAK_PLANKS);
|
||||||
|
set(16, y + 2, z, Blocks.GLASS_PANE);
|
||||||
|
set(16, y + 3, z, Blocks.GLASS_PANE);
|
||||||
|
set(16, y + 4, z, Blocks.OAK_SLAB.defaultBlockState().setValue(BlockStateProperties.SLAB_TYPE, SlabType.TOP));
|
||||||
|
}
|
||||||
|
for (int z : new int[]{3, 7, 10, 14}) box(16, y + 1, z, 16, y + 4, z, pier());
|
||||||
|
// Original wall remains the sill, with an open bay above it; privacy walls restore themselves later.
|
||||||
|
for (int z : new int[]{4, 5, 6, 11, 12, 13}) box(15, y + 2, z, 15, y + 3, z, AIR);
|
||||||
|
bevelFront(y);
|
||||||
|
}
|
||||||
|
case HOTEL -> {
|
||||||
|
for (int x : new int[]{2, 5, 11, 14}) box(x, y + 1, 0, x, y + 4, 0, pier());
|
||||||
|
for (int x : new int[]{3, 4, 12, 13}) set(x, y + 1, 0, Blocks.OAK_FENCE);
|
||||||
|
box(2, y + 4, 0, 5, y + 4, 0, Blocks.OAK_PLANKS.defaultBlockState());
|
||||||
|
box(11, y + 4, 0, 14, y + 4, 0, Blocks.OAK_PLANKS.defaultBlockState());
|
||||||
|
}
|
||||||
|
case LIBRARY -> {
|
||||||
|
// Tall fins frame broad glazing and project beyond the reading rooms.
|
||||||
|
for (int x : new int[]{2, 5, 11, 14}) box(x, y + 1, 0, x, y + 4, 0, Blocks.STRIPPED_OAK_LOG.defaultBlockState());
|
||||||
|
for (int x : new int[]{3, 4, 12, 13}) {
|
||||||
|
set(x, y + 4, 0, Blocks.OAK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
|
||||||
|
}
|
||||||
|
bevelFront(y);
|
||||||
|
}
|
||||||
|
case RESTAURANT -> {
|
||||||
|
// Separate fascia and timber valance shelter the shop windows.
|
||||||
|
for (int x = 10; x <= 14; x++) {
|
||||||
|
set(x, y + 3, 0, Blocks.OAK_SLAB);
|
||||||
|
set(x, y + 4, 0, Blocks.OAK_PLANKS);
|
||||||
|
}
|
||||||
|
for (int z : new int[]{3, 7, 11, 14}) set(16, y + 4, z, Blocks.ANDESITE);
|
||||||
|
}
|
||||||
|
case BARRACKS -> {
|
||||||
|
for (int z : new int[]{2, 7, 12, 15}) box(0, y + 1, z, 0, y + 4, z, pier());
|
||||||
|
for (int x : new int[]{2, 5, 11, 14}) box(x, y + 1, 0, x, y + 4, 0, Blocks.STONE_BRICKS.defaultBlockState());
|
||||||
|
for (int x : new int[]{3, 4, 12, 13}) set(x, y + 1, 0, Blocks.CHISELED_STONE_BRICKS);
|
||||||
|
}
|
||||||
|
case HALL -> {
|
||||||
|
for (int x : new int[]{2, 5, 11, 14}) box(x, y + 1, 0, x, y + 4, 0, pier());
|
||||||
|
for (int x = 2; x <= 14; x++) set(x, y + 4, 0,
|
||||||
|
Blocks.ANDESITE_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
|
||||||
|
}
|
||||||
|
default -> throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockState stairTrim(Direction outward) {
|
||||||
|
Block block = Blocks.STONE_BRICK_STAIRS;
|
||||||
|
return facing(block, outward.getOpposite()).setValue(BlockStateProperties.HALF,
|
||||||
|
net.minecraft.world.level.block.state.properties.Half.TOP);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bevelFront(int y) {
|
||||||
|
// Remove two rectangular corners and close them with actual stepped returns.
|
||||||
|
for (int x : new int[]{1, 15}) {
|
||||||
|
box(x, y, 1, x, y + 4, 1, AIR);
|
||||||
|
int adjacent = x == 1 ? 2 : 14;
|
||||||
|
box(adjacent, y + 1, 1, adjacent, y + 4, 1, pier());
|
||||||
|
box(x, y + 1, 2, x, y + 4, 2, pier());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void partitions(int floor) {
|
||||||
|
int y = floor * STOREY;
|
||||||
|
int front = frontWall(p, floor);
|
||||||
|
// The west stairwell, central corridor and vestibule are independent of furniture.
|
||||||
|
box(5, y + 1, 2, 5, y + 4, 9, SMOOTH);
|
||||||
|
door(5, y + 1, 8, Direction.WEST, false);
|
||||||
|
box(10, y + 1, 2, 10, y + 4, front, SMOOTH);
|
||||||
|
box(10, y + 1, front, 15, y + 4, front, SMOOTH);
|
||||||
|
door(10, y + 1, 6, Direction.EAST, false);
|
||||||
|
if (floor == 0) {
|
||||||
|
box(6, 1, 4, 9, 4, 4, SMOOTH);
|
||||||
|
door(8, 1, 4, Direction.SOUTH, false);
|
||||||
|
}
|
||||||
|
if (p.kind() == Kind.HALL && floor < 2) {
|
||||||
|
if (floor == 0) {
|
||||||
|
box(2, 1, 10, 14, 4, 10, SMOOTH);
|
||||||
|
door(7, 1, 10, Direction.SOUTH, true);
|
||||||
|
door(8, 1, 10, Direction.SOUTH, false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Area west = rear(p, floor, false), east = rear(p, floor, true);
|
||||||
|
box(1, y + 1, west.front() - 1, west.right() + 1, y + 4, west.front() - 1, SMOOTH);
|
||||||
|
box(west.right() + 1, y + 1, west.front() - 1, west.right() + 1, y + 4, 15, SMOOTH);
|
||||||
|
box(east.left() - 1, y + 1, east.front() - 1, 15, y + 4, east.front() - 1, SMOOTH);
|
||||||
|
box(east.left() - 1, y + 1, east.front() - 1, east.left() - 1, y + 4, 15, SMOOTH);
|
||||||
|
door(west.right() + 1, y + 1, 11, Direction.WEST, false);
|
||||||
|
door(east.left() - 1, y + 1, 11, Direction.EAST, false);
|
||||||
|
}
|
||||||
|
// A hanging lantern touches a full ceiling block, never the recessed underside of a top slab.
|
||||||
|
set(8, y + 4, 8, SMOOTH);
|
||||||
|
set(8, y + 3, 8, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
|
||||||
|
// Weathering stays on facades and roofs: it must not replace a privacy partition.
|
||||||
|
}
|
||||||
|
|
||||||
|
void furnishings(int floor) {
|
||||||
|
int y = floor * STOREY;
|
||||||
|
int variant = layout(p, floor);
|
||||||
|
switch (p.kind()) {
|
||||||
|
case APARTMENT -> {
|
||||||
|
if (variant == 0) { kitchenFront(y); bedroom(floor, false); }
|
||||||
|
else { frontBedroom(floor); kitchenRear(floor, false); }
|
||||||
|
}
|
||||||
|
case HOTEL -> {
|
||||||
|
if (floor == 0) { reception(y); lounge(floor, false); laundry(floor); }
|
||||||
|
else {
|
||||||
|
anchoredBed(14, y + 1, frontWall(p, floor) - 2, Direction.SOUTH);
|
||||||
|
set(11, y + 1, 5, Blocks.BARREL); set(11, y + 2, 5, Blocks.FLOWER_POT);
|
||||||
|
if (variant == 0) { bedroom(floor, false); bedroom(floor, true); }
|
||||||
|
else lounge(floor, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case LIBRARY -> {
|
||||||
|
if (variant == 0) {
|
||||||
|
box(14, y + 1, 2, 14, y + 3, 6, Blocks.BOOKSHELF.defaultBlockState());
|
||||||
|
box(12, y + 1, 2, 12, y + 2, 3, Blocks.BOOKSHELF.defaultBlockState());
|
||||||
|
desk(12, y, 5, Direction.SOUTH);
|
||||||
|
readingRoom(floor, false); archive(floor, true);
|
||||||
|
} else {
|
||||||
|
readingFront(y); readingRoom(floor, false);
|
||||||
|
set(2, y + 1, 14, Blocks.CARTOGRAPHY_TABLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case RESTAURANT -> {
|
||||||
|
if (variant == 0) {
|
||||||
|
table(13, y, 3); table(13, y, 5);
|
||||||
|
set(11, y + 1, 2, Blocks.POTTED_DEAD_BUSH);
|
||||||
|
kitchenRear(floor, true);
|
||||||
|
} else {
|
||||||
|
kitchenFront(y); diningRear(floor, false); pantry(floor, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case BARRACKS -> {
|
||||||
|
if (variant == 0) {
|
||||||
|
anchoredBed(12, y + 1, 3, Direction.NORTH); anchoredBed(14, y + 1, 3, Direction.NORTH);
|
||||||
|
set(11, y + 1, 2, Blocks.BARREL); set(11, y + 2, 2, Blocks.BARREL);
|
||||||
|
seat(13, y + 1, 6, Direction.NORTH);
|
||||||
|
} else { reception(y); bedroom(floor, false); }
|
||||||
|
box(12, y + 1, 14, 14, y + 2, 14, Blocks.BARREL.defaultBlockState());
|
||||||
|
set(14, y + 1, 11, Blocks.FLETCHING_TABLE);
|
||||||
|
set(12, y + 1, 11, Blocks.GRINDSTONE.defaultBlockState()
|
||||||
|
.setValue(BlockStateProperties.ATTACH_FACE,
|
||||||
|
net.minecraft.world.level.block.state.properties.AttachFace.FLOOR));
|
||||||
|
}
|
||||||
|
case HALL -> {
|
||||||
|
if (floor == 0 || floor > 1 && variant == 0) reception(y); else readingFront(y);
|
||||||
|
if (floor == 0) auditorium();
|
||||||
|
else if (floor >= 2) {
|
||||||
|
if (variant == 0) readingRoom(floor, false);
|
||||||
|
else { archive(floor, false); readingRoom(floor, true); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default -> throw new IllegalArgumentException("Not an interior kind");
|
||||||
|
}
|
||||||
|
for (var bath : bathroomPlans(p)) if (bath.floor() == floor) bathroom(bath);
|
||||||
|
}
|
||||||
|
|
||||||
|
void kitchenFront(int y) {
|
||||||
|
kitchenLine(y, 11, 2, Direction.SOUTH);
|
||||||
|
table(13, y, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
void kitchenRear(int floor, boolean east) {
|
||||||
|
int y = floor * STOREY;
|
||||||
|
Area room = rear(p, floor, east);
|
||||||
|
kitchenLine(y, room.left(), 14, Direction.NORTH);
|
||||||
|
if (east) {
|
||||||
|
set(14, y + 1, 11, SMOOTH); set(14, y + 2, 11, Blocks.FLOWER_POT);
|
||||||
|
set(12, y + 1, 11, Blocks.CRAFTING_TABLE);
|
||||||
|
} else table(room.left() + 2, y, 11);
|
||||||
|
}
|
||||||
|
|
||||||
|
void kitchenLine(int y, int left, int z, Direction face) {
|
||||||
|
set(left, y + 1, z, facing(Blocks.FURNACE, face));
|
||||||
|
set(left + 1, y + 1, z, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 2));
|
||||||
|
set(left + 2, y + 1, z, facing(Blocks.SMOKER, face));
|
||||||
|
set(left + 3, y + 1, z, Blocks.BARREL);
|
||||||
|
box(left, y + 3, z, left + 3, y + 3, z, SMOOTH);
|
||||||
|
set(left + 2, y + 2, z, Blocks.IRON_TRAPDOOR);
|
||||||
|
for (int x = left; x <= left + 3; x++) set(x, y + 3, z, Blocks.SMOOTH_STONE);
|
||||||
|
for (int x = left; x <= left + 3; x++) set(x, y, z, (x & 1) == 0 ? SMOOTH : TILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bedroom(int floor, boolean east) {
|
||||||
|
int y = floor * STOREY;
|
||||||
|
Area room = rear(p, floor, east);
|
||||||
|
anchoredBed(east ? room.right() - 1 : room.left() + 1, y + 1, 13, east ? Direction.EAST : Direction.WEST);
|
||||||
|
set(east ? room.right() : room.left(), y + 1, 11, Blocks.BARREL);
|
||||||
|
desk(east ? room.left() + 1 : room.right() - 1, y, 11, Direction.SOUTH);
|
||||||
|
set(east ? room.left() : room.right(), y + 1, 14, Blocks.BOOKSHELF);
|
||||||
|
}
|
||||||
|
|
||||||
|
void frontBedroom(int floor) {
|
||||||
|
int y = floor * STOREY;
|
||||||
|
anchoredBed(14, y + 1, 3, Direction.NORTH);
|
||||||
|
set(11, y + 1, 2, Blocks.BARREL); set(11, y + 2, 2, Blocks.FLOWER_POT);
|
||||||
|
desk(12, y, 5, Direction.SOUTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bathroom(BathPlan bath) {
|
||||||
|
int y = bath.floor() * STOREY;
|
||||||
|
for (int x = bath.x0(); x <= bath.x1(); x++) for (int z = bath.z0(); z <= bath.z1(); z++) {
|
||||||
|
set(x, y, z, ((x + z) & 1) == 0 ? SMOOTH : TILE);
|
||||||
|
set(x, y + 4, z, SMOOTH);
|
||||||
|
boolean wall = x == bath.x0() || x == bath.x1() || z == bath.z0() || z == bath.z1();
|
||||||
|
for (int up = 1; up <= 3; up++) set(x, y + up, z, wall ? SMOOTH : AIR);
|
||||||
|
}
|
||||||
|
int doorZ = bath.southDoor() ? bath.z1() : bath.z0();
|
||||||
|
door(bath.doorX(), y + 1, doorZ, bath.southDoor() ? Direction.NORTH : Direction.SOUTH, false);
|
||||||
|
int fixtureZ = bath.southDoor() ? bath.z0() + 1 : bath.z1() - 1;
|
||||||
|
int toiletX = bath.southDoor() ? bath.x1() - 2 : bath.x0() + 1;
|
||||||
|
Direction view = bath.southDoor() ? Direction.SOUTH : Direction.EAST;
|
||||||
|
set(toiletX, y + 1, fixtureZ, seating(Blocks.POLISHED_ANDESITE_STAIRS, view));
|
||||||
|
set(toiletX, y + 2, fixtureZ, Blocks.IRON_TRAPDOOR);
|
||||||
|
set(bath.x1() - 1, y + 1, fixtureZ, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
void reception(int y) {
|
||||||
|
box(12, y + 1, 5, 14, y + 1, 5, Blocks.OAK_PLANKS.defaultBlockState());
|
||||||
|
set(13, y + 2, 5, Blocks.FLOWER_POT);
|
||||||
|
set(14, y + 1, 2, Blocks.BOOKSHELF); set(14, y + 2, 2, Blocks.BARREL);
|
||||||
|
desk(12, y, 3, Direction.SOUTH);
|
||||||
|
set(11, y + 1, 2, Blocks.POTTED_DEAD_BUSH);
|
||||||
|
}
|
||||||
|
|
||||||
|
void laundry(int floor) {
|
||||||
|
int y = floor * STOREY;
|
||||||
|
for (int z = 11; z <= 12; z++) set(14, y + 1, z, Blocks.WATER_CAULDRON.defaultBlockState()
|
||||||
|
.setValue(BlockStateProperties.LEVEL_CAULDRON, z == 11 ? 1 : 2));
|
||||||
|
box(12, y + 1, 14, 14, y + 1, 14, Blocks.BARREL.defaultBlockState());
|
||||||
|
// The rack stands on the storage counter; recessed rear glazing cannot carry it.
|
||||||
|
for (int x : new int[]{12, 14}) set(x, y + 2, 14, Blocks.OAK_PLANKS);
|
||||||
|
box(12, y + 3, 14, 14, y + 3, 14, Blocks.OAK_SLAB.defaultBlockState());
|
||||||
|
set(12, y + 1, 11, Blocks.LOOM);
|
||||||
|
}
|
||||||
|
|
||||||
|
void lounge(int floor, boolean east) {
|
||||||
|
int y = floor * STOREY;
|
||||||
|
Area room = rear(p, floor, east);
|
||||||
|
box(room.left(), y + 1, 14, room.left() + 2, y + 1, 14, seating(Blocks.OAK_STAIRS, Direction.NORTH));
|
||||||
|
desk(room.left() + 1, y, 11, Direction.SOUTH);
|
||||||
|
set(room.left(), y + 1, 12, Blocks.BOOKSHELF);
|
||||||
|
}
|
||||||
|
|
||||||
|
void readingRoom(int floor, boolean east) {
|
||||||
|
int y = floor * STOREY;
|
||||||
|
Area room = rear(p, floor, east);
|
||||||
|
desk(east ? room.left() + 1 : room.right() - 1, y, 11, Direction.SOUTH);
|
||||||
|
box(room.left(), y + 1, 14, room.right(), y + 2, 14, Blocks.BOOKSHELF.defaultBlockState());
|
||||||
|
set(east ? room.right() : room.left(), y + 1, 11, facing(Blocks.LECTERN, Direction.NORTH));
|
||||||
|
}
|
||||||
|
|
||||||
|
void readingFront(int y) {
|
||||||
|
desk(12, y, 3, Direction.SOUTH);
|
||||||
|
box(14, y + 1, 2, 14, y + 2, 5, Blocks.BOOKSHELF.defaultBlockState());
|
||||||
|
set(11, y + 1, 2, facing(Blocks.LECTERN, Direction.SOUTH));
|
||||||
|
}
|
||||||
|
|
||||||
|
void archive(int floor, boolean east) {
|
||||||
|
int y = floor * STOREY;
|
||||||
|
Area room = rear(p, floor, east);
|
||||||
|
box(room.left(), y + 1, 14, room.right(), y + 3, 14, Blocks.BOOKSHELF.defaultBlockState());
|
||||||
|
box(east ? room.right() : room.left(), y + 1, 11, east ? room.right() : room.left(), y + 2, 12, Blocks.BARREL.defaultBlockState());
|
||||||
|
set(room.left() + 1, y + 1, 12, facing(Blocks.LECTERN, Direction.NORTH));
|
||||||
|
}
|
||||||
|
|
||||||
|
void pantry(int floor, boolean east) {
|
||||||
|
int y = floor * STOREY;
|
||||||
|
Area room = rear(p, floor, east);
|
||||||
|
box(room.left(), y + 1, 14, room.right(), y + 2, 14, Blocks.BARREL.defaultBlockState());
|
||||||
|
set(room.right(), y + 1, 11, Blocks.CRAFTING_TABLE);
|
||||||
|
set(room.right(), y + 2, 11, Blocks.FLOWER_POT);
|
||||||
|
}
|
||||||
|
|
||||||
|
void diningRear(int floor, boolean east) {
|
||||||
|
Area room = rear(p, floor, east);
|
||||||
|
int y = floor * STOREY;
|
||||||
|
table(room.left() + 2, y, 12);
|
||||||
|
box(room.left(), y + 1, 14, room.right(), y + 1, 14, seating(Blocks.OAK_STAIRS, Direction.NORTH));
|
||||||
|
}
|
||||||
|
|
||||||
|
void auditorium() {
|
||||||
|
for (int x : new int[]{3, 4, 5, 11, 12, 13}) seat(x, 1, 11, Direction.SOUTH);
|
||||||
|
box(3, 1, 13, 13, 1, 14, WOOD);
|
||||||
|
set(8, 1, 12, facing(Blocks.OAK_STAIRS, Direction.SOUTH));
|
||||||
|
set(8, 2, 14, facing(Blocks.LECTERN, Direction.NORTH));
|
||||||
|
for (int x : new int[]{3, 13}) {
|
||||||
|
box(x, 2, 14, x, 3, 14, WOOD);
|
||||||
|
set(x, 4, 14, Blocks.STONE_BRICK_SLAB);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void stairs() {
|
||||||
|
for (int floor = 0; floor < p.floors(); floor++) {
|
||||||
|
int base = floor * STOREY;
|
||||||
|
for (int step = 1; step <= STOREY; step++) {
|
||||||
|
int z = step + 2;
|
||||||
|
box(2, base + step, z, 3, base + step + 3, z, AIR);
|
||||||
|
for (int x = 2; x <= 3; x++) {
|
||||||
|
// A one-block inclined spine joins successive treads physically, without
|
||||||
|
// filling the headroom above the preceding storey's matching flight.
|
||||||
|
set(x, base + step - 1, z, Blocks.STONE_BRICKS);
|
||||||
|
set(x, base + step, z, facing(Blocks.STONE_BRICK_STAIRS, Direction.SOUTH));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
box(2, base + STOREY, 8, 4, base + STOREY, 9, TILE);
|
||||||
|
box(2, base + STOREY + 1, 8, 4, base + STOREY + 3, 9, AIR);
|
||||||
|
}
|
||||||
|
// This full-height side aisle joins every lower stair approach to its landing door.
|
||||||
|
for (int floor = 0; floor <= p.floors(); floor++) {
|
||||||
|
int y = floor * STOREY;
|
||||||
|
box(4, y + 1, 2, 4, y + 3, 9, AIR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entrance() {
|
||||||
|
for (int x : new int[]{6, 9}) box(x, 1, 0, x, 3, 0, SMOOTH);
|
||||||
|
box(6, 4, 0, 9, 4, 1, Blocks.STONE_BRICK_SLAB.defaultBlockState());
|
||||||
|
box(7, 0, 0, 8, 0, 3, TILE);
|
||||||
|
// Native SOUTH/RIGHT opens against the west jamb; SOUTH/LEFT against the east jamb.
|
||||||
|
door(7, 1, 1, Direction.SOUTH, true);
|
||||||
|
door(8, 1, 1, Direction.SOUTH, false);
|
||||||
|
set(4, 1, 0, Blocks.POTTED_DEAD_BUSH);
|
||||||
|
set(12, 1, 16, Blocks.COMPOSTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
void roof(int y) {
|
||||||
|
// Clipped parapets and a heavy coping keep each accessible roof legible from below.
|
||||||
|
for (int n = 2; n <= 14; n++) {
|
||||||
|
set(n, y + 1, 1, Blocks.STONE_BRICKS); set(n, y + 1, 15, Blocks.STONE_BRICKS);
|
||||||
|
set(1, y + 1, n, Blocks.STONE_BRICKS); set(15, y + 1, n, Blocks.STONE_BRICKS);
|
||||||
|
set(n, y + 2, 1, Blocks.STONE_BRICK_SLAB); set(n, y + 2, 15, Blocks.STONE_BRICK_SLAB);
|
||||||
|
set(1, y + 2, n, Blocks.STONE_BRICK_SLAB); set(15, y + 2, n, Blocks.STONE_BRICK_SLAB);
|
||||||
|
}
|
||||||
|
switch (p.kind()) {
|
||||||
|
case APARTMENT -> {
|
||||||
|
// Two small roof gardens and a timber portico leave the stair landing and central crossing free.
|
||||||
|
roofPlanter(10, y, 4); roofPlanter(10, y, 12);
|
||||||
|
for (int x : new int[]{6, 13}) box(x, y + 1, 14, x, y + 3, 14, Blocks.STRIPPED_OAK_LOG.defaultBlockState());
|
||||||
|
box(6, y + 4, 14, 13, y + 4, 14, Blocks.OAK_SLAB.defaultBlockState());
|
||||||
|
desk(8, y, 12, Direction.NORTH);
|
||||||
|
}
|
||||||
|
case HOTEL -> {
|
||||||
|
roofRoom(y, 9, 3, 14, 7, Blocks.SMOOTH_STONE);
|
||||||
|
door(9, y + 1, 5, Direction.EAST, false);
|
||||||
|
set(12, y + 1, 4, Blocks.BARREL); set(13, y + 1, 4, Blocks.LOOM);
|
||||||
|
box(11, y + 1, 12, 13, y + 1, 12, seating(Blocks.OAK_STAIRS, Direction.NORTH));
|
||||||
|
}
|
||||||
|
case LIBRARY -> {
|
||||||
|
// One transparent cap above a hollow timber curb, open all the way to the stacks.
|
||||||
|
// No opaque former roof plate or second glass floor remains under this canopy.
|
||||||
|
for (int x = 10; x <= 13; x++) for (int z = 3; z <= 7; z++) {
|
||||||
|
boolean edge = x == 10 || x == 13 || z == 3 || z == 7;
|
||||||
|
set(x, y + 1, z, edge ? Blocks.STRIPPED_OAK_LOG.defaultBlockState() : AIR);
|
||||||
|
set(x, y + 2, z, edge ? Blocks.OAK_SLAB : Blocks.GLASS);
|
||||||
|
}
|
||||||
|
box(11, y - 1, 4, 12, y + 1, 6, AIR);
|
||||||
|
desk(11, y, 12, Direction.NORTH); set(14, y + 1, 12, Blocks.LECTERN);
|
||||||
|
}
|
||||||
|
case RESTAURANT -> {
|
||||||
|
// A real dining terrace and rear kitchen extraction with full supporting columns.
|
||||||
|
table(11, y, 6); table(11, y, 11);
|
||||||
|
for (int z : new int[]{3, 14}) box(14, y + 1, z, 14, y + 3, z, Blocks.STRIPPED_OAK_LOG.defaultBlockState());
|
||||||
|
box(14, y + 4, 3, 14, y + 4, 14, Blocks.OAK_PLANKS.defaultBlockState());
|
||||||
|
box(6, y + 1, 14, 7, y + 3, 14, Blocks.STONE_BRICKS.defaultBlockState());
|
||||||
|
set(6, y + 4, 14, Blocks.IRON_BARS); set(7, y + 4, 14, Blocks.IRON_BARS);
|
||||||
|
}
|
||||||
|
case BARRACKS -> {
|
||||||
|
// Enclosed stair head gives the barracks a service tower, without hiding its usable exit.
|
||||||
|
// The whole last flight sits inside the stair head: a front wall at z=6
|
||||||
|
// would cross the headroom over its penultimate step.
|
||||||
|
roofRoom(y, 1, 2, 5, 10, Blocks.SMOOTH_STONE);
|
||||||
|
door(5, y + 1, 8, Direction.EAST, false);
|
||||||
|
box(10, y + 1, 12, 13, y + 1, 14, Blocks.POLISHED_ANDESITE.defaultBlockState());
|
||||||
|
set(11, y + 2, 13, Blocks.IRON_TRAPDOOR);
|
||||||
|
}
|
||||||
|
case HALL -> {
|
||||||
|
for (int step = 0; step < 3; step++) {
|
||||||
|
int edge = 2 + step * 2;
|
||||||
|
box(edge, y + 1 + step, 1, 16 - edge, y + 1 + step, 2, Blocks.STONE_BRICKS.defaultBlockState());
|
||||||
|
}
|
||||||
|
for (int x : new int[]{6, 10}) set(x, y + 4, 2, Blocks.CHISELED_STONE_BRICKS);
|
||||||
|
desk(11, y, 12, Direction.NORTH);
|
||||||
|
}
|
||||||
|
default -> throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void roofRoom(int y, int x0, int z0, int x1, int z1, Block wall) {
|
||||||
|
for (int x = x0; x <= x1; x++) for (int z = z0; z <= z1; z++) {
|
||||||
|
boolean edge = x == x0 || x == x1 || z == z0 || z == z1;
|
||||||
|
for (int up = 1; up <= 3; up++) set(x, y + up, z, edge ? wall.defaultBlockState() : AIR);
|
||||||
|
// A bottom slab touches the full-height walls immediately below it.
|
||||||
|
// A top slab here would float half a block above the pavilion.
|
||||||
|
set(x, y + 4, z, Blocks.STONE_BRICK_SLAB.defaultBlockState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void roofPlanter(int x, int y, int z) {
|
||||||
|
for (int dx = 0; dx <= 3; dx++) set(x + dx, y + 1, z, Blocks.STONE_BRICKS);
|
||||||
|
set(x + 1, y + 1, z, Blocks.DIRT); set(x + 2, y + 1, z, Blocks.DIRT);
|
||||||
|
set(x + 1, y + 2, z, Blocks.OXEYE_DAISY); set(x + 2, y + 2, z, Blocks.CORNFLOWER);
|
||||||
|
}
|
||||||
|
|
||||||
|
void door(int x, int y, int z, Direction facing, boolean rightHinge) {
|
||||||
|
BlockState state = facing(Blocks.OAK_DOOR, facing)
|
||||||
|
.setValue(BlockStateProperties.DOOR_HINGE, rightHinge ? DoorHingeSide.RIGHT : DoorHingeSide.LEFT)
|
||||||
|
.setValue(BlockStateProperties.OPEN, false)
|
||||||
|
.setValue(BlockStateProperties.POWERED, false);
|
||||||
|
set(x, y, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.LOWER));
|
||||||
|
set(x, y + 1, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.UPPER));
|
||||||
|
}
|
||||||
|
void anchoredBed(int x, int y, int z, Direction facing) {
|
||||||
|
BlockState state = facing(Blocks.BED.white(), facing);
|
||||||
|
set(x, y, z, state.setValue(BlockStateProperties.BED_PART, BedPart.FOOT));
|
||||||
|
set(x + facing.getStepX(), y, z + facing.getStepZ(), state.setValue(BlockStateProperties.BED_PART, BedPart.HEAD));
|
||||||
|
int wallX = x + facing.getStepX() * 2, wallZ = z + facing.getStepZ() * 2;
|
||||||
|
Direction alongWall = facing.getClockWise();
|
||||||
|
for (int width = -1; width <= 1; width++) for (int up = 0; up <= 3; up++)
|
||||||
|
set(wallX + alongWall.getStepX() * width, y + up, wallZ + alongWall.getStepZ() * width, SMOOTH);
|
||||||
|
}
|
||||||
|
void table(int x, int y, int z) {
|
||||||
|
set(x, y + 1, z, Blocks.OAK_FENCE); set(x, y + 2, z, Blocks.OAK_PRESSURE_PLATE);
|
||||||
|
seat(x - 1, y + 1, z, Direction.EAST);
|
||||||
|
seat(x + 1, y + 1, z, Direction.WEST);
|
||||||
|
}
|
||||||
|
void desk(int x, int y, int z, Direction approachSide) {
|
||||||
|
set(x, y + 1, z, Blocks.OAK_FENCE); set(x, y + 2, z, Blocks.OAK_PRESSURE_PLATE);
|
||||||
|
seat(x + approachSide.getStepX(), y + 1, z + approachSide.getStepZ(), approachSide.getOpposite());
|
||||||
|
}
|
||||||
|
void seat(int x, int y, int z, Direction lookingToward) {
|
||||||
|
set(x, y, z, seating(Blocks.OAK_STAIRS, lookingToward));
|
||||||
|
}
|
||||||
|
BlockState seating(Block stair, Direction lookingToward) {
|
||||||
|
// Stair FACING points uphill, into its full-height back, opposite a seated player's view.
|
||||||
|
return facing(stair, lookingToward.getOpposite());
|
||||||
|
}
|
||||||
|
BlockState rail(boolean eastWest) { return connected(Blocks.IRON_BARS, eastWest); }
|
||||||
|
BlockState pane(boolean eastWest) { return connected(Blocks.GLASS_PANE, eastWest); }
|
||||||
|
BlockState connected(Block block, boolean eastWest) {
|
||||||
|
return block.defaultBlockState().setValue(BlockStateProperties.EAST, eastWest)
|
||||||
|
.setValue(BlockStateProperties.WEST, eastWest).setValue(BlockStateProperties.NORTH, !eastWest)
|
||||||
|
.setValue(BlockStateProperties.SOUTH, !eastWest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.city;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.StructureManager;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||||
|
import net.minecraft.world.level.storage.loot.LootTable;
|
||||||
|
|
||||||
|
/** Generation21 surface parcel. Its distinct native type preserves all published city renderers. */
|
||||||
|
public final class LostCityPiece21 extends StructurePiece {
|
||||||
|
private static final int SCHEMA = 1;
|
||||||
|
private final LostCityPlan.Parcel parcel;
|
||||||
|
|
||||||
|
public LostCityPiece21(LostCityPlan.Parcel parcel) {
|
||||||
|
super(LostCityStructures.PIECE21, 0, bounds(parcel));
|
||||||
|
this.parcel = parcel;
|
||||||
|
setOrientation(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LostCityPiece21(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||||
|
super(LostCityStructures.PIECE21, tag);
|
||||||
|
if (tag.getInt("SanctuarySchema").orElseThrow() != SCHEMA)
|
||||||
|
throw new IllegalArgumentException("Unsupported Lost City piece schema");
|
||||||
|
parcel = new LostCityPlan.Parcel(LostCityPlan.Kind.valueOf(tag.getString("Kind").orElseThrow()),
|
||||||
|
tag.getInt("MinX").orElseThrow(), tag.getInt("MinZ").orElseThrow(),
|
||||||
|
tag.getInt("Width").orElseThrow(), tag.getInt("Depth").orElseThrow(),
|
||||||
|
tag.getInt("FloorY").orElseThrow(), tag.getInt("Floors").orElseThrow(),
|
||||||
|
tag.getInt("Facing").orElseThrow(), tag.getLong("ParcelSeed").orElseThrow(),
|
||||||
|
tag.getInt("FoundationDepth").orElseThrow(), tag.getIntArray("SurfaceHeights").orElseThrow());
|
||||||
|
if (!getBoundingBox().equals(bounds(parcel)))
|
||||||
|
throw new IllegalArgumentException("Lost City parcel disagrees with its saved native bounding box");
|
||||||
|
setOrientation(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LostCityPlan.Parcel parcel() { return parcel; }
|
||||||
|
|
||||||
|
@Override public BlockPos getLocatorPosition() {
|
||||||
|
int x = parcel.width() / 2, z = parcel.depth() / 2;
|
||||||
|
return new BlockPos(parcel.minX() + x, parcel.surfaceY(x, z) + 1, parcel.minZ() + z);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||||
|
tag.putInt("SanctuarySchema", SCHEMA);
|
||||||
|
tag.putString("Kind", parcel.kind().name());
|
||||||
|
tag.putInt("MinX", parcel.minX()); tag.putInt("MinZ", parcel.minZ());
|
||||||
|
tag.putInt("Width", parcel.width()); tag.putInt("Depth", parcel.depth());
|
||||||
|
tag.putInt("FloorY", parcel.floorY()); tag.putInt("Floors", parcel.floors());
|
||||||
|
tag.putInt("Facing", parcel.facing()); tag.putLong("ParcelSeed", parcel.seed());
|
||||||
|
tag.putInt("FoundationDepth", parcel.foundationDepth());
|
||||||
|
tag.putIntArray("SurfaceHeights", parcel.surfaceHeights());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void postProcess(WorldGenLevel level, StructureManager structures, ChunkGenerator generator,
|
||||||
|
RandomSource random, BoundingBox chunkClip, ChunkPos chunkPos, BlockPos pivot) {
|
||||||
|
LostCityStructures.placing(() -> render(parcel, new LostCityRenderer.BlockWriter() {
|
||||||
|
@Override public void set(int x, int y, int z, BlockState block) {
|
||||||
|
BlockPos pos = world(x, y, z);
|
||||||
|
if (getBoundingBox().isInside(pos) && chunkClip.isInside(pos))
|
||||||
|
placeBlock(level, block, pos.getX(), pos.getY(), pos.getZ(), chunkClip);
|
||||||
|
}
|
||||||
|
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
|
||||||
|
BlockPos pos = world(x, y, z);
|
||||||
|
if (getBoundingBox().isInside(pos) && chunkClip.isInside(pos))
|
||||||
|
createChest(level, chunkClip, RandomSource.create(seed), pos, loot, null);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void render(LostCityPlan.Parcel parcel, LostCityRenderer.BlockWriter writer) {
|
||||||
|
LostCitySurface21.render(parcel, writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BlockPos world(int x, int y, int z) {
|
||||||
|
return new BlockPos(parcel.minX() + x, parcel.floorY() + y, parcel.minZ() + z);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BoundingBox bounds(LostCityPlan.Parcel parcel) {
|
||||||
|
int low = Math.min(parcel.floorY(), Arrays.stream(parcel.surfaceHeights()).min().orElse(parcel.floorY()))
|
||||||
|
- parcel.foundationDepth();
|
||||||
|
int high = Math.max(parcel.maxY(), Arrays.stream(parcel.surfaceHeights()).max().orElse(parcel.floorY()) + 5);
|
||||||
|
return new BoundingBox(parcel.minX(), low, parcel.minZ(), parcel.maxX(), high, parcel.maxZ());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.city;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.worldgen.city.LostCityPlan.*;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/** Preserve the certified compact16 core. At most two modest natural outlying blocks are admitted
|
||||||
|
* together with complete three-wide approaches; a failed route publishes neither its road nor its block. */
|
||||||
|
public final class LostCityPlanner21 {
|
||||||
|
public static final int MAX_EXTRA_COLUMNS = 4_000, MAX_EXTRA_CHECKS = 220_000;
|
||||||
|
public static final int MAX_COLUMNS = LostCityPlanner16.MAX_COLUMNS + MAX_EXTRA_COLUMNS;
|
||||||
|
public static final int MAX_VOLUME_CHECKS = LostCityPlanner16.MAX_VOLUME_CHECKS + MAX_EXTRA_CHECKS;
|
||||||
|
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
|
||||||
|
private static final int INVALID = Integer.MIN_VALUE;
|
||||||
|
private LostCityPlanner21() {}
|
||||||
|
public record Point(int x, int floorY, int z) {}
|
||||||
|
public record Connection(int districtIndex, List<Point> path) { public Connection { path = List.copyOf(path); } }
|
||||||
|
public record Result(LostCityPlanner16.Result core, LostCityPlan surface,
|
||||||
|
List<LostCityPlanner16.Support> supports, int columns, int volumeChecks,
|
||||||
|
List<Connection> connections, Map<String, Integer> diagnostics) {
|
||||||
|
public Result {
|
||||||
|
supports = List.copyOf(supports); connections = List.copyOf(connections); diagnostics = Map.copyOf(diagnostics);
|
||||||
|
if (columns > MAX_COLUMNS || volumeChecks > MAX_VOLUME_CHECKS || connections.size() > 2)
|
||||||
|
throw new IllegalArgumentException("Generation21 city exceeded its bounded extension");
|
||||||
|
}
|
||||||
|
public LostCityPlanner16.Fallback reservation() { return core.reservation(); }
|
||||||
|
}
|
||||||
|
public static Result plan(long seed, int cx, int cz, int diameter, boolean origin, LostCityPlanner16.Terrain terrain) {
|
||||||
|
var core = LostCityPlanner16.plan(seed, cx, cz, diameter, origin, terrain);
|
||||||
|
var districts = new ArrayList<>(core.surface().districts()); var supports = new ArrayList<>(core.supports());
|
||||||
|
var links = new ArrayList<Connection>(); var probe = new Probe(terrain);
|
||||||
|
if (origin && !districts.isEmpty()) {
|
||||||
|
var center = districts.getFirst(); int mx = (center.minX() + center.maxX()) / 2, mz = (center.minZ() + center.maxZ()) / 2;
|
||||||
|
int first = (int) Long.remainderUnsigned(mix(seed ^ 0x21c175L), 4);
|
||||||
|
try {
|
||||||
|
for (int attempt = 0; attempt < 8 && links.size() < 2; attempt++) {
|
||||||
|
int d = (first + attempt) & 3;
|
||||||
|
int radius = Math.max(center.maxX() - center.minX(), center.maxZ() - center.minZ()) / 2 + (attempt < 4 ? 42 : 58);
|
||||||
|
int x = mx + DX[d] * radius, z = mz + DZ[d] * radius;
|
||||||
|
probe.reject("cluster-attempt");
|
||||||
|
// The old one-lot natural solver already certifies foundations, ramps and short supported bridges.
|
||||||
|
var mini = LostCityPlanner16.plan(mix(seed ^ attempt * 3191L ^ 0x2100L), x, z, 96, false, probe);
|
||||||
|
if (mini.surface().isEmpty()) { probe.reject("no-natural-block"); continue; }
|
||||||
|
var district = mini.surface().districts().getFirst();
|
||||||
|
if (overlap(district, districts, 14)) { probe.reject("insufficient-natural-gap"); continue; }
|
||||||
|
var all = new ArrayList<>(districts); all.add(district);
|
||||||
|
var path = connect(seed ^ attempt, center, district, all, probe);
|
||||||
|
if (path == null) { probe.reject("no-complete-approach"); continue; }
|
||||||
|
var parcels = new ArrayList<>(district.parcels()); parcels.addAll(path.roads());
|
||||||
|
District joined;
|
||||||
|
try { joined = new District(district.startChunkX(), district.startChunkZ(), district.seed(), parcels); }
|
||||||
|
catch (IllegalArgumentException tooWide) { probe.reject("native-reference-radius"); continue; }
|
||||||
|
links.add(new Connection(districts.size(), path.path())); districts.add(joined); supports.addAll(mini.supports());
|
||||||
|
probe.reject("connected-cluster");
|
||||||
|
}
|
||||||
|
} catch (Exhausted exhausted) { probe.reject("extension-budget"); }
|
||||||
|
}
|
||||||
|
return new Result(core, new LostCityPlan(districts), supports, core.columns() + probe.columns.size(),
|
||||||
|
core.volumeChecks() + probe.checks, links, probe.reasons);
|
||||||
|
}
|
||||||
|
private record Entry(int x, int y, int z, int facing) {}
|
||||||
|
private record XZ(int x, int z) { XZ move(int d, int n) { return new XZ(x + DX[d] * n, z + DZ[d] * n); } }
|
||||||
|
private record Pair(Entry a, Entry b) {}
|
||||||
|
private record Road(int x, int z, int floor, int foundation) {}
|
||||||
|
private record Footprint(int x, int z, int width, int depth) {}
|
||||||
|
private record Route(List<Point> path, List<Parcel> roads) {}
|
||||||
|
private static Route connect(long seed, District core, District extra, List<District> all, Probe probe) {
|
||||||
|
var pairs = new ArrayList<Pair>();
|
||||||
|
for (var a : entries(core, all)) for (var b : entries(extra, all)) pairs.add(new Pair(a, b));
|
||||||
|
pairs.sort(Comparator.comparingInt(p -> Math.abs(p.a.x - p.b.x) + Math.abs(p.a.z - p.b.z)));
|
||||||
|
for (var pair : pairs.stream().limit(6).toList()) {
|
||||||
|
var a = pair.a; var b = pair.b; var outA = new XZ(a.x, a.z).move(a.facing, 3); var outB = new XZ(b.x, b.z).move(b.facing, 3);
|
||||||
|
for (int variant = 0; variant < 4; variant++) {
|
||||||
|
var points = new ArrayList<XZ>(); points.add(new XZ(a.x, a.z)); line(points, outA);
|
||||||
|
if (variant == 0) line(points, new XZ(outB.x, outA.z));
|
||||||
|
else if (variant == 1) line(points, new XZ(outA.x, outB.z));
|
||||||
|
else if (variant == 2) { int mid = (outA.x + outB.x) / 2; line(points, new XZ(mid, outA.z)); line(points, new XZ(mid, outB.z)); }
|
||||||
|
else { int mid = (outA.z + outB.z) / 2; line(points, new XZ(outA.x, mid)); line(points, new XZ(outB.x, mid)); }
|
||||||
|
line(points, outB); line(points, new XZ(b.x, b.z));
|
||||||
|
if (points.size() > 192 || new java.util.HashSet<>(points).size() != points.size()) continue;
|
||||||
|
var route = profile(seed, points, a, b, all, probe); if (route != null) return route;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
private static List<Entry> entries(District district, List<District> all) {
|
||||||
|
var result = new ArrayList<Entry>();
|
||||||
|
for (var p : district.parcels()) if (p.kind() == Kind.STREET && p.width() >= 3 && p.depth() >= 3) {
|
||||||
|
for (int d = 0; d < 4; d++) {
|
||||||
|
int lx = d == 1 ? p.width() - 1 : d == 3 ? 0 : p.width() / 2;
|
||||||
|
int lz = d == 2 ? p.depth() - 1 : d == 0 ? 0 : p.depth() / 2;
|
||||||
|
int x = p.minX() + lx, z = p.minZ() + lz, floor = p.surfaceY(lx, lz); boolean valid = true;
|
||||||
|
for (int side = -1; side <= 1; side++) {
|
||||||
|
int px = x + DZ[d] * side, pz = z + DX[d] * side;
|
||||||
|
if (existingFloor(px, pz, all) != floor || existingFloor(px + DX[d], pz + DZ[d], all) != INVALID) { valid = false; break; }
|
||||||
|
}
|
||||||
|
if (valid) result.add(new Entry(x, floor, z, d));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
private static Route profile(long seed, List<XZ> path, Entry a, Entry b, List<District> all, Probe probe) {
|
||||||
|
int n = path.size(); int[] low = new int[n], high = new int[n], floor = new int[n];
|
||||||
|
// Partition the complete footprint before choosing elevations. Every partition boundary
|
||||||
|
// will be a level landing, so a renderer never needs a neighboring parcel to form a stair.
|
||||||
|
var footprintCells = new LinkedHashMap<Long, XZ>();
|
||||||
|
for (int i = 0; i < n; i++) for (var cell : strip(path, i))
|
||||||
|
if (existingFloor(cell.x, cell.z, all) == INVALID) footprintCells.putIfAbsent(key(cell.x, cell.z), cell);
|
||||||
|
var footprints = footprints(footprintCells);
|
||||||
|
var owner = new HashMap<Long, Integer>();
|
||||||
|
for (int i = 0; i < footprints.size(); i++) {
|
||||||
|
var f = footprints.get(i);
|
||||||
|
for (int z = f.z; z < f.z + f.depth; z++) for (int x = f.x; x < f.x + f.width; x++) owner.put(key(x, z), i);
|
||||||
|
}
|
||||||
|
int[] grade = new int[n];
|
||||||
|
for (int i = 1; i < n; i++) {
|
||||||
|
grade[i] = 1;
|
||||||
|
for (var aCell : strip(path, i - 1)) for (var bCell : strip(path, i)) {
|
||||||
|
int distance = Math.abs(aCell.x - bCell.x) + Math.abs(aCell.z - bCell.z);
|
||||||
|
if (distance == 0 || distance == 1 && !java.util.Objects.equals(
|
||||||
|
owner.get(key(aCell.x, aCell.z)), owner.get(key(bCell.x, bCell.z)))) grade[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
low[i] = 176; high[i] = 350;
|
||||||
|
for (var p : strip(path, i)) {
|
||||||
|
int old = existingFloor(p.x, p.z, all);
|
||||||
|
if (old == -1) return null;
|
||||||
|
if (old != INVALID) { low[i] = Math.max(low[i], old); high[i] = Math.min(high[i], old); }
|
||||||
|
else {
|
||||||
|
var g = probe.ground(p.x, p.z);
|
||||||
|
if (!g.dry() || g.protectedTerrain() || g.solidDepth() < 3) return null;
|
||||||
|
low[i] = Math.max(low[i], g.top() - 2); high[i] = Math.min(high[i], g.top() + 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
low[0] = high[0] = a.y; low[n - 1] = high[n - 1] = b.y;
|
||||||
|
for (int i = 1; i < n; i++) { low[i] = Math.max(low[i], low[i - 1] - grade[i]); high[i] = Math.min(high[i], high[i - 1] + grade[i]); }
|
||||||
|
for (int i = n - 2; i >= 0; i--) { low[i] = Math.max(low[i], low[i + 1] - grade[i + 1]); high[i] = Math.min(high[i], high[i + 1] + grade[i + 1]); }
|
||||||
|
var cells = new LinkedHashMap<Long, Road>(); var result = new ArrayList<Point>();
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
if (low[i] > high[i]) return null;
|
||||||
|
int min = i == 0 ? low[i] : Math.max(low[i], floor[i - 1] - grade[i]), max = i == 0 ? high[i] : Math.min(high[i], floor[i - 1] + grade[i]);
|
||||||
|
if (min > max) return null;
|
||||||
|
var p = path.get(i); floor[i] = Math.clamp(probe.ground(p.x, p.z).top(), min, max); result.add(new Point(p.x, floor[i], p.z));
|
||||||
|
for (var c : strip(path, i)) {
|
||||||
|
int old = existingFloor(c.x, c.z, all); if (old != INVALID) { if (old != floor[i]) return null; continue; }
|
||||||
|
var g = probe.ground(c.x, c.z); int foundation = Math.max(1, floor[i] - g.top() + 1), bottom = floor[i] - foundation;
|
||||||
|
if (!probe.solid(c.x, bottom, c.z) || !probe.solid(c.x, bottom - 1, c.z)) return null;
|
||||||
|
for (int y = bottom - 1; y <= floor[i] + 8; y++) if (probe.protectedAt(c.x, y, c.z)) return null;
|
||||||
|
var previous = cells.putIfAbsent(key(c.x, c.z), new Road(c.x, c.z, floor[i], foundation));
|
||||||
|
if (previous != null && previous.floor != floor[i]) return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Bends may put nonconsecutive centerline strips next to each other. Their shared
|
||||||
|
// partition edge obeys the same level-landing contract, including existing town roads.
|
||||||
|
for (var cell : cells.values()) for (int d = 0; d < 4; d++) {
|
||||||
|
int nx = cell.x + DX[d], nz = cell.z + DZ[d];
|
||||||
|
var neighbor = cells.get(key(nx, nz));
|
||||||
|
if (neighbor != null && !owner.get(key(cell.x, cell.z)).equals(owner.get(key(nx, nz)))
|
||||||
|
&& neighbor.floor != cell.floor) return null;
|
||||||
|
int old = existingFloor(nx, nz, all);
|
||||||
|
if (old >= 0 && old != cell.floor) return null;
|
||||||
|
}
|
||||||
|
var parcels = new ArrayList<Parcel>();
|
||||||
|
for (var f : footprints) {
|
||||||
|
int foundation = 1;
|
||||||
|
for (int z = f.z; z < f.z + f.depth; z++) for (int x = f.x; x < f.x + f.width; x++)
|
||||||
|
foundation = Math.max(foundation, cells.get(key(x, z)).foundation);
|
||||||
|
int[] heights = new int[f.width * f.depth];
|
||||||
|
for (int z = f.z; z < f.z + f.depth; z++) for (int x = f.x; x < f.x + f.width; x++) {
|
||||||
|
var cell = cells.get(key(x, z)); int bottom = cell.floor - foundation;
|
||||||
|
// A rectangular parcel uses one foundation depth. Certify its deeper columns
|
||||||
|
// explicitly rather than extending another column's rock certificate sideways.
|
||||||
|
if (!probe.solid(x, bottom, z) || !probe.solid(x, bottom - 1, z)) return null;
|
||||||
|
for (int y = bottom - 1; y < cell.floor - cell.foundation - 1; y++)
|
||||||
|
if (probe.protectedAt(x, y, z)) return null;
|
||||||
|
heights[(z - f.z) * f.width + x - f.x] = cell.floor;
|
||||||
|
}
|
||||||
|
parcels.add(new Parcel(Kind.STREET, f.x, f.z, f.width, f.depth, heights[0], 0, 0,
|
||||||
|
mix(seed ^ key(f.x, f.z)), foundation, heights));
|
||||||
|
}
|
||||||
|
return new Route(List.copyOf(result), List.copyOf(parcels));
|
||||||
|
}
|
||||||
|
/** Disjoint, completely occupied rectangles, each contained in a single native chunk. */
|
||||||
|
private static List<Footprint> footprints(Map<Long, XZ> cells) {
|
||||||
|
var remaining = new java.util.HashSet<>(cells.keySet()); var result = new ArrayList<Footprint>();
|
||||||
|
for (var at : cells.values().stream().sorted(Comparator.comparingInt(XZ::z).thenComparingInt(XZ::x)).toList()) {
|
||||||
|
if (!remaining.contains(key(at.x, at.z))) continue;
|
||||||
|
int width = 1, depth = 1;
|
||||||
|
while ((at.x + width >> 4) == (at.x >> 4) && remaining.contains(key(at.x + width, at.z))) width++;
|
||||||
|
while ((at.z + depth >> 4) == (at.z >> 4)) {
|
||||||
|
boolean complete = true;
|
||||||
|
for (int x = at.x; x < at.x + width; x++) complete &= remaining.contains(key(x, at.z + depth));
|
||||||
|
if (!complete) break;
|
||||||
|
depth++;
|
||||||
|
}
|
||||||
|
for (int z = at.z; z < at.z + depth; z++) for (int x = at.x; x < at.x + width; x++) remaining.remove(key(x, z));
|
||||||
|
result.add(new Footprint(at.x, at.z, width, depth));
|
||||||
|
}
|
||||||
|
return List.copyOf(result);
|
||||||
|
}
|
||||||
|
private static List<XZ> strip(List<XZ> points, int i) {
|
||||||
|
var p = points.get(i); var result = new java.util.LinkedHashSet<XZ>(); result.add(p);
|
||||||
|
for (int next : new int[]{i - 1, i + 1}) if (next >= 0 && next < points.size()) {
|
||||||
|
var q = points.get(next); int dx = Integer.signum(q.x - p.x), dz = Integer.signum(q.z - p.z);
|
||||||
|
result.add(new XZ(p.x + dz, p.z + dx)); result.add(new XZ(p.x - dz, p.z - dx));
|
||||||
|
}
|
||||||
|
return List.copyOf(result);
|
||||||
|
}
|
||||||
|
private static void line(List<XZ> points, XZ goal) {
|
||||||
|
var p = points.getLast(); while (p.x != goal.x) { p = new XZ(p.x + Integer.signum(goal.x - p.x), p.z); points.add(p); }
|
||||||
|
while (p.z != goal.z) { p = new XZ(p.x, p.z + Integer.signum(goal.z - p.z)); points.add(p); }
|
||||||
|
}
|
||||||
|
private static int existingFloor(int x, int z, List<District> all) {
|
||||||
|
for (var d : all) for (var p : d.parcels()) if (x >= p.minX() && x <= p.maxX() && z >= p.minZ() && z <= p.maxZ())
|
||||||
|
return p.kind() == Kind.STREET ? p.surfaceY(x - p.minX(), z - p.minZ()) : -1;
|
||||||
|
return INVALID;
|
||||||
|
}
|
||||||
|
private static boolean overlap(District candidate, List<District> all, int gap) {
|
||||||
|
for (var d : all) if (candidate.minX() <= d.maxX() + gap && candidate.maxX() >= d.minX() - gap
|
||||||
|
&& candidate.minZ() <= d.maxZ() + gap && candidate.maxZ() >= d.minZ() - gap) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
private static final class Exhausted extends RuntimeException {}
|
||||||
|
private static final class Probe implements LostCityPlanner16.Terrain {
|
||||||
|
final LostCityPlanner16.Terrain source; final Map<Long, LostCityPlanner.Ground> columns = new HashMap<>();
|
||||||
|
final Map<String, Integer> reasons = new LinkedHashMap<>(); int checks;
|
||||||
|
Probe(LostCityPlanner16.Terrain source) { this.source = source; }
|
||||||
|
public boolean owns(int x, int z) { return source.owns(x, z); }
|
||||||
|
public LostCityPlanner.Ground ground(int x, int z) {
|
||||||
|
if (!owns(x, z)) return new LostCityPlanner.Ground(0, 0, false, true);
|
||||||
|
long key = key(x, z); var old = columns.get(key); if (old != null) return old;
|
||||||
|
if (columns.size() >= MAX_EXTRA_COLUMNS) throw new Exhausted();
|
||||||
|
var value = source.ground(x, z); columns.put(key, value); return value;
|
||||||
|
}
|
||||||
|
public boolean solid(int x, int y, int z) { count(); ground(x, z); return owns(x, z) && source.solid(x, y, z); }
|
||||||
|
public boolean protectedAt(int x, int y, int z) { count(); ground(x, z); return !owns(x, z) || source.protectedAt(x, y, z); }
|
||||||
|
void count() { if (checks >= MAX_EXTRA_CHECKS) throw new Exhausted(); checks++; }
|
||||||
|
void reject(String reason) { reasons.merge(reason, 1, Integer::sum); }
|
||||||
|
}
|
||||||
|
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
|
||||||
|
private static long mix(long v) { v = (v ^ v >>> 30) * 0xbf58476d1ce4e5b9L; v = (v ^ v >>> 27) * 0x94d049bb133111ebL; return v ^ v >>> 31; }
|
||||||
|
}
|
||||||
@@ -29,17 +29,18 @@ public final class LostCityStructure extends Structure {
|
|||||||
throw new IllegalStateException("Lost City district exceeds native structure reference range");
|
throw new IllegalStateException("Lost City district exceeds native structure reference range");
|
||||||
}
|
}
|
||||||
return Optional.of(new GenerationStub(LostCityStructures.locator(district), pieces -> {
|
return Optional.of(new GenerationStub(LostCityStructures.locator(district), pieces -> {
|
||||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20) && generator.cityReservation16(island) != null)
|
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)))
|
||||||
pieces.addPiece(new LostCityFoundationPiece16(generator.cityReservation16(island), district.seed()));
|
pieces.addPiece(new LostCityFoundationPiece16(generator.cityReservation16(island), district.seed()));
|
||||||
if (generator.rootGeneration() == 15) for (var support : generator.citySupports(island)) {
|
if (generator.rootGeneration() == 15) for (var support : generator.citySupports(island)) {
|
||||||
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
|
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
|
||||||
pieces.addPiece(new LostCitySupportPiece15(support));
|
pieces.addPiece(new LostCitySupportPiece15(support));
|
||||||
}
|
}
|
||||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20)) for (var support : generator.citySupports16(island)) {
|
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21)) for (var support : generator.citySupports16(island)) {
|
||||||
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
|
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
|
||||||
pieces.addPiece(new LostCitySupportPiece16(support));
|
pieces.addPiece(new LostCitySupportPiece16(support));
|
||||||
}
|
}
|
||||||
district.parcels().forEach(parcel -> pieces.addPiece(generator.rootGeneration() == 20 ? new LostCityPiece20(parcel) : (generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20) ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
|
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
|
||||||
? new LostCityPiece15(parcel) : new LostCityPiece(parcel)));
|
? new LostCityPiece15(parcel) : new LostCityPiece(parcel)));
|
||||||
if (generator.rootGeneration() == 15) for (var network : generator.undergroundPlan(island).networks()) {
|
if (generator.rootGeneration() == 15) for (var network : generator.undergroundPlan(island).networks()) {
|
||||||
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
|
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
|
||||||
@@ -49,13 +50,13 @@ public final class LostCityStructure extends Structure {
|
|||||||
network.modules().stream().filter(m -> m.kind() == LostCityUnderground15.Kind.ACCESS)
|
network.modules().stream().filter(m -> m.kind() == LostCityUnderground15.Kind.ACCESS)
|
||||||
.forEach(m -> pieces.addPiece(new LostCityUndergroundPiece15(m)));
|
.forEach(m -> pieces.addPiece(new LostCityUndergroundPiece15(m)));
|
||||||
}
|
}
|
||||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20)) for (var network : generator.undergroundPlan16(island).networks()) {
|
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21)) for (var network : generator.undergroundPlan16(island).networks()) {
|
||||||
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
|
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
|
||||||
// Surface comes first, rooms/corridors next, shafts last so every entrance remains open.
|
// Surface comes first, rooms/corridors next, shafts last so every entrance remains open.
|
||||||
network.modules().stream().filter(m -> m.kind() != LostCityUnderground16.Kind.ACCESS)
|
network.modules().stream().filter(m -> m.kind() != LostCityUnderground16.Kind.ACCESS)
|
||||||
.forEach(m -> pieces.addPiece(generator.rootGeneration() == 20 ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
|
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
|
||||||
network.modules().stream().filter(m -> m.kind() == LostCityUnderground16.Kind.ACCESS)
|
network.modules().stream().filter(m -> m.kind() == LostCityUnderground16.Kind.ACCESS)
|
||||||
.forEach(m -> pieces.addPiece(generator.rootGeneration() == 20 ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
|
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ public final class LostCityStructures {
|
|||||||
BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_underground_v15"), LostCityUndergroundPiece15::new);
|
BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_underground_v15"), LostCityUndergroundPiece15::new);
|
||||||
public static final StructurePieceType SUPPORT15 = Registry.register(
|
public static final StructurePieceType SUPPORT15 = Registry.register(
|
||||||
BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_support_v15"), LostCitySupportPiece15::new);
|
BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_support_v15"), LostCitySupportPiece15::new);
|
||||||
|
public static final StructurePieceType PIECE21 = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
|
||||||
|
SanctuaryMod.id("lost_city_parcel_v21"), LostCityPiece21::new);
|
||||||
public static final StructurePieceType PIECE20 = Registry.register(
|
public static final StructurePieceType PIECE20 = Registry.register(
|
||||||
BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_parcel_v20"), LostCityPiece20::new);
|
BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("lost_city_parcel_v20"), LostCityPiece20::new);
|
||||||
public static final StructurePieceType PIECE16 = Registry.register(
|
public static final StructurePieceType PIECE16 = Registry.register(
|
||||||
|
|||||||
@@ -0,0 +1,302 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.city;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.Rotation;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||||
|
import net.minecraft.world.level.block.state.properties.Half;
|
||||||
|
import net.minecraft.world.level.block.state.properties.SlabType;
|
||||||
|
import net.minecraft.world.level.storage.loot.LootTable;
|
||||||
|
|
||||||
|
/** New surface architecture on the saved generation16 footprints. Published renderers remain immutable. */
|
||||||
|
public final class LostCitySurface21 {
|
||||||
|
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
|
||||||
|
private LostCitySurface21() {}
|
||||||
|
|
||||||
|
public static void render(LostCityPlan.Parcel parcel, LostCityRenderer.BlockWriter output) {
|
||||||
|
if (parcel.kind().building()) { LostCityInteriors21.render(parcel, output); return; }
|
||||||
|
var connected = ConnectedBlocks20.buffer(output);
|
||||||
|
if (parcel.kind() == LostCityPlan.Kind.STREET || parcel.kind() == LostCityPlan.Kind.BRIDGE) {
|
||||||
|
road(parcel, connected);
|
||||||
|
} else {
|
||||||
|
new Exterior(parcel, connected).render();
|
||||||
|
}
|
||||||
|
connected.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void road(LostCityPlan.Parcel p, LostCityRenderer.BlockWriter output) {
|
||||||
|
boolean alongX = p.width() > p.depth(), bridge = p.kind() == LostCityPlan.Kind.BRIDGE;
|
||||||
|
for (int z = 0; z < p.depth(); z++) for (int x = 0; x < p.width(); x++) {
|
||||||
|
int y = p.surfaceY(x, z) - p.floorY();
|
||||||
|
int across = alongX ? z : x, along = alongX ? x : z, breadth = alongX ? p.depth() : p.width();
|
||||||
|
boolean edge = across == 0 || across == breadth - 1;
|
||||||
|
// A legible curb and patterned shoulders, not per-block random pavement.
|
||||||
|
Block paving = edge ? Blocks.POLISHED_TUFF : across == 1 || across == breadth - 2
|
||||||
|
? along % 4 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS
|
||||||
|
: Blocks.POLISHED_ANDESITE;
|
||||||
|
if (!bridge) for (int down = 1; down <= p.foundationDepth(); down++)
|
||||||
|
output.set(x, y - down, z, Blocks.STONE_BRICKS.defaultBlockState());
|
||||||
|
output.set(x, y, z, paving.defaultBlockState());
|
||||||
|
for (int up = 1; up <= 8; up++) output.set(x, y + up, z, Blocks.AIR.defaultBlockState());
|
||||||
|
if (bridge && edge) {
|
||||||
|
// Posts grow from the existing deck only; the certified natural bearings are untouched.
|
||||||
|
output.set(x, y + 1, z, (along % 5 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.IRON_BARS).defaultBlockState());
|
||||||
|
if (along % 5 == 0) output.set(x, y + 2, z, Blocks.TUFF_BRICK_SLAB.defaultBlockState());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (var direction : HORIZONTAL) {
|
||||||
|
int nx = x + direction.getStepX(), nz = z + direction.getStepZ();
|
||||||
|
if (nx >= 0 && nx < p.width() && nz >= 0 && nz < p.depth()
|
||||||
|
&& p.surfaceY(nx, nz) == p.surfaceY(x, z) + 1) {
|
||||||
|
output.set(x, y + 1, z, Blocks.TUFF_BRICK_STAIRS.defaultBlockState()
|
||||||
|
.setValue(BlockStateProperties.HORIZONTAL_FACING, direction));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Exterior implements LostCityRenderer.BlockWriter {
|
||||||
|
final LostCityPlan.Parcel p;
|
||||||
|
final LostCityRenderer.BlockWriter output;
|
||||||
|
final Rotation turn;
|
||||||
|
Exterior(LostCityPlan.Parcel p, LostCityRenderer.BlockWriter output) {
|
||||||
|
this.p = p; this.output = output;
|
||||||
|
turn = switch (p.facing()) { case 1 -> Rotation.CLOCKWISE_90; case 2 -> Rotation.CLOCKWISE_180;
|
||||||
|
case 3 -> Rotation.COUNTERCLOCKWISE_90; default -> Rotation.NONE; };
|
||||||
|
}
|
||||||
|
int x(int x, int z) { return switch (p.facing()) { case 1 -> p.width() - 1 - z; case 2 -> p.width() - 1 - x; case 3 -> z; default -> x; }; }
|
||||||
|
int z(int x, int z) { return switch (p.facing()) { case 1 -> x; case 2 -> p.depth() - 1 - z; case 3 -> p.depth() - 1 - x; default -> z; }; }
|
||||||
|
@Override public void set(int x, int y, int z, BlockState state) {
|
||||||
|
output.set(x(x, z), y, z(x, z), palette(y, state).rotate(turn));
|
||||||
|
}
|
||||||
|
BlockState palette(int y, BlockState state) {
|
||||||
|
Block block = null;
|
||||||
|
if (state.is(Blocks.STONE) && y < 0) block = Blocks.STONE_BRICKS;
|
||||||
|
else if (state.is(Blocks.DARK_OAK_PLANKS)) block = Blocks.OAK_PLANKS;
|
||||||
|
else if (state.is(Blocks.DARK_OAK_FENCE)) block = Blocks.OAK_FENCE;
|
||||||
|
else if (state.is(Blocks.DARK_OAK_STAIRS)) block = Blocks.OAK_STAIRS;
|
||||||
|
else if (state.is(Blocks.DARK_OAK_SLAB)) block = Blocks.OAK_SLAB;
|
||||||
|
else if (state.is(Blocks.CALCITE)) block = Blocks.SMOOTH_STONE;
|
||||||
|
else if (y > 0) {
|
||||||
|
if (state.is(Blocks.TUFF_BRICKS)) block = Blocks.STONE_BRICKS;
|
||||||
|
else if (state.is(Blocks.CHISELED_TUFF_BRICKS) || state.is(Blocks.CHISELED_TUFF)) block = Blocks.CHISELED_STONE_BRICKS;
|
||||||
|
else if (state.is(Blocks.TUFF_BRICK_SLAB)) block = Blocks.STONE_BRICK_SLAB;
|
||||||
|
else if (state.is(Blocks.POLISHED_TUFF_SLAB)) block = Blocks.SMOOTH_STONE_SLAB;
|
||||||
|
else if (state.is(Blocks.TUFF_BRICK_STAIRS)) block = Blocks.STONE_BRICK_STAIRS;
|
||||||
|
else if (state.is(Blocks.POLISHED_TUFF)) block = Blocks.POLISHED_ANDESITE;
|
||||||
|
}
|
||||||
|
if (block == null) return state;
|
||||||
|
BlockState result = block.defaultBlockState();
|
||||||
|
for (var property : state.getProperties()) if (result.hasProperty(property)) result = copy(result, state, property);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
<T extends Comparable<T>> BlockState copy(BlockState into, BlockState from,
|
||||||
|
net.minecraft.world.level.block.state.properties.Property<T> property) {
|
||||||
|
return into.setValue(property, from.getValue(property));
|
||||||
|
}
|
||||||
|
void set(int x, int y, int z, Block block) { set(x, y, z, block.defaultBlockState()); }
|
||||||
|
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
|
||||||
|
output.chest(x(x, z), y, z(x, z), table, seed ^ 0x21c17a7eL);
|
||||||
|
}
|
||||||
|
void box(int x0, int y0, int z0, int x1, int y1, int z1, Block block) {
|
||||||
|
for (int x = x0; x <= x1; x++) for (int z = z0; z <= z1; z++) for (int y = y0; y <= y1; y++) set(x, y, z, block);
|
||||||
|
}
|
||||||
|
void render() {
|
||||||
|
if (p.width() != 17 || p.depth() != 17) throw new IllegalArgumentException("Generation21 amenities require the certified 17x17 footprint");
|
||||||
|
// Keep the proven pool containment, excavation steps and garage ramps. All new ornament is
|
||||||
|
// composed before connection resolution, so clipping cannot split a rail or window design.
|
||||||
|
var north = new LostCityPlan.Parcel(p.kind(), p.minX(), p.minZ(), 17, 17, p.floorY(), p.floors(), 0,
|
||||||
|
p.seed(), p.foundationDepth(), p.surfaceHeights());
|
||||||
|
LostCitySurface16.render(north, new LostCityRenderer.BlockWriter() {
|
||||||
|
@Override public void set(int x, int y, int z, BlockState state) {
|
||||||
|
Exterior.this.set(x, y, z, state);
|
||||||
|
}
|
||||||
|
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { Exterior.this.chest(x, y, z, table, seed); }
|
||||||
|
});
|
||||||
|
// The north threshold is a level promenade, shared by every admitted parcel.
|
||||||
|
for (int x = 0; x <= 16; x++) set(x, 0, 0, x % 4 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS);
|
||||||
|
switch (p.kind()) {
|
||||||
|
case PARK -> park();
|
||||||
|
case PARK_GARDEN -> garden();
|
||||||
|
case PARK_PLAYGROUND -> playground();
|
||||||
|
case PARK_MEMORIAL -> memorial();
|
||||||
|
case PLAZA -> plaza();
|
||||||
|
case MARKET -> market();
|
||||||
|
case PARKING -> garage();
|
||||||
|
case POOL -> { if (p.foundationDepth() >= 8) pool(); else park(); }
|
||||||
|
case EXCAVATION -> { if (p.foundationDepth() >= 8) excavation(); else park(); }
|
||||||
|
default -> throw new IllegalArgumentException("Not an exterior amenity");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void paths() {
|
||||||
|
for (int n = 1; n <= 16; n++) for (int across = 7; across <= 9; across++) {
|
||||||
|
set(across, 0, n, across == 8 ? Blocks.POLISHED_TUFF : Blocks.TUFF_BRICKS);
|
||||||
|
set(n, 0, across, across == 8 ? Blocks.POLISHED_TUFF : Blocks.TUFF_BRICKS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void bench(int x, int y, int z, int length, Direction view) {
|
||||||
|
for (int dx = 0; dx < length; dx++) set(x + dx, y, z, Blocks.OAK_STAIRS.defaultBlockState()
|
||||||
|
.setValue(BlockStateProperties.HORIZONTAL_FACING, view.getOpposite()));
|
||||||
|
set(x - 1, y, z, Blocks.TUFF_BRICK_SLAB); set(x + length, y, z, Blocks.TUFF_BRICK_SLAB);
|
||||||
|
}
|
||||||
|
void lamp(int x, int z) {
|
||||||
|
set(x, 1, z, Blocks.CHISELED_TUFF_BRICKS); box(x, 2, z, x, 3, z, Blocks.OAK_FENCE);
|
||||||
|
set(x, 4, z, Blocks.OAK_PLANKS); set(x, 5, z, Blocks.LANTERN);
|
||||||
|
}
|
||||||
|
void park() {
|
||||||
|
paths(); bench(10, 1, 3, 3, Direction.SOUTH); bench(10, 1, 11, 3, Direction.NORTH);
|
||||||
|
lamp(5, 11); lamp(11, 5);
|
||||||
|
// A small oak screen joins the existing trees without introducing another wood palette.
|
||||||
|
box(3, 1, 12, 3, 4, 12, Blocks.OAK_LOG);
|
||||||
|
for (int x = 2; x <= 4; x++) for (int z = 11; z <= 13; z++)
|
||||||
|
set(x, 5, z, Blocks.OAK_LEAVES.defaultBlockState().setValue(BlockStateProperties.PERSISTENT, true));
|
||||||
|
for (int z = 2; z <= 5; z++) set(6, 0, z, Blocks.POLISHED_ANDESITE);
|
||||||
|
}
|
||||||
|
void garden() {
|
||||||
|
paths();
|
||||||
|
// Four parterres have low carved kerbs, soil beds and a shared central promenade.
|
||||||
|
for (int x0 : new int[]{2, 10}) for (int z0 : new int[]{2, 10}) {
|
||||||
|
for (int n = 0; n < 5; n++) {
|
||||||
|
set(x0 + n, 0, z0, Blocks.SMOOTH_STONE); set(x0 + n, 0, z0 + 4, Blocks.SMOOTH_STONE);
|
||||||
|
set(x0, 0, z0 + n, Blocks.SMOOTH_STONE); set(x0 + 4, 0, z0 + n, Blocks.SMOOTH_STONE);
|
||||||
|
}
|
||||||
|
set(x0, 1, z0, Blocks.CHISELED_TUFF); set(x0 + 4, 1, z0 + 4, Blocks.CHISELED_TUFF);
|
||||||
|
}
|
||||||
|
bench(7, 1, 15, 3, Direction.NORTH);
|
||||||
|
// Pergola along the west planting bed, not across a circulation route.
|
||||||
|
for (int z : new int[]{2, 6}) box(1, 1, z, 1, 3, z, Blocks.STRIPPED_OAK_LOG);
|
||||||
|
box(1, 4, 2, 1, 4, 6, Blocks.OAK_SLAB); lamp(15, 8);
|
||||||
|
}
|
||||||
|
void playground() {
|
||||||
|
paths();
|
||||||
|
for (int x = 1; x <= 7; x++) { set(x, 0, 1, Blocks.OAK_PLANKS); set(x, 0, 7, Blocks.OAK_PLANKS); }
|
||||||
|
for (int z = 1; z <= 7; z++) { set(1, 0, z, Blocks.OAK_PLANKS); set(7, 0, z, Blocks.OAK_PLANKS); }
|
||||||
|
// The hanging seat's upper half meets the chain above; a bottom slab would leave a gap.
|
||||||
|
set(4, 2, 4, Blocks.OAK_SLAB.defaultBlockState().setValue(BlockStateProperties.SLAB_TYPE, SlabType.TOP));
|
||||||
|
// The last ascending step used to touch its neighbors only diagonally, over empty air.
|
||||||
|
set(12, 1, 10, Blocks.OAK_PLANKS);
|
||||||
|
// Braced climbing platform: low guards leave the three front steps open.
|
||||||
|
for (int x : new int[]{11, 13}) set(x, 4, 13, Blocks.OAK_FENCE);
|
||||||
|
set(12, 4, 13, Blocks.OAK_FENCE);
|
||||||
|
set(11, 4, 11, Blocks.OAK_FENCE); set(13, 4, 11, Blocks.OAK_FENCE);
|
||||||
|
bench(3, 1, 13, 3, Direction.NORTH); lamp(15, 3);
|
||||||
|
}
|
||||||
|
void plaza() {
|
||||||
|
for (int x = 1; x <= 15; x++) for (int z = 1; z <= 15; z++) {
|
||||||
|
int d = Math.max(Math.abs(x - 8), Math.abs(z - 8));
|
||||||
|
set(x, 0, z, d == 3 || d == 6 ? Blocks.POLISHED_ANDESITE : d == 4 || d == 7 ? Blocks.TUFF_BRICKS : Blocks.SMOOTH_STONE);
|
||||||
|
}
|
||||||
|
// Dry civic fountain: bevelled bowl, carved pedestal, projecting cap.
|
||||||
|
for (int x = 6; x <= 10; x++) for (int z = 6; z <= 10; z++) {
|
||||||
|
if (x != 6 && x != 10 && z != 6 && z != 10) continue;
|
||||||
|
boolean corner = (x == 6 || x == 10) && (z == 6 || z == 10);
|
||||||
|
set(x, 1, z, corner ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICK_SLAB);
|
||||||
|
}
|
||||||
|
set(8, 1, 8, Blocks.CHISELED_TUFF); set(8, 2, 8, Blocks.CHISELED_TUFF_BRICKS);
|
||||||
|
set(8, 3, 8, Blocks.POLISHED_TUFF_SLAB); lamp(2, 12); lamp(14, 4);
|
||||||
|
}
|
||||||
|
void memorial() {
|
||||||
|
// The memorial keeps its open central approach and gains stepped stone shoulders.
|
||||||
|
for (int x = 1; x <= 15; x++) for (int z = 1; z <= 15; z++)
|
||||||
|
set(x, 0, z, x == 5 || x == 11 || z == 5 || z == 11 ? Blocks.TUFF_BRICKS : Blocks.POLISHED_ANDESITE);
|
||||||
|
for (int x = 7; x <= 9; x++) {
|
||||||
|
set(x, 1, 7, Blocks.TUFF_BRICK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
|
||||||
|
set(x, 1, 9, Blocks.TUFF_BRICK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
|
||||||
|
}
|
||||||
|
set(8, 2, 8, Blocks.CHISELED_TUFF); set(8, 3, 8, Blocks.CHISELED_TUFF_BRICKS);
|
||||||
|
set(8, 4, 8, Blocks.CHISELED_STONE_BRICKS); set(8, 5, 8, Blocks.SMOOTH_STONE_SLAB);
|
||||||
|
lamp(3, 8); lamp(13, 8);
|
||||||
|
}
|
||||||
|
void market() {
|
||||||
|
for (int x = 1; x <= 15; x++) for (int z = 1; z <= 15; z++)
|
||||||
|
set(x, 0, z, x >= 7 && x <= 9 || z >= 7 && z <= 9 ? Blocks.TUFF_BRICKS : Blocks.POLISHED_ANDESITE);
|
||||||
|
for (int x0 : new int[]{2, 11}) for (int z0 : new int[]{2, 11}) {
|
||||||
|
for (int x = x0; x <= x0 + 3; x++) for (int z = z0; z <= z0 + 2; z++)
|
||||||
|
set(x, 4, z, Blocks.OAK_SLAB);
|
||||||
|
for (int x : new int[]{x0, x0 + 3}) {
|
||||||
|
set(x, 1, z0, Blocks.CHISELED_TUFF_BRICKS);
|
||||||
|
box(x, 2, z0, x, 3, z0, Blocks.STRIPPED_OAK_LOG);
|
||||||
|
}
|
||||||
|
// Only one roof layer: the former raised bottom slab was separated by half a block.
|
||||||
|
// Rear posts and a full-width beam make the canopy's load path visible from the stall.
|
||||||
|
for (int x : new int[]{x0, x0 + 3}) box(x, 2, z0 + 2, x, 3, z0 + 2, Blocks.STRIPPED_OAK_LOG);
|
||||||
|
box(x0, 3, z0, x0 + 3, 3, z0, Blocks.OAK_PLANKS);
|
||||||
|
box(x0, 3, z0 + 2, x0 + 3, 3, z0 + 2, Blocks.OAK_PLANKS);
|
||||||
|
}
|
||||||
|
// The shared chest replaces this stall's counter: a pot cannot sit on its recessed lid.
|
||||||
|
set(13, 2, 13, Blocks.AIR); set(12, 2, 13, Blocks.FLOWER_POT);
|
||||||
|
lamp(1, 8); lamp(15, 8);
|
||||||
|
}
|
||||||
|
void pool() {
|
||||||
|
// Each submerged tread bears on a masonry riser down to the basin floor;
|
||||||
|
// water between diagonal stairs is not a structural connection.
|
||||||
|
for (int step = 1; step <= 3; step++) for (int x = 7; x <= 9; x++)
|
||||||
|
for (int y = -3; y < -step; y++) set(x, y, step + 4, Blocks.SMOOTH_STONE);
|
||||||
|
for (int x = 3; x <= 13; x++) for (int z = 4; z <= 13; z++) {
|
||||||
|
if (x == 3 || x == 13 || z == 4 || z == 13) set(x, 0, z, Blocks.SMOOTH_STONE);
|
||||||
|
}
|
||||||
|
// Changing/showers canopy stands beside the deck; it never enters the sealed basin.
|
||||||
|
for (int x : new int[]{10, 15}) box(x, 1, 1, x, 3, 1, Blocks.STRIPPED_OAK_LOG);
|
||||||
|
for (int x : new int[]{10, 15}) box(x, 1, 3, x, 3, 3, Blocks.STRIPPED_OAK_LOG);
|
||||||
|
box(10, 3, 1, 15, 3, 1, Blocks.OAK_PLANKS);
|
||||||
|
box(10, 3, 3, 15, 3, 3, Blocks.OAK_PLANKS);
|
||||||
|
box(10, 4, 1, 15, 4, 3, Blocks.OAK_SLAB);
|
||||||
|
for (int x = 11; x <= 14; x++) set(x, 1, 1, Blocks.WATER_CAULDRON.defaultBlockState()
|
||||||
|
.setValue(BlockStateProperties.LEVEL_CAULDRON, 2));
|
||||||
|
set(15, 4, 2, Blocks.OAK_PLANKS); set(15, 5, 2, Blocks.LANTERN);
|
||||||
|
for (int z : new int[]{5, 9}) set(15, 1, z, Blocks.OAK_STAIRS.defaultBlockState()
|
||||||
|
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST));
|
||||||
|
box(2, 0, 14, 14, 0, 15, Blocks.TUFF_BRICKS);
|
||||||
|
}
|
||||||
|
void excavation() {
|
||||||
|
// Each descending stone step rests on a small riser down to the certified pit floor.
|
||||||
|
for (int step = 1; step <= 5; step++) for (int x = 7; x <= 9; x++)
|
||||||
|
for (int y = -4; y < -step; y++) set(x, y, step + 2, Blocks.STONE_BRICKS);
|
||||||
|
// Reinforced temporary footings and timber trestles stay out of the central descent.
|
||||||
|
for (int x : new int[]{4, 12}) {
|
||||||
|
box(x, -4, 11, x, -1, 11, Blocks.CHISELED_STONE_BRICKS);
|
||||||
|
set(x, 0, 11, Blocks.OAK_PLANKS);
|
||||||
|
}
|
||||||
|
box(3, 3, 13, 13, 3, 13, Blocks.STRIPPED_OAK_LOG);
|
||||||
|
for (int x : new int[]{3, 13}) box(x, -4, 13, x, 2, 13, Blocks.STONE_BRICKS);
|
||||||
|
// Replace the single beam with an open construction shelter, one canopy on grounded posts.
|
||||||
|
// All four trestles start on the excavated floor, not on the missing former ground plane.
|
||||||
|
for (int x : new int[]{3, 13}) box(x, -4, 10, x, 2, 10, Blocks.STONE_BRICKS);
|
||||||
|
box(3, 3, 10, 13, 3, 10, Blocks.STRIPPED_OAK_LOG);
|
||||||
|
for (int x : new int[]{3, 8, 13}) box(x, 3, 10, x, 3, 13, Blocks.OAK_PLANKS);
|
||||||
|
box(3, 4, 10, 13, 4, 13, Blocks.OAK_SLAB);
|
||||||
|
set(3, 4, 13, Blocks.OAK_PLANKS);
|
||||||
|
set(3, 5, 13, Blocks.LANTERN);
|
||||||
|
set(1, 1, 11, Blocks.GRINDSTONE); set(1, 1, 10, Blocks.CRAFTING_TABLE);
|
||||||
|
}
|
||||||
|
void garage() {
|
||||||
|
int base = p.foundationDepth() >= 8 ? -5 : 0;
|
||||||
|
// The ramp's five-wide masonry wedge joins every tread to the garage floor.
|
||||||
|
if (base < 0) for (int z = 0; z <= 4; z++) for (int x = 6; x <= 10; x++)
|
||||||
|
for (int y = base + 1; y < -z; y++) set(x, y, z, Blocks.STONE_BRICKS);
|
||||||
|
for (int x : new int[]{3, 13}) for (int z : new int[]{3, 13}) {
|
||||||
|
box(x, base + 1, z, x, base + 4, z, Blocks.POLISHED_ANDESITE);
|
||||||
|
set(x, base + 1, z, Blocks.CHISELED_TUFF_BRICKS);
|
||||||
|
}
|
||||||
|
for (int z = 6; z <= 15; z++) for (int x = 1; x <= 15; x++)
|
||||||
|
if (x <= 5 || x >= 11) set(x, base, z, z % 4 == 0 ? Blocks.SMOOTH_STONE : Blocks.POLISHED_ANDESITE);
|
||||||
|
// A ribbed structural fascia and ventilation blades make this an open modern garage.
|
||||||
|
for (int x = 0; x <= 16; x++) {
|
||||||
|
// The underground ramp cuts through this former ceiling strip: never paint it back.
|
||||||
|
if (base == 0 || x < 6 || x > 10)
|
||||||
|
set(x, base + 5, 0, x % 4 == 0 ? Blocks.CHISELED_STONE_BRICKS : Blocks.SMOOTH_STONE);
|
||||||
|
if (x < 6 || x > 10) set(x, base + 3, 0, Blocks.IRON_TRAPDOOR.defaultBlockState()
|
||||||
|
.setValue(BlockStateProperties.OPEN, true).setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
|
||||||
|
}
|
||||||
|
for (int x : new int[]{5, 11}) {
|
||||||
|
set(x, base + 4, 9, Blocks.SMOOTH_STONE);
|
||||||
|
set(x, base + 3, 9, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.city;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||||
|
import fr.koka.sanctuary.expansion.ExpansionTerrain;
|
||||||
|
import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
|
import net.minecraft.world.level.NoiseColumn;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||||
|
import net.minecraft.world.level.levelgen.RandomState;
|
||||||
|
|
||||||
|
/** Generation21 bounded noise-column cache shared by surface and underground planning, never loaded chunks. */
|
||||||
|
public final class LostCityTerrain21 implements LostCityUnderground16.Terrain, LostCityPlanner16.Terrain {
|
||||||
|
public static final int MAX_COLUMNS = 24_096;
|
||||||
|
private final ExpansionIsland island;
|
||||||
|
private final NoiseBasedChunkGenerator generator;
|
||||||
|
private final RandomState random;
|
||||||
|
private final LevelHeightAccessor height;
|
||||||
|
private final Predicate<BlockPos> protectedVolume;
|
||||||
|
private final Map<Long, NoiseColumn> columns = new HashMap<>();
|
||||||
|
private final BlockPos fluidCandidate;
|
||||||
|
|
||||||
|
public LostCityTerrain21(ExpansionIsland island, NoiseBasedChunkGenerator generator, RandomState random,
|
||||||
|
LevelHeightAccessor height, Predicate<BlockPos> protectedVolume) {
|
||||||
|
this.island = island; this.generator = generator; this.random = random;
|
||||||
|
this.height = height; this.protectedVolume = protectedVolume;
|
||||||
|
fluidCandidate = ExpansionTerrain.pocketCandidate(island);
|
||||||
|
}
|
||||||
|
|
||||||
|
private NoiseColumn column(int x, int z) {
|
||||||
|
if (!owns(x, z)) return null;
|
||||||
|
long key = ((long) x << 32) ^ (z & 0xffffffffL);
|
||||||
|
var cached = columns.get(key);
|
||||||
|
if (cached != null) return cached;
|
||||||
|
if (columns.size() >= MAX_COLUMNS) return null;
|
||||||
|
var result = generator.getBaseColumn(x, z, height, random);
|
||||||
|
columns.put(key, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LostCityPlanner.Ground ground(int x, int z) {
|
||||||
|
var column = column(x, z);
|
||||||
|
if (column == null) return new LostCityPlanner.Ground(0, 0, false, true);
|
||||||
|
int top = 383;
|
||||||
|
while (top > 0 && column.getBlock(top).isAir()) top--;
|
||||||
|
int bottom = top;
|
||||||
|
while (bottom > 0 && top - bottom < 24 && column.getBlock(bottom).isSolid()
|
||||||
|
&& column.getBlock(bottom).getFluidState().isEmpty()) bottom--;
|
||||||
|
boolean dry = column.getBlock(top).isSolid() && column.getBlock(top).getFluidState().isEmpty();
|
||||||
|
boolean water = island.origin() && top >= 168 && dry && top - bottom >= 6
|
||||||
|
&& PopulationHydrologyRuntime.protects(generator, random, new BlockPos(x, top, z), 1, 30, 12);
|
||||||
|
return new LostCityPlanner.Ground(top, top - bottom, dry, water);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public boolean solid(int x, int y, int z) {
|
||||||
|
if (y < 0 || y >= 384) return false;
|
||||||
|
var column = column(x, z);
|
||||||
|
return column != null && column.getBlock(y).isSolid() && column.getBlock(y).getFluidState().isEmpty();
|
||||||
|
}
|
||||||
|
@Override public boolean protectedAt(int x, int y, int z) {
|
||||||
|
if (y < 0 || y >= 384) return true;
|
||||||
|
var column = column(x, z);
|
||||||
|
if (column == null || !column.getBlock(y).getFluidState().isEmpty()) return true;
|
||||||
|
if (!island.origin() && island.relief().equals("volcano")
|
||||||
|
&& Math.abs(x - fluidCandidate.getX()) <= 2 && Math.abs(z - fluidCandidate.getZ()) <= 2) return true;
|
||||||
|
return protectedVolume.test(new BlockPos(x, y, z));
|
||||||
|
}
|
||||||
|
@Override public boolean exteriorAir(int x, int y, int z, int dx, int dz) {
|
||||||
|
if (Math.abs(dx) + Math.abs(dz) != 1 || y < 1 || y >= 380 || !owns(x, z)) return false;
|
||||||
|
for (int distance = 0; distance <= 32; distance++) {
|
||||||
|
int px = x + dx * distance, pz = z + dz * distance;
|
||||||
|
if (!owns(px, pz)) return false; // Never infer exterior air in another present or future owner.
|
||||||
|
var sample = column(px, pz);
|
||||||
|
if (sample == null) return false;
|
||||||
|
for (int dy = -3; dy < 3; dy++) if (!sample.getBlock(y + dy).isAir()
|
||||||
|
|| protectedVolume.test(new BlockPos(px, y + dy, pz))) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
@Override public boolean owns(int x, int z) { return island.contains(x, z); }
|
||||||
|
public int columnsUsed() { return columns.size(); }
|
||||||
|
}
|
||||||
+6
-1
@@ -19,7 +19,7 @@ public final class HeritageCommands19 {
|
|||||||
var level = source.getLevel();
|
var level = source.getLevel();
|
||||||
if (!Level.OVERWORLD.equals(level.dimension())
|
if (!Level.OVERWORLD.equals(level.dimension())
|
||||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||||
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20) || !generator.experimentalStructures()) {
|
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21) || !generator.experimentalStructures()) {
|
||||||
source.sendFailure(Component.translatable("sanctuary.heritage.unavailable"));
|
source.sendFailure(Component.translatable("sanctuary.heritage.unavailable"));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,11 @@ public final class HeritageCommands19 {
|
|||||||
for (var site : plan.sites()) source.sendSuccess(() -> Component.translatable("sanctuary.heritage.entry",
|
for (var site : plan.sites()) source.sendSuccess(() -> Component.translatable("sanctuary.heritage.entry",
|
||||||
Component.translatable("sanctuary.heritage.kind." + site.kind().name().toLowerCase(Locale.ROOT)),
|
Component.translatable("sanctuary.heritage.kind." + site.kind().name().toLowerCase(Locale.ROOT)),
|
||||||
site.entry().x(), site.entry().y(), site.entry().z()), false);
|
site.entry().x(), site.entry().y(), site.entry().z()), false);
|
||||||
|
if (generator.rootGeneration() == 21) 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();
|
return plan.sites().size();
|
||||||
}))));
|
}))));
|
||||||
}
|
}
|
||||||
|
|||||||
+185
@@ -0,0 +1,185 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.heritage;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Set;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.nbt.ListTag;
|
||||||
|
import net.minecraft.nbt.Tag;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.StructureManager;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||||
|
import net.minecraft.world.level.storage.loot.LootTable;
|
||||||
|
|
||||||
|
/** Self-contained schema21 geometry. Reload never needs the planner or a distant route destination. */
|
||||||
|
public final class HeritagePiece21 extends StructurePiece {
|
||||||
|
private static final Set<String> SITE_KEYS = Set.of("Schema", "Type", "Id", "Kind", "MinX", "FloorY", "MinZ",
|
||||||
|
"Width", "Depth", "Height", "Facing", "Seed", "FoundationDepth", "Supports", "Entry");
|
||||||
|
private static final Set<String> ROUTE_KEYS = Set.of("Schema", "Type", "Id", "FromId", "ToId", "Kind",
|
||||||
|
"StartChunkX", "StartChunkZ", "Cells", "Seed");
|
||||||
|
private static final Set<String> SUPPORT_KEYS = Set.of("X", "MinY", "Z", "MaxY");
|
||||||
|
private static final Set<String> ENTRY_KEYS = Set.of("X", "Y", "Z", "Facing");
|
||||||
|
private static final Set<String> CELL_KEYS = Set.of("X", "FloorY", "Z", "FoundationDepth", "Rail", "RailPresent");
|
||||||
|
private final HeritagePlan19.Site site;
|
||||||
|
private final HeritagePlan19.Segment segment;
|
||||||
|
|
||||||
|
public HeritagePiece21(HeritagePlan19.Site site) {
|
||||||
|
super(HeritageStructures19.PIECE21, 0, bounds(site));
|
||||||
|
this.site = site; this.segment = null; setOrientation(null);
|
||||||
|
}
|
||||||
|
public HeritagePiece21(HeritagePlan19.Segment segment) {
|
||||||
|
super(HeritageStructures19.PIECE21, 0, bounds(segment));
|
||||||
|
this.site = null; this.segment = segment; setOrientation(null);
|
||||||
|
}
|
||||||
|
public HeritagePiece21(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||||
|
super(HeritageStructures19.PIECE21, tag);
|
||||||
|
var payload = tag.getCompound("SanctuaryHeritage").orElseThrow(() -> new IllegalArgumentException("Missing saved heritage geometry"));
|
||||||
|
String type = string(payload, "Type");
|
||||||
|
if (type.equals("SITE")) { site = decodeSite(payload); segment = null; }
|
||||||
|
else if (type.equals("ROUTE")) { site = null; segment = decodeSegment(payload); }
|
||||||
|
else throw new IllegalArgumentException("Unknown heritage piece type");
|
||||||
|
if (!getBoundingBox().equals(isSite() ? bounds(site) : bounds(segment)))
|
||||||
|
throw new IllegalArgumentException("Heritage geometry disagrees with its saved native bounds");
|
||||||
|
setOrientation(null);
|
||||||
|
}
|
||||||
|
public boolean isSite() { return site != null; }
|
||||||
|
public HeritagePlan19.Site site() { if (site == null) throw new IllegalStateException("This heritage piece is a route"); return site; }
|
||||||
|
public HeritagePlan19.Segment segment() { if (segment == null) throw new IllegalStateException("This heritage piece is a site"); return segment; }
|
||||||
|
@Override public BlockPos getLocatorPosition() {
|
||||||
|
if (isSite()) return locator(site);
|
||||||
|
var cell = segment.cells().getFirst(); return new BlockPos(cell.x(), cell.floorY() + 1, cell.z());
|
||||||
|
}
|
||||||
|
public static BlockPos locator(HeritagePlan19.Site site) { return new BlockPos(site.entry().x(), site.entry().y(), site.entry().z()); }
|
||||||
|
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||||
|
tag.put("SanctuaryHeritage", isSite() ? encodeSite(site) : encodeSegment(segment));
|
||||||
|
}
|
||||||
|
public static CompoundTag encodeSite(HeritagePlan19.Site site) {
|
||||||
|
var tag = header("SITE", site.id(), site.kind().name(), site.seed());
|
||||||
|
tag.putInt("MinX", site.minX()); tag.putInt("FloorY", site.floorY()); tag.putInt("MinZ", site.minZ());
|
||||||
|
tag.putInt("Width", site.width()); tag.putInt("Depth", site.depth()); tag.putInt("Height", site.height());
|
||||||
|
tag.putInt("Facing", site.facing()); tag.putInt("FoundationDepth", site.foundationDepth());
|
||||||
|
var entry = new CompoundTag(); entry.putInt("X", site.entry().x()); entry.putInt("Y", site.entry().y());
|
||||||
|
entry.putInt("Z", site.entry().z()); entry.putInt("Facing", site.entry().facing()); tag.put("Entry", entry);
|
||||||
|
var supports = new ListTag();
|
||||||
|
for (var support : site.supports()) {
|
||||||
|
var value = new CompoundTag(); value.putInt("X", support.x()); value.putInt("MinY", support.minY());
|
||||||
|
value.putInt("Z", support.z()); value.putInt("MaxY", support.maxY()); supports.add(value);
|
||||||
|
}
|
||||||
|
tag.put("Supports", supports); return tag;
|
||||||
|
}
|
||||||
|
public static CompoundTag encodeSegment(HeritagePlan19.Segment segment) {
|
||||||
|
var tag = header("ROUTE", segment.id(), segment.kind().name(), segment.seed());
|
||||||
|
tag.putString("FromId", segment.fromId()); tag.putString("ToId", segment.toId());
|
||||||
|
tag.putInt("StartChunkX", segment.startChunkX()); tag.putInt("StartChunkZ", segment.startChunkZ());
|
||||||
|
var cells = new ListTag();
|
||||||
|
for (var cell : segment.cells()) {
|
||||||
|
var value = new CompoundTag(); value.putInt("X", cell.x()); value.putInt("FloorY", cell.floorY());
|
||||||
|
value.putInt("Z", cell.z()); value.putInt("FoundationDepth", cell.foundationDepth());
|
||||||
|
value.putString("Rail", cell.rail().name()); value.putBoolean("RailPresent", cell.railPresent()); cells.add(value);
|
||||||
|
}
|
||||||
|
tag.put("Cells", cells); return tag;
|
||||||
|
}
|
||||||
|
public static HeritagePlan19.Site decodeSite(CompoundTag tag) {
|
||||||
|
schema(tag, "SITE", SITE_KEYS);
|
||||||
|
var entry = compound(tag, "Entry", ENTRY_KEYS);
|
||||||
|
var saved = list(tag, "Supports", 9); var supports = new ArrayList<HeritagePlan19.Support>();
|
||||||
|
for (var value : saved) {
|
||||||
|
var support = element(value, SUPPORT_KEYS);
|
||||||
|
supports.add(new HeritagePlan19.Support(coordinate(support, "X"), integer(support, "MinY"),
|
||||||
|
coordinate(support, "Z"), integer(support, "MaxY")));
|
||||||
|
}
|
||||||
|
return new HeritagePlan19.Site(id(tag, "Id"), HeritagePlan19.Kind.valueOf(string(tag, "Kind")),
|
||||||
|
coordinate(tag, "MinX"), integer(tag, "FloorY"), coordinate(tag, "MinZ"), integer(tag, "Width"),
|
||||||
|
integer(tag, "Depth"), integer(tag, "Height"), integer(tag, "Facing"), seed(tag), integer(tag, "FoundationDepth"),
|
||||||
|
supports, new HeritagePlan19.Entry(coordinate(entry, "X"), integer(entry, "Y"), coordinate(entry, "Z"), integer(entry, "Facing")));
|
||||||
|
}
|
||||||
|
public static HeritagePlan19.Segment decodeSegment(CompoundTag tag) {
|
||||||
|
schema(tag, "ROUTE", ROUTE_KEYS);
|
||||||
|
var saved = list(tag, "Cells", 256); var cells = new ArrayList<HeritagePlan19.Cell>();
|
||||||
|
for (var value : saved) {
|
||||||
|
var cell = element(value, CELL_KEYS);
|
||||||
|
type(cell, "RailPresent", Tag.TAG_BYTE);
|
||||||
|
byte present = cell.getByte("RailPresent").orElseThrow();
|
||||||
|
if (present != 0 && present != 1) throw new IllegalArgumentException("Invalid saved rail presence");
|
||||||
|
cells.add(new HeritagePlan19.Cell(coordinate(cell, "X"), integer(cell, "FloorY"), coordinate(cell, "Z"),
|
||||||
|
integer(cell, "FoundationDepth"), HeritagePlan19.Rail.valueOf(string(cell, "Rail")), present == 1));
|
||||||
|
}
|
||||||
|
String from = id(tag, "FromId"), to = id(tag, "ToId");
|
||||||
|
var kind = HeritagePlan19.RouteKind.valueOf(string(tag, "Kind"));
|
||||||
|
if (from.equals(to) && kind != HeritagePlan19.RouteKind.PATH)
|
||||||
|
throw new IllegalArgumentException("Only a local pedestrian landing may join a destination to itself");
|
||||||
|
return new HeritagePlan19.Segment(id(tag, "Id"), from, to, kind,
|
||||||
|
integer(tag, "StartChunkX"), integer(tag, "StartChunkZ"), cells, seed(tag));
|
||||||
|
}
|
||||||
|
private static CompoundTag header(String type, String id, String kind, long seed) {
|
||||||
|
var value = new CompoundTag(); value.putInt("Schema", 21); value.putString("Type", type);
|
||||||
|
value.putString("Id", id); value.putString("Kind", kind); value.putLong("Seed", seed); return value;
|
||||||
|
}
|
||||||
|
private static void schema(CompoundTag tag, String expected, Set<String> keys) {
|
||||||
|
if (!tag.keySet().equals(keys) || integer(tag, "Schema") != 21 || !string(tag, "Type").equals(expected))
|
||||||
|
throw new IllegalArgumentException("Unsupported or incomplete saved heritage schema");
|
||||||
|
}
|
||||||
|
private static CompoundTag compound(CompoundTag tag, String key, Set<String> keys) {
|
||||||
|
type(tag, key, Tag.TAG_COMPOUND); return element(tag.get(key), keys);
|
||||||
|
}
|
||||||
|
private static CompoundTag element(Tag tag, Set<String> keys) {
|
||||||
|
if (!(tag instanceof CompoundTag value) || !value.keySet().equals(keys))
|
||||||
|
throw new IllegalArgumentException("Unsupported or incomplete saved heritage element");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
private static ListTag list(CompoundTag tag, String key, int maximum) {
|
||||||
|
type(tag, key, Tag.TAG_LIST); var value = tag.getList(key).orElseThrow();
|
||||||
|
if (value.size() > maximum) throw new IllegalArgumentException("Oversized saved heritage list"); return value;
|
||||||
|
}
|
||||||
|
private static long seed(CompoundTag tag) { type(tag, "Seed", Tag.TAG_LONG); return tag.getLong("Seed").orElseThrow(); }
|
||||||
|
private static String id(CompoundTag tag, String key) {
|
||||||
|
var value = string(tag, key);
|
||||||
|
if (value.isBlank() || value.length() > 128) throw new IllegalArgumentException("Invalid saved heritage identifier"); return value;
|
||||||
|
}
|
||||||
|
private static String string(CompoundTag tag, String key) { type(tag, key, Tag.TAG_STRING); return tag.getString(key).orElseThrow(); }
|
||||||
|
private static int coordinate(CompoundTag tag, String key) {
|
||||||
|
int value = integer(tag, key);
|
||||||
|
if (value < -30_000_000 || value > 30_000_000) throw new IllegalArgumentException("Heritage coordinate outside world bounds"); return value;
|
||||||
|
}
|
||||||
|
private static int integer(CompoundTag tag, String key) { type(tag, key, Tag.TAG_INT); return tag.getInt(key).orElseThrow(); }
|
||||||
|
private static void type(CompoundTag tag, String key, byte id) {
|
||||||
|
if (tag.get(key) == null || tag.get(key).getId() != id) throw new IllegalArgumentException("Invalid saved heritage field: " + key);
|
||||||
|
}
|
||||||
|
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
|
||||||
|
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
|
||||||
|
final int x0 = isSite() ? site.minX() : segment.minX();
|
||||||
|
final int y0 = isSite() ? site.floorY() : segment.minY();
|
||||||
|
final int z0 = isSite() ? site.minZ() : segment.minZ();
|
||||||
|
var writer = new LostCityRenderer.BlockWriter() {
|
||||||
|
private BlockPos world(int x, int y, int z) { return new BlockPos(Math.addExact(x0, x), Math.addExact(y0, y), Math.addExact(z0, z)); }
|
||||||
|
@Override public void set(int x, int y, int z, BlockState block) {
|
||||||
|
var position = world(x, y, z);
|
||||||
|
if (getBoundingBox().isInside(position) && clip.isInside(position))
|
||||||
|
placeBlock(level, block, position.getX(), position.getY(), position.getZ(), clip);
|
||||||
|
}
|
||||||
|
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
|
||||||
|
var position = world(x, y, z);
|
||||||
|
if (getBoundingBox().isInside(position) && clip.isInside(position))
|
||||||
|
createChest(level, clip, RandomSource.create(seed), position, loot, null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
HeritageStructures19.placing(() -> {
|
||||||
|
if (isSite()) HeritageRenderer21.render(site, writer);
|
||||||
|
else HeritageRenderer21.renderSegment(segment, writer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public static BoundingBox bounds(HeritagePlan19.Site site) {
|
||||||
|
return new BoundingBox(site.minX(), site.boundsMinY(), site.minZ(), site.maxX(), site.boundsMaxY(), site.maxZ());
|
||||||
|
}
|
||||||
|
public static BoundingBox bounds(HeritagePlan19.Segment segment) {
|
||||||
|
return new BoundingBox(segment.minX(), segment.boundsMinY(), segment.minZ(), segment.maxX(), segment.boundsMaxY(), segment.maxZ());
|
||||||
|
}
|
||||||
|
}
|
||||||
+616
@@ -0,0 +1,616 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.heritage;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.*;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/** Generation21 only. A finite catalogue is admitted before complete, repairable routes are published.
|
||||||
|
* Everything samples native columns; no chunk, player object or future continent is requested. */
|
||||||
|
public final class HeritagePlanner21 {
|
||||||
|
public static final int MAX_COLUMNS = HeritagePlan19.MAX_COLUMNS, MAX_SAMPLES = HeritagePlan19.MAX_SAMPLES;
|
||||||
|
// Local17x17 sites may cut8 blocks, with the same at-most8-block slab and certified short piers.
|
||||||
|
// Three-wide paths may cut4/fill5; grade stays one block, and water/old construction is never waived.
|
||||||
|
public static final int MAX_SITE_CUT = 8, MAX_ROUTE_CUT = 4, MAX_ROUTE_FILL = 5;
|
||||||
|
private static final int SITE_COLUMNS = 11_000, SITE_SAMPLES = 540_000;
|
||||||
|
private static final int INVALID = Integer.MIN_VALUE;
|
||||||
|
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
|
||||||
|
private HeritagePlanner21() {}
|
||||||
|
public interface Terrain extends HeritagePlanner19.Terrain {
|
||||||
|
boolean owns(int x, int z);
|
||||||
|
int surfaceY(int x, int z);
|
||||||
|
boolean solid(int x, int y, int z);
|
||||||
|
boolean protectedAt(int x, int y, int z);
|
||||||
|
default List<Landmark> landmarks() { return List.of(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HeritagePlan19 plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
|
||||||
|
if (!List.of(512, 724, 1024).contains(diameter)) throw new IllegalArgumentException("Heritage21 requires a starter size");
|
||||||
|
var probe = new Probe(terrain, cx, cz, diameter);
|
||||||
|
List<Landmark> landmarks = List.copyOf(terrain.landmarks()).stream().limit(16).toList();
|
||||||
|
var sites = new ArrayList<Site>(); var cells = new LinkedHashMap<Long, OwnedCell>(); var diagnostics = new EnumMap<Kind, String>(Kind.class);
|
||||||
|
for (var kind : Kind.values()) {
|
||||||
|
int remaining = Kind.values().length - kind.ordinal() - 1;
|
||||||
|
probe.columnLimit = SITE_COLUMNS - remaining * (SITE_COLUMNS / 14);
|
||||||
|
probe.sampleLimit = SITE_SAMPLES - remaining * (SITE_SAMPLES / 14);
|
||||||
|
SiteDraft admitted = null; int attempts = 0;
|
||||||
|
var reasons = new java.util.TreeMap<String, Integer>(); probe.reasons = reasons;
|
||||||
|
try {
|
||||||
|
for (var candidate : candidates(seed, kind, cx, cz, diameter, sites, landmarks, probe)) {
|
||||||
|
attempts++;
|
||||||
|
admitted = site(seed, kind, candidate, sites, landmarks, cells, probe);
|
||||||
|
if (admitted != null) break;
|
||||||
|
}
|
||||||
|
} catch (BudgetExceeded exhausted) { reasons.merge(exhausted.columns ? "column-budget" : "probe-budget", 1, Integer::sum); }
|
||||||
|
if (admitted != null) {
|
||||||
|
sites.add(admitted.site());
|
||||||
|
Request landing = new Request(admitted.site().id() + "_landing", admitted.site().id(), admitted.site().id(), RouteKind.PATH);
|
||||||
|
for (var cell : admitted.landings()) cells.put(key(cell.x(), cell.z()), new OwnedCell(landing, cell));
|
||||||
|
}
|
||||||
|
diagnostics.put(kind, (admitted == null ? "not admitted" : "admitted") + "; candidates=" + attempts + "; " + reasons);
|
||||||
|
}
|
||||||
|
probe.columnLimit = MAX_COLUMNS; probe.sampleLimit = MAX_SAMPLES;
|
||||||
|
var links = new ArrayList<Link>();
|
||||||
|
var routeResults = new LinkedHashMap<String, String>();
|
||||||
|
var routeReasons = new java.util.TreeMap<String, Integer>(); probe.reasons = routeReasons;
|
||||||
|
Map<String, Endpoint> endpoints = new LinkedHashMap<>();
|
||||||
|
for (var site : sites) endpoints.put(site.id(), new Endpoint(site.id(), site.entry(), false));
|
||||||
|
for (var landmark : landmarks) if (!endpoints.containsKey(landmark.id()))
|
||||||
|
endpoints.put(landmark.id(), new Endpoint(landmark.id(), landmark.entry(), true));
|
||||||
|
var requests = new ArrayList<>(requests(sites, landmarks).stream()
|
||||||
|
.filter(r -> endpoints.containsKey(r.from()) && endpoints.containsKey(r.to())).toList());
|
||||||
|
var components = new Components(endpoints.keySet());
|
||||||
|
// Multiple exterior entries into the certified connected city represent one already walkable destination.
|
||||||
|
String firstCity = null;
|
||||||
|
for (var landmark : landmarks) if (landmark.kind() == LandmarkKind.CITY) {
|
||||||
|
if (firstCity == null) firstCity = landmark.id(); else components.join(firstCity, landmark.id());
|
||||||
|
}
|
||||||
|
Request longFreight = requests.stream().filter(r -> r.kind() == RouteKind.FREIGHT
|
||||||
|
&& span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry()) >= diameter / 4)
|
||||||
|
.max(Comparator.comparingInt(r -> span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry()))).orElse(null);
|
||||||
|
var siteIds = new HashSet<String>(); sites.forEach(site -> siteIds.add(site.id()));
|
||||||
|
var addressed = new HashSet<String>();
|
||||||
|
var destinationAttempts = new HashMap<String, Integer>();
|
||||||
|
int attempted = 0;
|
||||||
|
while (!requests.isEmpty() && attempted < 36 && links.size() < 24) {
|
||||||
|
// Give each isolated site a first useful route before spending its columns on a cycle.
|
||||||
|
// Repeated city gates already belong to one walkable component; no redundant freight bypass is needed.
|
||||||
|
requests.removeIf(r -> components.same(r.from(), r.to()));
|
||||||
|
if (requests.isEmpty()) break;
|
||||||
|
requests.sort(Comparator.comparingInt((Request r) -> isolatedSites(r, siteIds, addressed) > 0
|
||||||
|
&& isolatedAttempts(r, siteIds, addressed, destinationAttempts) == 0 ? 0 : 1)
|
||||||
|
.thenComparingInt(r -> span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry()))
|
||||||
|
.thenComparingInt(r -> r.kind() == RouteKind.FREIGHT ? 0 : 1).thenComparing(Request::id));
|
||||||
|
var request = attempted == 0 && longFreight != null ? longFreight : requests.getFirst(); requests.remove(request);
|
||||||
|
attempted++;
|
||||||
|
int distance = Math.abs(endpoints.get(request.from()).entry().x() - endpoints.get(request.to()).entry().x())
|
||||||
|
+ Math.abs(endpoints.get(request.from()).entry().z() - endpoints.get(request.to()).entry().z());
|
||||||
|
boolean longAxis = span(endpoints.get(request.from()).entry(), endpoints.get(request.to()).entry()) >= diameter / 4;
|
||||||
|
int isolated = isolatedSites(request, siteIds, addressed);
|
||||||
|
var waiting = new HashSet<String>();
|
||||||
|
for (var next : requests) for (var id : List.of(next.from(), next.to()))
|
||||||
|
if (siteIds.contains(id) && !addressed.contains(id) && !id.equals(request.from()) && !id.equals(request.to())) waiting.add(id);
|
||||||
|
int reserve = Math.min(Math.min(waiting.size(), 8), 36 - attempted) * 100;
|
||||||
|
int allowance = longAxis && attempted == 1 ? 3_800 : isolated > 0 ? Math.clamp(distance * 5 + 350, 900, 2_400) : 900;
|
||||||
|
probe.columnLimit = Math.min(MAX_COLUMNS - reserve, probe.columns.size() + allowance);
|
||||||
|
probe.sampleLimit = Math.min(MAX_SAMPLES - reserve * 20, probe.samples + (longAxis ? 150_000 : 80_000));
|
||||||
|
for (var id : List.of(request.from(), request.to())) if (siteIds.contains(id) && !addressed.contains(id))
|
||||||
|
destinationAttempts.merge(id, 1, Integer::sum);
|
||||||
|
Endpoint from = endpoints.get(request.from()), to = endpoints.get(request.to());
|
||||||
|
int beforeRouteColumns = probe.columns.size();
|
||||||
|
var beforeReasons = new HashMap<>(routeReasons);
|
||||||
|
try {
|
||||||
|
RouteDraft route = route(seed, request, from, to, sites, landmarks, cells, probe);
|
||||||
|
if (route == null) { probe.reject("no-complete-route"); routeResults.put(request.id(), "refused; columns=" + (probe.columns.size() - beforeRouteColumns) + rejectionDelta(beforeReasons, routeReasons, probe.routeBlock)); continue; }
|
||||||
|
var groups = new HashSet<SegmentKey>();
|
||||||
|
cells.values().forEach(c -> groups.add(new SegmentKey(c.request(), c.cell().x() >> 4, c.cell().z() >> 4)));
|
||||||
|
route.cells().values().stream().filter(c -> !cells.containsKey(key(c.x(), c.z())))
|
||||||
|
.forEach(c -> groups.add(new SegmentKey(request, c.x() >> 4, c.z() >> 4)));
|
||||||
|
if (groups.size() > 512) { routeResults.put(request.id(), "refused; native-piece-budget"); continue; }
|
||||||
|
links.add(new Link(request.id(), request.from(), request.to(), request.kind(), route.path()));
|
||||||
|
components.join(request.from(), request.to());
|
||||||
|
addressed.add(request.from()); addressed.add(request.to());
|
||||||
|
routeResults.put(request.id(), "admitted; span=" + span(from.entry(), to.entry()) + "; length=" + route.path().size() + "; columns=" + (probe.columns.size() - beforeRouteColumns));
|
||||||
|
for (var cell : route.cells().values()) {
|
||||||
|
long key = key(cell.x(), cell.z());
|
||||||
|
OwnedCell old = cells.get(key);
|
||||||
|
if (old == null) cells.put(key, new OwnedCell(request, cell));
|
||||||
|
else if (cell.rail() != Rail.NONE && old.cell().rail() == Rail.NONE)
|
||||||
|
cells.put(key, new OwnedCell(old.request(), cell));
|
||||||
|
else if (cell.rail() != Rail.NONE && old.cell().rail() != Rail.NONE && cell.rail() != old.cell().rail()) {
|
||||||
|
// A missing switch is intentional; never emit a rail with contradictory connections.
|
||||||
|
var c = old.cell(); cells.put(key, new OwnedCell(old.request(),
|
||||||
|
new Cell(c.x(), c.floorY(), c.z(), c.foundationDepth(), c.rail(), false)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (BudgetExceeded exhausted) { probe.reject(exhausted.columns ? "route-column-budget" : "route-probe-budget"); routeResults.put(request.id(), "budget; columns=" + (probe.columns.size() - beforeRouteColumns) + rejectionDelta(beforeReasons, routeReasons, probe.routeBlock)); }
|
||||||
|
}
|
||||||
|
for (var site : sites) {
|
||||||
|
var incident = links.stream().filter(l -> l.fromId().equals(site.id()) || l.toId().equals(site.id())).map(Link::id).toList();
|
||||||
|
var attempts = new LinkedHashMap<String, String>();
|
||||||
|
routeResults.forEach((id, value) -> { if (id.contains(site.id())) attempts.put(id, value); });
|
||||||
|
diagnostics.put(site.kind(), diagnostics.get(site.kind()) + "; connected=" + !incident.isEmpty()
|
||||||
|
+ "; accepted-links=" + incident + "; route-attempts=" + attempts);
|
||||||
|
}
|
||||||
|
var segments = segments(seed, cells);
|
||||||
|
if (Boolean.getBoolean("sanctuary.test.heritage21") || Boolean.getBoolean("sanctuary.heritage.plannerDiagnostics"))
|
||||||
|
System.getLogger(HeritagePlanner21.class.getName()).log(System.Logger.Level.INFO,
|
||||||
|
"Heritage21 seed=" + seed + " diameter=" + diameter + " sites=" + sites.stream().map(Site::kind).toList()
|
||||||
|
+ " links=" + links.size() + " segments=" + segments.size() + " columns=" + probe.columns.size()
|
||||||
|
+ " samples=" + probe.samples + " catalogue=" + diagnostics + " routes=" + routeReasons + " routeResults=" + routeResults);
|
||||||
|
return new HeritagePlan19(sites, landmarks, links, segments, probe.samples, probe.columns.size(), diagnostics);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Candidate> candidates(long seed, Kind kind, int cx, int cz, int diameter,
|
||||||
|
List<Site> sites, List<Landmark> landmarks, Probe probe) {
|
||||||
|
double angle = unit(mix(seed ^ 0x19ACEL)) * Math.PI * 2 + kind.ordinal() * 2.399963229728653;
|
||||||
|
double radius = diameter * (kind == Kind.VOID_DOCK ? .40 : kind == Kind.OBSERVATORY || kind == Kind.RADIO_RELAY ? .31 : .22);
|
||||||
|
int x = cx + (int) Math.round(Math.cos(angle) * radius), z = cz + (int) Math.round(Math.sin(angle) * radius);
|
||||||
|
Kind parent = switch (kind) {
|
||||||
|
case POST_OFFICE, COMMUNAL_DEPOT, RAIL_WORKSHOP -> Kind.FREIGHT_STATION;
|
||||||
|
case EXPEDITION_OFFICE, PHOTO_CLUB -> Kind.POST_OFFICE;
|
||||||
|
case SUBSTATION, CONSTRUCTION_REFUGE -> Kind.RAIL_WORKSHOP;
|
||||||
|
case GREENHOUSE, IMPOSSIBLE_SERVICE -> Kind.PUMP_STATION;
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
Site neighbor = parent == null ? null : find(sites, parent);
|
||||||
|
if (neighbor != null) {
|
||||||
|
double local = unit(mix(seed ^ kind.ordinal() * 179L)) * Math.PI * 2;
|
||||||
|
int distance = kind == Kind.RAIL_WORKSHOP ? 65 : 40;
|
||||||
|
x = neighbor.centerX() + (int) Math.round(Math.cos(local) * distance);
|
||||||
|
z = neighbor.centerZ() + (int) Math.round(Math.sin(local) * distance);
|
||||||
|
}
|
||||||
|
var candidates = new ArrayList<Candidate>();
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
double turn = angle + i * Math.PI / 3;
|
||||||
|
int offset = i == 0 ? 0 : i < 5 ? 24 : i < 8 ? 48 : i < 12 ? 72 : 96;
|
||||||
|
int px = x + (int) Math.round(Math.cos(turn) * offset), pz = z + (int) Math.round(Math.sin(turn) * offset);
|
||||||
|
if (Math.hypot((double) px - cx, (double) pz - cz) > diameter / 2.0 - 18) continue;
|
||||||
|
if (overlapsSites(px - 15, pz - 15, px + 15, pz + 15, sites)) continue;
|
||||||
|
int low = 384, high = -1; boolean dry = true;
|
||||||
|
for (int dz : new int[] {-8, 0, 8}) for (int dx : new int[] {-8, 0, 8}) {
|
||||||
|
int top = probe.surface(px + dx, pz + dz);
|
||||||
|
if (top < 48 || top > 330 || !probe.solid(px + dx, top, pz + dz)
|
||||||
|
|| probe.protectedAt(px + dx, top, pz + dz)) { dry = false; break; }
|
||||||
|
low = Math.min(low, top); high = Math.max(high, top);
|
||||||
|
}
|
||||||
|
if (!dry || high - low > MAX_SITE_CUT + 7) { probe.reject("coarse-terrain"); continue; }
|
||||||
|
int facing = direction(px, pz, neighbor == null ? cx : neighbor.centerX(), neighbor == null ? cz : neighbor.centerZ());
|
||||||
|
if (kind == Kind.VOID_DOCK) facing = (direction(px, pz, cx, cz) + 2) & 3;
|
||||||
|
int score = (i >= 8 ? 1_000 : 0) + high - low + (kind == Kind.OBSERVATORY || kind == Kind.RADIO_RELAY ? (300 - high) / 4 : 0);
|
||||||
|
candidates.add(new Candidate(px - 8, pz - 8, facing, score, mix(seed ^ kind.ordinal() * 463L ^ i)));
|
||||||
|
}
|
||||||
|
candidates.sort(Comparator.comparingInt(Candidate::score).thenComparingLong(Candidate::seed));
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SiteDraft site(long seed, Kind kind, Candidate candidate, List<Site> previous,
|
||||||
|
List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
|
||||||
|
int x = candidate.x(), z = candidate.z(), low = 384, high = -1;
|
||||||
|
for (var landmark : landmarks) if (landmark.entry().x() >= x - 2 && landmark.entry().x() <= x + 18
|
||||||
|
&& landmark.entry().z() >= z - 2 && landmark.entry().z() <= z + 18) { probe.reject("existing-entry"); return null; }
|
||||||
|
var heights = new ArrayList<Integer>();
|
||||||
|
for (int dz = 0; dz < 17; dz++) for (int dx = 0; dx < 17; dx++) {
|
||||||
|
int top = probe.surface(x + dx, z + dz);
|
||||||
|
if (top < 48 || top > 330 || !probe.solid(x + dx, top, z + dz)) { probe.reject("dry-footprint"); return null; }
|
||||||
|
heights.add(top); low = Math.min(low, top); high = Math.max(high, top);
|
||||||
|
}
|
||||||
|
int minimum = Math.max(48, high - MAX_SITE_CUT), maximum = Math.min(330, low + 7);
|
||||||
|
if (minimum > maximum) { probe.reject("site-slope"); return null; }
|
||||||
|
heights.sort(Integer::compareTo);
|
||||||
|
int floor = Math.clamp(heights.get(heights.size() / 2), minimum, maximum);
|
||||||
|
int foundation = Math.max(3, floor - low + 1), height = height(kind);
|
||||||
|
int base = floor - foundation;
|
||||||
|
for (int dz = 0; dz < 17; dz++) for (int dx = 0; dx < 17; dx++)
|
||||||
|
for (int y = base; y <= floor + height; y++) if (probe.protectedAt(x + dx, y, z + dz)) {
|
||||||
|
probe.reject("site-protected-volume"); return null;
|
||||||
|
}
|
||||||
|
var supports = new ArrayList<Support>();
|
||||||
|
for (int dz : new int[] {1, 8, 15}) for (int dx : new int[] {1, 8, 15}) {
|
||||||
|
int bottom = base - 1, rock = bottom;
|
||||||
|
while (rock >= Math.max(2, base - 13) && (!probe.solid(x + dx, rock, z + dz)
|
||||||
|
|| !probe.solid(x + dx, rock - 1, z + dz))) rock--;
|
||||||
|
if (rock < Math.max(2, base - 13)) { probe.reject("site-no-bearing"); return null; }
|
||||||
|
for (int y = rock - 1; y <= bottom; y++) if (probe.protectedAt(x + dx, y, z + dz)) { probe.reject("support-protected"); return null; }
|
||||||
|
if (rock < bottom) supports.add(new Support(x + dx, rock + 1, z + dz, bottom));
|
||||||
|
}
|
||||||
|
// The square envelope and bearing grid have already been certified. A difficult front slope
|
||||||
|
// should try another real facade before rejecting the entire building; no terrain is weakened.
|
||||||
|
for (int turn : new int[]{0, 1, 3, 2}) {
|
||||||
|
int facing = (candidate.facing() + turn) & 3;
|
||||||
|
int ex = facing == 1 ? x + 17 : facing == 3 ? x - 1 : x + 8;
|
||||||
|
int ez = facing == 2 ? z + 17 : facing == 0 ? z - 1 : z + 8;
|
||||||
|
Entry entry = new Entry(ex, floor + 1, ez, facing);
|
||||||
|
Site result = new Site(id(kind), kind, x, floor, z, 17, 17, height, facing,
|
||||||
|
mix(seed ^ candidate.seed()), foundation, supports, entry);
|
||||||
|
var landings = landing(result.entry(), previous, probe);
|
||||||
|
if (landings == null) { probe.reject("entry-landing"); continue; }
|
||||||
|
boolean collision = false;
|
||||||
|
for (var cell : landings) {
|
||||||
|
OwnedCell old = existing.get(key(cell.x(), cell.z()));
|
||||||
|
if (old != null && old.cell().floorY() != cell.floorY()) { collision = true; break; }
|
||||||
|
}
|
||||||
|
if (collision) { probe.reject("landing-collision"); continue; }
|
||||||
|
if (turn != 0) probe.reject("admitted-other-facade");
|
||||||
|
return new SiteDraft(result, landings);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int height(Kind kind) {
|
||||||
|
return switch (kind) {
|
||||||
|
case FREIGHT_STATION, POST_OFFICE, RAIL_WORKSHOP, SUBSTATION, GREENHOUSE, PHOTO_CLUB -> 10;
|
||||||
|
case EXPEDITION_OFFICE, IMPOSSIBLE_SERVICE -> 12;
|
||||||
|
case COMMUNAL_DEPOT, VOID_DOCK, CONSTRUCTION_REFUGE -> 14;
|
||||||
|
case PUMP_STATION -> 18; case OBSERVATORY -> 20; case RADIO_RELAY -> 22;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
private static List<Request> requests(List<Site> sites, List<Landmark> landmarks) {
|
||||||
|
var result = new LinkedHashMap<String, Request>();
|
||||||
|
var endpoints = new ArrayList<Endpoint>();
|
||||||
|
sites.forEach(s -> endpoints.add(new Endpoint(s.id(), s.entry(), false)));
|
||||||
|
landmarks.forEach(l -> endpoints.add(new Endpoint(l.id(), l.entry(), true)));
|
||||||
|
var freight = new HashSet<String>();
|
||||||
|
for (var kind : new Kind[] {Kind.FREIGHT_STATION, Kind.COMMUNAL_DEPOT, Kind.RAIL_WORKSHOP, Kind.PUMP_STATION}) {
|
||||||
|
var site = find(sites, kind); if (site != null) freight.add(site.id());
|
||||||
|
}
|
||||||
|
landmarks.stream().filter(l -> l.kind() == LandmarkKind.MINE).forEach(l -> freight.add(l.id()));
|
||||||
|
for (var a : endpoints) {
|
||||||
|
// Four alternatives per entrance tolerate one protected cut without a quadratic terrain search.
|
||||||
|
var nearest = endpoints.stream().filter(b -> !a.id().equals(b.id()))
|
||||||
|
.sorted(Comparator.comparingInt((Endpoint b) -> span(a.entry(), b.entry())).thenComparing(Endpoint::id)).limit(4).toList();
|
||||||
|
for (var b : nearest) addRequest(result, a, b, freight.contains(a.id()) && freight.contains(b.id()) ? RouteKind.FREIGHT : RouteKind.PATH);
|
||||||
|
}
|
||||||
|
for (var pair : List.of(new Kind[]{Kind.FREIGHT_STATION, Kind.COMMUNAL_DEPOT},
|
||||||
|
new Kind[]{Kind.FREIGHT_STATION, Kind.RAIL_WORKSHOP}, new Kind[]{Kind.COMMUNAL_DEPOT, Kind.PUMP_STATION})) {
|
||||||
|
var a = find(sites, pair[0]); var b = find(sites, pair[1]);
|
||||||
|
if (a != null && b != null) addRequest(result, new Endpoint(a.id(), a.entry(), false), new Endpoint(b.id(), b.entry(), false), RouteKind.FREIGHT);
|
||||||
|
}
|
||||||
|
return List.copyOf(result.values());
|
||||||
|
}
|
||||||
|
private static void addRequest(Map<String, Request> result, Endpoint a, Endpoint b, RouteKind kind) {
|
||||||
|
if (a.id().compareTo(b.id()) > 0) { var swap = a; a = b; b = swap; }
|
||||||
|
String key = a.id() + "__" + b.id(); var old = result.get(key);
|
||||||
|
if (old == null || kind == RouteKind.FREIGHT) result.put(key, new Request("graph_" + key, a.id(), b.id(), kind));
|
||||||
|
}
|
||||||
|
private static String rejectionDelta(Map<String, Integer> before, Map<String, Integer> after, XZ obstacle) {
|
||||||
|
var changes = new java.util.TreeMap<String, Integer>();
|
||||||
|
after.forEach((key, count) -> { int difference = count - before.getOrDefault(key, 0); if (difference > 0) changes.put(key, difference); });
|
||||||
|
return "; reasons=" + changes + (obstacle == null ? "" : "; last-column=" + obstacle.x() + "," + obstacle.z());
|
||||||
|
}
|
||||||
|
private static int isolatedSites(Request request, Set<String> siteIds, Set<String> addressed) {
|
||||||
|
return (siteIds.contains(request.from()) && !addressed.contains(request.from()) ? 1 : 0)
|
||||||
|
+ (siteIds.contains(request.to()) && !addressed.contains(request.to()) ? 1 : 0);
|
||||||
|
}
|
||||||
|
private static int isolatedAttempts(Request request, Set<String> siteIds, Set<String> addressed, Map<String, Integer> attempts) {
|
||||||
|
int result = Integer.MAX_VALUE;
|
||||||
|
for (var id : List.of(request.from(), request.to())) if (siteIds.contains(id) && !addressed.contains(id))
|
||||||
|
result = Math.min(result, attempts.getOrDefault(id, 0));
|
||||||
|
return result == Integer.MAX_VALUE ? 0 : result;
|
||||||
|
}
|
||||||
|
private static final class Components {
|
||||||
|
final Map<String, String> parents = new HashMap<>();
|
||||||
|
Components(Set<String> ids) { for (var id : ids) parents.put(id, id); }
|
||||||
|
String root(String id) { String parent = parents.get(id); while (!parent.equals(id)) { id = parent; parent = parents.get(id); } return parent; }
|
||||||
|
boolean same(String a, String b) { return root(a).equals(root(b)); }
|
||||||
|
void join(String a, String b) { parents.put(root(b), root(a)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Always persisted, including for an isolated destination: five steps return to natural terrain. */
|
||||||
|
private static List<Cell> landing(Entry entry, List<Site> sites, Probe probe) {
|
||||||
|
int facing = entry.facing(), floor = entry.y() - 1;
|
||||||
|
int endX = entry.x() + DX[facing] * 4, endZ = entry.z() + DZ[facing] * 4;
|
||||||
|
int target = probe.surface(endX, endZ);
|
||||||
|
if (target < 14 || target > 350 || Math.abs(target - floor) > 3) return null;
|
||||||
|
var result = new ArrayList<Cell>();
|
||||||
|
for (int step = 0; step < 5; step++) {
|
||||||
|
// Keep the first two rows flat for the facade and the first rail turn.
|
||||||
|
int y = floor + (int) Math.round((target - floor) * Math.max(0, step - 1) / 3.0);
|
||||||
|
for (int cross = -1; cross <= 1; cross++) {
|
||||||
|
int x = entry.x() + DX[facing] * step + DZ[facing] * cross;
|
||||||
|
int z = entry.z() + DZ[facing] * step - DX[facing] * cross;
|
||||||
|
if (overlapsSites(x, z, x, z, sites)) return null;
|
||||||
|
Cell cell = routeCell(x, y, z, Rail.NONE, false, probe);
|
||||||
|
if (cell == null) return null;
|
||||||
|
result.add(cell);
|
||||||
|
if (step == 4) {
|
||||||
|
int beyond = probe.surface(x + DX[facing], z + DZ[facing]);
|
||||||
|
if (Math.abs(beyond - y) > 1 || !probe.solid(x + DX[facing], beyond, z + DZ[facing])) return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RouteDraft route(long seed, Request request, Endpoint from, Endpoint to, List<Site> sites,
|
||||||
|
List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
|
||||||
|
Entry a = from.entry(), b = to.entry();
|
||||||
|
XZ start = new XZ(a.x(), a.z()), end = new XZ(b.x(), b.z());
|
||||||
|
XZ outA = start.move(a.facing(), 6), outB = end.move(b.facing(), 6);
|
||||||
|
int middleX = (outA.x() + outB.x()) / 2, middleZ = (outA.z() + outB.z()) / 2;
|
||||||
|
var alternatives = List.of(
|
||||||
|
List.of(outA, new XZ(outB.x(), outA.z()), outB),
|
||||||
|
List.of(outA, new XZ(outA.x(), outB.z()), outB),
|
||||||
|
List.of(outA, new XZ(middleX, outA.z()), new XZ(middleX, outB.z()), outB),
|
||||||
|
List.of(outA, new XZ(outA.x(), middleZ), new XZ(outB.x(), middleZ), outB),
|
||||||
|
List.of(outA, new XZ(outA.x(), middleZ - 16), new XZ(outB.x(), middleZ - 16), outB),
|
||||||
|
List.of(outA, new XZ(middleX + 16, outA.z()), new XZ(middleX + 16, outB.z()), outB));
|
||||||
|
for (var waypoints : alternatives) {
|
||||||
|
var points = new ArrayList<XZ>(); points.add(start);
|
||||||
|
boolean okay = true;
|
||||||
|
for (var waypoint : waypoints) if (!line(points, waypoint)) { okay = false; break; }
|
||||||
|
if (!okay || !line(points, end) || points.size() > 1024 || new HashSet<>(points).size() != points.size()) continue;
|
||||||
|
RouteDraft result = profile(seed, request, from, to, points, sites, landmarks, existing, probe);
|
||||||
|
if (result != null) return result;
|
||||||
|
}
|
||||||
|
return adaptive(seed, request, from, to, start, end, outA, outB, sites, landmarks, existing, probe);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Adaptive fallback. The coarse graph only proposes an alignment; it grants no writing permission. */
|
||||||
|
private static RouteDraft adaptive(long seed, Request request, Endpoint from, Endpoint to, XZ start, XZ end,
|
||||||
|
XZ outA, XZ outB, List<Site> sites, List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
|
||||||
|
int stepSize = span(from.entry(), to.entry()) >= 160 ? 8 : 4;
|
||||||
|
int certificateReserve = Math.min(2_200, distance(outA, outB) * 3 + 80);
|
||||||
|
var excluded = new HashSet<XZ>();
|
||||||
|
for (int attempt = 0; attempt < 3; attempt++) {
|
||||||
|
var open = new java.util.PriorityQueue<SearchNode>(Comparator.comparingDouble(SearchNode::priority)
|
||||||
|
.thenComparingDouble(SearchNode::cost).thenComparingInt(n -> n.position().x()).thenComparingInt(n -> n.position().z()));
|
||||||
|
var best = new HashMap<XZ, Double>(); var parents = new HashMap<XZ, XZ>(); var closed = new HashSet<XZ>();
|
||||||
|
var terrainHeights = new HashMap<XZ, Integer>();
|
||||||
|
int begin = coarseRoute(outA, sites, terrainHeights, probe);
|
||||||
|
if (begin == INVALID) return null;
|
||||||
|
open.add(new SearchNode(outA, 0, distance(outA, outB))); best.put(outA, 0.0);
|
||||||
|
List<XZ> found = null;
|
||||||
|
int searchColumns = Math.max(probe.columns.size() + 150, probe.columnLimit - certificateReserve);
|
||||||
|
while (!open.isEmpty() && closed.size() < 500 && probe.columns.size() < searchColumns) {
|
||||||
|
var current = open.remove(); XZ p = current.position();
|
||||||
|
if (!closed.add(p) || current.cost() > best.getOrDefault(p, Double.POSITIVE_INFINITY)) continue;
|
||||||
|
if (distance(p, outB) <= stepSize * 2) {
|
||||||
|
var reversed = new ArrayList<XZ>(); XZ ancestor = p;
|
||||||
|
while (ancestor != null) { reversed.add(ancestor); ancestor = parents.get(ancestor); }
|
||||||
|
java.util.Collections.reverse(reversed);
|
||||||
|
for (int order = 0; order < 2 && found == null; order++) {
|
||||||
|
var candidate = new ArrayList<XZ>(); candidate.add(start); boolean valid = true;
|
||||||
|
for (XZ step : reversed) if (!line(candidate, step)) { valid = false; break; }
|
||||||
|
XZ turn = order == 0 ? new XZ(outB.x(), p.z()) : new XZ(p.x(), outB.z());
|
||||||
|
if (valid && line(candidate, turn) && line(candidate, outB) && line(candidate, end)
|
||||||
|
&& new HashSet<>(candidate).size() == candidate.size()) found = candidate;
|
||||||
|
}
|
||||||
|
if (found != null) break;
|
||||||
|
}
|
||||||
|
int top = terrainHeights.get(p); XZ parent = parents.get(p);
|
||||||
|
for (int d = 0; d < 4; d++) {
|
||||||
|
if (parent == null && d == ((from.entry().facing() + 2) & 3)) continue;
|
||||||
|
XZ next = p.move(d, stepSize);
|
||||||
|
if (closed.contains(next) || excluded.contains(next)
|
||||||
|
|| Math.min(distance(next, outA), distance(next, outB)) > distance(outA, outB) + 64) continue;
|
||||||
|
int height = coarseRoute(next, sites, terrainHeights, probe);
|
||||||
|
if (height == INVALID || Math.abs(height - top) > stepSize + 3) continue;
|
||||||
|
double turnCost = parent == null || parent.x() == next.x() || parent.z() == next.z() ? 0 : 1.5;
|
||||||
|
double cost = current.cost() + stepSize + Math.abs(height - top) * 1.1 + turnCost;
|
||||||
|
if (cost >= best.getOrDefault(next, Double.POSITIVE_INFINITY)) continue;
|
||||||
|
best.put(next, cost); parents.put(next, p);
|
||||||
|
open.add(new SearchNode(next, cost, cost + distance(next, outB) * 1.6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found == null) { probe.reject("adaptive-no-alignment"); return null; }
|
||||||
|
RouteDraft certified = profile(seed, request, from, to, found, sites, landmarks, existing, probe);
|
||||||
|
if (certified != null) { probe.reject("adaptive-admitted"); return certified; }
|
||||||
|
if (probe.routeBlock == null) return null;
|
||||||
|
// A failed exact corridor removes only nearby coarse nodes on the next proposal.
|
||||||
|
XZ bad = probe.routeBlock;
|
||||||
|
int gx = outA.x() + (int) Math.round((bad.x() - outA.x()) / (double) stepSize) * stepSize;
|
||||||
|
int gz = outA.z() + (int) Math.round((bad.z() - outA.z()) / (double) stepSize) * stepSize;
|
||||||
|
excluded.add(new XZ(gx, gz));
|
||||||
|
for (int d = 0; d < 4; d++) excluded.add(new XZ(gx, gz).move(d, stepSize));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
private record SearchNode(XZ position, double cost, double priority) {}
|
||||||
|
private static int distance(XZ a, XZ b) { return Math.abs(a.x() - b.x()) + Math.abs(a.z() - b.z()); }
|
||||||
|
private static int coarseRoute(XZ p, List<Site> sites, Map<XZ, Integer> cache, Probe probe) {
|
||||||
|
Integer old = cache.get(p); if (old != null) return old;
|
||||||
|
if (overlapsSites(p.x() - 2, p.z() - 2, p.x() + 2, p.z() + 2, sites)) { cache.put(p, INVALID); return INVALID; }
|
||||||
|
int top = probe.surface(p.x(), p.z());
|
||||||
|
if (top < 14 || top > 350 || !probe.solid(p.x(), top, p.z()) || probe.protectedAt(p.x(), top, p.z())) {
|
||||||
|
cache.put(p, INVALID); return INVALID;
|
||||||
|
}
|
||||||
|
for (int d = 0; d < 4; d++) {
|
||||||
|
XZ side = p.move(d, 1); int sideTop = probe.surface(side.x(), side.z());
|
||||||
|
if (sideTop < 14 || sideTop > 350 || Math.abs(sideTop - top) > MAX_ROUTE_CUT + 1 || probe.protectedAt(side.x(), sideTop, side.z())) {
|
||||||
|
cache.put(p, INVALID); return INVALID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cache.put(p, top); return top;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Finite orthogonal alignments, each certified at full block resolution before publication. */
|
||||||
|
private static boolean line(List<XZ> points, XZ destination) {
|
||||||
|
XZ p = points.getLast();
|
||||||
|
if (p.x() != destination.x() && p.z() != destination.z()) return false;
|
||||||
|
int dx = Integer.signum(destination.x() - p.x()), dz = Integer.signum(destination.z() - p.z());
|
||||||
|
while (!p.equals(destination)) {
|
||||||
|
p = new XZ(p.x() + dx, p.z() + dz); points.add(p);
|
||||||
|
if (points.size() > 1024) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RouteDraft profile(long seed, Request request, Endpoint from, Endpoint to, List<XZ> points,
|
||||||
|
List<Site> sites, List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
|
||||||
|
int size = points.size(); probe.routeBlock = null;
|
||||||
|
int[] lo = new int[size], hi = new int[size], desired = new int[size], group = new int[size];
|
||||||
|
boolean[] flat = new boolean[size - 1];
|
||||||
|
for (int i = 1; i < size - 1; i++) if (turn(points, i)) { flat[i - 1] = true; flat[i] = true; }
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
XZ point = points.get(i); probe.routeBlock = point; lo[i] = 14; hi[i] = 350;
|
||||||
|
desired[i] = probe.surface(point.x(), point.z());
|
||||||
|
boolean readOnly = i == 0 && from.readOnly() || i == size - 1 && to.readOnly();
|
||||||
|
for (XZ column : strip(points, i)) {
|
||||||
|
if (overlapsSites(column.x(), column.z(), column.x(), column.z(), sites)) { probe.reject("route-building"); return null; }
|
||||||
|
int top = probe.surface(column.x(), column.z());
|
||||||
|
if (top < 14 || top > 350 || !probe.solid(column.x(), top, column.z())) { probe.reject("route-dry-ground"); return null; }
|
||||||
|
lo[i] = Math.max(lo[i], top - MAX_ROUTE_CUT); hi[i] = Math.min(hi[i], top + MAX_ROUTE_FILL);
|
||||||
|
OwnedCell old = existing.get(key(column.x(), column.z()));
|
||||||
|
if (old != null) { lo[i] = Math.max(lo[i], old.cell().floorY()); hi[i] = Math.min(hi[i], old.cell().floorY()); }
|
||||||
|
if (readOnly && top != (i == 0 ? from.entry().y() : to.entry().y()) - 1) return null;
|
||||||
|
}
|
||||||
|
if (i == 0 || i == size - 1) {
|
||||||
|
int exact = (i == 0 ? from.entry().y() : to.entry().y()) - 1;
|
||||||
|
lo[i] = Math.max(lo[i], exact); hi[i] = Math.min(hi[i], exact);
|
||||||
|
}
|
||||||
|
if (lo[i] > hi[i]) { probe.reject("route-grade-interval"); return null; }
|
||||||
|
}
|
||||||
|
// Curves and both adjacent rails share one elevation; a Minecraft curved rail cannot slope.
|
||||||
|
int groups = 1;
|
||||||
|
for (int i = 1; i < size; i++) { if (!flat[i - 1]) groups++; group[i] = groups - 1; }
|
||||||
|
int[] gl = new int[groups], gh = new int[groups], gd = new int[groups], y = new int[groups];
|
||||||
|
java.util.Arrays.fill(gl, 14); java.util.Arrays.fill(gh, 350);
|
||||||
|
for (int i = 0; i < size; i++) { int g = group[i]; gl[g] = Math.max(gl[g], lo[i]); gh[g] = Math.min(gh[g], hi[i]); gd[g] = desired[i]; }
|
||||||
|
for (int g = 1; g < groups; g++) { gl[g] = Math.max(gl[g], gl[g - 1] - 1); gh[g] = Math.min(gh[g], gh[g - 1] + 1); }
|
||||||
|
for (int g = groups - 2; g >= 0; g--) { gl[g] = Math.max(gl[g], gl[g + 1] - 1); gh[g] = Math.min(gh[g], gh[g + 1] + 1); }
|
||||||
|
for (int g = 0; g < groups; g++) {
|
||||||
|
if (gl[g] > gh[g]) { probe.reject("route-grade"); return null; }
|
||||||
|
int lower = g == 0 ? gl[g] : Math.max(gl[g], y[g - 1] - 1), upper = g == 0 ? gh[g] : Math.min(gh[g], y[g - 1] + 1);
|
||||||
|
y[g] = Math.clamp(gd[g], lower, upper);
|
||||||
|
}
|
||||||
|
var path = new ArrayList<Point>(); var floors = new LinkedHashMap<Long, Integer>();
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
XZ point = points.get(i); int floor = y[group[i]]; path.add(new Point(point.x(), floor, point.z()));
|
||||||
|
if (i == 0 && from.readOnly() || i == size - 1 && to.readOnly()) continue;
|
||||||
|
for (XZ column : strip(points, i)) {
|
||||||
|
long key = key(column.x(), column.z()); Integer old = floors.putIfAbsent(key, floor);
|
||||||
|
if (old != null && old != floor) { probe.reject("route-self-grade"); return null; }
|
||||||
|
// Historical endpoints and their lateral landing are read-only, even at another nearby turn.
|
||||||
|
for (var landmark : landmarks) if (landingColumn(landmark.entry(), column)) {
|
||||||
|
boolean actualEnd = from.readOnly() && landmark.id().equals(from.id())
|
||||||
|
|| to.readOnly() && landmark.id().equals(to.id());
|
||||||
|
if (!actualEnd || floor != landmark.entry().y() - 1) { probe.reject("route-historical-entry"); return null; }
|
||||||
|
floors.remove(key); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var cells = new LinkedHashMap<Long, Cell>();
|
||||||
|
for (var entry : floors.entrySet()) {
|
||||||
|
int x = (int) (entry.getKey() >> 32), z = (int) (long) entry.getKey();
|
||||||
|
probe.routeBlock = new XZ(x, z);
|
||||||
|
Cell cell = routeCell(x, entry.getValue(), z, Rail.NONE, false, probe);
|
||||||
|
if (cell == null) { probe.reject("route-column-certificate"); return null; }
|
||||||
|
cells.put(entry.getKey(), cell);
|
||||||
|
}
|
||||||
|
if (request.kind() == RouteKind.FREIGHT) for (int i = 0; i < path.size(); i++) {
|
||||||
|
Point point = path.get(i); long key = key(point.x(), point.z()); Cell cell = cells.get(key);
|
||||||
|
if (cell == null) continue;
|
||||||
|
Rail rail = rail(path, i);
|
||||||
|
// A one-block valley cannot carry two ascending slopes on the same rail. Keep a repairable rail gap.
|
||||||
|
boolean intact = rail != Rail.NONE && Math.floorMod(mix(seed ^ key ^ request.id().hashCode()), 19) != 0;
|
||||||
|
if (rail == Rail.NONE) rail = points.get(Math.max(0, i - 1)).x() == points.get(Math.min(size - 1, i + 1)).x() ? Rail.NORTH_SOUTH : Rail.EAST_WEST;
|
||||||
|
cells.put(key, new Cell(cell.x(), cell.floorY(), cell.z(), cell.foundationDepth(), rail, intact));
|
||||||
|
}
|
||||||
|
return new RouteDraft(List.copyOf(path), cells);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean landingColumn(Entry entry, XZ column) {
|
||||||
|
int dx = column.x() - entry.x(), dz = column.z() - entry.z();
|
||||||
|
return DX[entry.facing()] * dx + DZ[entry.facing()] * dz == 0 && Math.abs(dx) + Math.abs(dz) <= 1;
|
||||||
|
}
|
||||||
|
private static List<XZ> strip(List<XZ> points, int i) {
|
||||||
|
XZ p = points.get(i); var result = new ArrayList<XZ>();
|
||||||
|
if (turn(points, i)) {
|
||||||
|
for (int dx = -1; dx <= 1; dx++) for (int dz = -1; dz <= 1; dz++) result.add(new XZ(p.x() + dx, p.z() + dz));
|
||||||
|
} else {
|
||||||
|
XZ other = points.get(i == points.size() - 1 ? i - 1 : i + 1);
|
||||||
|
int dx = Integer.signum(other.x() - p.x()), dz = Integer.signum(other.z() - p.z());
|
||||||
|
for (int cross = -1; cross <= 1; cross++) result.add(new XZ(p.x() + dz * cross, p.z() - dx * cross));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
private static boolean turn(List<XZ> points, int i) {
|
||||||
|
if (i == 0 || i == points.size() - 1) return false;
|
||||||
|
XZ a = points.get(i - 1), b = points.get(i + 1);
|
||||||
|
return a.x() != b.x() && a.z() != b.z();
|
||||||
|
}
|
||||||
|
private static Rail rail(List<Point> path, int i) {
|
||||||
|
Point p = path.get(i), a = path.get(Math.max(0, i - 1)), b = path.get(Math.min(path.size() - 1, i + 1));
|
||||||
|
if (a.floorY() > p.floorY() && b.floorY() > p.floorY()) return Rail.NONE;
|
||||||
|
Point high = a.floorY() > p.floorY() ? a : b.floorY() > p.floorY() ? b : null;
|
||||||
|
if (high != null) return switch (direction(p.x(), p.z(), high.x(), high.z())) {
|
||||||
|
case 0 -> Rail.ASCENDING_NORTH; case 1 -> Rail.ASCENDING_EAST; case 2 -> Rail.ASCENDING_SOUTH; default -> Rail.ASCENDING_WEST;
|
||||||
|
};
|
||||||
|
if (a.x() == b.x()) return Rail.NORTH_SOUTH;
|
||||||
|
if (a.z() == b.z()) return Rail.EAST_WEST;
|
||||||
|
boolean n = a.z() < p.z() || b.z() < p.z(), s = a.z() > p.z() || b.z() > p.z();
|
||||||
|
boolean e = a.x() > p.x() || b.x() > p.x();
|
||||||
|
return n ? e ? Rail.NORTH_EAST : Rail.NORTH_WEST : s && e ? Rail.SOUTH_EAST : Rail.SOUTH_WEST;
|
||||||
|
}
|
||||||
|
private static Cell routeCell(int x, int floor, int z, Rail rail, boolean present, Probe probe) {
|
||||||
|
int top = probe.surface(x, z);
|
||||||
|
if (floor < 14 || floor > 350 || top < floor - MAX_ROUTE_FILL || top > floor + MAX_ROUTE_CUT) return null;
|
||||||
|
int foundation = Math.max(1, floor - top + 1), base = floor - foundation;
|
||||||
|
if (!probe.solid(x, base - 1, z) || !probe.solid(x, base, z)) return null;
|
||||||
|
for (int y = base - 1; y <= floor + 3; y++) if (probe.protectedAt(x, y, z)) return null;
|
||||||
|
return new Cell(x, floor, z, foundation, rail, present);
|
||||||
|
}
|
||||||
|
private record XZ(int x, int z) { XZ move(int facing, int distance) { return new XZ(x + DX[facing] * distance, z + DZ[facing] * distance); } }
|
||||||
|
private record SegmentKey(Request request, int cx, int cz) {}
|
||||||
|
private static List<Segment> segments(long seed, Map<Long, OwnedCell> cells) {
|
||||||
|
var groups = new LinkedHashMap<SegmentKey, List<Cell>>();
|
||||||
|
for (var owned : cells.values()) {
|
||||||
|
Cell c = owned.cell(); var key = new SegmentKey(owned.request(), c.x() >> 4, c.z() >> 4);
|
||||||
|
groups.computeIfAbsent(key, ignored -> new ArrayList<>()).add(c);
|
||||||
|
}
|
||||||
|
var result = new ArrayList<Segment>();
|
||||||
|
for (var entry : groups.entrySet()) {
|
||||||
|
var k = entry.getKey(); var r = k.request();
|
||||||
|
// IDs are stable, bounded and independent of native generation order.
|
||||||
|
String id = "piece_" + r.id() + "_" + k.cx() + "_" + k.cz();
|
||||||
|
result.add(new Segment(id, r.from(), r.to(), r.kind(), k.cx(), k.cz(), entry.getValue(), mix(seed ^ key(k.cx(), k.cz()) ^ r.id().hashCode())));
|
||||||
|
}
|
||||||
|
return List.copyOf(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static int span(Entry a, Entry b) { return Math.max(Math.abs(a.x() - b.x()), Math.abs(a.z() - b.z())); }
|
||||||
|
private static Site find(List<Site> sites, Kind kind) { return sites.stream().filter(site -> site.kind() == kind).findFirst().orElse(null); }
|
||||||
|
private static String id(Kind kind) { return kind.name().toLowerCase(Locale.ROOT); }
|
||||||
|
private static int direction(int x, int z, int tx, int tz) { return Math.abs(tx - x) >= Math.abs(tz - z) ? tx > x ? 1 : 3 : tz > z ? 2 : 0; }
|
||||||
|
private static boolean overlapsSites(int x0, int z0, int x1, int z1, List<Site> sites) {
|
||||||
|
for (var site : sites) if (x0 <= site.maxX() && x1 >= site.minX() && z0 <= site.maxZ() && z1 >= site.minZ()) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
private record SiteDraft(Site site, List<Cell> landings) {}
|
||||||
|
private record Candidate(int x, int z, int facing, int score, long seed) {}
|
||||||
|
private record Endpoint(String id, Entry entry, boolean readOnly) {}
|
||||||
|
private record Request(String id, String from, String to, RouteKind kind) {}
|
||||||
|
private record OwnedCell(Request request, Cell cell) {}
|
||||||
|
private record RouteDraft(List<Point> path, Map<Long, Cell> cells) {}
|
||||||
|
private static final class BudgetExceeded extends RuntimeException {
|
||||||
|
final boolean columns; BudgetExceeded(boolean columns) { this.columns = columns; }
|
||||||
|
}
|
||||||
|
private static final class Probe {
|
||||||
|
final Terrain terrain; final int cx, cz; final double radius;
|
||||||
|
final Map<Long, Integer> columns = new HashMap<>();
|
||||||
|
XZ routeBlock; Map<String, Integer> reasons = new HashMap<>(); int samples, columnLimit, sampleLimit;
|
||||||
|
Probe(Terrain terrain, int cx, int cz, int diameter) { this.terrain = java.util.Objects.requireNonNull(terrain); this.cx = cx; this.cz = cz; radius = diameter / 2.0 - 2; }
|
||||||
|
int surface(int x, int z) {
|
||||||
|
if (Math.hypot((double) x - cx, (double) z - cz) > radius || !terrain.owns(x, z)) return INVALID;
|
||||||
|
long key = key(x, z); var cached = columns.get(key); if (cached != null) return cached;
|
||||||
|
if (columns.size() >= columnLimit) throw new BudgetExceeded(true);
|
||||||
|
count(); int top = terrain.surfaceY(x, z); columns.put(key, top); return top;
|
||||||
|
}
|
||||||
|
boolean solid(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) == INVALID) return false; count(); return terrain.solid(x, y, z); }
|
||||||
|
boolean protectedAt(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) == INVALID) return true; count(); return terrain.protectedAt(x, y, z); }
|
||||||
|
void count() { if (samples >= sampleLimit) throw new BudgetExceeded(false); samples++; }
|
||||||
|
void reject(String reason) { reasons.merge(reason, 1, Integer::sum); }
|
||||||
|
}
|
||||||
|
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
|
||||||
|
private static double unit(long value) { return (value >>> 11) * 0x1.0p-53; }
|
||||||
|
private static long mix(long value) { value = (value ^ value >>> 30) * 0xbf58476d1ce4e5b9L; value = (value ^ value >>> 27) * 0x94d049bb133111ebL; return value ^ value >>> 31; }
|
||||||
|
}
|
||||||
+770
@@ -0,0 +1,770 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.heritage;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.SanctuaryMod;
|
||||||
|
import fr.koka.sanctuary.worldgen.city.LostCityPalette;
|
||||||
|
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||||
|
import fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.Rotation;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.block.state.properties.AttachFace;
|
||||||
|
import net.minecraft.world.level.block.state.properties.BedPart;
|
||||||
|
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||||
|
import net.minecraft.world.level.block.state.properties.DoorHingeSide;
|
||||||
|
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
|
||||||
|
import net.minecraft.world.level.block.state.properties.Half;
|
||||||
|
import net.minecraft.world.level.block.state.properties.RailShape;
|
||||||
|
|
||||||
|
/** Generation21 restrained architectural rendition of the immutable generation19 site geometry. The saved site controls all geometry and loot.
|
||||||
|
* Site writes are local to minX/floorY/minZ; route writes are local to the segment's minimum bounds. */
|
||||||
|
public final class HeritageRenderer21 {
|
||||||
|
private HeritageRenderer21() {}
|
||||||
|
public record Target(String name, int x, int y, int z) {}
|
||||||
|
|
||||||
|
/** These are already rotated local standing positions, not world coordinates. */
|
||||||
|
public static List<Target> targets(HeritagePlan19.Site site) {
|
||||||
|
var b = new Builder(site, null); b.layout(); return List.copyOf(b.targets);
|
||||||
|
}
|
||||||
|
public static void render(HeritagePlan19.Site site, LostCityRenderer.BlockWriter writer) {
|
||||||
|
var buffer = ConnectedBlocks20.buffer(writer);
|
||||||
|
var b = new Builder(site, buffer); b.ground(); b.layout();
|
||||||
|
// Footings have absolute saved coordinates and must not undergo a second facade rotation.
|
||||||
|
for (var support : site.supports()) for (int y = support.minY(); y <= support.maxY(); y++)
|
||||||
|
buffer.set(support.x() - site.minX(), y - site.floorY(), support.z() - site.minZ(),
|
||||||
|
y == support.minY() ? Blocks.CHISELED_TUFF_BRICKS.defaultBlockState() : Blocks.STONE_BRICKS.defaultBlockState());
|
||||||
|
buffer.flush();
|
||||||
|
}
|
||||||
|
public static void renderSegment(HeritagePlan19.Segment segment, LostCityRenderer.BlockWriter writer) {
|
||||||
|
var buffer = ConnectedBlocks20.buffer(writer);
|
||||||
|
var occupied = new java.util.HashSet<Long>();
|
||||||
|
for (var cell : segment.cells()) occupied.add(columnKey(cell.x(), cell.z()));
|
||||||
|
int x0 = segment.minX(), y0 = segment.minY(), z0 = segment.minZ();
|
||||||
|
for (var cell : segment.cells()) {
|
||||||
|
int x = cell.x() - x0, z = cell.z() - z0, floor = cell.floorY() - y0;
|
||||||
|
for (int y = cell.minY() - y0; y <= floor; y++) {
|
||||||
|
Block material = Blocks.STONE_BRICKS;
|
||||||
|
if (y == floor) {
|
||||||
|
boolean edge = false;
|
||||||
|
for (Direction d : new Direction[] {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}) {
|
||||||
|
int nx = cell.x() + d.getStepX(), nz = cell.z() + d.getStepZ();
|
||||||
|
// Unknown neighboring chunks are continuations, not artificial crosswise curbs.
|
||||||
|
if ((nx >> 4) == segment.startChunkX() && (nz >> 4) == segment.startChunkZ()
|
||||||
|
&& !occupied.contains(columnKey(nx, nz))) edge = true;
|
||||||
|
}
|
||||||
|
if (cell.rail() != HeritagePlan19.Rail.NONE) material = Math.floorMod(cell.x() + cell.z(), 3) == 0 ? Blocks.OAK_PLANKS : Blocks.GRAVEL;
|
||||||
|
else if (edge) material = Math.floorMod(cell.x() + cell.z(), 8) == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.TUFF_BRICKS;
|
||||||
|
else material = LostCityPalette.roll(segment.seed(), cell.x(), 0, cell.z(), 5) == 0 ? Blocks.TUFF : Blocks.POLISHED_TUFF;
|
||||||
|
}
|
||||||
|
buffer.set(x, y, z, material.defaultBlockState());
|
||||||
|
}
|
||||||
|
for (int y = floor + 1; y <= floor + 3; y++) buffer.set(x, y, z, Blocks.AIR.defaultBlockState());
|
||||||
|
if (cell.railPresent()) buffer.set(x, floor + 1, z, Blocks.RAIL.defaultBlockState()
|
||||||
|
.setValue(BlockStateProperties.RAIL_SHAPE, RailShape.valueOf(cell.rail().name())));
|
||||||
|
}
|
||||||
|
buffer.flush();
|
||||||
|
}
|
||||||
|
private static long columnKey(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
|
||||||
|
|
||||||
|
private static final class Builder {
|
||||||
|
final HeritagePlan19.Site s;
|
||||||
|
final LostCityRenderer.BlockWriter out;
|
||||||
|
final List<Target> targets = new ArrayList<>();
|
||||||
|
final Rotation rotation;
|
||||||
|
Builder(HeritagePlan19.Site s, LostCityRenderer.BlockWriter out) {
|
||||||
|
this.s = s; this.out = out;
|
||||||
|
rotation = switch (s.facing()) { case 1 -> Rotation.CLOCKWISE_90; case 2 -> Rotation.CLOCKWISE_180;
|
||||||
|
case 3 -> Rotation.COUNTERCLOCKWISE_90; default -> Rotation.NONE; };
|
||||||
|
}
|
||||||
|
int rx(int x, int z) { return switch (s.facing()) { case 1 -> 16 - z; case 2 -> 16 - x; case 3 -> z; default -> x; }; }
|
||||||
|
int rz(int x, int z) { return switch (s.facing()) { case 1 -> x; case 2 -> 16 - z; case 3 -> 16 - x; default -> z; }; }
|
||||||
|
void set(int x, int y, int z, BlockState state) {
|
||||||
|
if (x < 0 || x > 16 || z < 0 || z > 16 || y < -s.foundationDepth() || y > s.height())
|
||||||
|
throw new IllegalArgumentException("Heritage " + s.kind() + " renderer escaped saved site: " + x + "/" + y + "/" + z);
|
||||||
|
if (out != null) out.set(rx(x, z), y, rz(x, z), palette(state, x, y, z).rotate(rotation));
|
||||||
|
}
|
||||||
|
void set(int x, int y, int z, Block b) { set(x, y, z, b.defaultBlockState()); }
|
||||||
|
void box(int x0, int y0, int z0, int x1, int y1, int z1, Block b) {
|
||||||
|
for (int y = y0; y <= y1; y++) for (int z = z0; z <= z1; z++) for (int x = x0; x <= x1; x++) set(x, y, z, b);
|
||||||
|
}
|
||||||
|
void target(String name, int x, int y, int z) { targets.add(new Target(name, rx(x, z), y, rz(x, z))); }
|
||||||
|
void ground() {
|
||||||
|
for (int y = -s.foundationDepth(); y <= s.height(); y++) for (int z = 0; z <= 16; z++) for (int x = 0; x <= 16; x++)
|
||||||
|
set(x, y, z, y < 0 ? Blocks.STONE_BRICKS.defaultBlockState() : y == 0
|
||||||
|
? (x == 0 || z == 0 || x == 16 || z == 16 ? Blocks.TUFF_BRICKS
|
||||||
|
: x >= 7 && x <= 9 ? Blocks.POLISHED_TUFF
|
||||||
|
: LostCityPalette.roll(s.seed(), x, 0, z, 7) == 0 ? Blocks.TUFF : Blocks.ANDESITE).defaultBlockState()
|
||||||
|
: Blocks.AIR.defaultBlockState());
|
||||||
|
}
|
||||||
|
void layout() {
|
||||||
|
target("main_entry", 8, 1, 0);
|
||||||
|
switch (s.kind()) {
|
||||||
|
case FREIGHT_STATION -> station();
|
||||||
|
case POST_OFFICE -> post();
|
||||||
|
case COMMUNAL_DEPOT -> depot();
|
||||||
|
case EXPEDITION_OFFICE -> expedition();
|
||||||
|
case OBSERVATORY -> observatory();
|
||||||
|
case RAIL_WORKSHOP -> railway();
|
||||||
|
case PUMP_STATION -> pumping();
|
||||||
|
case SUBSTATION -> substation();
|
||||||
|
case GREENHOUSE -> greenhouse();
|
||||||
|
case VOID_DOCK -> dock();
|
||||||
|
case RADIO_RELAY -> radio();
|
||||||
|
case PHOTO_CLUB -> photo();
|
||||||
|
case IMPOSSIBLE_SERVICE -> impossible();
|
||||||
|
case CONSTRUCTION_REFUGE -> refuge();
|
||||||
|
}
|
||||||
|
architecture();
|
||||||
|
roofs21();
|
||||||
|
}
|
||||||
|
void shell(int x0, int floor, int z0, int x1, int ceiling, int z1) {
|
||||||
|
for (int y = floor; y <= ceiling; y++) for (int z = z0; z <= z1; z++) for (int x = x0; x <= x1; x++) {
|
||||||
|
boolean wall = x == x0 || x == x1 || z == z0 || z == z1;
|
||||||
|
set(x, y, z, y == floor ? floorMaterial().defaultBlockState() : y == ceiling ? Blocks.SMOOTH_STONE.defaultBlockState()
|
||||||
|
: wall ? wallMaterial(y - floor, ceiling - floor).defaultBlockState() : Blocks.AIR.defaultBlockState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Block floorMaterial() {
|
||||||
|
return switch (s.kind()) {
|
||||||
|
case POST_OFFICE, EXPEDITION_OFFICE, RADIO_RELAY, CONSTRUCTION_REFUGE -> Blocks.OAK_PLANKS;
|
||||||
|
case PHOTO_CLUB, GREENHOUSE -> Blocks.BIRCH_PLANKS;
|
||||||
|
case IMPOSSIBLE_SERVICE -> Blocks.POLISHED_DIORITE;
|
||||||
|
default -> Blocks.POLISHED_ANDESITE;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Block wallMaterial(int y, int height) {
|
||||||
|
if (y == 1) return Blocks.STONE_BRICKS;
|
||||||
|
if (y == height - 1) return Blocks.ANDESITE;
|
||||||
|
return s.kind() == HeritagePlan19.Kind.GREENHOUSE ? Blocks.BIRCH_PLANKS : Blocks.SMOOTH_STONE;
|
||||||
|
}
|
||||||
|
BlockState palette(BlockState state, int x, int y, int z) {
|
||||||
|
Block original = state.getBlock(), replacement = original;
|
||||||
|
boolean birch = s.kind() == HeritagePlan19.Kind.GREENHOUSE;
|
||||||
|
if (original == Blocks.DARK_OAK_PLANKS || original == Blocks.OAK_PLANKS || original == Blocks.BIRCH_PLANKS)
|
||||||
|
replacement = birch ? Blocks.BIRCH_PLANKS : Blocks.OAK_PLANKS;
|
||||||
|
else if (original == Blocks.OAK_WOOD || original == Blocks.BIRCH_WOOD || original == Blocks.STRIPPED_OAK_WOOD)
|
||||||
|
replacement = birch ? Blocks.BIRCH_WOOD : Blocks.OAK_WOOD;
|
||||||
|
else if (original == Blocks.OAK_LOG || original == Blocks.BIRCH_LOG || original == Blocks.STRIPPED_OAK_LOG)
|
||||||
|
replacement = birch ? Blocks.STRIPPED_BIRCH_LOG : Blocks.STRIPPED_OAK_LOG;
|
||||||
|
else if (original == Blocks.DARK_OAK_SLAB || original == Blocks.OAK_SLAB || original == Blocks.BIRCH_SLAB)
|
||||||
|
replacement = birch ? Blocks.BIRCH_SLAB : Blocks.OAK_SLAB;
|
||||||
|
else if (original == Blocks.DARK_OAK_STAIRS || original == Blocks.OAK_STAIRS || original == Blocks.BIRCH_STAIRS)
|
||||||
|
replacement = birch ? Blocks.BIRCH_STAIRS : Blocks.OAK_STAIRS;
|
||||||
|
else if (original == Blocks.DARK_OAK_FENCE || original == Blocks.OAK_FENCE || original == Blocks.BIRCH_FENCE)
|
||||||
|
replacement = birch ? Blocks.BIRCH_FENCE : Blocks.OAK_FENCE;
|
||||||
|
else if (original == Blocks.DARK_OAK_DOOR || original == Blocks.OAK_DOOR || original == Blocks.BIRCH_DOOR)
|
||||||
|
replacement = birch ? Blocks.BIRCH_DOOR : Blocks.OAK_DOOR;
|
||||||
|
else if (original == Blocks.DARK_OAK_TRAPDOOR || original == Blocks.OAK_TRAPDOOR || original == Blocks.BIRCH_TRAPDOOR)
|
||||||
|
replacement = birch ? Blocks.BIRCH_TRAPDOOR : Blocks.OAK_TRAPDOOR;
|
||||||
|
else if (original == Blocks.GRANITE || original == Blocks.POLISHED_GRANITE || original == Blocks.DIORITE
|
||||||
|
|| original == Blocks.POLISHED_DIORITE || original == Blocks.POLISHED_ANDESITE)
|
||||||
|
replacement = original == Blocks.POLISHED_ANDESITE ? Blocks.ANDESITE : Blocks.SMOOTH_STONE;
|
||||||
|
else if (original == Blocks.POLISHED_GRANITE_SLAB || original == Blocks.POLISHED_DIORITE_SLAB
|
||||||
|
|| original == Blocks.POLISHED_ANDESITE_SLAB) replacement = Blocks.SMOOTH_STONE_SLAB;
|
||||||
|
if (y > 0) {
|
||||||
|
if (original == Blocks.TUFF || original == Blocks.POLISHED_TUFF) replacement = Blocks.SMOOTH_STONE;
|
||||||
|
else if (original == Blocks.TUFF_BRICKS) replacement = Blocks.STONE_BRICKS;
|
||||||
|
else if (original == Blocks.CHISELED_TUFF || original == Blocks.CHISELED_TUFF_BRICKS) replacement = Blocks.CHISELED_STONE_BRICKS;
|
||||||
|
else if (original == Blocks.TUFF_BRICK_SLAB) replacement = Blocks.SMOOTH_STONE_SLAB;
|
||||||
|
else if (original == Blocks.TUFF_BRICK_STAIRS) replacement = Blocks.STONE_BRICK_STAIRS;
|
||||||
|
else if (original == Blocks.TUFF_BRICK_WALL) replacement = Blocks.STONE_BRICK_WALL;
|
||||||
|
}
|
||||||
|
if ((original == Blocks.STONE_BRICKS || original == Blocks.CRACKED_STONE_BRICKS
|
||||||
|
|| original == Blocks.MOSSY_STONE_BRICKS) && y > 1 && y % 5 != 0)
|
||||||
|
replacement = Blocks.SMOOTH_STONE;
|
||||||
|
if (replacement == Blocks.ANDESITE && y > 0 && Math.floorMod(x + z, 4) == 0)
|
||||||
|
replacement = Blocks.STONE;
|
||||||
|
// Quiet, reproducible weathering follows construction joints, never random holes in access floors.
|
||||||
|
if (replacement == Blocks.STONE_BRICKS && y > 0) {
|
||||||
|
int weather = LostCityPalette.roll(s.seed(), x, y, z, 23);
|
||||||
|
if (weather == 0) replacement = Blocks.CRACKED_STONE_BRICKS;
|
||||||
|
else if (weather == 1 && y <= 3) replacement = Blocks.MOSSY_STONE_BRICKS;
|
||||||
|
}
|
||||||
|
if (replacement == original) return state;
|
||||||
|
var mapped = replacement.defaultBlockState();
|
||||||
|
for (var property : state.getProperties()) mapped = copyProperty(mapped, state, property);
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
|
private static <T extends Comparable<T>> BlockState copyProperty(BlockState destination, BlockState source,
|
||||||
|
net.minecraft.world.level.block.state.properties.Property<T> property) {
|
||||||
|
return destination.hasProperty(property) ? destination.setValue(property, source.getValue(property)) : destination;
|
||||||
|
}
|
||||||
|
// These additions compose each use separately; all thresholds and maintenance passages stay in the saved footprint.
|
||||||
|
void architecture() {
|
||||||
|
switch (s.kind()) {
|
||||||
|
case FREIGHT_STATION -> stationArchitecture();
|
||||||
|
case POST_OFFICE -> postArchitecture();
|
||||||
|
case COMMUNAL_DEPOT -> depotArchitecture();
|
||||||
|
case EXPEDITION_OFFICE -> expeditionArchitecture();
|
||||||
|
case OBSERVATORY -> observatoryArchitecture();
|
||||||
|
case RAIL_WORKSHOP -> railwayArchitecture();
|
||||||
|
case PUMP_STATION -> pumpingArchitecture();
|
||||||
|
case SUBSTATION -> substationArchitecture();
|
||||||
|
case GREENHOUSE -> greenhouseArchitecture();
|
||||||
|
case VOID_DOCK -> dockArchitecture();
|
||||||
|
case RADIO_RELAY -> radioArchitecture();
|
||||||
|
case PHOTO_CLUB -> photoArchitecture();
|
||||||
|
case IMPOSSIBLE_SERVICE -> impossibleArchitecture();
|
||||||
|
case CONSTRUCTION_REFUGE -> refugeArchitecture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void slab(int x, int y, int z, Block block) { set(x, y, z, block.defaultBlockState().setValue(BlockStateProperties.SLAB_TYPE, net.minecraft.world.level.block.state.properties.SlabType.BOTTOM)); }
|
||||||
|
void stair(int x, int y, int z, Block block, Direction direction, Half half) {
|
||||||
|
set(x, y, z, block.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, direction).setValue(BlockStateProperties.HALF, half));
|
||||||
|
}
|
||||||
|
/** Cut only the four exterior corner cells; the two adjacent wall faces keep the interior sealed. */
|
||||||
|
void bevel(int x0, int z0, int x1, int z1, int low, int high) {
|
||||||
|
for (int x : new int[] {x0, x1}) for (int z : new int[] {z0, z1}) {
|
||||||
|
box(x, low, z, x, high, z, Blocks.AIR);
|
||||||
|
set(x, low - 1, z, Blocks.CHISELED_STONE_BRICKS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void rim(int x0, int z0, int x1, int z1, int y, Block material) {
|
||||||
|
for (int x = x0; x <= x1; x++) { slab(x, y, z0, material); slab(x, y, z1, material); }
|
||||||
|
for (int z = z0 + 1; z < z1; z++) { slab(x0, y, z, material); slab(x1, y, z, material); }
|
||||||
|
}
|
||||||
|
void screen(int x0, int z, int x1, int y) {
|
||||||
|
for (int x = x0; x <= x1; x++) set(x, y, z, Blocks.BIRCH_TRAPDOOR.defaultBlockState()
|
||||||
|
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH).setValue(BlockStateProperties.OPEN, true));
|
||||||
|
}
|
||||||
|
void stationArchitecture() {
|
||||||
|
bevel(4, 3, 12, 11, 1, 6);
|
||||||
|
box(4, 7, 3, 12, 10, 11, Blocks.AIR);
|
||||||
|
rim(3, 2, 13, 12, 6, Blocks.TUFF_BRICK_SLAB);
|
||||||
|
for (int z = 5; z <= 9; z++) for (int x = 5; x <= 11; x++) {
|
||||||
|
if (x == 5 || x == 11 || z == 5 || z == 9) box(x, 7, z, x, 8, z, Blocks.GLASS_PANE);
|
||||||
|
slab(x, 9, z, Blocks.POLISHED_DIORITE_SLAB);
|
||||||
|
}
|
||||||
|
// Open the former ceiling beneath the sealed roof glazing. The middle beam
|
||||||
|
// still supports the existing hanging lantern and joins the two hall walls.
|
||||||
|
for (int z = 6; z <= 8; z++) for (int x = 6; x <= 10; x++) {
|
||||||
|
if (z != 7) set(x, 6, z, Blocks.AIR);
|
||||||
|
set(x, 9, z, Blocks.GLASS);
|
||||||
|
}
|
||||||
|
// The long cantilever reads as a train platform, distinct from the glazed passenger hall.
|
||||||
|
box(0, 5, 4, 3, 5, 12, Blocks.OAK_PLANKS);
|
||||||
|
for (int z : new int[] {4, 8, 12}) stair(0, 4, z, Blocks.TUFF_BRICK_STAIRS, Direction.EAST, Half.TOP);
|
||||||
|
for (int z : new int[] {5, 11}) box(1, 1, z, 1, 4, z, Blocks.OAK_WOOD);
|
||||||
|
for (int x : new int[] {4, 12}) box(x, 2, 4, x, 4, 4, Blocks.CHISELED_TUFF_BRICKS);
|
||||||
|
screen(5, 4, 6, 3); box(5, 0, 4, 11, 0, 10, Blocks.POLISHED_DIORITE);
|
||||||
|
box(7, 0, 3, 9, 0, 10, Blocks.BIRCH_PLANKS);
|
||||||
|
rim(9, 11, 15, 15, 4, Blocks.TUFF_BRICK_SLAB);
|
||||||
|
}
|
||||||
|
void postArchitecture() {
|
||||||
|
bevel(2, 2, 14, 14, 1, 6); rim(1, 1, 15, 15, 6, Blocks.POLISHED_GRANITE_SLAB);
|
||||||
|
box(4, 7, 4, 12, 7, 12, Blocks.TUFF_BRICKS);
|
||||||
|
rim(4, 4, 12, 12, 8, Blocks.TUFF_BRICK_SLAB);
|
||||||
|
for (int x : new int[] {2, 7, 9, 14}) {
|
||||||
|
box(x, 3, 1, x, 4, 1, Blocks.POLISHED_DIORITE);
|
||||||
|
stair(x, 5, 1, Blocks.TUFF_BRICK_STAIRS, Direction.SOUTH, Half.TOP);
|
||||||
|
}
|
||||||
|
box(5, 4, 1, 11, 4, 1, Blocks.CHISELED_TUFF_BRICKS);
|
||||||
|
box(4, 1, 6, 6, 1, 6, Blocks.BIRCH_PLANKS); screen(4, 6, 6, 3);
|
||||||
|
for (int z = 9; z <= 12; z++) { set(3, 3, z, Blocks.OAK_TRAPDOOR.defaultBlockState().setValue(BlockStateProperties.HALF, Half.TOP)); }
|
||||||
|
for (int x = 4; x <= 12; x++) set(x, 0, 5, x % 2 == 0 ? Blocks.POLISHED_GRANITE : Blocks.OAK_PLANKS);
|
||||||
|
box(10, 0, 8, 13, 0, 9, Blocks.DIORITE);
|
||||||
|
}
|
||||||
|
void depotArchitecture() {
|
||||||
|
bevel(1, 2, 15, 14, 1, 9); box(1, 10, 2, 15, 14, 14, Blocks.AIR);
|
||||||
|
// Three industrial north-light roof bays, not another pitched house roof.
|
||||||
|
for (int z : new int[] {3, 7, 11}) {
|
||||||
|
box(2, 10, z, 14, 11, z, Blocks.GLASS_PANE);
|
||||||
|
for (int x = 2; x <= 14; x++) {
|
||||||
|
slab(x, 12, z, Blocks.TUFF_BRICK_SLAB);
|
||||||
|
stair(x, 11, z + 1, Blocks.TUFF_BRICK_STAIRS, Direction.NORTH, Half.BOTTOM);
|
||||||
|
stair(x, 10, z + 2, Blocks.TUFF_BRICK_STAIRS, Direction.NORTH, Half.BOTTOM);
|
||||||
|
if (x > 2 && x < 14) {
|
||||||
|
// Both transparent faces close the north-light well, while the
|
||||||
|
// retained roof outside these bands keeps the warehouse sealed.
|
||||||
|
set(x, 9, z, Blocks.GLASS);
|
||||||
|
set(x, 12, z, Blocks.GLASS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int z : new int[] {3, 7, 11}) for (int x : new int[] {1, 15}) box(x, 1, z, x, 9, z, Blocks.POLISHED_ANDESITE);
|
||||||
|
for (int x : new int[] {6, 10}) { box(x, 1, 1, x, 3, 1, Blocks.CHISELED_TUFF); stair(x, 4, 1, Blocks.TUFF_BRICK_STAIRS, Direction.SOUTH, Half.TOP); }
|
||||||
|
box(6, 4, 1, 10, 4, 1, Blocks.TUFF_BRICKS);
|
||||||
|
for (int z : new int[] {5, 8}) {
|
||||||
|
// Thin wooden covers sit directly on the barrel stack and the lower packing bench.
|
||||||
|
set(4, 3, z, Blocks.OAK_TRAPDOOR.defaultBlockState().setValue(BlockStateProperties.HALF, Half.BOTTOM));
|
||||||
|
set(12, 2, z, Blocks.OAK_TRAPDOOR.defaultBlockState().setValue(BlockStateProperties.HALF, Half.BOTTOM));
|
||||||
|
}
|
||||||
|
for (int z = 3; z <= 9; z++) { set(6, 0, z, Blocks.TUFF_BRICKS); set(10, 0, z, Blocks.TUFF_BRICKS); }
|
||||||
|
box(2, 5, 11, 9, 5, 13, Blocks.BIRCH_PLANKS);
|
||||||
|
}
|
||||||
|
void expeditionArchitecture() {
|
||||||
|
bevel(2, 2, 12, 12, 1, 6); rim(1, 1, 13, 13, 6, Blocks.TUFF_BRICK_SLAB);
|
||||||
|
box(3, 7, 3, 6, 9, 7, Blocks.BIRCH_WOOD); rim(3, 3, 6, 7, 10, Blocks.TUFF_BRICK_SLAB);
|
||||||
|
box(4, 8, 3, 5, 8, 3, Blocks.GLASS_PANE);
|
||||||
|
// Sheltered map table, archive wing and broad shaded rear balcony.
|
||||||
|
box(7, 4, 1, 9, 4, 1, Blocks.CHISELED_TUFF_BRICKS);
|
||||||
|
for (int x : new int[] {2, 12}) box(x, 2, 3, x, 5, 3, Blocks.OAK_WOOD);
|
||||||
|
screen(3, 4, 5, 3); box(3, 3, 9, 5, 3, 9, Blocks.OAK_PLANKS);
|
||||||
|
for (int x = 3; x <= 14; x++) for (int z = 13; z <= 15; z++) set(x, 0, z, x % 3 == 0 ? Blocks.OAK_WOOD : Blocks.OAK_PLANKS);
|
||||||
|
for (int x = 3; x <= 14; x++) slab(x, 5, 14, Blocks.BIRCH_SLAB);
|
||||||
|
box(3, 1, 14, 3, 4, 14, Blocks.BIRCH_FENCE); box(14, 1, 14, 14, 4, 14, Blocks.BIRCH_FENCE);
|
||||||
|
}
|
||||||
|
void observatoryArchitecture() {
|
||||||
|
// Eight faceted buttresses below a broken observatory dome with an open central viewing slot.
|
||||||
|
for (int[] p : new int[][] {{3, 8}, {5, 4}, {11, 4}, {13, 8}, {11, 12}, {5, 12}}) {
|
||||||
|
box(p[0], 1, p[1], p[0], 11, p[1], Blocks.DIORITE);
|
||||||
|
set(p[0], 5, p[1], Blocks.CHISELED_TUFF_BRICKS);
|
||||||
|
stair(p[0], 12, p[1], Blocks.TUFF_BRICK_STAIRS, p[0] < 8 ? Direction.EAST : Direction.WEST, Half.BOTTOM);
|
||||||
|
}
|
||||||
|
for (int z = 4; z <= 12; z++) for (int x = 4; x <= 12; x++) {
|
||||||
|
int distance = Math.max(Math.abs(x - 8), Math.abs(z - 8));
|
||||||
|
if (distance == 4) set(x, 15, z, Blocks.GLASS_PANE);
|
||||||
|
if (distance == 3 && x != 8) slab(x, 17, z, Blocks.TUFF_BRICK_SLAB);
|
||||||
|
}
|
||||||
|
for (int x : new int[] {4, 12}) for (int z : new int[] {5, 11}) box(x, 13, z, x, 14, z, Blocks.CHISELED_TUFF_BRICKS);
|
||||||
|
// Four short ribs visibly carry the inset crown; the central viewing slot stays open.
|
||||||
|
for (int x : new int[] {4, 12}) for (int z : new int[] {5, 11}) {
|
||||||
|
box(x, 15, z, x, 16, z, Blocks.CHISELED_TUFF_BRICKS);
|
||||||
|
stair(x < 8 ? x + 1 : x - 1, 16, z, Blocks.TUFF_BRICK_STAIRS,
|
||||||
|
x < 8 ? Direction.EAST : Direction.WEST, Half.BOTTOM);
|
||||||
|
}
|
||||||
|
box(7, 0, 5, 9, 0, 11, Blocks.BIRCH_PLANKS);
|
||||||
|
box(7, 6, 5, 9, 6, 10, Blocks.POLISHED_DIORITE);
|
||||||
|
}
|
||||||
|
void railwayArchitecture() {
|
||||||
|
bevel(1, 2, 15, 14, 1, 6); box(1, 7, 2, 15, 10, 14, Blocks.AIR);
|
||||||
|
// Broad barrel-vault ribs sit above the two inspection tracks.
|
||||||
|
for (int z = 2; z <= 14; z++) for (int x = 1; x <= 15; x++) {
|
||||||
|
int rise = Math.min(3, Math.min(x - 1, 15 - x));
|
||||||
|
if (z % 4 == 2) set(x, 6 + rise, z, Blocks.TUFF_BRICKS);
|
||||||
|
else if (x >= 6 && x <= 10) set(x, 6 + rise, z, Blocks.GLASS);
|
||||||
|
else slab(x, 6 + rise, z, Blocks.TUFF_BRICK_SLAB);
|
||||||
|
}
|
||||||
|
for (int z : new int[] {3, 7, 11}) for (int x : new int[] {1, 15}) box(x, 2, z, x, 5, z, Blocks.GRANITE);
|
||||||
|
box(7, -2, 5, 9, -2, 11, Blocks.POLISHED_ANDESITE);
|
||||||
|
for (int z = 6; z <= 11; z++) { set(7, -1, z, Blocks.CHISELED_TUFF_BRICKS); set(9, -1, z, Blocks.CHISELED_TUFF_BRICKS); }
|
||||||
|
// Side ledges leave the one-block maintenance aisle and its stairs clear.
|
||||||
|
screen(2, 3, 3, 3); box(2, 3, 11, 4, 3, 11, Blocks.OAK_PLANKS);
|
||||||
|
}
|
||||||
|
void pumpingArchitecture() {
|
||||||
|
bevel(2, 3, 9, 12, 1, 6); rim(1, 2, 9, 13, 6, Blocks.TUFF_BRICK_SLAB);
|
||||||
|
for (int x : new int[] {10, 15}) for (int z : new int[] {5, 11}) {
|
||||||
|
box(x, 1, z, x, 7, z, Blocks.CHISELED_TUFF_BRICKS);
|
||||||
|
set(x, 8, z, Blocks.POLISHED_ANDESITE);
|
||||||
|
}
|
||||||
|
rim(9, 4, 16, 12, 13, Blocks.TUFF_BRICK_SLAB);
|
||||||
|
for (int x = 10; x <= 15; x++) for (int z = 5; z <= 11; z++) slab(x, 14, z, Blocks.POLISHED_ANDESITE_SLAB);
|
||||||
|
box(3, 7, 5, 3, 12, 5, Blocks.TUFF_BRICK_WALL);
|
||||||
|
box(3, 3, 6, 3, 5, 8, Blocks.IRON_BARS);
|
||||||
|
box(3, 0, 4, 8, 0, 9, Blocks.ANDESITE); box(5, 0, 4, 7, 0, 10, Blocks.POLISHED_DIORITE);
|
||||||
|
screen(3, 11, 4, 3);
|
||||||
|
}
|
||||||
|
void substationArchitecture() {
|
||||||
|
bevel(10, 4, 15, 10, 1, 5); rim(9, 3, 16, 15, 5, Blocks.TUFF_BRICK_SLAB);
|
||||||
|
for (int z : new int[] {2, 6, 10, 14}) for (int x : new int[] {1, 15}) {
|
||||||
|
set(x, 0, z, Blocks.STONE_BRICKS); set(x, 1, z, Blocks.CHISELED_TUFF_BRICKS);
|
||||||
|
}
|
||||||
|
box(2, 4, 5, 5, 4, 5, Blocks.IRON_BARS); box(2, 4, 8, 5, 4, 8, Blocks.IRON_BARS);
|
||||||
|
for (int x : new int[] {2, 5}) for (int z : new int[] {5, 8}) box(x, 1, z, x, 3, z, Blocks.TUFF_BRICK_WALL);
|
||||||
|
box(11, 6, 5, 14, 6, 9, Blocks.POLISHED_TUFF);
|
||||||
|
for (int z = 5; z <= 9; z += 2) screen(11, z, 14, 7);
|
||||||
|
for (int z = 3; z <= 8; z++) set(7, 0, z, Blocks.GRANITE);
|
||||||
|
box(6, -2, 10, 7, -2, 13, Blocks.CHISELED_TUFF_BRICKS);
|
||||||
|
}
|
||||||
|
void greenhouseArchitecture() {
|
||||||
|
// Tall birch ribs and a steep glass ridge give the glasshouse an agricultural silhouette.
|
||||||
|
for (int z = 2; z <= 14; z++) for (int x = 1; x <= 11; x++) {
|
||||||
|
int rise = Math.min(3, Math.min(x - 1, 11 - x));
|
||||||
|
if (z == 2 || z == 8 || z == 14 || x == 6) set(x, 6 + rise, z, Blocks.BIRCH_WOOD);
|
||||||
|
else set(x, 6 + rise, z, Blocks.GLASS);
|
||||||
|
}
|
||||||
|
for (int z : new int[] {3, 7, 11}) { box(1, 1, z, 1, 5, z, Blocks.BIRCH_WOOD); box(11, 1, z, 11, 5, z, Blocks.BIRCH_WOOD); }
|
||||||
|
rim(11, 9, 15, 15, 5, Blocks.TUFF_BRICK_SLAB);
|
||||||
|
box(12, 3, 10, 14, 3, 10, Blocks.OAK_PLANKS); screen(12, 10, 14, 4);
|
||||||
|
for (int z = 3; z <= 13; z++) set(6, 0, z, z % 3 == 0 ? Blocks.CHISELED_TUFF_BRICKS : Blocks.POLISHED_TUFF);
|
||||||
|
}
|
||||||
|
void dockArchitecture() {
|
||||||
|
for (int x = 1; x <= 15; x++) for (int z = 0; z <= 15; z++) set(x, 0, z, x % 4 == 0 ? Blocks.OAK_WOOD : Blocks.OAK_PLANKS);
|
||||||
|
bevel(2, 3, 7, 10, 1, 5); rim(1, 2, 8, 15, 5, Blocks.BIRCH_SLAB);
|
||||||
|
for (int z : new int[] {2, 8, 14}) {
|
||||||
|
box(0, 1, z, 0, 2, z, Blocks.CHISELED_TUFF_BRICKS);
|
||||||
|
box(16, 1, z, 16, 2, z, Blocks.CHISELED_TUFF_BRICKS);
|
||||||
|
slab(0, 3, z, Blocks.TUFF_BRICK_SLAB); slab(16, 3, z, Blocks.TUFF_BRICK_SLAB);
|
||||||
|
}
|
||||||
|
// Brackets and a separated customs roof leave the departure platform open to the void.
|
||||||
|
for (int x = 2; x <= 7; x++) for (int z = 3; z <= 15; z++) if (z % 4 == 0) slab(x, 6, z, Blocks.BIRCH_SLAB);
|
||||||
|
stair(11, 9, 5, Blocks.OAK_STAIRS, Direction.EAST, Half.TOP);
|
||||||
|
stair(13, 9, 5, Blocks.OAK_STAIRS, Direction.WEST, Half.TOP);
|
||||||
|
screen(3, 4, 6, 3);
|
||||||
|
}
|
||||||
|
void radioArchitecture() {
|
||||||
|
bevel(2, 3, 10, 12, 1, 6); rim(1, 2, 10, 13, 6, Blocks.TUFF_BRICK_SLAB);
|
||||||
|
for (int x = 2; x <= 10; x++) for (int z = 3; z <= 12; z++) {
|
||||||
|
int roofY = 7 + Math.min(2, Math.min(x - 2, 10 - x));
|
||||||
|
slab(x, roofY, z, x <= 5 ? Blocks.BIRCH_SLAB : Blocks.TUFF_BRICK_SLAB);
|
||||||
|
}
|
||||||
|
box(9, 7, 4, 9, 11, 4, Blocks.CHISELED_TUFF_BRICKS);
|
||||||
|
for (int y : new int[] {10, 14, 18}) { box(12, y, 8, 14, y, 8, Blocks.IRON_BARS); box(13, y, 7, 13, y, 9, Blocks.IRON_BARS); }
|
||||||
|
box(3, 0, 8, 5, 0, 11, Blocks.BIRCH_PLANKS); screen(7, 4, 9, 3);
|
||||||
|
}
|
||||||
|
void photoArchitecture() {
|
||||||
|
bevel(2, 2, 14, 14, 1, 6); rim(1, 1, 15, 15, 6, Blocks.POLISHED_DIORITE_SLAB);
|
||||||
|
box(3, 7, 9, 13, 7, 13, Blocks.TUFF_BRICKS); rim(3, 9, 13, 13, 8, Blocks.TUFF_BRICK_SLAB);
|
||||||
|
for (int x : new int[] {3, 6, 10, 13}) stair(x, 4, 1, Blocks.TUFF_BRICK_STAIRS, Direction.SOUTH, Half.TOP);
|
||||||
|
box(3, 5, 1, 13, 5, 1, Blocks.CHISELED_TUFF_BRICKS);
|
||||||
|
for (int x = 4; x <= 12; x++) set(x, 0, 4, x % 2 == 0 ? Blocks.POLISHED_GRANITE : Blocks.BIRCH_PLANKS);
|
||||||
|
box(3, 0, 9, 7, 0, 13, Blocks.POLISHED_ANDESITE);
|
||||||
|
screen(10, 5, 12, 2); box(3, 3, 10, 3, 3, 12, Blocks.OAK_PLANKS);
|
||||||
|
}
|
||||||
|
void impossibleArchitecture() {
|
||||||
|
bevel(1, 7, 15, 15, 1, 10); rim(0, 6, 16, 16, 10, Blocks.TUFF_BRICK_SLAB);
|
||||||
|
box(4, 11, 9, 12, 11, 13, Blocks.TUFF_BRICKS);
|
||||||
|
for (int x : new int[] {4, 12}) for (int z : new int[] {9, 13}) set(x, 12, z, Blocks.CHISELED_TUFF);
|
||||||
|
// A cramped civil facade gives way to an over-scaled, deliberately ceremonial service hall.
|
||||||
|
for (int x : new int[] {3, 11}) box(x, 1, 1, x, 4, 1, Blocks.CHISELED_TUFF_BRICKS);
|
||||||
|
box(3, 5, 1, 11, 5, 1, Blocks.TUFF_BRICKS);
|
||||||
|
for (int x : new int[] {3, 6, 10, 13}) { set(x, 1, 11, Blocks.CHISELED_TUFF_BRICKS); set(x, 8, 11, Blocks.CHISELED_TUFF_BRICKS); }
|
||||||
|
screen(4, 3, 4, 3); box(12, 3, 14, 14, 3, 14, Blocks.BIRCH_PLANKS);
|
||||||
|
}
|
||||||
|
void refugeArchitecture() {
|
||||||
|
bevel(5, 6, 13, 14, 5, 10);
|
||||||
|
box(5, 11, 6, 13, 14, 14, Blocks.AIR);
|
||||||
|
for (int z = 6; z <= 14; z++) for (int x = 5; x <= 13; x++) {
|
||||||
|
int rise = Math.min(3, (x - 5) / 2);
|
||||||
|
slab(x, 11 + rise, z, (z & 1) == 0 ? Blocks.BIRCH_SLAB : Blocks.TUFF_BRICK_SLAB);
|
||||||
|
}
|
||||||
|
for (int x : new int[] {3, 14}) for (int z : new int[] {5, 15}) { box(x, 1, z, x, 3, z, Blocks.CHISELED_TUFF_BRICKS); set(x, 4, z, Blocks.OAK_WOOD); }
|
||||||
|
for (int z = 6; z <= 14; z += 4) box(4, 1, z, 4, 7, z, Blocks.IRON_BARS);
|
||||||
|
box(2, 1, 3, 3, 10, 4, Blocks.TUFF_BRICKS); box(2, 11, 3, 14, 11, 4, Blocks.OAK_WOOD);
|
||||||
|
// Upper lockers and a birch sleeping corner avoid the saved hatch and its complete ladder shaft.
|
||||||
|
screen(11, 7, 12, 7); box(7, 4, 7, 11, 4, 9, Blocks.BIRCH_PLANKS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Replacement roofs: one weather skin, connected rafters and retained lamp-bearing beams.
|
||||||
|
* Clearing starts at the old roof, never at a floor, furnishing or standing threshold. */
|
||||||
|
void roofs21() {
|
||||||
|
switch (s.kind()) {
|
||||||
|
case RAIL_WORKSHOP -> {
|
||||||
|
vaultRoof(1, 2, 15, 14, 6, false, true);
|
||||||
|
box(1, 6, 8, 15, 6, 8, Blocks.OAK_WOOD); // existing workshop lamp
|
||||||
|
// Two missing panes expose an intact neighbouring rafter, not a floating roof fragment.
|
||||||
|
box(7, 9, 4, 8, 9, 4, Blocks.AIR);
|
||||||
|
}
|
||||||
|
case GREENHOUSE -> {
|
||||||
|
vaultRoof(1, 2, 11, 14, 6, true, true);
|
||||||
|
box(1, 6, 5, 11, 6, 5, Blocks.BIRCH_WOOD); // existing aisle lamp
|
||||||
|
}
|
||||||
|
case COMMUNAL_DEPOT -> sawtoothRoof();
|
||||||
|
case RADIO_RELAY -> {
|
||||||
|
vaultRoof(2, 3, 10, 12, 6, true, false);
|
||||||
|
box(2, 6, 6, 10, 6, 6, Blocks.OAK_WOOD);
|
||||||
|
box(2, 6, 10, 10, 6, 10, Blocks.OAK_WOOD);
|
||||||
|
box(9, 7, 4, 9, 11, 4, Blocks.STONE_BRICKS);
|
||||||
|
}
|
||||||
|
case CONSTRUCTION_REFUGE -> {
|
||||||
|
shedRoof(5, 6, 13, 14, 10);
|
||||||
|
box(5, 10, 8, 13, 10, 8, Blocks.OAK_WOOD); // lamp above the shared sleeping room
|
||||||
|
}
|
||||||
|
case POST_OFFICE -> box(5, 7, 5, 11, 7, 11, Blocks.AIR); // a parapet, not a second slab ceiling
|
||||||
|
case PHOTO_CLUB -> box(4, 7, 10, 12, 7, 12, Blocks.AIR);
|
||||||
|
default -> { } // Other roofs already have a single supported slab or a sealed working tank.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void vaultRoof(int x0, int z0, int x1, int z1, int eave, boolean timber, boolean glazed) {
|
||||||
|
box(x0, eave, z0, x1, s.height(), z1, Blocks.AIR);
|
||||||
|
int mid = (z0 + z1) / 2;
|
||||||
|
for (int z = z0; z <= z1; z++) for (int x = x0; x <= x1; x++) {
|
||||||
|
int top = vaultY(x, x0, x1, eave);
|
||||||
|
int adjoining = Math.min(vaultY(Math.max(x0, x - 1), x0, x1, eave),
|
||||||
|
vaultY(Math.min(x1, x + 1), x0, x1, eave));
|
||||||
|
boolean rib = z == z0 || z == mid || z == z1;
|
||||||
|
Block skin = rib ? Blocks.OAK_WOOD : glazed && x > x0 + 2 && x < x1 - 2
|
||||||
|
? Blocks.GLASS : timber ? Blocks.OAK_PLANKS : Blocks.SMOOTH_STONE;
|
||||||
|
// This short riser joins neighbouring height steps by a face, not just a diagonal corner.
|
||||||
|
box(x, Math.min(top, adjoining), z, x, top, z, skin);
|
||||||
|
if (z == z0 || z == z1) box(x, eave, z, x, top, z,
|
||||||
|
glazed && x > x0 + 1 && x < x1 - 1 ? Blocks.GLASS : Blocks.OAK_WOOD);
|
||||||
|
}
|
||||||
|
// Truss feet sit on the existing two long walls; no columns are introduced in an aisle.
|
||||||
|
box(x0, eave, mid, x1, eave, mid, Blocks.OAK_WOOD);
|
||||||
|
for (int x : new int[] {x0, x1}) box(x, eave, z0, x, eave, z1, Blocks.OAK_WOOD);
|
||||||
|
}
|
||||||
|
int vaultY(int x, int x0, int x1, int eave) {
|
||||||
|
return eave + Math.min(3, Math.min(x - x0, x1 - x));
|
||||||
|
}
|
||||||
|
void shedRoof(int x0, int z0, int x1, int z1, int eave) {
|
||||||
|
box(x0, eave, z0, x1, s.height(), z1, Blocks.AIR);
|
||||||
|
for (int z = z0; z <= z1; z++) for (int x = x0; x <= x1; x++) {
|
||||||
|
int top = eave + (x - x0) / 3;
|
||||||
|
int previous = eave + Math.max(0, x - 1 - x0) / 3;
|
||||||
|
box(x, previous, z, x, top, z,
|
||||||
|
z == z0 || z == z1 || z == (z0 + z1) / 2 ? Blocks.OAK_WOOD : Blocks.OAK_PLANKS);
|
||||||
|
if (z == z0 || z == z1 || x == x1) box(x, eave, z, x, top, z, Blocks.OAK_WOOD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void sawtoothRoof() {
|
||||||
|
box(1, 9, 2, 15, s.height(), 14, Blocks.AIR);
|
||||||
|
for (int z = 2; z <= 14; z++) for (int x = 1; x <= 15; x++) {
|
||||||
|
int phase = Math.floorMod(z - 2, 4), top = z == 14 ? 9 : 12 - phase;
|
||||||
|
boolean frame = x == 1 || x == 15;
|
||||||
|
if (phase == 0 && z != 14) box(x, 9, z, x, top, z, frame ? Blocks.OAK_WOOD : Blocks.GLASS);
|
||||||
|
else box(x, Math.max(9, top - 1), z, x, top, z,
|
||||||
|
frame ? Blocks.OAK_WOOD : phase == 1 ? Blocks.GLASS : Blocks.SMOOTH_STONE);
|
||||||
|
if (frame) box(x, 9, z, x, top, z, Blocks.OAK_WOOD);
|
||||||
|
}
|
||||||
|
box(1, 9, 6, 15, 9, 6, Blocks.OAK_WOOD); // carries the original hall lamp
|
||||||
|
// Local, readable storm damage. The perimeter frame and remaining panes still share their bearings.
|
||||||
|
box(12, 10, 11, 13, 11, 11, Blocks.AIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
void gable(int x0, int z0, int x1, int z1, int y) {
|
||||||
|
int rise = Math.min(3, (x1 - x0) / 2);
|
||||||
|
for (int step = 0; step <= rise; step++)
|
||||||
|
box(x0 + step, y + step, z0, x1 - step, y + step, z1, step == rise ? Blocks.STONE_BRICK_SLAB : Blocks.STONE_BRICKS);
|
||||||
|
}
|
||||||
|
void door(int x, int y, int z, Direction facing) { door(x, y, z, facing, false); }
|
||||||
|
void door(int x, int y, int z, Direction facing, boolean right) {
|
||||||
|
BlockState state = Blocks.DARK_OAK_DOOR.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing)
|
||||||
|
.setValue(BlockStateProperties.DOOR_HINGE, right ? DoorHingeSide.RIGHT : DoorHingeSide.LEFT)
|
||||||
|
.setValue(BlockStateProperties.OPEN, false).setValue(BlockStateProperties.POWERED, false);
|
||||||
|
set(x, y, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.LOWER));
|
||||||
|
set(x, y + 1, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.UPPER));
|
||||||
|
}
|
||||||
|
void doubleDoor(int x, int y, int z) { door(x, y, z, Direction.SOUTH, true); door(x + 1, y, z, Direction.SOUTH, false); }
|
||||||
|
void northWindows(int x0, int x1, int z, int y) { box(x0, y, z, x1, y + 1, z, Blocks.GLASS_PANE); }
|
||||||
|
void hang(int x, int ceiling, int z) { set(x, ceiling - 1, z, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true)); }
|
||||||
|
void seat(int x, int y, int z, Direction view) {
|
||||||
|
set(x, y, z, Blocks.DARK_OAK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, view.getOpposite()));
|
||||||
|
}
|
||||||
|
void lever(int x, int y, int z, Direction facing) {
|
||||||
|
set(x, y, z, Blocks.LEVER.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR)
|
||||||
|
.setValue(BlockStateProperties.HORIZONTAL_FACING, facing));
|
||||||
|
}
|
||||||
|
void rail(int x, int z, RailShape shape) { set(x, 1, z, Blocks.RAIL.defaultBlockState().setValue(BlockStateProperties.RAIL_SHAPE, shape)); }
|
||||||
|
void loot(int x, int y, int z, int approachX, int approachZ) {
|
||||||
|
target("loot_approach", approachX, y, approachZ);
|
||||||
|
if (out != null) out.chest(rx(x, z), y, rz(x, z), ResourceKey.create(Registries.LOOT_TABLE,
|
||||||
|
SanctuaryMod.id("chests/heritage19/" + s.kind().name().toLowerCase(Locale.ROOT))), s.seed() ^ 0x19CA7A10L);
|
||||||
|
}
|
||||||
|
void stairs(int x, int z, int base, int rise) {
|
||||||
|
for (int step = 1; step <= rise; step++) for (int xx = x; xx <= x + 1; xx++) {
|
||||||
|
box(xx, base + step + 1, z + step - 1, xx, base + step + 3, z + step - 1, Blocks.AIR);
|
||||||
|
set(xx, base + step - 1, z + step - 1, Blocks.STONE_BRICKS);
|
||||||
|
set(xx, base + step, z + step - 1, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
|
||||||
|
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void bed(int x, int y, int z, Direction facing) {
|
||||||
|
BlockState state = Blocks.BED.white().defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing);
|
||||||
|
set(x, y, z, state.setValue(BlockStateProperties.BED_PART, BedPart.FOOT));
|
||||||
|
set(x + facing.getStepX(), y, z + facing.getStepZ(), state.setValue(BlockStateProperties.BED_PART, BedPart.HEAD));
|
||||||
|
int wx = x + facing.getStepX() * 2, wz = z + facing.getStepZ() * 2;
|
||||||
|
Direction along = facing.getClockWise();
|
||||||
|
for (int side = -1; side <= 1; side++) for (int up = 0; up <= 2; up++)
|
||||||
|
set(wx + along.getStepX() * side, y + up, wz + along.getStepZ() * side, Blocks.SMOOTH_STONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void station() {
|
||||||
|
shell(4, 0, 3, 12, 6, 11); gable(4, 3, 12, 11, 6);
|
||||||
|
door(8, 1, 3, Direction.SOUTH); northWindows(5, 6, 3, 2); northWindows(10, 11, 3, 2);
|
||||||
|
box(5, 1, 5, 6, 1, 5, Blocks.SMOOTH_STONE); set(5, 1, 8, Blocks.BARREL); seat(10, 1, 5, Direction.WEST);
|
||||||
|
for (int z = 2; z <= 14; z++) rail(2, z, RailShape.NORTH_SOUTH);
|
||||||
|
set(2, 1, 15, Blocks.DARK_OAK_FENCE);
|
||||||
|
rail(8, 0, RailShape.NORTH_SOUTH); rail(8, 1, RailShape.NORTH_WEST);
|
||||||
|
for (int x = 3; x <= 7; x++) rail(x, 1, RailShape.EAST_WEST);
|
||||||
|
rail(2, 1, RailShape.SOUTH_EAST);
|
||||||
|
box(0, 5, 5, 3, 5, 11, Blocks.DARK_OAK_PLANKS);
|
||||||
|
for (int z : new int[] {5, 11}) box(1, 1, z, 1, 4, z, Blocks.STRIPPED_OAK_LOG);
|
||||||
|
shell(9, 0, 11, 15, 4, 15); door(10, 1, 11, Direction.SOUTH); door(12, 1, 15, Direction.NORTH);
|
||||||
|
set(10, 1, 13, Blocks.SMOOTH_STONE); lever(10, 2, 13, Direction.EAST);
|
||||||
|
northWindows(13, 14, 15, 2); hang(8, 6, 7); hang(12, 4, 12);
|
||||||
|
target("ticket_counter", 8, 1, 5); target("freight_platform", 3, 1, 9); target("signal_office", 12, 1, 12);
|
||||||
|
target("secondary_entry", 12, 1, 16); loot(14, 1, 14, 13, 14);
|
||||||
|
}
|
||||||
|
void post() {
|
||||||
|
shell(2, 0, 2, 14, 6, 14); door(8, 1, 2, Direction.SOUTH);
|
||||||
|
northWindows(3, 6, 2, 2); northWindows(10, 13, 2, 2);
|
||||||
|
box(3, 1, 7, 13, 5, 7, Blocks.SMOOTH_STONE); door(8, 1, 7, Direction.SOUTH);
|
||||||
|
box(4, 1, 6, 6, 1, 6, Blocks.DARK_OAK_PLANKS);
|
||||||
|
for (int z = 9; z <= 11; z++) { set(3, 1, z, Blocks.BARREL); set(3, 2, z, Blocks.BARREL); }
|
||||||
|
set(6, 1, 10, Blocks.CRAFTING_TABLE); set(8, 1, 12, Blocks.BARREL);
|
||||||
|
shell(10, 0, 10, 14, 6, 14); door(10, 1, 12, Direction.EAST); door(14, 1, 12, Direction.WEST);
|
||||||
|
set(11, 1, 11, Blocks.CARTOGRAPHY_TABLE); hang(8, 6, 4); hang(7, 6, 10); hang(12, 6, 12);
|
||||||
|
target("public_hall", 8, 1, 4); target("sorting_aisle", 7, 1, 10); target("factor_office", 12, 1, 12);
|
||||||
|
target("secondary_entry", 15, 1, 12); loot(13, 1, 13, 12, 13);
|
||||||
|
}
|
||||||
|
void depot() {
|
||||||
|
shell(1, 0, 2, 15, 9, 14); gable(1, 2, 15, 14, 9);
|
||||||
|
box(7, 1, 2, 9, 3, 2, Blocks.AIR); door(4, 1, 2, Direction.SOUTH);
|
||||||
|
for (int z : new int[] {5, 8}) {
|
||||||
|
box(4, 1, z, 5, 1, z + 1, Blocks.DARK_OAK_PLANKS); set(4, 2, z, Blocks.BARREL);
|
||||||
|
box(11, 1, z, 13, 1, z + 1, Blocks.DARK_OAK_PLANKS); set(13, 2, z + 1, Blocks.BARREL);
|
||||||
|
}
|
||||||
|
box(2, 5, 10, 14, 5, 13, Blocks.SMOOTH_STONE);
|
||||||
|
for (int x = 5; x <= 14; x++) set(x, 6, 10, Blocks.IRON_BARS);
|
||||||
|
shell(10, 5, 10, 14, 9, 14); door(10, 6, 12, Direction.EAST);
|
||||||
|
set(11, 6, 11, Blocks.LECTERN.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
|
||||||
|
stairs(2, 5, 0, 5); door(15, 1, 12, Direction.WEST); hang(8, 9, 6); hang(12, 9, 12);
|
||||||
|
for (int z = 0; z <= 8; z++) rail(8, z, RailShape.NORTH_SOUTH);
|
||||||
|
set(8, 1, 9, Blocks.DARK_OAK_FENCE);
|
||||||
|
target("unloading_bay", 8, 1, 5); target("inventory_mezzanine", 5, 6, 12);
|
||||||
|
target("secondary_entry", 16, 1, 12); loot(13, 6, 13, 12, 13);
|
||||||
|
}
|
||||||
|
void expedition() {
|
||||||
|
shell(2, 0, 2, 12, 6, 12); door(8, 1, 2, Direction.SOUTH);
|
||||||
|
northWindows(3, 5, 2, 2); northWindows(10, 11, 2, 2);
|
||||||
|
set(4, 1, 4, Blocks.CARTOGRAPHY_TABLE); set(5, 1, 4, Blocks.LECTERN.defaultBlockState()
|
||||||
|
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH)); set(3, 1, 6, Blocks.BARREL);
|
||||||
|
shell(2, 0, 8, 6, 6, 12); door(6, 1, 10, Direction.WEST); set(3, 1, 9, Blocks.BOOKSHELF);
|
||||||
|
box(3, 0, 13, 14, 0, 15, Blocks.DARK_OAK_PLANKS);
|
||||||
|
box(3, 1, 15, 14, 1, 15, Blocks.DARK_OAK_FENCE); door(10, 1, 12, Direction.NORTH);
|
||||||
|
seat(6, 1, 13, Direction.SOUTH); hang(8, 6, 5); hang(4, 6, 10);
|
||||||
|
target("expedition_table", 5, 1, 5); target("balcony", 10, 1, 14);
|
||||||
|
target("secondary_entry", 10, 1, 13); loot(3, 1, 11, 4, 11);
|
||||||
|
}
|
||||||
|
boolean tower(int x, int z) { return x >= 3 && x <= 13 && z >= 3 && z <= 13 && Math.abs(x - 8) + Math.abs(z - 8) <= 8; }
|
||||||
|
void observatory() {
|
||||||
|
for (int y = 0; y <= 12; y++) for (int z = 3; z <= 13; z++) for (int x = 3; x <= 13; x++) if (tower(x, z)) {
|
||||||
|
boolean wall = !tower(x - 1, z) || !tower(x + 1, z) || !tower(x, z - 1) || !tower(x, z + 1);
|
||||||
|
set(x, y, z, y == 0 || y == 6 || y == 12 ? Blocks.SMOOTH_STONE.defaultBlockState()
|
||||||
|
: wall ? LostCityPalette.masonry(s.seed(), x, y, z) : Blocks.AIR.defaultBlockState());
|
||||||
|
if (y == 12 && wall) set(x, 13, z, Blocks.IRON_BARS);
|
||||||
|
}
|
||||||
|
door(8, 1, 3, Direction.SOUTH); door(8, 1, 13, Direction.NORTH);
|
||||||
|
northWindows(7, 9, 3, 8); box(13, 8, 7, 13, 9, 9, Blocks.GLASS_PANE);
|
||||||
|
shell(9, 0, 9, 12, 4, 12); door(9, 1, 10, Direction.EAST); set(10, 1, 11, Blocks.BARREL);
|
||||||
|
set(10, 7, 5, Blocks.CARTOGRAPHY_TABLE); set(11, 7, 8, Blocks.SMOOTH_STONE); lever(11, 8, 8, Direction.WEST);
|
||||||
|
stairs(4, 5, 0, 6); stairs(4, 5, 6, 6);
|
||||||
|
box(4, 12, 11, 6, 12, 11, Blocks.SMOOTH_STONE); box(4, 13, 11, 6, 15, 11, Blocks.AIR);
|
||||||
|
box(10, 13, 9, 10, 18, 9, Blocks.DARK_OAK_FENCE); box(9, 18, 9, 11, 18, 9, Blocks.IRON_BARS);
|
||||||
|
hang(8, 6, 6); hang(10, 4, 11);
|
||||||
|
target("instruments", 9, 7, 8); target("sky_platform", 8, 13, 8);
|
||||||
|
target("secondary_entry", 8, 1, 14); loot(11, 1, 11, 11, 10);
|
||||||
|
}
|
||||||
|
void railway() {
|
||||||
|
shell(1, 0, 2, 15, 6, 14); gable(1, 2, 15, 14, 6); doubleDoor(7, 1, 2);
|
||||||
|
for (int z = 3; z <= 13; z++) for (int x : new int[] {5, 11}) set(x, 1, z, Blocks.RAIL);
|
||||||
|
box(4, 1, 2, 6, 3, 2, Blocks.AIR); rail(8, 0, RailShape.NORTH_SOUTH); rail(8, 1, RailShape.NORTH_WEST);
|
||||||
|
rail(7, 1, RailShape.EAST_WEST); rail(6, 1, RailShape.EAST_WEST); rail(5, 1, RailShape.SOUTH_EAST); rail(5, 2, RailShape.NORTH_SOUTH);
|
||||||
|
box(7, -2, 5, 9, -2, 11, Blocks.SMOOTH_STONE); box(7, -1, 5, 9, 0, 11, Blocks.AIR);
|
||||||
|
for (int step = 0; step <= 2; step++) set(8, -step, 4 + step, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
|
||||||
|
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
|
||||||
|
for (int z = 6; z <= 11; z++) { set(6, 1, z, Blocks.IRON_BARS); set(10, 1, z, Blocks.IRON_BARS); }
|
||||||
|
set(2, 1, 4, Blocks.CRAFTING_TABLE); set(3, 1, 4, Blocks.FURNACE.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
|
||||||
|
set(2, 1, 6, Blocks.GRINDSTONE.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR));
|
||||||
|
shell(1, 0, 10, 5, 5, 14); door(5, 1, 12, Direction.WEST); door(8, 1, 14, Direction.NORTH);
|
||||||
|
set(5, 1, 9, Blocks.DARK_OAK_FENCE);
|
||||||
|
set(2, 1, 11, Blocks.ANVIL); hang(8, 6, 8); hang(3, 5, 12);
|
||||||
|
target("maintenance_pit", 8, -1, 9); target("workbench", 3, 1, 5);
|
||||||
|
target("secondary_entry", 8, 1, 15); loot(2, 1, 13, 3, 13);
|
||||||
|
}
|
||||||
|
void pumping() {
|
||||||
|
shell(2, 0, 3, 9, 6, 12); door(8, 1, 3, Direction.SOUTH);
|
||||||
|
set(3, 1, 5, Blocks.DISPENSER.defaultBlockState().setValue(BlockStateProperties.FACING, Direction.EAST));
|
||||||
|
set(4, 1, 5, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 3));
|
||||||
|
set(3, 2, 5, Blocks.POLISHED_ANDESITE); box(3, 3, 5, 3, 5, 8, Blocks.IRON_BARS);
|
||||||
|
for (int x : new int[] {10, 15}) for (int z : new int[] {5, 11}) box(x, 1, z, x, 7, z, Blocks.POLISHED_ANDESITE);
|
||||||
|
shell(10, 8, 5, 15, 13, 11); box(11, 9, 6, 14, 11, 10, Blocks.WATER);
|
||||||
|
box(10, 3, 6, 14, 3, 13, Blocks.SMOOTH_STONE); stairs(10, 3, 0, 3);
|
||||||
|
shell(2, 0, 10, 6, 5, 15); door(5, 1, 10, Direction.SOUTH); door(5, 1, 15, Direction.NORTH);
|
||||||
|
set(3, 1, 11, Blocks.BARREL); hang(7, 6, 7); hang(4, 5, 12);
|
||||||
|
target("pump_room", 6, 1, 6); target("reservoir_walkway", 13, 4, 12);
|
||||||
|
target("secondary_entry", 5, 1, 16); loot(3, 1, 14, 4, 14);
|
||||||
|
}
|
||||||
|
void substation() {
|
||||||
|
for (int z = 2; z <= 14; z++) { set(1, 1, z, Blocks.IRON_BARS); set(15, 1, z, Blocks.IRON_BARS); }
|
||||||
|
for (int x = 1; x <= 15; x++) if (x < 7 || x > 9) set(x, 1, 2, Blocks.IRON_BARS);
|
||||||
|
for (int z : new int[] {5, 8}) {
|
||||||
|
box(3, 1, z, 4, 2, z + 1, Blocks.POLISHED_ANDESITE); box(3, 3, z, 4, 3, z, Blocks.IRON_BARS);
|
||||||
|
}
|
||||||
|
shell(10, 0, 4, 15, 5, 10); door(10, 1, 6, Direction.EAST); northWindows(12, 14, 4, 2);
|
||||||
|
set(12, 1, 5, Blocks.SMOOTH_STONE); lever(12, 2, 5, Direction.SOUTH); set(14, 1, 5, Blocks.CRAFTING_TABLE);
|
||||||
|
shell(10, 0, 10, 15, 5, 15); door(12, 1, 10, Direction.SOUTH); door(15, 1, 13, Direction.WEST);
|
||||||
|
box(6, -2, 10, 7, -2, 13, Blocks.SMOOTH_STONE); box(6, -1, 10, 7, 0, 13, Blocks.AIR);
|
||||||
|
for (int step = 0; step <= 2; step++) set(6, -step, 9 + step, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
|
||||||
|
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
|
||||||
|
hang(13, 5, 7); hang(12, 5, 13); target("distribution_yard", 8, 1, 6); target("cable_trench", 6, -1, 12);
|
||||||
|
target("secondary_entry", 16, 1, 13); loot(14, 1, 14, 13, 14);
|
||||||
|
}
|
||||||
|
void greenhouse() {
|
||||||
|
shell(1, 0, 2, 11, 6, 14);
|
||||||
|
for (int y = 2; y <= 5; y++) for (int z = 3; z <= 13; z++) { set(1, y, z, Blocks.GLASS); set(11, y, z, Blocks.GLASS); }
|
||||||
|
northWindows(2, 5, 2, 2); northWindows(7, 10, 2, 2); door(6, 1, 2, Direction.SOUTH);
|
||||||
|
for (int x = 2; x <= 10; x++) for (int z = 3; z <= 13; z++) set(x, 6, z, x == 6 ? Blocks.SMOOTH_STONE : Blocks.GLASS);
|
||||||
|
for (int x : new int[] {2, 3, 4, 8, 9, 10}) for (int z = 4; z <= 10; z++) {
|
||||||
|
set(x, 0, z, Blocks.FARMLAND.defaultBlockState().setValue(BlockStateProperties.MOISTURE, 7));
|
||||||
|
set(x, 1, z, Blocks.WHEAT.defaultBlockState().setValue(BlockStateProperties.AGE_7,
|
||||||
|
2 + LostCityPalette.roll(s.seed(), x, 1, z, 6)));
|
||||||
|
}
|
||||||
|
set(3, 0, 6, Blocks.WATER); set(9, 0, 6, Blocks.WATER);
|
||||||
|
set(3, 1, 6, Blocks.AIR); set(9, 1, 6, Blocks.AIR);
|
||||||
|
shell(11, 0, 9, 15, 5, 15); door(11, 1, 12, Direction.EAST); door(13, 1, 15, Direction.NORTH);
|
||||||
|
set(12, 1, 10, Blocks.CRAFTING_TABLE); set(13, 1, 10, Blocks.FLOWER_POT); set(9, 1, 12, Blocks.COMPOSTER);
|
||||||
|
hang(6, 6, 5); hang(13, 5, 12); target("glasshouse_aisle", 6, 1, 7); target("potting_shed", 13, 1, 12);
|
||||||
|
target("secondary_entry", 13, 1, 16); loot(14, 1, 14, 13, 14);
|
||||||
|
}
|
||||||
|
void dock() {
|
||||||
|
box(0, 0, 0, 16, 0, 16, Blocks.DARK_OAK_PLANKS); box(0, 1, 16, 16, 1, 16, Blocks.DARK_OAK_FENCE);
|
||||||
|
box(0, 1, 1, 0, 1, 15, Blocks.DARK_OAK_FENCE); box(16, 1, 1, 16, 1, 15, Blocks.DARK_OAK_FENCE);
|
||||||
|
shell(2, 0, 3, 7, 5, 10); door(7, 1, 6, Direction.WEST); seat(3, 1, 5, Direction.EAST);
|
||||||
|
shell(2, 0, 10, 7, 5, 15); door(5, 1, 10, Direction.SOUTH); door(7, 1, 13, Direction.WEST);
|
||||||
|
box(12, 1, 5, 12, 11, 5, Blocks.STRIPPED_OAK_LOG); box(8, 11, 5, 14, 11, 5, Blocks.STRIPPED_OAK_LOG);
|
||||||
|
box(14, 7, 5, 14, 10, 5, Blocks.IRON_CHAIN); hang(10, 11, 5); hang(5, 5, 6); hang(5, 5, 12);
|
||||||
|
target("departure_deck", 8, 1, 14); target("waiting_room", 5, 1, 6); target("secondary_entry", 8, 1, 13);
|
||||||
|
loot(3, 1, 14, 4, 14);
|
||||||
|
}
|
||||||
|
void radio() {
|
||||||
|
shell(2, 0, 3, 10, 6, 12); door(8, 1, 3, Direction.SOUTH); northWindows(3, 5, 3, 2);
|
||||||
|
bed(4, 1, 5, Direction.NORTH); set(8, 1, 5, Blocks.CARTOGRAPHY_TABLE);
|
||||||
|
shell(2, 0, 8, 6, 6, 12); door(6, 1, 10, Direction.WEST); door(2, 1, 10, Direction.EAST);
|
||||||
|
box(11, 5, 6, 15, 5, 10, Blocks.SMOOTH_STONE);
|
||||||
|
for (int x : new int[] {11, 15}) for (int z : new int[] {6, 10}) box(x, 1, z, x, 4, z, Blocks.POLISHED_ANDESITE);
|
||||||
|
box(13, 1, 8, 13, 18, 8, Blocks.POLISHED_ANDESITE); box(11, 17, 8, 15, 17, 8, Blocks.IRON_BARS);
|
||||||
|
box(13, 19, 8, 13, 21, 8, Blocks.IRON_BARS); stairs(11, 1, 0, 5);
|
||||||
|
hang(7, 6, 6); hang(4, 6, 10);
|
||||||
|
target("caretaker_room", 7, 1, 6); target("antenna_platform", 12, 6, 9); target("secondary_entry", 1, 1, 10);
|
||||||
|
loot(3, 1, 11, 4, 11);
|
||||||
|
}
|
||||||
|
void photo() {
|
||||||
|
shell(2, 0, 2, 14, 6, 14); door(8, 1, 2, Direction.SOUTH); northWindows(3, 6, 2, 2); northWindows(10, 13, 2, 2);
|
||||||
|
box(3, 1, 8, 13, 5, 8, Blocks.SMOOTH_STONE); door(4, 1, 8, Direction.SOUTH);
|
||||||
|
box(8, 1, 9, 8, 5, 13, Blocks.SMOOTH_STONE); door(8, 1, 11, Direction.EAST);
|
||||||
|
box(10, 1, 5, 12, 1, 5, Blocks.DARK_OAK_PLANKS); set(3, 1, 4, Blocks.BOOKSHELF);
|
||||||
|
for (int z : new int[] {10, 12}) set(3, 1, z, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 3));
|
||||||
|
box(11, 4, 10, 11, 5, 12, Blocks.IRON_CHAIN); set(12, 1, 10, Blocks.CRAFTING_TABLE);
|
||||||
|
door(12, 1, 14, Direction.NORTH); hang(8, 6, 4); hang(5, 6, 11); hang(12, 6, 12);
|
||||||
|
target("club_room", 8, 1, 4); target("darkroom", 5, 1, 11); target("drying_room", 10, 1, 11);
|
||||||
|
target("secondary_entry", 12, 1, 15); loot(13, 1, 13, 12, 13);
|
||||||
|
}
|
||||||
|
void impossible() {
|
||||||
|
shell(3, 0, 2, 11, 4, 7); door(8, 1, 2, Direction.SOUTH);
|
||||||
|
for (int z = 3; z <= 5; z++) { set(4, 1, z, Blocks.BARREL); set(4, 2, z, Blocks.BARREL); }
|
||||||
|
seat(10, 1, 4, Direction.WEST);
|
||||||
|
shell(1, 0, 7, 15, 10, 15); door(8, 1, 7, Direction.SOUTH);
|
||||||
|
for (int x = 2; x <= 14; x++) for (int z = 8; z <= 14; z++) set(x, 0, z,
|
||||||
|
((x + z) & 1) == 0 ? Blocks.CALCITE : Blocks.POLISHED_ANDESITE);
|
||||||
|
for (int x : new int[] {3, 6, 10, 13}) box(x, 1, 11, x, 9, 11, Blocks.CALCITE);
|
||||||
|
shell(1, 0, 12, 5, 5, 15); door(5, 1, 13, Direction.WEST); door(3, 1, 15, Direction.NORTH);
|
||||||
|
set(2, 2, 12, Blocks.GLASS); set(2, 1, 13, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 2));
|
||||||
|
hang(8, 4, 4); hang(8, 10, 11); hang(3, 5, 13);
|
||||||
|
target("lockers", 8, 1, 4); target("oversized_tiled_hall", 8, 1, 11);
|
||||||
|
target("secondary_entry", 3, 1, 16); loot(2, 1, 14, 3, 14);
|
||||||
|
}
|
||||||
|
void refuge() {
|
||||||
|
box(3, 4, 5, 14, 4, 15, Blocks.DARK_OAK_PLANKS);
|
||||||
|
for (int x : new int[] {3, 14}) for (int z : new int[] {5, 15}) box(x, 1, z, x, 3, z, Blocks.STONE_BRICKS);
|
||||||
|
shell(5, 4, 6, 13, 10, 14); gable(5, 6, 13, 14, 10); door(8, 5, 6, Direction.SOUTH);
|
||||||
|
bed(6, 5, 8, Direction.NORTH); set(12, 5, 8, Blocks.CRAFTING_TABLE);
|
||||||
|
shell(6, 0, 9, 11, 4, 14); door(6, 1, 11, Direction.EAST);
|
||||||
|
// Carry the ladder through the floor so opening the hatch leaves a climbable lip.
|
||||||
|
for (int y = 1; y <= 4; y++) set(10, y, 11, Blocks.LADDER.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST));
|
||||||
|
set(10, 5, 11, Blocks.DARK_OAK_TRAPDOOR.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST)
|
||||||
|
.setValue(BlockStateProperties.HALF, Half.BOTTOM));
|
||||||
|
stairs(7, 1, 0, 4); box(2, 1, 3, 3, 11, 4, Blocks.STONE_BRICKS); box(2, 11, 3, 14, 11, 4, Blocks.STRIPPED_OAK_LOG);
|
||||||
|
hang(10, 10, 8); hang(8, 4, 12);
|
||||||
|
target("suspended_hut", 9, 5, 8); target("hidden_floor_hatch", 10, 5, 11); target("secondary_entry", 5, 1, 11);
|
||||||
|
loot(7, 1, 13, 8, 13);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-4
@@ -14,7 +14,7 @@ public final class HeritageStructure19 extends Structure {
|
|||||||
|
|
||||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||||
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||||
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20) || !generator.experimentalStructures()) return Optional.empty();
|
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21) || !generator.experimentalStructures()) return Optional.empty();
|
||||||
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
|
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
|
||||||
if (island == null || !island.origin() || (island.generation() != generator.rootGeneration())) return Optional.empty();
|
if (island == null || !island.origin() || (island.generation() != generator.rootGeneration())) return Optional.empty();
|
||||||
var plan = generator.heritagePlan19();
|
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.
|
// Local route segments render before buildings. No piece needs a distant link endpoint loaded.
|
||||||
for (var segment : plan.segments())
|
for (var segment : plan.segments())
|
||||||
if (segment.startChunkX() == x && segment.startChunkZ() == z)
|
if (segment.startChunkX() == x && segment.startChunkZ() == z)
|
||||||
pieces.add(generator.rootGeneration() == 20 ? new HeritagePiece20(segment) : new HeritagePiece19(segment));
|
pieces.add(generator.rootGeneration() == 21 ? new HeritagePiece21(segment) : generator.rootGeneration() == 20 ? new HeritagePiece20(segment) : new HeritagePiece19(segment));
|
||||||
for (var site : plan.sites()) if (site.startChunkX() == x && site.startChunkZ() == z)
|
for (var site : plan.sites()) if (site.startChunkX() == x && site.startChunkZ() == z)
|
||||||
pieces.add(generator.rootGeneration() == 20 ? new HeritagePiece20(site) : new HeritagePiece19(site));
|
pieces.add(generator.rootGeneration() == 21 ? new HeritagePiece21(site) : generator.rootGeneration() == 20 ? new HeritagePiece20(site) : new HeritagePiece19(site));
|
||||||
if (pieces.isEmpty()) return Optional.empty();
|
if (pieces.isEmpty()) return Optional.empty();
|
||||||
for (var piece : pieces) {
|
for (var piece : pieces) {
|
||||||
var box = piece.getBoundingBox();
|
var box = piece.getBoundingBox();
|
||||||
@@ -35,7 +35,7 @@ public final class HeritageStructure19 extends Structure {
|
|||||||
|| Math.abs((long) Math.floorDiv(box.maxZ(), 16) - z) > 7)
|
|| Math.abs((long) Math.floorDiv(box.maxZ(), 16) - z) > 7)
|
||||||
throw new IllegalStateException("Heritage piece exceeds its local native reference contract");
|
throw new IllegalStateException("Heritage piece exceeds its local native reference contract");
|
||||||
}
|
}
|
||||||
var locator = pieces.stream().filter(piece -> piece instanceof HeritagePiece19 old && old.isSite() || piece instanceof HeritagePiece20 detail && detail.isSite()).findFirst().orElse(pieces.getFirst()).getLocatorPosition();
|
var locator = pieces.stream().filter(piece -> piece instanceof HeritagePiece19 old && old.isSite() || piece instanceof HeritagePiece20 detail && detail.isSite() || piece instanceof HeritagePiece21 current && current.isSite()).findFirst().orElse(pieces.getFirst()).getLocatorPosition();
|
||||||
return Optional.of(new GenerationStub(locator, builder -> pieces.forEach(builder::addPiece)));
|
return Optional.of(new GenerationStub(locator, builder -> pieces.forEach(builder::addPiece)));
|
||||||
}
|
}
|
||||||
@Override public StructureType<?> type() { return HeritageStructures19.TYPE; }
|
@Override public StructureType<?> type() { return HeritageStructures19.TYPE; }
|
||||||
|
|||||||
+5
-3
@@ -35,6 +35,8 @@ public final class HeritageStructures19 {
|
|||||||
SanctuaryMod.id("heritage_networks_v19"), () -> HeritageStructure19.CODEC);
|
SanctuaryMod.id("heritage_networks_v19"), () -> HeritageStructure19.CODEC);
|
||||||
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
|
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
|
||||||
SanctuaryMod.id("heritage_piece_v19"), HeritagePiece19::new);
|
SanctuaryMod.id("heritage_piece_v19"), HeritagePiece19::new);
|
||||||
|
public static final StructurePieceType PIECE21 = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
|
||||||
|
SanctuaryMod.id("heritage_piece_v21"), HeritagePiece21::new);
|
||||||
public static final StructurePieceType PIECE20 = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
|
public static final StructurePieceType PIECE20 = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
|
||||||
SanctuaryMod.id("heritage_piece_v20"), HeritagePiece20::new);
|
SanctuaryMod.id("heritage_piece_v20"), HeritagePiece20::new);
|
||||||
private static final ThreadLocal<Integer> PLACEMENT_DEPTH = ThreadLocal.withInitial(() -> 0);
|
private static final ThreadLocal<Integer> PLACEMENT_DEPTH = ThreadLocal.withInitial(() -> 0);
|
||||||
@@ -55,7 +57,7 @@ public final class HeritageStructures19 {
|
|||||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||||
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
||||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 19 && generator.rootGeneration() != 20)
|
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21)
|
||||||
|| !sanctuary.experimentalStructures()) return;
|
|| !sanctuary.experimentalStructures()) return;
|
||||||
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
||||||
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
|
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
|
||||||
@@ -77,7 +79,7 @@ public final class HeritageStructures19 {
|
|||||||
BlockPos origin, int radius, boolean skipReferenced) {
|
BlockPos origin, int radius, boolean skipReferenced) {
|
||||||
if (!Level.OVERWORLD.equals(level.dimension())
|
if (!Level.OVERWORLD.equals(level.dimension())
|
||||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||||
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20) || !generator.experimentalStructures()
|
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21) || !generator.experimentalStructures()
|
||||||
|| !level.structureManager().shouldGenerateStructures()) return null;
|
|| !level.structureManager().shouldGenerateStructures()) return null;
|
||||||
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||||
if (!structures.contains(holder)) return null;
|
if (!structures.contains(holder)) return null;
|
||||||
@@ -99,7 +101,7 @@ public final class HeritageStructures19 {
|
|||||||
if (start == null || !start.isValid() || skipReferenced && !start.canBeReferenced()) continue;
|
if (start == null || !start.isValid() || skipReferenced && !start.canBeReferenced()) continue;
|
||||||
if (skipReferenced) level.structureManager().addReference(start);
|
if (skipReferenced) level.structureManager().addReference(start);
|
||||||
var entrance = start.getPieces().stream().filter(p -> (p instanceof HeritagePiece19 site && site.isSite())
|
var entrance = start.getPieces().stream().filter(p -> (p instanceof HeritagePiece19 site && site.isSite())
|
||||||
|| (p instanceof HeritagePiece20 detail && detail.isSite())).findFirst().orElseThrow();
|
|| (p instanceof HeritagePiece20 detail && detail.isSite()) || (p instanceof HeritagePiece21 current && current.isSite())).findFirst().orElseThrow();
|
||||||
return Pair.of(entrance.getLocatorPosition(), holder);
|
return Pair.of(entrance.getLocatorPosition(), holder);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.heritage;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||||
|
import fr.koka.sanctuary.worldgen.city.LostCityPlan;
|
||||||
|
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
|
import net.minecraft.world.level.NoiseColumn;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||||
|
import net.minecraft.world.level.levelgen.RandomState;
|
||||||
|
|
||||||
|
/** Read-only pre-decoration terrain. No loaded chunks or player-edited blocks enter a plan. */
|
||||||
|
public final class HeritageTerrain21 implements HeritagePlanner21.Terrain {
|
||||||
|
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
|
||||||
|
private final ExpansionIsland island;
|
||||||
|
private final NoiseBasedChunkGenerator generator;
|
||||||
|
private final RandomState random;
|
||||||
|
private final LevelHeightAccessor height;
|
||||||
|
private final Predicate<BlockPos> protection;
|
||||||
|
private final Map<Long, NoiseColumn> columns = new HashMap<>();
|
||||||
|
private List<HeritagePlan19.Landmark> landmarks = List.of();
|
||||||
|
|
||||||
|
public HeritageTerrain21(ExpansionIsland island, NoiseBasedChunkGenerator generator, RandomState random,
|
||||||
|
LevelHeightAccessor height, Predicate<BlockPos> protection) {
|
||||||
|
this.island = island; this.generator = generator; this.random = random;
|
||||||
|
this.height = height; this.protection = protection;
|
||||||
|
}
|
||||||
|
private NoiseColumn column(int x, int z) {
|
||||||
|
if (!owns(x, z)) return null;
|
||||||
|
long key = ((long) x << 32) ^ (z & 0xffffffffL);
|
||||||
|
var previous = columns.get(key);
|
||||||
|
if (previous != null) return previous;
|
||||||
|
if (columns.size() >= HeritagePlan19.MAX_COLUMNS) return null;
|
||||||
|
var result = generator.getBaseColumn(x, z, height, random);
|
||||||
|
columns.put(key, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
@Override public boolean owns(int x, int z) { return island.contains(x, z); }
|
||||||
|
@Override public boolean solid(int x, int y, int z) {
|
||||||
|
if (y < 0 || y >= 384) return false;
|
||||||
|
var column = column(x, z);
|
||||||
|
return column != null && column.getBlock(y).isSolid() && column.getBlock(y).getFluidState().isEmpty();
|
||||||
|
}
|
||||||
|
@Override public boolean protectedAt(int x, int y, int z) {
|
||||||
|
if (y < 0 || y >= 384) return true;
|
||||||
|
var column = column(x, z);
|
||||||
|
return column == null || !column.getBlock(y).getFluidState().isEmpty()
|
||||||
|
|| protection.test(new BlockPos(x, y, z));
|
||||||
|
}
|
||||||
|
@Override public int surfaceY(int x, int z) {
|
||||||
|
var column = column(x, z);
|
||||||
|
if (column == null) return -1;
|
||||||
|
for (int y = 380; y >= 1; y--) {
|
||||||
|
var state = column.getBlock(y);
|
||||||
|
if (!state.isAir()) return state.isSolid() && state.getFluidState().isEmpty() ? y : -1;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
@Override public List<HeritagePlan19.Landmark> landmarks() { return landmarks; }
|
||||||
|
public int columnsUsed() { return columns.size(); }
|
||||||
|
|
||||||
|
/** Select actual dry ground abutting existing walkable platforms. Their protected blocks stay intact. */
|
||||||
|
public void discoverLandmarks(LostCityPlan city, RuinsPlan17 ruins) {
|
||||||
|
var result = new ArrayList<HeritagePlan19.Landmark>();
|
||||||
|
for (var network : ruins.networks()) {
|
||||||
|
var access = network.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS).findFirst().orElse(null);
|
||||||
|
if (access == null) continue;
|
||||||
|
int deck = access.maxY() - 2;
|
||||||
|
for (int direction = 0; direction < 4; direction++) {
|
||||||
|
var entry = dryEntry(access.centerX() + DX[direction] * 3, access.centerZ() + DZ[direction] * 3, deck, direction);
|
||||||
|
if (entry == null) continue;
|
||||||
|
var kind = network.family() == RuinsPlan17.Family.MINE ? HeritagePlan19.LandmarkKind.MINE : HeritagePlan19.LandmarkKind.UTILITY;
|
||||||
|
result.add(new HeritagePlan19.Landmark("old_" + network.id(), kind, entry));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int cityEntries = 0;
|
||||||
|
for (var district : city.districts()) {
|
||||||
|
int districtEntries = 0;
|
||||||
|
districtLoop: for (var parcel : district.parcels()) {
|
||||||
|
if (parcel.kind() != LostCityPlan.Kind.STREET || parcel.width() < 3 || parcel.depth() < 3) continue;
|
||||||
|
for (int direction = 0; direction < 4; direction++) {
|
||||||
|
int x = direction == 1 ? parcel.width() - 1 : direction == 3 ? 0 : parcel.width() / 2;
|
||||||
|
int z = direction == 2 ? parcel.depth() - 1 : direction == 0 ? 0 : parcel.depth() / 2;
|
||||||
|
var entry = dryEntry(parcel.minX() + x + DX[direction], parcel.minZ() + z + DZ[direction], parcel.surfaceY(x, z), direction);
|
||||||
|
if (entry == null) continue;
|
||||||
|
result.add(new HeritagePlan19.Landmark("old_city_" + cityEntries++, HeritagePlan19.LandmarkKind.CITY, entry));
|
||||||
|
if (++districtEntries == 2 || result.size() >= 16) break districtLoop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.size() >= 16) break;
|
||||||
|
}
|
||||||
|
landmarks = List.copyOf(result);
|
||||||
|
}
|
||||||
|
private HeritagePlan19.Entry dryEntry(int x, int z, int deck, int direction) {
|
||||||
|
int y = surfaceY(x, z);
|
||||||
|
if (y < 32 || y > 348 || Math.abs(y - deck) > 1) return null;
|
||||||
|
// A three-wide arrival can abut a platform without cutting its wall or footings.
|
||||||
|
for (int side = -1; side <= 1; side++) {
|
||||||
|
int px = x + DZ[direction] * side, pz = z + DX[direction] * side;
|
||||||
|
var column = column(px, pz);
|
||||||
|
if (column == null || surfaceY(px, pz) != y || !solid(px, y, pz)) return null;
|
||||||
|
for (int by = y - 1; by <= y + 3; by++) if (protectedAt(px, by, pz)) return null;
|
||||||
|
for (int by = y + 1; by <= y + 3; by++) if (!column.getBlock(by).isAir()) return null;
|
||||||
|
}
|
||||||
|
return new HeritagePlan19.Entry(x, y + 1, z, direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ public final class RuinsStructure17 extends Structure {
|
|||||||
|
|
||||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||||
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||||
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20) || !generator.experimentalStructures()) return Optional.empty();
|
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21) || !generator.experimentalStructures()) return Optional.empty();
|
||||||
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
|
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
|
||||||
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return Optional.empty();
|
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return Optional.empty();
|
||||||
var networks = generator.ruinsPlan17().networks().stream()
|
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 -> {
|
return Optional.of(new GenerationStub(RuinsStructures17.locator(networks.getFirst()), pieces -> {
|
||||||
// Shafts come last so their surface hatches cannot be filled by a connected room.
|
// 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)
|
networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() != RuinsPlan17.Kind.ACCESS)
|
||||||
.forEach(r -> pieces.addPiece(generator.rootGeneration() == 20 ? new RuinsPiece20(r) : new RuinsPiece17(r))));
|
.forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
|
||||||
networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS)
|
networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS)
|
||||||
.forEach(r -> pieces.addPiece(generator.rootGeneration() == 20 ? new RuinsPiece20(r) : new RuinsPiece17(r))));
|
.forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@Override public StructureType<?> type() { return RuinsStructures17.TYPE; }
|
@Override public StructureType<?> type() { return RuinsStructures17.TYPE; }
|
||||||
|
|||||||
+2
-2
@@ -55,7 +55,7 @@ public final class RuinsStructures17 {
|
|||||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||||
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
||||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20)
|
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21)
|
||||||
|| !sanctuary.experimentalStructures()) return;
|
|| !sanctuary.experimentalStructures()) return;
|
||||||
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
||||||
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
|
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
|
||||||
@@ -78,7 +78,7 @@ public final class RuinsStructures17 {
|
|||||||
BlockPos origin, int radius, boolean skipReferenced) {
|
BlockPos origin, int radius, boolean skipReferenced) {
|
||||||
if (!Level.OVERWORLD.equals(level.dimension())
|
if (!Level.OVERWORLD.equals(level.dimension())
|
||||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||||
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20) || !generator.experimentalStructures()
|
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21) || !generator.experimentalStructures()
|
||||||
|| !level.structureManager().shouldGenerateStructures()) return null;
|
|| !level.structureManager().shouldGenerateStructures()) return null;
|
||||||
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||||
if (!structures.contains(holder)) return null;
|
if (!structures.contains(holder)) return null;
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.transit;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Set;
|
||||||
|
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.nbt.ListTag;
|
||||||
|
import net.minecraft.nbt.Tag;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.StructureManager;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||||
|
import net.minecraft.world.level.storage.loot.LootTable;
|
||||||
|
|
||||||
|
/** Saved generation21 room geometry is rendered without replanning or reading neighboring chunks. */
|
||||||
|
public final class TransitPiece21 extends StructurePiece {
|
||||||
|
private static final Set<String> MODULE_KEYS = Set.of("Schema", "Kind", "MinX", "MinY", "MinZ", "Width",
|
||||||
|
"Height", "Depth", "Facing", "Openings", "ModuleSeed", "Supports");
|
||||||
|
private static final Set<String> SUPPORT_KEYS = Set.of("X", "MinY", "Z", "MaxY");
|
||||||
|
private final TransitPlan21.Module room;
|
||||||
|
|
||||||
|
public TransitPiece21(TransitPlan21.Module room) {
|
||||||
|
super(TransitStructures21.PIECE, 0, bounds(room));
|
||||||
|
this.room = room;
|
||||||
|
setOrientation(null);
|
||||||
|
}
|
||||||
|
public TransitPiece21(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||||
|
super(TransitStructures21.PIECE, tag);
|
||||||
|
room = decodeModule(tag.getCompound("SanctuaryModule")
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Missing saved transit room")));
|
||||||
|
if (!getBoundingBox().equals(bounds(room)))
|
||||||
|
throw new IllegalArgumentException("Transit room disagrees with its saved native bounding box");
|
||||||
|
setOrientation(null);
|
||||||
|
}
|
||||||
|
public TransitPlan21.Module module() { return room; }
|
||||||
|
@Override public BlockPos getLocatorPosition() { return locator(room); }
|
||||||
|
public static BlockPos locator(TransitPlan21.Module room) {
|
||||||
|
return new BlockPos(room.minX() + room.width() / 2,
|
||||||
|
room.kind() == TransitPlan21.Kind.ACCESS ? room.surfaceY() + 1 : room.floorY() + 1,
|
||||||
|
room.kind() == TransitPlan21.Kind.ACCESS ? room.minZ() : room.minZ() + room.depth() / 2);
|
||||||
|
}
|
||||||
|
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||||
|
tag.put("SanctuaryModule", encodeModule(room));
|
||||||
|
}
|
||||||
|
public static CompoundTag encodeModule(TransitPlan21.Module room) {
|
||||||
|
var tag = new CompoundTag(); tag.putInt("Schema", 1); tag.putString("Kind", room.kind().name());
|
||||||
|
tag.putInt("MinX", room.minX()); tag.putInt("MinY", room.minY()); tag.putInt("MinZ", room.minZ());
|
||||||
|
tag.putInt("Width", room.width()); tag.putInt("Height", room.height()); tag.putInt("Depth", room.depth());
|
||||||
|
tag.putInt("Facing", room.facing()); tag.putInt("Openings", room.openings()); tag.putLong("ModuleSeed", room.seed());
|
||||||
|
var supports = new ListTag();
|
||||||
|
for (var support : room.supports()) {
|
||||||
|
var value = new CompoundTag(); value.putInt("X", support.x()); value.putInt("MinY", support.minY());
|
||||||
|
value.putInt("Z", support.z()); value.putInt("MaxY", support.maxY()); supports.add(value);
|
||||||
|
}
|
||||||
|
tag.put("Supports", supports);
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
public static TransitPlan21.Module decodeModule(CompoundTag tag) {
|
||||||
|
if (!tag.keySet().equals(MODULE_KEYS)) throw new IllegalArgumentException("Incomplete or unknown transit room fields");
|
||||||
|
if (integer(tag, "Schema") != 1) throw new IllegalArgumentException("Unsupported transit room schema");
|
||||||
|
var kind = TransitPlan21.Kind.valueOf(tag.getString("Kind").orElseThrow(() -> new IllegalArgumentException("Missing room kind")));
|
||||||
|
type(tag, "ModuleSeed", Tag.TAG_LONG); type(tag, "Supports", Tag.TAG_LIST);
|
||||||
|
var saved = tag.getList("Supports").orElseThrow();
|
||||||
|
if (saved.size() > 64) throw new IllegalArgumentException("Too many saved transit room supports");
|
||||||
|
var supports = new ArrayList<TransitPlan21.Support>();
|
||||||
|
for (var entry : saved) {
|
||||||
|
if (!(entry instanceof CompoundTag value) || !value.keySet().equals(SUPPORT_KEYS))
|
||||||
|
throw new IllegalArgumentException("Invalid transit room support fields");
|
||||||
|
supports.add(new TransitPlan21.Support(integer(value, "X"), integer(value, "MinY"), integer(value, "Z"), integer(value, "MaxY")));
|
||||||
|
}
|
||||||
|
return new TransitPlan21.Module(kind, integer(tag, "MinX"), integer(tag, "MinY"), integer(tag, "MinZ"),
|
||||||
|
integer(tag, "Width"), integer(tag, "Height"), integer(tag, "Depth"), integer(tag, "Facing"), integer(tag, "Openings"),
|
||||||
|
tag.getLong("ModuleSeed").orElseThrow(), supports);
|
||||||
|
}
|
||||||
|
private static int integer(CompoundTag tag, String key) {
|
||||||
|
type(tag, key, Tag.TAG_INT);
|
||||||
|
return tag.getInt(key).orElseThrow();
|
||||||
|
}
|
||||||
|
private static void type(CompoundTag tag, String key, byte id) {
|
||||||
|
if (tag.get(key) == null || tag.get(key).getId() != id)
|
||||||
|
throw new IllegalArgumentException("Invalid saved transit room field: " + key);
|
||||||
|
}
|
||||||
|
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
|
||||||
|
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
|
||||||
|
TransitStructures21.placing(() -> TransitRenderer21.render(room, new LostCityRenderer.BlockWriter() {
|
||||||
|
private BlockPos world(int x, int y, int z) { return new BlockPos(room.minX() + x, room.minY() + y, room.minZ() + z); }
|
||||||
|
@Override public void set(int x, int y, int z, BlockState block) {
|
||||||
|
var position = world(x, y, z);
|
||||||
|
if (getBoundingBox().isInside(position) && clip.isInside(position))
|
||||||
|
placeBlock(level, block, position.getX(), position.getY(), position.getZ(), clip);
|
||||||
|
}
|
||||||
|
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
|
||||||
|
var position = world(x, y, z);
|
||||||
|
if (getBoundingBox().isInside(position) && clip.isInside(position))
|
||||||
|
createChest(level, clip, RandomSource.create(seed), position, loot, null);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
public static BoundingBox bounds(TransitPlan21.Module room) {
|
||||||
|
int minY = room.minY(), maxY = Math.addExact(room.minY(), room.height() - 1);
|
||||||
|
for (var support : room.supports()) {
|
||||||
|
minY = Math.min(minY, support.minY()); maxY = Math.max(maxY, support.maxY());
|
||||||
|
}
|
||||||
|
return new BoundingBox(room.minX(), minY, room.minZ(), Math.addExact(room.minX(), room.width() - 1),
|
||||||
|
maxY, Math.addExact(room.minZ(), room.depth() - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.transit;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Immutable final geometry. Each module owns a small native start, never an island-wide start. */
|
||||||
|
public record TransitPlan21(List<Module> modules, int samples, String diagnostic) {
|
||||||
|
public static final int MAX_SAMPLES = 3_000_000;
|
||||||
|
public static final TransitPlan21 EMPTY = new TransitPlan21(List.of(), 0, "disabled");
|
||||||
|
public enum Kind { GALLERY, JUNCTION, HALL, ACCESS }
|
||||||
|
public record Support(int x, int minY, int z, int maxY) {
|
||||||
|
public Support {
|
||||||
|
if (minY < 8 || maxY > 375 || minY > maxY || maxY - minY >= 16)
|
||||||
|
throw new IllegalArgumentException("Invalid transit footing");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** World-aligned bounds. Openings N1/E2/S4/W8 are seven wide, local y2..8.
|
||||||
|
* Access top deck is maxY-4. Its north gate, at centerX/minZ, has a certified natural approach. */
|
||||||
|
public record Module(Kind kind, int minX, int minY, int minZ, int width, int height, int depth,
|
||||||
|
int facing, int openings, long seed, List<Support> supports) {
|
||||||
|
public Module {
|
||||||
|
supports = List.copyOf(supports);
|
||||||
|
if (kind == null || width < 9 || width > 25 || depth < 9 || depth > 25
|
||||||
|
|| (width & 1) == 0 || (depth & 1) == 0 || height < 10 || height > 160
|
||||||
|
|| minY < 16 || minY + height > 383 || facing < 0 || facing > 3
|
||||||
|
|| openings < 1 || openings > 15 || supports.size() > 64)
|
||||||
|
throw new IllegalArgumentException("Invalid transit module");
|
||||||
|
if (kind == Kind.ACCESS && (width != 17 || depth != 17 || height < 20))
|
||||||
|
throw new IllegalArgumentException("Transit stair access needs its complete 17x17 shaft");
|
||||||
|
if (kind != Kind.ACCESS && height != 10) throw new IllegalArgumentException("Unexpected gallery height");
|
||||||
|
var seen = new HashSet<Long>();
|
||||||
|
for (var s : supports) if (s.x() < minX || s.x() >= minX + width || s.z() < minZ
|
||||||
|
|| s.z() >= minZ + depth || s.maxY() != minY - 1 || !seen.add(key(s.x(), s.z())))
|
||||||
|
throw new IllegalArgumentException("Transit footing does not belong to its floor");
|
||||||
|
}
|
||||||
|
public int maxX() { return minX + width - 1; }
|
||||||
|
public int maxY() { return minY + height - 1; }
|
||||||
|
public int maxZ() { return minZ + depth - 1; }
|
||||||
|
public int centerX() { return minX + width / 2; }
|
||||||
|
public int centerZ() { return minZ + depth / 2; }
|
||||||
|
public int floorY() { return minY + 1; }
|
||||||
|
public int surfaceY() { return maxY() - 4; }
|
||||||
|
public int startChunkX() { return centerX() >> 4; }
|
||||||
|
public int startChunkZ() { return centerZ() >> 4; }
|
||||||
|
public int boundsMinY() { return supports.stream().mapToInt(Support::minY).min().orElse(minY); }
|
||||||
|
public boolean contains(int x, int y, int z) {
|
||||||
|
if (x < minX || x > maxX() || z < minZ || z > maxZ()) return false;
|
||||||
|
if (y >= minY && y <= maxY()) return true;
|
||||||
|
return supports.stream().anyMatch(s -> x == s.x() && z == s.z() && y >= s.minY() && y <= s.maxY());
|
||||||
|
}
|
||||||
|
public boolean protects(int x, int y, int z) {
|
||||||
|
if (contains(x, y, z)) return true;
|
||||||
|
if (kind == Kind.ACCESS && x >= centerX() - 2 && x <= centerX() + 2
|
||||||
|
&& z >= minZ - 3 && z < minZ && y >= surfaceY() - 2 && y <= surfaceY() + 3) return true;
|
||||||
|
// Two untouched layers carry the slab and the certified short piers.
|
||||||
|
if (x >= minX && x <= maxX() && z >= minZ && z <= maxZ() && y >= minY - 2 && y < minY) return true;
|
||||||
|
return supports.stream().anyMatch(s -> x == s.x() && z == s.z() && y >= s.minY() - 2 && y < s.minY());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public TransitPlan21 {
|
||||||
|
modules = List.copyOf(modules);
|
||||||
|
if (modules.size() > 64 || samples < 0 || samples > MAX_SAMPLES || diagnostic == null || diagnostic.length() > 1024)
|
||||||
|
throw new IllegalArgumentException("Invalid transit planning result");
|
||||||
|
if (!modules.isEmpty() && modules.stream().filter(m -> m.kind() == Kind.ACCESS).count() != 2)
|
||||||
|
throw new IllegalArgumentException("A through-gallery needs two surface accesses");
|
||||||
|
for (int i = 0; i < modules.size(); i++) for (int j = i + 1; j < modules.size(); j++) {
|
||||||
|
var a = modules.get(i); var b = modules.get(j);
|
||||||
|
if (a.minX() <= b.maxX() && a.maxX() >= b.minX() && a.minZ() <= b.maxZ() && a.maxZ() >= b.minZ())
|
||||||
|
throw new IllegalArgumentException("Transit modules overlap");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public boolean isEmpty() { return modules.isEmpty(); }
|
||||||
|
public List<Module> modulesForChunk(int x, int z) {
|
||||||
|
return modules.stream().filter(m -> m.startChunkX() == x && m.startChunkZ() == z).toList();
|
||||||
|
}
|
||||||
|
public boolean contains(int x, int y, int z) { return modules.stream().anyMatch(m -> m.contains(x, y, z)); }
|
||||||
|
public boolean protects(int x, int y, int z) { return modules.stream().anyMatch(m -> m.protects(x, y, z)); }
|
||||||
|
public boolean intersectsProtection(int x0, int y0, int z0, int x1, int y1, int z1) {
|
||||||
|
for (var m : modules) if (m.kind() == Kind.ACCESS && x0 <= m.centerX() + 2 && x1 >= m.centerX() - 2
|
||||||
|
&& z0 < m.minZ() && z1 >= m.minZ() - 3 && y0 <= m.surfaceY() + 3 && y1 >= m.surfaceY() - 2) return true;
|
||||||
|
for (var m : modules) if (x0 <= m.maxX() && x1 >= m.minX() && z0 <= m.maxZ() && z1 >= m.minZ()
|
||||||
|
&& y0 <= m.maxY() && y1 >= m.boundsMinY() - 2) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
|
||||||
|
}
|
||||||
+220
@@ -0,0 +1,220 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.transit;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind;
|
||||||
|
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Module;
|
||||||
|
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Support;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/** A bounded read-only through-route. Failed candidates never publish partial corridors or shafts. */
|
||||||
|
public final class TransitPlanner21 {
|
||||||
|
public static final int MAX_COLUMNS = 12_000;
|
||||||
|
private TransitPlanner21() {}
|
||||||
|
public interface Terrain {
|
||||||
|
boolean owns(int x, int z);
|
||||||
|
boolean solid(int x, int y, int z);
|
||||||
|
boolean protectedAt(int x, int y, int z);
|
||||||
|
int surfaceY(int x, int z);
|
||||||
|
default String obstruction(int x, int y, int z) { return "protected terrain"; }
|
||||||
|
}
|
||||||
|
public static TransitPlan21 plan(long seed, int centerX, int centerZ, int diameter, Terrain terrain) {
|
||||||
|
if (diameter < 512 || diameter > 1024) throw new IllegalArgumentException("Unsupported transit island diameter");
|
||||||
|
var probe = new Probe(terrain);
|
||||||
|
int count = (int) Math.ceil(diameter * .42 / 17);
|
||||||
|
if ((count & 1) == 0) count++;
|
||||||
|
String last = "no dry supported through-route";
|
||||||
|
var choices = new ArrayList<Choice>();
|
||||||
|
// Reserve most of the native-column budget for the exact scan of selected routes.
|
||||||
|
probe.sampleLimit = 1_800_000; probe.columnLimit = 7_500;
|
||||||
|
try {
|
||||||
|
search: for (int advance : new int[]{0, -34, 34}) for (boolean alongX : new boolean[]{true, false}) for (int shift : new int[]{-diameter / 8, diameter / 8, -diameter / 16, diameter / 16, 0, -3 * diameter / 16, 3 * diameter / 16}) {
|
||||||
|
int cx = centerX + (alongX ? advance : shift), cz = centerZ + (alongX ? shift : advance);
|
||||||
|
for (int floor : new int[]{216, 200, 224, 208, 184, 232, 160}) {
|
||||||
|
var route = candidate(seed, cx, cz, count, floor, alongX, probe, false);
|
||||||
|
String label = "axis=" + (alongX ? "east-west" : "north-south") + " offset=" + shift + " advance=" + advance + " floor=" + floor;
|
||||||
|
if (route == null) {
|
||||||
|
last = probe.reason;
|
||||||
|
diagnostic("preselection reject " + label + " " + last + " columns=" + probe.columns.size());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int cost = route.stream().mapToInt(m -> m.supports().stream().mapToInt(pier -> pier.maxY() - pier.minY() + 1).sum()
|
||||||
|
+ (m.kind() == Kind.ACCESS ? m.surfaceY() - m.floorY() : 0)).sum();
|
||||||
|
// Try the intermediate levels first; within each level prefer the better supported axis.
|
||||||
|
int heightOrder = switch (floor) { case 216 -> 0; case 200 -> 1; case 224 -> 2; case 208 -> 3; case 184 -> 4; case 232 -> 5; default -> 6; };
|
||||||
|
choices.add(new Choice(route, floor, alongX, heightOrder * 100_000 + cost, label));
|
||||||
|
diagnostic("preselection admitted " + label + " bearing_cost=" + cost);
|
||||||
|
if (choices.size() >= 8) break search;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (BudgetExceeded exhausted) { diagnostic("prescreen budget reached; retain " + choices.size() + " candidates"); }
|
||||||
|
probe.sampleLimit = TransitPlan21.MAX_SAMPLES; probe.columnLimit = MAX_COLUMNS;
|
||||||
|
choices.sort(java.util.Comparator.comparingInt(Choice::cost));
|
||||||
|
try {
|
||||||
|
for (var choice : choices) {
|
||||||
|
var finalRoute = new ArrayList<Module>();
|
||||||
|
for (var rough : choice.route()) {
|
||||||
|
var complete = certify(rough, probe, true);
|
||||||
|
if (complete == null) { last = probe.reason; break; }
|
||||||
|
finalRoute.add(complete);
|
||||||
|
}
|
||||||
|
if (finalRoute.size() != choice.route().size()) {
|
||||||
|
diagnostic("exact reject " + choice.label() + " " + last); continue;
|
||||||
|
}
|
||||||
|
addBranch(finalRoute, choice.floor(), choice.alongX(), seed, probe);
|
||||||
|
return new TransitPlan21(finalRoute, probe.samples, "accepted " + choice.label()
|
||||||
|
+ " span=" + ((count + 1) * 17) + " columns=" + probe.columns.size()
|
||||||
|
+ " candidates=" + choices.size() + " hall=" + finalRoute.stream().anyMatch(m -> m.kind() == Kind.HALL));
|
||||||
|
}
|
||||||
|
} catch (BudgetExceeded exhausted) { last = "bounded exact search exhausted: " + probe.reason; }
|
||||||
|
return new TransitPlan21(List.of(), probe.samples, last + "; candidates=" + choices.size() + "; columns=" + probe.columns.size());
|
||||||
|
}
|
||||||
|
private record Choice(List<Module> route, int floor, boolean alongX, int cost, String label) {}
|
||||||
|
private static List<Module> candidate(long seed, int cx, int cz, int count, int floor,
|
||||||
|
boolean alongX, Probe probe, boolean full) {
|
||||||
|
int first = -(count + 1) * 17 / 2;
|
||||||
|
int ax = cx + (alongX ? first : 0), az = cz + (alongX ? 0 : first);
|
||||||
|
int bx = cx - (alongX ? first : 0), bz = cz - (alongX ? 0 : first);
|
||||||
|
// Cheap route witnesses reject deep obstacles before two tall shafts are scanned in full.
|
||||||
|
for (int d = first + 17; d <= -first - 17; d += 8) for (int side : new int[]{-4, 0, 4})
|
||||||
|
for (int y : new int[]{floor - 2, floor, floor + 4, floor + 8}) {
|
||||||
|
int x = cx + (alongX ? d : side), z = cz + (alongX ? side : d);
|
||||||
|
if (probe.protectedAt(x, y, z)) {
|
||||||
|
probe.reason = "coarse route [" + x + "," + y + "," + z + "] " + probe.terrain.obstruction(x, y, z); return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Certify the two indispensable exits before spending the route's column budget.
|
||||||
|
Module left = access(ax, az, floor, alongX ? 2 : 4, seed ^ 0x2101L, probe, full);
|
||||||
|
if (left == null) return null;
|
||||||
|
Module right = access(bx, bz, floor, alongX ? 8 : 1, seed ^ 0x2102L, probe, full);
|
||||||
|
if (right == null) return null;
|
||||||
|
var result = new ArrayList<Module>(); result.add(left);
|
||||||
|
int unanchoredRun = 0;
|
||||||
|
for (int n = 0; n < count; n++) {
|
||||||
|
int d = first + 17 * (n + 1);
|
||||||
|
int x = cx + (alongX ? d - 8 : -4), z = cz + (alongX ? -4 : d - 8);
|
||||||
|
var module = new Module(Kind.GALLERY, x, floor - 1, z, alongX ? 17 : 9, 10, alongX ? 9 : 17,
|
||||||
|
alongX ? 1 : 0, alongX ? 10 : 5, seed ^ (n * 0x9E3779B97F4A7C15L), List.of());
|
||||||
|
module = certify(module, probe, full);
|
||||||
|
if (module == null) return null;
|
||||||
|
unanchoredRun = probe.bearings.get(module) == 15 ? 0 : unanchoredRun + 1;
|
||||||
|
if (unanchoredRun > 2) { probe.reason = "unsupported span exceeds 34 blocks"; return null; }
|
||||||
|
result.add(module);
|
||||||
|
}
|
||||||
|
result.add(right);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
private static Module access(int cx, int cz, int floor, int opening, long seed, Probe probe, boolean full) {
|
||||||
|
int top = -1, low = 384;
|
||||||
|
for (int x = cx - 2; x <= cx + 2; x++) for (int z = cz - 11; z <= cz - 9; z++) {
|
||||||
|
int surface = probe.surface(x, z); top = Math.max(top, surface); low = Math.min(low, surface);
|
||||||
|
}
|
||||||
|
if (low < 0 || top - low > 1) { probe.reason = "access approach relief [" + cx + "," + cz + "] " + low + ".." + top; return null; }
|
||||||
|
if (top < floor + 18 || top > floor + 145) { probe.reason = "surface access height"; return null; }
|
||||||
|
// Its visible north portico meets real ground; its three-wide approach is never an unsupported threshold.
|
||||||
|
for (int x = cx - 2; x <= cx + 2; x++) for (int z = cz - 11; z <= cz - 9; z++) {
|
||||||
|
int y = probe.surface(x, z);
|
||||||
|
if (y < top - 1 || y > top || probe.protectedAt(x, y, z)
|
||||||
|
|| probe.protectedAt(x, top + 1, z) || probe.protectedAt(x, top + 2, z)) {
|
||||||
|
probe.reason = "access approach [" + x + "," + top + "," + z + "] terrain=" + y; return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int x : new int[]{cx - 8, cx, cx + 8}) for (int z : new int[]{cz - 8, cz, cz + 8}) {
|
||||||
|
int y = probe.surface(x, z);
|
||||||
|
// The portico alone needs level terrain. A buried shaft can follow a hillside;
|
||||||
|
// its local walls and checked footings carry the deck without flattening the hill.
|
||||||
|
if (y < Math.max(floor + 8, top - 24) || y > top + 16) { probe.reason = "access hillside [" + x + "," + top + "," + z + "] terrain=" + y; return null; }
|
||||||
|
}
|
||||||
|
return certify(new Module(Kind.ACCESS, cx - 8, floor - 1, cz - 8, 17,
|
||||||
|
top - (floor - 1) + 5, 17, 0, opening, seed, List.of()), probe, full);
|
||||||
|
}
|
||||||
|
private static void addBranch(List<Module> route, int floor, boolean alongX, long seed, Probe probe) {
|
||||||
|
int index = 1 + (route.size() - 2) / 2;
|
||||||
|
var junction = route.get(index);
|
||||||
|
if (probe.bearings.getOrDefault(junction, 0) != 15) return;
|
||||||
|
for (int side : new int[]{-1, 1}) {
|
||||||
|
try {
|
||||||
|
int x = alongX ? junction.centerX() - 4 : side < 0 ? junction.minX() - 17 : junction.maxX() + 1;
|
||||||
|
int z = alongX ? side < 0 ? junction.minZ() - 17 : junction.maxZ() + 1 : junction.centerZ() - 4;
|
||||||
|
int width = alongX ? 9 : 17, depth = alongX ? 17 : 9;
|
||||||
|
var passage = certify(new Module(Kind.GALLERY, x, floor - 1, z, width, 10, depth,
|
||||||
|
alongX ? 0 : 1, alongX ? 5 : 10, seed ^ 0x21B1L ^ side, List.of()), probe, true);
|
||||||
|
if (passage == null) continue;
|
||||||
|
int hx = alongX ? junction.centerX() - 8 : side < 0 ? passage.minX() - 21 : passage.maxX() + 1;
|
||||||
|
int hz = alongX ? side < 0 ? passage.minZ() - 21 : passage.maxZ() + 1 : junction.centerZ() - 8;
|
||||||
|
int opening = alongX ? side < 0 ? 4 : 1 : side < 0 ? 2 : 8;
|
||||||
|
var hall = certify(new Module(Kind.HALL, hx, floor - 1, hz, alongX ? 17 : 21, 10,
|
||||||
|
alongX ? 21 : 17, alongX ? 0 : 1, opening, seed ^ 0x21A1L ^ side, List.of()), probe, true);
|
||||||
|
if (hall == null) continue;
|
||||||
|
int branch = alongX ? side < 0 ? 1 : 4 : side < 0 ? 8 : 2;
|
||||||
|
route.set(index, new Module(Kind.JUNCTION, junction.minX(), junction.minY(), junction.minZ(),
|
||||||
|
junction.width(), 10, junction.depth(), junction.facing(), junction.openings() | branch,
|
||||||
|
junction.seed(), junction.supports()));
|
||||||
|
route.add(passage); route.add(hall); return;
|
||||||
|
} catch (BudgetExceeded exhausted) { return; } // The already certified traversal remains complete.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static Module certify(Module module, Probe probe, boolean full) {
|
||||||
|
var supports = new ArrayList<Support>();
|
||||||
|
int bearingQuadrants = 0;
|
||||||
|
for (int z = module.minZ();; z = Math.min(z + 4, module.maxZ())) {
|
||||||
|
for (int x = module.minX();; x = Math.min(x + 4, module.maxX())) {
|
||||||
|
if (!probe.owns(x, z)) { probe.reason = "outside island"; return null; }
|
||||||
|
if (!full) for (int y = module.minY(); y <= module.maxY(); y += 4)
|
||||||
|
if (probe.protectedAt(x, y, z)) {
|
||||||
|
probe.reason = "coarse " + module.kind() + " [" + x + "," + y + "," + z + "] " + probe.terrain.obstruction(x, y, z); return null;
|
||||||
|
}
|
||||||
|
int anchor = module.minY() - 1;
|
||||||
|
while (anchor >= module.minY() - 17 && !(probe.solid(x, anchor, z) && probe.solid(x, anchor - 1, z))) anchor--;
|
||||||
|
if (anchor >= module.minY() - 17) {
|
||||||
|
for (int y = anchor - 1; y < module.minY(); y++) if (probe.protectedAt(x, y, z)) {
|
||||||
|
probe.reason = "protected footing"; return null;
|
||||||
|
}
|
||||||
|
if (anchor < module.minY() - 1) supports.add(new Support(x, anchor + 1, z, module.minY() - 1));
|
||||||
|
bearingQuadrants |= 1 << ((x < module.centerX() ? 0 : 1) + (z < module.centerZ() ? 0 : 2));
|
||||||
|
}
|
||||||
|
if (x == module.maxX()) break;
|
||||||
|
}
|
||||||
|
if (z == module.maxZ()) break;
|
||||||
|
}
|
||||||
|
// The continuous two-block deck can span gaps between its bearings. Require
|
||||||
|
// untouched rock in each quarter, not a pier at every point above a cavity.
|
||||||
|
if (bearingQuadrants != 15 && module.kind() != Kind.GALLERY) { probe.reason = "unsupported quarters " + module.kind()
|
||||||
|
+ " [" + module.minX() + "," + module.minY() + "," + module.minZ() + "] mask=" + bearingQuadrants; return null; }
|
||||||
|
if (full) {
|
||||||
|
// Hydrology and all previous structures are checked at every edited voxel after the cheap footing test.
|
||||||
|
for (int z = module.minZ(); z <= module.maxZ(); z++) for (int x = module.minX(); x <= module.maxX(); x++) {
|
||||||
|
if (!probe.owns(x, z)) { probe.reason = "outside island"; return null; }
|
||||||
|
for (int y = module.minY() - 2; y <= module.maxY(); y++) if (probe.protectedAt(x, y, z)) {
|
||||||
|
probe.reason = "volume " + module.kind() + " [" + x + "," + y + "," + z + "] " + probe.terrain.obstruction(x, y, z); return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var certified = new Module(module.kind(), module.minX(), module.minY(), module.minZ(), module.width(), module.height(),
|
||||||
|
module.depth(), module.facing(), module.openings(), module.seed(), supports);
|
||||||
|
probe.bearings.put(certified, bearingQuadrants);
|
||||||
|
return certified;
|
||||||
|
}
|
||||||
|
private static void diagnostic(String message) {
|
||||||
|
if (Boolean.getBoolean("sanctuary.test.transit21") || Boolean.getBoolean("sanctuary.transit.plannerDiagnostics"))
|
||||||
|
System.getLogger(TransitPlanner21.class.getName()).log(System.Logger.Level.INFO, "Transit21 " + message);
|
||||||
|
}
|
||||||
|
private static final class Probe {
|
||||||
|
final Terrain terrain; final Set<Long> columns = new HashSet<>(); int samples; int sampleLimit = TransitPlan21.MAX_SAMPLES, columnLimit = MAX_COLUMNS; String reason = "budget";
|
||||||
|
final java.util.Map<Module, Integer> bearings = new java.util.HashMap<>();
|
||||||
|
Probe(Terrain terrain) { this.terrain = terrain; }
|
||||||
|
void visit(int x, int z) {
|
||||||
|
if (samples >= sampleLimit) throw new BudgetExceeded();
|
||||||
|
samples++;
|
||||||
|
long key = (long) x << 32 ^ z & 0xffffffffL;
|
||||||
|
if (!columns.contains(key) && columns.size() >= columnLimit) throw new BudgetExceeded();
|
||||||
|
columns.add(key);
|
||||||
|
}
|
||||||
|
boolean owns(int x, int z) { visit(x, z); return terrain.owns(x, z); }
|
||||||
|
boolean solid(int x, int y, int z) { visit(x, z); return terrain.solid(x, y, z); }
|
||||||
|
boolean protectedAt(int x, int y, int z) { visit(x, z); return terrain.protectedAt(x, y, z); }
|
||||||
|
int surface(int x, int z) { visit(x, z); return terrain.surfaceY(x, z); }
|
||||||
|
}
|
||||||
|
private static final class BudgetExceeded extends RuntimeException { private static final long serialVersionUID = 1L; }
|
||||||
|
}
|
||||||
+208
@@ -0,0 +1,208 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.transit;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||||
|
import fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
|
||||||
|
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind;
|
||||||
|
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Module;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||||
|
import net.minecraft.world.level.storage.loot.BuiltInLootTables;
|
||||||
|
|
||||||
|
/** A seven-wide abandoned industrial route: five roadway cells and one pavement on each side. */
|
||||||
|
public final class TransitRenderer21 {
|
||||||
|
private TransitRenderer21() {}
|
||||||
|
public record Target(String name, int x, int y, int z) {}
|
||||||
|
public static List<Target> targets(Module m) {
|
||||||
|
var result = new ArrayList<Target>();
|
||||||
|
result.add(new Target("roadway", m.width() / 2, 2, m.depth() / 2));
|
||||||
|
if ((m.openings() & 1) != 0) result.add(new Target("north_connection", m.width() / 2, 2, 0));
|
||||||
|
if ((m.openings() & 2) != 0) result.add(new Target("east_connection", m.width() - 1, 2, m.depth() / 2));
|
||||||
|
if ((m.openings() & 4) != 0) result.add(new Target("south_connection", m.width() / 2, 2, m.depth() - 1));
|
||||||
|
if ((m.openings() & 8) != 0) result.add(new Target("west_connection", 0, 2, m.depth() / 2));
|
||||||
|
if (m.kind() == Kind.ACCESS) {
|
||||||
|
result.add(new Target("surface_portico", 8, m.surfaceY() - m.minY() + 1, 0));
|
||||||
|
result.add(new Target("surface_landing", 8, m.surfaceY() - m.minY() + 1, 12));
|
||||||
|
}
|
||||||
|
if (m.kind() == Kind.HALL) {
|
||||||
|
result.add(new Target("maintenance_bench", 3, 2, 4));
|
||||||
|
result.add(new Target("shared_tool_chest", m.width() - 4, 2, m.depth() - 4));
|
||||||
|
result.add(new Target("unfinished_rock_face", m.facing() == 1 ? m.width() / 2 : 3,
|
||||||
|
2, m.facing() == 1 ? 3 : m.depth() / 2));
|
||||||
|
}
|
||||||
|
return List.copyOf(result);
|
||||||
|
}
|
||||||
|
public static void render(Module module, LostCityRenderer.BlockWriter output) {
|
||||||
|
var connected = ConnectedBlocks20.buffer(output);
|
||||||
|
new Painter(module, connected).render();
|
||||||
|
connected.flush();
|
||||||
|
}
|
||||||
|
private static final class Painter {
|
||||||
|
final Module m; final LostCityRenderer.BlockWriter out;
|
||||||
|
Painter(Module module, LostCityRenderer.BlockWriter out) { m = module; this.out = out; }
|
||||||
|
void set(int x, int y, int z, Block block) { set(x, y, z, block.defaultBlockState()); }
|
||||||
|
void set(int x, int y, int z, BlockState state) { out.set(x, y, z, state); }
|
||||||
|
void box(int x0, int y0, int z0, int x1, int y1, int z1, Block block) {
|
||||||
|
for (int x = x0; x <= x1; x++) for (int z = z0; z <= z1; z++) for (int y = y0; y <= y1; y++) set(x, y, z, block);
|
||||||
|
}
|
||||||
|
BlockState stair(Direction direction) {
|
||||||
|
return Blocks.STONE_BRICK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, direction);
|
||||||
|
}
|
||||||
|
void render() {
|
||||||
|
int w = m.width(), d = m.depth();
|
||||||
|
for (var support : m.supports()) box(support.x() - m.minX(), support.minY() - m.minY(), support.z() - m.minZ(),
|
||||||
|
support.x() - m.minX(), -1, support.z() - m.minZ(), Blocks.STONE_BRICKS);
|
||||||
|
box(0, 0, 0, w - 1, 0, d - 1, Blocks.STONE_BRICKS);
|
||||||
|
box(0, 1, 0, w - 1, 1, d - 1, Blocks.SMOOTH_STONE);
|
||||||
|
box(0, 2, 0, w - 1, m.height() - 1, d - 1, Blocks.AIR);
|
||||||
|
if (m.kind() == Kind.ACCESS) { access(); return; }
|
||||||
|
for (int x = 0; x < w; x++) for (int z = 0; z < d; z++) {
|
||||||
|
boolean edge = x == 0 || x == w - 1 || z == 0 || z == d - 1;
|
||||||
|
if (edge) for (int y = 2; y <= 8; y++) set(x, y, z,
|
||||||
|
y == 2 || y == 7 ? Blocks.TUFF_BRICKS : ((x + z) % 5 == 0 ? Blocks.STONE_BRICKS : Blocks.SMOOTH_STONE));
|
||||||
|
set(x, 9, z, Blocks.STONE_BRICKS);
|
||||||
|
boolean alongX = m.facing() == 1;
|
||||||
|
int across = alongX ? z - d / 2 : x - w / 2;
|
||||||
|
if (Math.abs(across) <= 2) set(x, 1, z, Blocks.POLISHED_ANDESITE);
|
||||||
|
else if (Math.abs(across) == 3) set(x, 2, z, Blocks.SMOOTH_STONE_SLAB);
|
||||||
|
}
|
||||||
|
openings();
|
||||||
|
if (m.kind() == Kind.JUNCTION) {
|
||||||
|
// The side branch's mouth and central crossing retain all five roadway cells.
|
||||||
|
for (int x = w / 2 - 2; x <= w / 2 + 2; x++) for (int z = 0; z < d; z++) set(x, 2, z, Blocks.AIR);
|
||||||
|
for (int z = d / 2 - 2; z <= d / 2 + 2; z++) for (int x = 0; x < w; x++) set(x, 2, z, Blocks.AIR);
|
||||||
|
}
|
||||||
|
// Ceiling ribs and luminaires are physically joined to the continuous roof slab.
|
||||||
|
for (int n = 3; n < (m.facing() == 1 ? w : d) - 2; n += 6) {
|
||||||
|
int x = m.facing() == 1 ? n : w / 2, z = m.facing() == 1 ? d / 2 : n;
|
||||||
|
set(x, 8, z, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
|
||||||
|
if (m.facing() == 1) box(x, 8, 1, x, 8, 2, Blocks.POLISHED_TUFF);
|
||||||
|
else box(1, 8, z, 2, 8, z, Blocks.POLISHED_TUFF);
|
||||||
|
}
|
||||||
|
if (m.kind() == Kind.HALL) hall();
|
||||||
|
exposedRock();
|
||||||
|
}
|
||||||
|
void openings() {
|
||||||
|
for (int y = 2; y <= 8; y++) {
|
||||||
|
if ((m.openings() & 1) != 0) box(m.width() / 2 - 3, y, 0, m.width() / 2 + 3, y, 0, Blocks.AIR);
|
||||||
|
if ((m.openings() & 4) != 0) box(m.width() / 2 - 3, y, m.depth() - 1, m.width() / 2 + 3, y, m.depth() - 1, Blocks.AIR);
|
||||||
|
if ((m.openings() & 2) != 0) box(m.width() - 1, y, m.depth() / 2 - 3, m.width() - 1, y, m.depth() / 2 + 3, Blocks.AIR);
|
||||||
|
if ((m.openings() & 8) != 0) box(0, y, m.depth() / 2 - 3, 0, y, m.depth() / 2 + 3, Blocks.AIR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void hall() {
|
||||||
|
// Flush crossings join the work bays to the road through the raised pavements.
|
||||||
|
// They also provide an accessible route without requiring a jump over the curb.
|
||||||
|
if (m.facing() == 0) {
|
||||||
|
for (int x : new int[]{m.width() / 2 - 3, m.width() / 2 + 3})
|
||||||
|
for (int z0 : new int[]{4, m.depth() - 4}) box(x, 2, z0 - 1, x, 2, z0 + 1, Blocks.AIR);
|
||||||
|
} else {
|
||||||
|
for (int z : new int[]{m.depth() / 2 - 3, m.depth() / 2 + 3})
|
||||||
|
for (int x0 : new int[]{3, m.width() - 4}) box(x0 - 1, 2, z, x0 + 1, 2, z, Blocks.AIR);
|
||||||
|
}
|
||||||
|
// Maintenance workshop occupies side bays; the centered arrival and return remain unobstructed.
|
||||||
|
for (int z : new int[]{3, m.depth() - 4}) {
|
||||||
|
box(1, 2, z, 1, 7, z, Blocks.CHISELED_TUFF_BRICKS);
|
||||||
|
box(m.width() - 2, 2, z, m.width() - 2, 7, z, Blocks.CHISELED_TUFF_BRICKS);
|
||||||
|
box(1, 8, z, m.width() - 2, 8, z, Blocks.TUFF_BRICKS);
|
||||||
|
}
|
||||||
|
set(2, 2, 3, Blocks.CRAFTING_TABLE); set(2, 2, 4, Blocks.GRINDSTONE);
|
||||||
|
set(2, 2, 5, Blocks.FURNACE.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST));
|
||||||
|
for (int z = m.depth() - 6; z <= m.depth() - 3; z++) set(2, 2, z, Blocks.BARREL);
|
||||||
|
set(m.width() - 3, 2, 3, Blocks.CAULDRON);
|
||||||
|
out.chest(m.width() - 3, 2, m.depth() - 4, BuiltInLootTables.VILLAGE_TOOLSMITH, m.seed() ^ 0x21A1100L);
|
||||||
|
}
|
||||||
|
void exposedRock() {
|
||||||
|
// Abandoned cuts expose finite, ordinary mineable ore in the saved wall geometry.
|
||||||
|
// No ticking reward or regeneration: once mined, these blocks remain gone.
|
||||||
|
if (m.kind() != Kind.HALL && (m.kind() != Kind.GALLERY || Math.floorMod(m.seed(), 4) != 0)) return;
|
||||||
|
boolean alongX = m.facing() == 1;
|
||||||
|
int middle = alongX ? m.width() / 2 : m.depth() / 2;
|
||||||
|
Block[] ore = {Blocks.COAL_ORE, Blocks.COPPER_ORE, Blocks.IRON_ORE,
|
||||||
|
Blocks.GOLD_ORE, Blocks.REDSTONE_ORE, Blocks.LAPIS_ORE};
|
||||||
|
Block deposit = ore[Math.floorMod(m.seed() ^ 0x21CAFE, ore.length)];
|
||||||
|
for (int across = -2; across <= 2; across++) for (int y = 2; y <= 6; y++) {
|
||||||
|
int x = alongX ? middle + across : 0, z = alongX ? 0 : middle + across;
|
||||||
|
Block rock = y == 2 ? Blocks.TUFF : y >= 5 ? Blocks.STONE : Blocks.DEEPSLATE;
|
||||||
|
if (Math.abs(across) + Math.abs(y - 4) <= 1) rock = deposit;
|
||||||
|
if (m.kind() == Kind.HALL && y == 3 && across == -1) rock = Blocks.DEEPSLATE_DIAMOND_ORE;
|
||||||
|
if (m.kind() == Kind.HALL && y == 4 && across == 1) rock = Blocks.EMERALD_ORE;
|
||||||
|
set(x, y, z, rock);
|
||||||
|
}
|
||||||
|
if (m.kind() == Kind.HALL) {
|
||||||
|
// An unfinished excavation bay interrupts the masonry: irregular rock
|
||||||
|
// shoulders and a low stone arch leave its mineral face accessible.
|
||||||
|
for (int across = -3; across <= 3; across++) for (int reach = 0; reach <= 2; reach++) {
|
||||||
|
int x = alongX ? middle + across : reach, z = alongX ? reach : middle + across;
|
||||||
|
set(x, 1, z, Math.abs(across) == 3 ? Blocks.MOSSY_COBBLESTONE : Blocks.TUFF);
|
||||||
|
int ceiling = 7 + ((across + reach) & 1);
|
||||||
|
box(x, ceiling, z, x, 8, z, reach == 0 ? Blocks.DEEPSLATE : Blocks.STONE);
|
||||||
|
if (Math.abs(across) == 3) box(x, 2, z, x, 6 - reach, z,
|
||||||
|
reach == 2 ? Blocks.COBBLESTONE : Blocks.TUFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void access() {
|
||||||
|
int top = m.surfaceY() - m.minY();
|
||||||
|
// Enclosed local shaft, not a filled foundation under the whole route.
|
||||||
|
for (int n = 0; n <= 16; n++) {
|
||||||
|
box(n, 2, 0, n, top, 0, Blocks.SMOOTH_STONE);
|
||||||
|
box(n, 2, 16, n, top, 16, Blocks.SMOOTH_STONE);
|
||||||
|
box(0, 2, n, 0, top, n, Blocks.STONE_BRICKS);
|
||||||
|
box(16, 2, n, 16, top, n, Blocks.STONE_BRICKS);
|
||||||
|
}
|
||||||
|
box(1, top, 1, 15, top, 15, Blocks.SMOOTH_STONE);
|
||||||
|
// Narrow bearing walls sit beside the flights, with the bottom route crossing at z8..9.
|
||||||
|
box(2, 2, 3, 2, top - 1, 7, Blocks.STONE_BRICKS);
|
||||||
|
box(14, 2, 3, 14, top - 1, 7, Blocks.STONE_BRICKS);
|
||||||
|
openings();
|
||||||
|
int rise = top - 1;
|
||||||
|
for (int step = 1; step <= rise; step++) {
|
||||||
|
int flight = (step - 1) / 5, within = (step - 1) % 5 + 1;
|
||||||
|
boolean south = (flight & 1) == 0;
|
||||||
|
int z = south ? 2 + within : 8 - within, x0 = south ? 3 : 11, y = 1 + step;
|
||||||
|
box(x0, y, z, x0 + 2, Math.min(top + 3, y + 3), z, Blocks.AIR);
|
||||||
|
for (int x = x0; x <= x0 + 2; x++) set(x, y, z, stair(south ? Direction.SOUTH : Direction.NORTH));
|
||||||
|
if (within == 5) {
|
||||||
|
int z0 = south ? 8 : 1;
|
||||||
|
box(3, y, z0, 13, y, z0 + 1, Blocks.SMOOTH_STONE);
|
||||||
|
box(3, y + 1, z0, 13, Math.min(top + 3, y + 3), z0 + 1, Blocks.AIR);
|
||||||
|
if (y + 3 <= top) {
|
||||||
|
set(6, y + 3, z0, Blocks.SMOOTH_STONE);
|
||||||
|
set(6, y + 2, z0, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
|
||||||
|
// The bracket reaches an actual shaft wall; the last open-air landing needs no hanging lamp.
|
||||||
|
box(6, y + 3, south ? z0 : 0, 6, y + 3, south ? 16 : z0, Blocks.STONE_BRICKS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A north landing just below the surface needs a descending threshold instead
|
||||||
|
// of a slab sealing its headroom. The last flight still reaches the real top deck.
|
||||||
|
int belowNorthLanding = rise % 10;
|
||||||
|
if (belowNorthLanding >= 1 && belowNorthLanding <= 3) {
|
||||||
|
for (int z = 0; z <= belowNorthLanding; z++) for (int x = 7; x <= 9; x++) {
|
||||||
|
box(x, top - z, z, x, top + 3, z, Blocks.AIR);
|
||||||
|
set(x, top - z - 1, z, Blocks.STONE_BRICKS);
|
||||||
|
set(x, top - z, z, stair(Direction.NORTH));
|
||||||
|
}
|
||||||
|
if (belowNorthLanding == 3) {
|
||||||
|
for (int x : new int[]{6, 10}) {
|
||||||
|
set(x, top - 3, 3, Blocks.SMOOTH_STONE);
|
||||||
|
box(x, top - 2, 3, x, top - 1, 3, Blocks.AIR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A short, grounded portico identifies each naturally approachable surface gate.
|
||||||
|
for (int n = 0; n <= 16; n++) {
|
||||||
|
if (n < 6 || n > 10) set(n, top + 1, 0, Blocks.IRON_BARS);
|
||||||
|
set(n, top + 1, 16, Blocks.IRON_BARS);
|
||||||
|
set(0, top + 1, n, Blocks.IRON_BARS); set(16, top + 1, n, Blocks.IRON_BARS);
|
||||||
|
}
|
||||||
|
for (int x : new int[]{6, 10}) box(x, top + 1, 0, x, top + 3, 0, Blocks.STONE_BRICKS);
|
||||||
|
box(6, top + 4, 0, 10, top + 4, 1, Blocks.STONE_BRICK_SLAB);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.transit;
|
||||||
|
|
||||||
|
import com.mojang.serialization.MapCodec;
|
||||||
|
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||||
|
import java.util.Optional;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureType;
|
||||||
|
|
||||||
|
/** A local group of neighboring modules; long routes deliberately have multiple native starts. */
|
||||||
|
public final class TransitStructure21 extends Structure {
|
||||||
|
public static final MapCodec<TransitStructure21> CODEC = simpleCodec(TransitStructure21::new);
|
||||||
|
public TransitStructure21(StructureSettings settings) { super(settings); }
|
||||||
|
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||||
|
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||||
|
|| generator.rootGeneration() != 21 || !generator.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();
|
||||||
|
var modules = generator.transitPlan21().modulesForChunk(context.chunkPos().x(), context.chunkPos().z());
|
||||||
|
if (modules.isEmpty()) return Optional.empty();
|
||||||
|
for (var module : modules) {
|
||||||
|
if (Math.abs((module.minX() >> 4) - module.startChunkX()) > 7
|
||||||
|
|| Math.abs((module.maxX() >> 4) - module.startChunkX()) > 7
|
||||||
|
|| Math.abs((module.minZ() >> 4) - module.startChunkZ()) > 7
|
||||||
|
|| Math.abs((module.maxZ() >> 4) - module.startChunkZ()) > 7)
|
||||||
|
throw new IllegalStateException("Transit module exceeds local native reference range");
|
||||||
|
}
|
||||||
|
return Optional.of(new GenerationStub(TransitPiece21.locator(modules.getFirst()), pieces ->
|
||||||
|
modules.forEach(module -> pieces.addPiece(new TransitPiece21(module)))));
|
||||||
|
}
|
||||||
|
@Override public StructureType<?> type() { return TransitStructures21.TYPE; }
|
||||||
|
}
|
||||||
+85
@@ -0,0 +1,85 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.transit;
|
||||||
|
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
import fr.koka.sanctuary.SanctuaryMod;
|
||||||
|
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
|
||||||
|
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.core.Registry;
|
||||||
|
import net.minecraft.core.RegistryAccess;
|
||||||
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.level.StructureManager;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||||
|
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureType;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceType;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
|
|
||||||
|
public final class TransitStructures21 {
|
||||||
|
public static final ResourceKey<Structure> KEY = ResourceKey.create(Registries.STRUCTURE, SanctuaryMod.id("industrial_transit"));
|
||||||
|
public static final StructureType<TransitStructure21> TYPE = Registry.register(BuiltInRegistries.STRUCTURE_TYPE,
|
||||||
|
SanctuaryMod.id("industrial_transit_v21"), () -> TransitStructure21.CODEC);
|
||||||
|
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
|
||||||
|
SanctuaryMod.id("industrial_transit_module_v21"), TransitPiece21::new);
|
||||||
|
private static final ThreadLocal<Integer> PLACEMENT_DEPTH = ThreadLocal.withInitial(() -> 0);
|
||||||
|
private TransitStructures21() {}
|
||||||
|
public static void register() {}
|
||||||
|
public static boolean isPlacing() { return PLACEMENT_DEPTH.get() > 0; }
|
||||||
|
static void placing(Runnable action) {
|
||||||
|
int previous = PLACEMENT_DEPTH.get(); PLACEMENT_DEPTH.set(previous + 1);
|
||||||
|
try { action.run(); } finally { if (previous == 0) PLACEMENT_DEPTH.remove(); else PLACEMENT_DEPTH.set(previous); }
|
||||||
|
}
|
||||||
|
public static void generate(ExpansionChunkGenerator generator, RegistryAccess registries,
|
||||||
|
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||||
|
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||||
|
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
||||||
|
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || generator.rootGeneration() != 21
|
||||||
|
|| !sanctuary.experimentalStructures()) return;
|
||||||
|
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
||||||
|
if (island == null || !island.origin() || island.generation() != 21) return;
|
||||||
|
if (sanctuary.transitPlan21().modulesForChunk(chunk.getPos().x(), chunk.getPos().z()).isEmpty()) return;
|
||||||
|
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||||
|
StructureStart previous = manager.getStartForStructure(holder.value(), chunk);
|
||||||
|
if (previous != null && previous.isValid()) return;
|
||||||
|
var random = generator.randomState(island);
|
||||||
|
var sampler = random.createClimateSampler(SamplerContext.builder().enableCaches().build());
|
||||||
|
var start = holder.value().generate(holder, level, registries, generator, generator.getBiomeSource(), sampler,
|
||||||
|
random, templates, island.seed(), chunk.getPos(), previous == null ? 0 : previous.getReferences(),
|
||||||
|
chunk, holder.value().biomes()::contains);
|
||||||
|
if (start.isValid()) manager.setStartForStructure(holder.value(), start, chunk);
|
||||||
|
}
|
||||||
|
/** Locate selects a saved surface portico rather than teleporting the player into a deep road. */
|
||||||
|
public static Pair<BlockPos, Holder<Structure>> findNearest(ServerLevel level, HolderSet<Structure> structures,
|
||||||
|
BlockPos origin, int radius, boolean skipReferenced) {
|
||||||
|
if (!Level.OVERWORLD.equals(level.dimension())
|
||||||
|
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||||
|
|| generator.rootGeneration() != 21 || !generator.experimentalStructures()
|
||||||
|
|| !level.structureManager().shouldGenerateStructures()) return null;
|
||||||
|
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||||
|
if (!structures.contains(holder)) return null;
|
||||||
|
var candidates = generator.transitPlan21().modules().stream()
|
||||||
|
.filter(m -> m.kind() == TransitPlan21.Kind.ACCESS)
|
||||||
|
.filter(m -> Math.abs((long) m.startChunkX() - (origin.getX() >> 4)) <= radius
|
||||||
|
&& Math.abs((long) m.startChunkZ() - (origin.getZ() >> 4)) <= radius)
|
||||||
|
.sorted(Comparator.comparingDouble(m -> origin.distSqr(TransitPiece21.locator(m)))).toList();
|
||||||
|
for (var module : candidates) {
|
||||||
|
var chunk = level.getChunk(module.startChunkX(), module.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||||
|
var start = level.structureManager().getStartForStructure(holder.value(), chunk);
|
||||||
|
if (start == null || !start.isValid() || skipReferenced && !start.canBeReferenced()) continue;
|
||||||
|
if (skipReferenced) level.structureManager().addReference(start);
|
||||||
|
return Pair.of(TransitPiece21.locator(module), holder);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.transit;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
|
import net.minecraft.world.level.NoiseColumn;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||||
|
import net.minecraft.world.level.levelgen.RandomState;
|
||||||
|
|
||||||
|
/** Read-only, bounded native-noise cache. Unknown columns always reject construction. */
|
||||||
|
public final class TransitTerrain21 implements TransitPlanner21.Terrain {
|
||||||
|
public static final int MAX_COLUMNS = 12_000;
|
||||||
|
private final ExpansionIsland island;
|
||||||
|
private final NoiseBasedChunkGenerator generator;
|
||||||
|
private final RandomState random;
|
||||||
|
private final LevelHeightAccessor height;
|
||||||
|
private final Predicate<BlockPos> protection;
|
||||||
|
private final Map<Long, NoiseColumn> columns = new HashMap<>();
|
||||||
|
|
||||||
|
public TransitTerrain21(ExpansionIsland island, NoiseBasedChunkGenerator generator, RandomState random,
|
||||||
|
LevelHeightAccessor height, Predicate<BlockPos> protection) {
|
||||||
|
this.island = island; this.generator = generator; this.random = random;
|
||||||
|
this.height = height; this.protection = protection;
|
||||||
|
}
|
||||||
|
private NoiseColumn column(int x, int z) {
|
||||||
|
if (!owns(x, z)) return null;
|
||||||
|
long key = ((long) x << 32) ^ (z & 0xffffffffL);
|
||||||
|
var found = columns.get(key);
|
||||||
|
if (found != null) return found;
|
||||||
|
if (columns.size() >= MAX_COLUMNS) return null;
|
||||||
|
var result = generator.getBaseColumn(x, z, height, random);
|
||||||
|
columns.put(key, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
@Override public boolean owns(int x, int z) { return island.contains(x, z); }
|
||||||
|
@Override public boolean solid(int x, int y, int z) {
|
||||||
|
if (y < 0 || y >= 384) return false;
|
||||||
|
var column = column(x, z);
|
||||||
|
return column != null && column.getBlock(y).isSolid() && column.getBlock(y).getFluidState().isEmpty();
|
||||||
|
}
|
||||||
|
@Override public boolean protectedAt(int x, int y, int z) {
|
||||||
|
if (y < 0 || y >= 384) return true;
|
||||||
|
var column = column(x, z);
|
||||||
|
return column == null || !column.getBlock(y).getFluidState().isEmpty()
|
||||||
|
|| protection.test(new BlockPos(x, y, z));
|
||||||
|
}
|
||||||
|
@Override public int surfaceY(int x, int z) {
|
||||||
|
var column = column(x, z);
|
||||||
|
if (column == null) return -1;
|
||||||
|
for (int y = 380; y >= 1; y--) {
|
||||||
|
var state = column.getBlock(y);
|
||||||
|
if (!state.isAir()) return state.isSolid() && state.getFluidState().isEmpty() ? y : -1;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
@Override public String obstruction(int x, int y, int z) {
|
||||||
|
var sample = column(x, z);
|
||||||
|
if (sample == null) return "unknown/unowned column";
|
||||||
|
var state = sample.getBlock(y);
|
||||||
|
if (!state.getFluidState().isEmpty()) return "native fluid " + state;
|
||||||
|
return protection.test(new BlockPos(x, y, z)) ? "earlier structure/hydrology mask" : "none";
|
||||||
|
}
|
||||||
|
public int columnsUsed() { return columns.size(); }
|
||||||
|
}
|
||||||
@@ -144,7 +144,8 @@
|
|||||||
"sanctuary.heritage.kind.photo_club": "Photography club",
|
"sanctuary.heritage.kind.photo_club": "Photography club",
|
||||||
"sanctuary.heritage.kind.impossible_service": "Changing rooms and strange service rooms",
|
"sanctuary.heritage.kind.impossible_service": "Changing rooms and strange service rooms",
|
||||||
"sanctuary.heritage.kind.construction_refuge": "Construction refuge",
|
"sanctuary.heritage.kind.construction_refuge": "Construction refuge",
|
||||||
"sanctuary.heritage.unavailable": "This survey requires a Sanctuary alpha.19 or alpha.20 world with experimental structures enabled.",
|
"sanctuary.heritage.unavailable": "This survey requires a recent Sanctuary world with experimental structures enabled.",
|
||||||
"sanctuary.heritage.header": "Abandoned infrastructure: %s planned sites and %s links. Entrance coordinates:",
|
"sanctuary.heritage.header": "Abandoned infrastructure: %s planned sites and %s links. Entrance coordinates:",
|
||||||
"sanctuary.heritage.entry": "%s : %s / %s / %s"
|
"sanctuary.heritage.entry": "%s : %s / %s / %s",
|
||||||
|
"sanctuary.transit.entry": "Industrial galleries — entrance: %s %s %s"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,7 +144,8 @@
|
|||||||
"sanctuary.heritage.kind.photo_club": "Club photo",
|
"sanctuary.heritage.kind.photo_club": "Club photo",
|
||||||
"sanctuary.heritage.kind.impossible_service": "Vestiaires et salles étranges",
|
"sanctuary.heritage.kind.impossible_service": "Vestiaires et salles étranges",
|
||||||
"sanctuary.heritage.kind.construction_refuge": "Refuge de chantier",
|
"sanctuary.heritage.kind.construction_refuge": "Refuge de chantier",
|
||||||
"sanctuary.heritage.unavailable": "Ce relevé nécessite un monde Sanctuary alpha.19 ou alpha.20 avec les structures expérimentales activées.",
|
"sanctuary.heritage.unavailable": "Ce relevé nécessite un monde Sanctuary récent avec les structures expérimentales activées.",
|
||||||
"sanctuary.heritage.header": "Anciennes infrastructures : %s lieux et %s liaisons prévus. Coordonnées des entrées :",
|
"sanctuary.heritage.header": "Anciennes infrastructures : %s lieux et %s liaisons prévus. Coordonnées des entrées :",
|
||||||
"sanctuary.heritage.entry": "%s : %s / %s / %s"
|
"sanctuary.heritage.entry": "%s : %s / %s / %s",
|
||||||
|
"sanctuary.transit.entry": "Galeries industrielles — entrée : %s %s %s"
|
||||||
}
|
}
|
||||||
|
|||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"type": "sanctuary:industrial_transit_v21",
|
||||||
|
"biomes": "#sanctuary:has_structure/forgotten_networks",
|
||||||
|
"step": "underground_structures",
|
||||||
|
"terrain_adaptation": "none",
|
||||||
|
"spawn_overrides": {}
|
||||||
|
}
|
||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
"minecraft:overworld": {
|
"minecraft:overworld": {
|
||||||
"type": "sanctuary:sanctuary_384",
|
"type": "sanctuary:sanctuary_384",
|
||||||
"generator": {
|
"generator": {
|
||||||
"type": "sanctuary:island_v20",
|
"type": "sanctuary:island_v21",
|
||||||
"biome_source": {
|
"biome_source": {
|
||||||
"biomes": [
|
"biomes": [
|
||||||
"minecraft:the_void",
|
"minecraft:the_void",
|
||||||
|
|||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"dimensions": {
|
||||||
|
"minecraft:overworld": {
|
||||||
|
"type": "sanctuary:sanctuary_384",
|
||||||
|
"generator": {
|
||||||
|
"type": "sanctuary:island_v20",
|
||||||
|
"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,6 +72,18 @@ public final class ExpansionJournalSmoke {
|
|||||||
require(Arrays.equals(unifiedMediumBytes, Files.readAllBytes(unifiedMediumDirectory.resolve(ExpansionJournal.FILE_NAME))),
|
require(Arrays.equals(unifiedMediumBytes, Files.readAllBytes(unifiedMediumDirectory.resolve(ExpansionJournal.FILE_NAME))),
|
||||||
"Rejected cross-generation read changed its journal");
|
"Rejected cross-generation read changed its journal");
|
||||||
|
|
||||||
|
for (int generation : List.of(20, 21)) {
|
||||||
|
var saved = scratch.resolve("architecture-cross-version-" + generation);
|
||||||
|
var root21 = ExpansionIsland.origin(724, SEED, generation);
|
||||||
|
var journal21 = new ExpansionJournal(saved, SEED, 724, generation);
|
||||||
|
var child = ExpansionPlacement.candidates(SEED, List.of(root21), "city21", "sanctuary", "east", "lost_city", 256, "natural", 0).getFirst();
|
||||||
|
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");
|
||||||
|
require(Arrays.equals(bytes, Files.readAllBytes(saved.resolve(ExpansionJournal.FILE_NAME))), "Rejected upgrade altered saved journal");
|
||||||
|
}
|
||||||
|
|
||||||
Path cityDirectory = scratch.resolve("city-generation");
|
Path cityDirectory = scratch.resolve("city-generation");
|
||||||
var cityRoot = ExpansionIsland.origin(724, SEED, 14);
|
var cityRoot = ExpansionIsland.origin(724, SEED, 14);
|
||||||
var city = new ExpansionJournal(cityDirectory, SEED, 724, 14,
|
var city = new ExpansionJournal(cityDirectory, SEED, 724, 14,
|
||||||
|
|||||||
+555
@@ -0,0 +1,555 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.city;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Kind;
|
||||||
|
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Parcel;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.world.level.EmptyBlockGetter;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.DoorBlock;
|
||||||
|
import net.minecraft.world.level.block.Rotation;
|
||||||
|
import net.minecraft.world.level.block.StairBlock;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.block.state.properties.BedPart;
|
||||||
|
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||||
|
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
|
||||||
|
import net.minecraft.world.level.storage.loot.LootTable;
|
||||||
|
import net.minecraft.world.phys.AABB;
|
||||||
|
|
||||||
|
/** Exercises final block geometry rather than reconstructing the painter's room allocation. */
|
||||||
|
public final class LostCityInteriors21Smoke {
|
||||||
|
private static final Kind[] KINDS = {Kind.APARTMENT, Kind.HOTEL, Kind.LIBRARY,
|
||||||
|
Kind.RESTAURANT, Kind.BARRACKS, Kind.HALL};
|
||||||
|
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
run();
|
||||||
|
System.out.println("LostCityInteriors21Smoke: six building uses, alternating physical floor plans, four rotations, connected rooms, fully enclosed bathrooms, wall-backed beds with lateral access, native door/seat geometry, supported lanterns, touching roof supports, oak/smooth-stone palette and deferred loot passed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Also usable after the engine has already bootstrapped its registries. */
|
||||||
|
public static void run() {
|
||||||
|
var failures = new java.util.ArrayList<String>();
|
||||||
|
Set<Map<BlockPos, BlockState>> distinct = new HashSet<>();
|
||||||
|
Set<Set<BlockPos>> distinctVolumes = new HashSet<>();
|
||||||
|
var previews = new java.util.ArrayList<Object>();
|
||||||
|
for (Kind kind : KINDS) for (int floors : new int[]{2, 3, 5}) for (long seed : new long[]{0, 43}) {
|
||||||
|
try {
|
||||||
|
Capture canonical = render(parcel(kind, floors, seed, 0));
|
||||||
|
if (floors == 2 && seed == 0) {
|
||||||
|
distinct.add(canonical.blocks);
|
||||||
|
Set<BlockPos> volume = new HashSet<>();
|
||||||
|
canonical.blocks.forEach((at, state) -> { if (!state.isAir() && at.getY() > 0
|
||||||
|
&& (at.getY() > 10 || at.getX() == 0 || at.getX() == 16 || at.getZ() == 0 || at.getZ() == 16)) volume.add(at); });
|
||||||
|
require(distinctVolumes.add(volume), kind + " only recolors an existing facade/roof volume");
|
||||||
|
}
|
||||||
|
if (floors == 3 && seed == 43 && System.getProperty("sanctuary.city21.preview") != null) {
|
||||||
|
var blocks = new java.util.ArrayList<Object>();
|
||||||
|
canonical.blocks.forEach((at, state) -> { if (!state.isAir()) blocks.add(java.util.List.of(at.getX(), at.getY(), at.getZ(), state.toString())); });
|
||||||
|
previews.add(Map.of("kind", kind.name(), "seed", seed, "floors", floors, "facing", 0, "blocks", blocks));
|
||||||
|
}
|
||||||
|
for (int facing = 0; facing < 4; facing++) {
|
||||||
|
Parcel parcel = parcel(kind, floors, seed, facing);
|
||||||
|
Capture result = facing == 0 ? canonical : render(parcel);
|
||||||
|
Capture repeated = render(parcel);
|
||||||
|
require(result.blocks.equals(repeated.blocks) && result.loot.equals(repeated.loot), "Saved parcel is not deterministic");
|
||||||
|
rotationMatches(canonical, result, facing);
|
||||||
|
require(result.blocks.values().stream().anyMatch(state -> state.is(Blocks.STONE_BRICKS)), "Missing stone-brick foundations");
|
||||||
|
require(result.blocks.values().stream().anyMatch(state -> state.is(Blocks.SMOOTH_STONE)), "Missing smooth stone");
|
||||||
|
require(result.blocks.values().stream().noneMatch(state -> state.is(Blocks.SPAWNER)
|
||||||
|
|| !state.getFluidState().isEmpty()), "Building introduced a spawner or free flowing fluid");
|
||||||
|
require(result.loot.size() == 1, "Building must keep one deferred loot chest");
|
||||||
|
var entrance = LostCityInteriors21.rooms(parcel).getFirst();
|
||||||
|
Set<BlockPos> reached = walk(result, position(entrance));
|
||||||
|
for (var room : LostCityInteriors21.rooms(parcel)) require(reached.contains(position(room)),
|
||||||
|
label(parcel) + " cannot walk from its entrance to " + room.name() + " at " + position(room));
|
||||||
|
verifyDoors(result, reached);
|
||||||
|
verifyBeds(result, reached);
|
||||||
|
verifyBathrooms(result, reached);
|
||||||
|
verifyFurnitureDirections(result);
|
||||||
|
verifyLanternSupports(result);
|
||||||
|
for (BlockPos chest : result.loot.keySet()) require(adjacentReached(chest, reached),
|
||||||
|
label(parcel) + " loot cannot be approached");
|
||||||
|
semantics(result);
|
||||||
|
if (facing == 0) verifyStoreyVariation(result);
|
||||||
|
verifyArchitecture(result);
|
||||||
|
}
|
||||||
|
} catch (AssertionError | RuntimeException failure) {
|
||||||
|
failures.add(kind + " floors=" + floors + " seed=" + seed + ": " + failure.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require(failures.isEmpty(), "City21 interior fixture failures (" + failures.size() + "):\n" + String.join("\n", failures));
|
||||||
|
require(distinct.size() == KINDS.length, "Building uses only labels to vary an identical interior");
|
||||||
|
boolean rejected = false;
|
||||||
|
try { LostCityInteriors21.rooms(new Parcel(Kind.PARK, 0, 0, 17, 17, 160, 2, 0, 0, 4, new int[0])); }
|
||||||
|
catch (IllegalArgumentException expected) { rejected = true; }
|
||||||
|
require(rejected, "Interior dispatch must reject non-building parcels");
|
||||||
|
String preview = System.getProperty("sanctuary.city21.preview");
|
||||||
|
if (preview != null) try {
|
||||||
|
var file = java.nio.file.Path.of(preview); if (file.getParent() != null) java.nio.file.Files.createDirectories(file.getParent());
|
||||||
|
java.nio.file.Files.writeString(file, new com.google.gson.Gson().toJson(previews));
|
||||||
|
} catch (java.io.IOException e) { throw new AssertionError("Cannot write requested architecture preview", e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Parcel parcel(Kind kind, int floors, long seed, int facing) {
|
||||||
|
return new Parcel(kind, -81, 119, 17, 17, 160, floors, facing, seed, 4, new int[0]);
|
||||||
|
}
|
||||||
|
private static Capture render(Parcel parcel) {
|
||||||
|
var result = new Capture(parcel);
|
||||||
|
LostCityInteriors21.render(parcel, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
private static String label(Parcel p) { return p.kind() + " floors=" + p.floors() + " facing=" + p.facing() + " seed=" + p.seed(); }
|
||||||
|
private static BlockPos position(LostCityInteriors21.Room room) { return new BlockPos(room.x(), room.y(), room.z()); }
|
||||||
|
|
||||||
|
private static void rotationMatches(Capture canonical, Capture rotated, int facing) {
|
||||||
|
require(canonical.blocks.size() == rotated.blocks.size(), "Rotation changed the number of emitted positions");
|
||||||
|
Rotation rotation = switch (facing) {
|
||||||
|
case 0 -> Rotation.NONE; case 1 -> Rotation.CLOCKWISE_90;
|
||||||
|
case 2 -> Rotation.CLOCKWISE_180; default -> Rotation.COUNTERCLOCKWISE_90;
|
||||||
|
};
|
||||||
|
for (var entry : canonical.blocks.entrySet()) {
|
||||||
|
BlockPos target = rotate(entry.getKey(), facing);
|
||||||
|
// Native chest placement owns its final facing, so its loot position is checked separately.
|
||||||
|
if (canonical.loot.containsKey(entry.getKey())) continue;
|
||||||
|
require(rotated.block(target).equals(entry.getValue().rotate(rotation)), "Geometry or block-state rotation drifted at " + target);
|
||||||
|
}
|
||||||
|
for (var entry : canonical.loot.entrySet()) require(entry.getValue().equals(rotated.loot.get(rotate(entry.getKey(), facing))),
|
||||||
|
"Rotation changed deferred loot identity or position");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyDoors(Capture result, Set<BlockPos> reached) {
|
||||||
|
long lowerDoors = 0, pairedLeaves = 0;
|
||||||
|
for (var entry : result.blocks.entrySet()) {
|
||||||
|
BlockState state = entry.getValue();
|
||||||
|
if (!(state.getBlock() instanceof DoorBlock)) continue;
|
||||||
|
require(state.is(Blocks.OAK_DOOR) && !state.getValue(BlockStateProperties.POWERED), "A room door cannot be opened by an ordinary player");
|
||||||
|
boolean lower = state.getValue(BlockStateProperties.DOUBLE_BLOCK_HALF) == DoubleBlockHalf.LOWER;
|
||||||
|
BlockPos partner = lower ? entry.getKey().above() : entry.getKey().below();
|
||||||
|
BlockState expected = state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF,
|
||||||
|
lower ? DoubleBlockHalf.UPPER : DoubleBlockHalf.LOWER);
|
||||||
|
require(result.block(partner).equals(expected), label(result.parcel) + " has an incomplete/mismatched door at " + entry.getKey());
|
||||||
|
if (!lower) continue;
|
||||||
|
lowerDoors++;
|
||||||
|
require(result.block(entry.getKey().below()).isSolid(), "Door has no solid threshold");
|
||||||
|
Direction normal = state.getValue(BlockStateProperties.HORIZONTAL_FACING);
|
||||||
|
require(reached.contains(entry.getKey().relative(normal)) && reached.contains(entry.getKey().relative(normal.getOpposite())),
|
||||||
|
label(result.parcel) + " door does not connect two walkable sides at " + entry.getKey());
|
||||||
|
for (Direction side : new Direction[]{normal.getClockWise(), normal.getCounterClockWise()}) {
|
||||||
|
BlockState neighbor = result.block(entry.getKey().relative(side));
|
||||||
|
if (!neighbor.is(Blocks.OAK_DOOR)
|
||||||
|
|| neighbor.getValue(BlockStateProperties.DOUBLE_BLOCK_HALF) != DoubleBlockHalf.LOWER
|
||||||
|
|| neighbor.getValue(BlockStateProperties.HORIZONTAL_FACING) != normal) continue;
|
||||||
|
AABB opened = state.setValue(BlockStateProperties.OPEN, true)
|
||||||
|
.getCollisionShape(EmptyBlockGetter.INSTANCE, entry.getKey()).bounds();
|
||||||
|
require(againstOppositeEdge(opened, side), label(result.parcel) + " double door opens into its partner at " + entry.getKey());
|
||||||
|
pairedLeaves++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require(lowerDoors >= result.parcel.floors() * 3L + 2, "Missing actual doors between distinct rooms");
|
||||||
|
require(pairedLeaves >= 2, "Entrance must retain a double door with its hinges against the outside jambs");
|
||||||
|
for (var room : LostCityInteriors21.rooms(result.parcel)) if (room.name().endsWith("/stair_landing")) {
|
||||||
|
BlockPos landing = position(room);
|
||||||
|
boolean opensIntoStairwell = false;
|
||||||
|
for (Direction side : HORIZONTAL) {
|
||||||
|
BlockPos at = landing.relative(side);
|
||||||
|
BlockState state = result.block(at);
|
||||||
|
if (state.is(Blocks.OAK_DOOR)) opensIntoStairwell |=
|
||||||
|
at.relative(state.getValue(BlockStateProperties.HORIZONTAL_FACING)).equals(landing);
|
||||||
|
}
|
||||||
|
require(opensIntoStairwell, "Stairwell door must open into its landing, away from the central corridor");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean againstOppositeEdge(AABB box, Direction towardPartner) {
|
||||||
|
return switch (towardPartner) {
|
||||||
|
case EAST -> box.maxX <= .25; case WEST -> box.minX >= .75;
|
||||||
|
case SOUTH -> box.maxZ <= .25; case NORTH -> box.minZ >= .75;
|
||||||
|
default -> throw new AssertionError("Door partner must be horizontal");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyFurnitureDirections(Capture result) {
|
||||||
|
int tableSeats = 0;
|
||||||
|
for (var entry : result.blocks.entrySet()) {
|
||||||
|
if (!entry.getValue().is(Blocks.OAK_FENCE)
|
||||||
|
|| !result.block(entry.getKey().above()).is(Blocks.OAK_PRESSURE_PLATE)) continue;
|
||||||
|
int aroundThisTable = 0;
|
||||||
|
for (Direction side : HORIZONTAL) {
|
||||||
|
BlockPos chair = entry.getKey().relative(side);
|
||||||
|
if (!result.block(chair).is(Blocks.OAK_STAIRS)) continue;
|
||||||
|
seatLooksToward(result, chair, side.getOpposite());
|
||||||
|
aroundThisTable++; tableSeats++;
|
||||||
|
}
|
||||||
|
require(aroundThisTable > 0, "A desk/table lost every chair facing its surface");
|
||||||
|
}
|
||||||
|
if (result.parcel.kind() != Kind.BARRACKS)
|
||||||
|
require(tableSeats >= result.parcel.floors(), "Every furnished storey needs a usable table or desk seat");
|
||||||
|
if (result.parcel.kind() == Kind.HALL) for (int x : new int[]{3, 4, 5, 11, 12, 13})
|
||||||
|
seatLooksToward(result, rotate(new BlockPos(x, 1, 11), result.parcel.facing()), rotated(Direction.SOUTH, result.parcel.facing()));
|
||||||
|
if (result.parcel.kind() == Kind.HOTEL) for (int x = 2; x <= 4; x++)
|
||||||
|
seatLooksToward(result, rotate(new BlockPos(x, 1, 14), result.parcel.facing()), rotated(Direction.NORTH, result.parcel.facing()));
|
||||||
|
if (result.parcel.kind() == Kind.BARRACKS) for (int floor = 0; floor < result.parcel.floors(); floor++)
|
||||||
|
if (LostCityInteriors21.layout(result.parcel, floor) == 0)
|
||||||
|
seatLooksToward(result, rotate(new BlockPos(13, floor * 5 + 1, 6), result.parcel.facing()), rotated(Direction.NORTH, result.parcel.facing()));
|
||||||
|
for (var entry : result.blocks.entrySet()) if (entry.getValue().is(Blocks.POLISHED_ANDESITE_STAIRS)) {
|
||||||
|
Direction look = entry.getValue().getValue(BlockStateProperties.HORIZONTAL_FACING).getOpposite();
|
||||||
|
require(clear(result.block(entry.getKey().relative(look))), "Sanitary seat faces a wall instead of its cubicle");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void seatLooksToward(Capture result, BlockPos chair, Direction towardUse) {
|
||||||
|
BlockState state = result.block(chair);
|
||||||
|
require(state.is(Blocks.OAK_STAIRS), "Missing chair at " + chair);
|
||||||
|
var boxes = state.getCollisionShape(EmptyBlockGetter.INSTANCE, chair).toAabbs();
|
||||||
|
double frontX = .5 + towardUse.getStepX() * .25, frontZ = .5 + towardUse.getStepZ() * .25;
|
||||||
|
double backX = .5 - towardUse.getStepX() * .25, backZ = .5 - towardUse.getStepZ() * .25;
|
||||||
|
require(boxes.stream().noneMatch(box -> box.contains(frontX, .75, frontZ))
|
||||||
|
&& boxes.stream().anyMatch(box -> box.contains(backX, .75, backZ)),
|
||||||
|
label(result.parcel) + " chair's native back faces its table/stage at " + chair);
|
||||||
|
require(result.block(chair.below()).isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, chair.below()),
|
||||||
|
"Chair does not rest on a complete floor");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Direction rotated(Direction direction, int facing) {
|
||||||
|
for (int turn = 0; turn < facing; turn++) direction = direction.getClockWise();
|
||||||
|
return direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyLanternSupports(Capture result) {
|
||||||
|
int lanterns = 0;
|
||||||
|
for (var entry : result.blocks.entrySet()) if (entry.getValue().is(Blocks.LANTERN)) {
|
||||||
|
BlockPos support = entry.getValue().getValue(BlockStateProperties.HANGING) ? entry.getKey().above() : entry.getKey().below();
|
||||||
|
require(result.block(support).isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, support),
|
||||||
|
label(result.parcel) + " lantern floats against a slab or lacks a full support block at " + entry.getKey());
|
||||||
|
lanterns++;
|
||||||
|
}
|
||||||
|
require(lanterns == result.parcel.floors(), "A junction lost its supported lamp");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyBeds(Capture result, Set<BlockPos> reached) {
|
||||||
|
for (var entry : result.blocks.entrySet()) {
|
||||||
|
BlockState state = entry.getValue();
|
||||||
|
if (!state.is(Blocks.BED.white())) continue;
|
||||||
|
Direction direction = state.getValue(BlockStateProperties.HORIZONTAL_FACING);
|
||||||
|
BedPart part = state.getValue(BlockStateProperties.BED_PART);
|
||||||
|
BlockPos partner = entry.getKey().relative(part == BedPart.FOOT ? direction : direction.getOpposite());
|
||||||
|
require(result.block(partner).equals(state.setValue(BlockStateProperties.BED_PART,
|
||||||
|
part == BedPart.FOOT ? BedPart.HEAD : BedPart.FOOT)), label(result.parcel) + " has a severed bed at " + entry.getKey());
|
||||||
|
require(result.block(entry.getKey().below()).isSolid(), "Bed does not sit on a floor");
|
||||||
|
require(adjacentReached(entry.getKey(), reached) || adjacentReached(partner, reached),
|
||||||
|
label(result.parcel) + " bed cannot be approached from its room");
|
||||||
|
if (part == BedPart.HEAD) {
|
||||||
|
var backing = entry.getKey().relative(direction);
|
||||||
|
for (int up = 0; up < 3; up++) require(result.block(backing.above(up))
|
||||||
|
.isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, backing.above(up)),
|
||||||
|
label(result.parcel) + " bed head is not against a complete wall at " + entry.getKey());
|
||||||
|
} else {
|
||||||
|
boolean lateral = false;
|
||||||
|
for (Direction side : new Direction[]{direction.getClockWise(), direction.getCounterClockWise()})
|
||||||
|
lateral |= reached.contains(entry.getKey().relative(side)) && reached.contains(partner.relative(side));
|
||||||
|
require(lateral, label(result.parcel) + " bed lacks a reachable side along its full length at " + entry.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyBathrooms(Capture result, Set<BlockPos> reached) {
|
||||||
|
for (var bath : LostCityInteriors21.bathrooms(result.parcel)) {
|
||||||
|
BlockPos door = new BlockPos(bath.doorX(), bath.doorY(), bath.doorZ());
|
||||||
|
require(result.block(door).is(Blocks.OAK_DOOR), "Enclosed bathroom lost its actual door: " + bath.name());
|
||||||
|
Direction intoRoom = result.block(door).getValue(BlockStateProperties.HORIZONTAL_FACING);
|
||||||
|
require(reached.contains(door.relative(intoRoom)), "Bathroom cannot be entered through its door: " + bath.name());
|
||||||
|
require(bath.minX() == 1 || bath.minZ() == 1 || bath.maxX() == 15 || bath.maxZ() == 15,
|
||||||
|
"Bathroom enclosure is detached from the exterior room wall");
|
||||||
|
for (int x = bath.minX(); x <= bath.maxX(); x++) for (int z = bath.minZ(); z <= bath.maxZ(); z++)
|
||||||
|
for (int y = bath.minY(); y <= bath.maxY(); y++) {
|
||||||
|
boolean shell = x == bath.minX() || x == bath.maxX() || z == bath.minZ() || z == bath.maxZ()
|
||||||
|
|| y == bath.minY() || y == bath.maxY();
|
||||||
|
if (!shell) continue;
|
||||||
|
BlockPos at = new BlockPos(x, y, z);
|
||||||
|
if (at.equals(door) || at.equals(door.above())) continue;
|
||||||
|
require(result.block(at).isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, at),
|
||||||
|
label(result.parcel) + " bathroom has an incomplete floor, ceiling or connected wall at " + at + " in " + bath.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyStoreyVariation(Capture result) {
|
||||||
|
for (int floor = 1; floor < result.parcel.floors(); floor++) {
|
||||||
|
require(LostCityInteriors21.layout(result.parcel, floor - 1) != LostCityInteriors21.layout(result.parcel, floor),
|
||||||
|
"Adjacent storeys selected the same plan");
|
||||||
|
Set<BlockPos> before = rearPartitions(result, floor - 1), after = rearPartitions(result, floor);
|
||||||
|
require(!before.equals(after), label(result.parcel) + " only changed furniture or labels between storeys " + (floor - 1) + " and " + floor);
|
||||||
|
Map<BlockPos, BlockState> beforeFixtures = furnishings(result, floor - 1), afterFixtures = furnishings(result, floor);
|
||||||
|
require(!beforeFixtures.equals(afterFixtures), label(result.parcel)
|
||||||
|
+ " copied the same furniture between storeys " + (floor - 1) + " and " + floor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<BlockPos> rearPartitions(Capture result, int floor) {
|
||||||
|
Set<BlockPos> walls = new HashSet<>();
|
||||||
|
for (int x = 2; x <= 14; x++) for (int z = 9; z <= 14; z++) {
|
||||||
|
boolean continuous = true;
|
||||||
|
for (int y = 1; y <= 3; y++) continuous &= result.block(new BlockPos(x, floor * 5 + y, z)).is(Blocks.SMOOTH_STONE);
|
||||||
|
if (continuous) walls.add(new BlockPos(x, 0, z));
|
||||||
|
}
|
||||||
|
return walls;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<BlockPos, BlockState> furnishings(Capture result, int floor) {
|
||||||
|
Map<BlockPos, BlockState> objects = new HashMap<>();
|
||||||
|
for (int x = 2; x <= 14; x++) for (int z = 2; z <= 14; z++) for (int up = 1; up <= 3; up++) {
|
||||||
|
BlockState state = result.block(new BlockPos(x, floor * 5 + up, z));
|
||||||
|
if (state.is(Blocks.BED.white()) || state.is(Blocks.BARREL) || state.is(Blocks.FURNACE)
|
||||||
|
|| state.is(Blocks.BOOKSHELF) || state.is(Blocks.WATER_CAULDRON) || state.is(Blocks.OAK_FENCE)
|
||||||
|
|| state.is(Blocks.LECTERN) || state.is(Blocks.CRAFTING_TABLE) || state.is(Blocks.OAK_STAIRS)
|
||||||
|
|| state.is(Blocks.OAK_PLANKS) || state.is(Blocks.OAK_PLANKS) || state.is(Blocks.OAK_PLANKS)) objects.put(new BlockPos(x, up, z), state);
|
||||||
|
}
|
||||||
|
return objects;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void semantics(Capture result) {
|
||||||
|
Map<BlockState, Long> counts = new HashMap<>();
|
||||||
|
result.blocks.values().forEach(state -> counts.merge(state, 1L, Long::sum));
|
||||||
|
java.util.function.Predicate<net.minecraft.world.level.block.Block> has = block ->
|
||||||
|
counts.keySet().stream().anyMatch(state -> state.is(block));
|
||||||
|
switch (result.parcel.kind()) {
|
||||||
|
case APARTMENT -> require(has.test(Blocks.FURNACE) && has.test(Blocks.BED.white())
|
||||||
|
&& has.test(Blocks.WATER_CAULDRON), "Apartment lacks cooking, sleeping or washing fixtures");
|
||||||
|
case HOTEL -> require(has.test(Blocks.LOOM) && has.test(Blocks.BED.white())
|
||||||
|
&& has.test(Blocks.WATER_CAULDRON), "Hotel lacks guest rooms or laundry fixtures");
|
||||||
|
case LIBRARY -> require(result.blocks.values().stream().filter(state -> state.is(Blocks.BOOKSHELF)).count() >= result.parcel.floors() * 12L
|
||||||
|
&& has.test(Blocks.LECTERN), "Library is missing usable stacks and reading fixtures");
|
||||||
|
case RESTAURANT -> require(has.test(Blocks.SMOKER) && has.test(Blocks.FURNACE)
|
||||||
|
&& has.test(Blocks.CRAFTING_TABLE) && has.test(Blocks.WATER_CAULDRON), "Restaurant lacks kitchen and washroom services");
|
||||||
|
case BARRACKS -> require(has.test(Blocks.BED.white()) && has.test(Blocks.FLETCHING_TABLE)
|
||||||
|
&& has.test(Blocks.GRINDSTONE), "Barracks lacks dormitory and equipment fixtures");
|
||||||
|
case HALL -> {
|
||||||
|
require(has.test(Blocks.LECTERN) && has.test(Blocks.IRON_BARS), "Hall lacks stage or gallery guards");
|
||||||
|
require(result.block(rotate(new BlockPos(8, 5, 12), result.parcel.facing())).isAir(), "Auditorium lost its double-height volume");
|
||||||
|
}
|
||||||
|
default -> throw new AssertionError("Unexpected building");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Player-sized path: two clear cells above support, one-block steps and manually openable doors. */
|
||||||
|
private static Set<BlockPos> walk(Capture result, BlockPos start) {
|
||||||
|
require(standable(result, start), label(result.parcel) + " has a blocked entrance");
|
||||||
|
Set<BlockPos> seen = new HashSet<>();
|
||||||
|
ArrayDeque<BlockPos> queue = new ArrayDeque<>();
|
||||||
|
seen.add(start); queue.add(start);
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
BlockPos current = queue.removeFirst();
|
||||||
|
for (Direction direction : HORIZONTAL) for (int dy = -1; dy <= 1; dy++) {
|
||||||
|
BlockPos next = current.relative(direction).offset(0, dy, 0);
|
||||||
|
if (dy > 0 && !clearBody(result, current.above())) continue;
|
||||||
|
if (standable(result, next) && seen.add(next)) queue.addLast(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return seen;
|
||||||
|
}
|
||||||
|
private static boolean standable(Capture result, BlockPos pos) {
|
||||||
|
if (pos.getX() < 0 || pos.getX() > 16 || pos.getZ() < 0 || pos.getZ() > 16
|
||||||
|
|| pos.getY() < 1 || pos.getY() > result.parcel.floors() * 5 + 4) return false;
|
||||||
|
if (!clearBody(result, pos)) return false;
|
||||||
|
var below = pos.below();
|
||||||
|
return opened(result.block(below)).getCollisionShape(EmptyBlockGetter.INSTANCE, below).toAabbs().stream()
|
||||||
|
.anyMatch(b -> b.maxY >= .5 && b.minX <= .5 && b.maxX >= .5 && b.minZ <= .5 && b.maxZ >= .5);
|
||||||
|
}
|
||||||
|
private static BlockState opened(BlockState state) {
|
||||||
|
return state.getBlock() instanceof net.minecraft.world.level.block.DoorBlock
|
||||||
|
|| state.getBlock() instanceof net.minecraft.world.level.block.TrapDoorBlock
|
||||||
|
? state.setValue(BlockStateProperties.OPEN, true) : state;
|
||||||
|
}
|
||||||
|
private static boolean clearBody(Capture result, BlockPos pos) {
|
||||||
|
var body = new AABB(pos.getX() + .2, pos.getY(), pos.getZ() + .2, pos.getX() + .8, pos.getY() + 1.8, pos.getZ() + .8);
|
||||||
|
for (var at : new BlockPos[]{pos, pos.above()}) {
|
||||||
|
var state = opened(result.block(at));
|
||||||
|
if (!state.getFluidState().isEmpty()) return false;
|
||||||
|
for (var shape : state.getCollisionShape(EmptyBlockGetter.INSTANCE, at).toAabbs())
|
||||||
|
if (shape.move(at.getX(), at.getY(), at.getZ()).intersects(body)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
private static boolean clear(BlockState state) { return state.isAir() || state.is(Blocks.OAK_DOOR); }
|
||||||
|
private static void verifyArchitecture(Capture c) {
|
||||||
|
for (int x = 0; x <= 16; x++) for (int z = 0; z <= 16; z++)
|
||||||
|
require(c.block(new BlockPos(x, -1, z)).is(Blocks.STONE_BRICKS), "Foundation is not dressed stone brick");
|
||||||
|
long outsideVolume = c.blocks.entrySet().stream().filter(e -> e.getKey().getY() > 0
|
||||||
|
&& (e.getKey().getX() == 0 || e.getKey().getX() == 16 || e.getKey().getZ() == 0 || e.getKey().getZ() == 16)
|
||||||
|
&& !e.getValue().isAir()).count();
|
||||||
|
require(outsideVolume >= 40, label(c.parcel) + " has no cornice, bay, fins or entrance projection");
|
||||||
|
verifyPalette(c.blocks, true, label(c.parcel));
|
||||||
|
verifyGroundedGeometry(c.blocks, 0, label(c.parcel));
|
||||||
|
if (c.parcel.kind() == Kind.BARRACKS) for (int floor = 0; floor < c.parcel.floors(); floor++) {
|
||||||
|
var grinder = rotate(new BlockPos(12, floor * 5 + 1, 11), c.parcel.facing());
|
||||||
|
require(c.block(grinder).is(Blocks.GRINDSTONE)
|
||||||
|
&& c.block(grinder).getValue(BlockStateProperties.ATTACH_FACE)
|
||||||
|
== net.minecraft.world.level.block.state.properties.AttachFace.FLOOR,
|
||||||
|
label(c.parcel) + " workshop grinder lacks its floor-mounted frame");
|
||||||
|
}
|
||||||
|
if (c.parcel.kind() == Kind.HOTEL) for (int x : new int[]{12, 14}) {
|
||||||
|
var counter = rotate(new BlockPos(x, 1, 14), c.parcel.facing());
|
||||||
|
require(c.block(counter).is(Blocks.BARREL)
|
||||||
|
&& c.block(counter.above()).is(Blocks.OAK_PLANKS)
|
||||||
|
&& c.block(counter.above(2)).is(Blocks.OAK_SLAB),
|
||||||
|
label(c.parcel) + " laundry shelf lacks its counter-mounted timber frame");
|
||||||
|
}
|
||||||
|
long smooth = 0, facingMaterial = 0;
|
||||||
|
for (int x = 1; x <= 15; x++) for (int z = 1; z <= 15; z++) {
|
||||||
|
if (x != 1 && x != 15 && z != 1 && z != 15) continue;
|
||||||
|
for (int y = 1; y < c.parcel.floors() * 5; y++) {
|
||||||
|
var at = rotate(new BlockPos(x, y, z), c.parcel.facing());
|
||||||
|
var state = c.block(at);
|
||||||
|
if (state.isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, at)) {
|
||||||
|
facingMaterial++;
|
||||||
|
if (state.is(Blocks.SMOOTH_STONE)) smooth++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require(smooth * 2 >= facingMaterial, label(c.parcel) + " no longer has predominantly smooth-stone facades: " + smooth + "/" + facingMaterial);
|
||||||
|
if (c.parcel.kind() == Kind.APARTMENT || c.parcel.kind() == Kind.LIBRARY)
|
||||||
|
require(c.block(rotate(new BlockPos(1, 2, 1), c.parcel.facing())).isAir(), "Front corner was not physically cut back");
|
||||||
|
if (c.parcel.kind() == Kind.LIBRARY) {
|
||||||
|
int roof = c.parcel.floors() * 5;
|
||||||
|
for (int x = 11; x <= 12; x++) for (int z = 4; z <= 6; z++) {
|
||||||
|
for (int y = roof - 1; y <= roof; y++)
|
||||||
|
require(c.block(rotate(new BlockPos(x, y, z), c.parcel.facing())).isAir(),
|
||||||
|
label(c.parcel) + " skylight is blocked by the roof plate or last-storey ceiling");
|
||||||
|
require(c.block(rotate(new BlockPos(x, roof + 1, z), c.parcel.facing())).isAir(),
|
||||||
|
label(c.parcel) + " skylight contains a redundant glazed floor under its cap");
|
||||||
|
require(c.block(rotate(new BlockPos(x, roof + 2, z), c.parcel.facing())).is(Blocks.GLASS),
|
||||||
|
label(c.parcel) + " skylight lost its continuous transparent cap");
|
||||||
|
for (int y = roof + 3; y <= roof + 4; y++)
|
||||||
|
require(c.block(rotate(new BlockPos(x, y, z), c.parcel.facing())).isAir(),
|
||||||
|
label(c.parcel) + " another roof feature shades the light well");
|
||||||
|
}
|
||||||
|
for (int x = 10; x <= 13; x++) for (int z = 3; z <= 7; z++) {
|
||||||
|
if (x > 10 && x < 13 && z > 3 && z < 7) continue;
|
||||||
|
var base = rotate(new BlockPos(x, roof, z), c.parcel.facing());
|
||||||
|
require(c.block(base).isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, base)
|
||||||
|
&& c.block(base.above()).is(Blocks.STRIPPED_OAK_LOG),
|
||||||
|
label(c.parcel) + " skylight cut through its supporting perimeter or timber frame");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (c.parcel.kind() == Kind.HOTEL || c.parcel.kind() == Kind.BARRACKS) {
|
||||||
|
int roof = c.parcel.floors() * 5;
|
||||||
|
int x = c.parcel.kind() == Kind.HOTEL ? 9 : 1;
|
||||||
|
int z = c.parcel.kind() == Kind.HOTEL ? 3 : 2;
|
||||||
|
var top = rotate(new BlockPos(x, roof + 4, z), c.parcel.facing());
|
||||||
|
require(c.block(top).is(Blocks.STONE_BRICK_SLAB)
|
||||||
|
&& c.block(top).getValue(BlockStateProperties.SLAB_TYPE)
|
||||||
|
== net.minecraft.world.level.block.state.properties.SlabType.BOTTOM,
|
||||||
|
label(c.parcel) + " pavilion roof floats half a block above its walls");
|
||||||
|
require(c.block(top.below()).isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, top.below()),
|
||||||
|
label(c.parcel) + " pavilion lost the wall carrying its roof");
|
||||||
|
}
|
||||||
|
// Resolve every glazing edge from final neighbors, not precomputed straight strips.
|
||||||
|
for (var e : c.blocks.entrySet()) if (e.getValue().is(Blocks.GLASS_PANE)) {
|
||||||
|
for (Direction d : HORIZONTAL) {
|
||||||
|
var neighbor = c.block(e.getKey().relative(d));
|
||||||
|
if (!neighbor.isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, e.getKey().relative(d))) continue;
|
||||||
|
var property = switch(d) { case NORTH -> BlockStateProperties.NORTH; case EAST -> BlockStateProperties.EAST;
|
||||||
|
case SOUTH -> BlockStateProperties.SOUTH; case WEST -> BlockStateProperties.WEST; default -> throw new AssertionError(); };
|
||||||
|
require(e.getValue().getValue(property), "Window does not meet its actual stone or timber jamb");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** The construction palette is explicit; functional fixtures and planting retain vanilla materials. */
|
||||||
|
public static void verifyPalette(Map<BlockPos, BlockState> blocks, boolean building, String label) {
|
||||||
|
for (var e : blocks.entrySet()) {
|
||||||
|
String name = e.getValue().toString();
|
||||||
|
require(!name.matches(".*(?:birch|dark_oak|spruce|jungle|acacia|mangrove|cherry|pale_oak|bamboo)_.*"),
|
||||||
|
label + " introduced a second timber palette at " + e.getKey() + ": " + name);
|
||||||
|
require(!name.contains("granite") && !name.contains("diorite"),
|
||||||
|
label + " introduced contrasting granite/diorite at " + e.getKey());
|
||||||
|
require(!name.contains("tuff") || !building && e.getKey().getY() <= 0,
|
||||||
|
label + " used tuff above the ground/subsoil at " + e.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Contact uses native collision volumes: merely neighboring slabs can still leave a real gap.
|
||||||
|
* Every solid fixture, roof and facade must join the foundation through touching material. */
|
||||||
|
public static void verifyGroundedGeometry(Map<BlockPos, BlockState> blocks, int groundY, String label) {
|
||||||
|
Map<BlockState, java.util.List<AABB>> shapes = new HashMap<>();
|
||||||
|
Map<BlockPos, java.util.List<AABB>> geometry = new HashMap<>();
|
||||||
|
for (var entry : blocks.entrySet()) {
|
||||||
|
// A lantern's collision omits its thin hanging chain. Its explicit native support
|
||||||
|
// is checked below instead of pretending that invisible collision gap is floating masonry.
|
||||||
|
if (entry.getValue().is(Blocks.LANTERN)) continue;
|
||||||
|
var shape = shapes.computeIfAbsent(entry.getValue(), state ->
|
||||||
|
state.getCollisionShape(EmptyBlockGetter.INSTANCE, BlockPos.ZERO).toAabbs());
|
||||||
|
if (!shape.isEmpty()) geometry.put(entry.getKey(), shape);
|
||||||
|
}
|
||||||
|
Set<BlockPos> supported = new HashSet<>();
|
||||||
|
ArrayDeque<BlockPos> queue = new ArrayDeque<>();
|
||||||
|
for (BlockPos at : geometry.keySet()) if (at.getY() <= groundY) {
|
||||||
|
supported.add(at); queue.add(at);
|
||||||
|
}
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
var at = queue.removeFirst();
|
||||||
|
for (Direction direction : Direction.values()) {
|
||||||
|
var next = at.relative(direction);
|
||||||
|
if (supported.contains(next) || !geometry.containsKey(next)) continue;
|
||||||
|
boolean touches = false;
|
||||||
|
for (AABB a : geometry.get(at)) for (AABB b : geometry.get(next)) {
|
||||||
|
double dx = Math.min(a.maxX, b.maxX + direction.getStepX()) - Math.max(a.minX, b.minX + direction.getStepX());
|
||||||
|
double dy = Math.min(a.maxY, b.maxY + direction.getStepY()) - Math.max(a.minY, b.minY + direction.getStepY());
|
||||||
|
double dz = Math.min(a.maxZ, b.maxZ + direction.getStepZ()) - Math.max(a.minZ, b.minZ + direction.getStepZ());
|
||||||
|
if (dx >= -1e-7 && dy >= -1e-7 && dz >= -1e-7
|
||||||
|
&& (dx > 1e-7 && dy > 1e-7 || dx > 1e-7 && dz > 1e-7 || dy > 1e-7 && dz > 1e-7)) touches = true;
|
||||||
|
}
|
||||||
|
if (touches) { supported.add(next); queue.addLast(next); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var at : geometry.keySet()) require(supported.contains(at),
|
||||||
|
label + " has a floating structural/fixture island at " + at + ": " + blocks.get(at));
|
||||||
|
for (var entry : blocks.entrySet()) if (entry.getValue().is(Blocks.LANTERN)) {
|
||||||
|
var anchor = entry.getValue().getValue(BlockStateProperties.HANGING) ? entry.getKey().above() : entry.getKey().below();
|
||||||
|
var state = blocks.getOrDefault(anchor, Blocks.AIR.defaultBlockState());
|
||||||
|
require(state.isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, anchor) && supported.contains(anchor),
|
||||||
|
label + " lamp lacks a full, grounded native support at " + entry.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean adjacentReached(BlockPos pos, Set<BlockPos> reached) {
|
||||||
|
for (Direction direction : HORIZONTAL) if (reached.contains(pos.relative(direction))) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
private static BlockPos rotate(BlockPos pos, int facing) {
|
||||||
|
return switch (facing) {
|
||||||
|
case 0 -> pos; case 1 -> new BlockPos(16 - pos.getZ(), pos.getY(), pos.getX());
|
||||||
|
case 2 -> new BlockPos(16 - pos.getX(), pos.getY(), 16 - pos.getZ());
|
||||||
|
default -> new BlockPos(pos.getZ(), pos.getY(), 16 - pos.getX());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Loot(ResourceKey<LootTable> table, long seed) {}
|
||||||
|
private static final class Capture implements LostCityRenderer.BlockWriter {
|
||||||
|
final Parcel parcel;
|
||||||
|
final Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||||
|
final Map<BlockPos, Loot> loot = new HashMap<>();
|
||||||
|
Capture(Parcel parcel) { this.parcel = parcel; }
|
||||||
|
@Override public void set(int x, int y, int z, BlockState state) {
|
||||||
|
require(x >= 0 && x < 17 && z >= 0 && z < 17 && y >= parcel.minY() - parcel.floorY()
|
||||||
|
&& y <= parcel.maxY() - parcel.floorY(), label(parcel) + " writes outside its saved bounds at " + x + "," + y + "," + z);
|
||||||
|
blocks.put(new BlockPos(x, y, z), state);
|
||||||
|
}
|
||||||
|
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
|
||||||
|
set(x, y, z, Blocks.CHEST.defaultBlockState());
|
||||||
|
loot.put(new BlockPos(x, y, z), new Loot(table, seed));
|
||||||
|
}
|
||||||
|
BlockState block(BlockPos pos) { return blocks.getOrDefault(pos, Blocks.AIR.defaultBlockState()); }
|
||||||
|
}
|
||||||
|
private static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); }
|
||||||
|
}
|
||||||
+92
@@ -0,0 +1,92 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.city;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.IntBinaryOperator;
|
||||||
|
|
||||||
|
/** Natural extensions publish only with a supported player route; the old core/fallback is retained verbatim. */
|
||||||
|
public final class LostCityPlanner21Smoke {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
int extras = 0;
|
||||||
|
for (int size : new int[]{512, 724, 1024}) for (long seed : new long[]{0, 42}) {
|
||||||
|
var terrain = new Terrain((x, z) -> 220);
|
||||||
|
var result = LostCityPlanner21.plan(seed, 0, 0, size, true, terrain); verify(result, terrain);
|
||||||
|
require(!result.core().surface().isEmpty(), "New solver lost the old starter core");
|
||||||
|
require(result.surface().districts().size() == 1 + result.connections().size(), "An isolated district was published");
|
||||||
|
extras += result.connections().size();
|
||||||
|
var repeat = LostCityPlanner21.plan(seed, 0, 0, size, true, new Terrain((x, z) -> 220));
|
||||||
|
require(signature(result).equals(signature(repeat)), "Extension depends on invocation order");
|
||||||
|
}
|
||||||
|
require(extras >= 6, "Flat fixtures do not demonstrate modest connected neighborhoods: " + extras);
|
||||||
|
var baseline = LostCityPlanner21.plan(0, 0, 0, 724, true, new Terrain((x, z) -> 220));
|
||||||
|
var core = baseline.core().surface().districts().getFirst();
|
||||||
|
Terrain cutoff = new Terrain((x, z) -> 220);
|
||||||
|
cutoff.blocked = (x, y, z) -> x < core.minX() - 2 || x > core.maxX() + 2 || z < core.minZ() - 2 || z > core.maxZ() + 2;
|
||||||
|
var stopped = LostCityPlanner21.plan(0, 0, 0, 724, true, cutoff); verify(stopped, cutoff);
|
||||||
|
require(stopped.connections().isEmpty(), "Protected outskirts acquired fictional connecting roads");
|
||||||
|
int northSouthGrades = 0;
|
||||||
|
for (int sign : new int[]{-1, 1}) for (long seed : new long[]{0, 42}) {
|
||||||
|
Terrain slope = new Terrain((x, z) -> 220 + sign * Math.floorDiv(z, 15));
|
||||||
|
northSouthGrades += verify(LostCityPlanner21.plan(seed, 0, 0, 724, true, slope), slope);
|
||||||
|
}
|
||||||
|
require(northSouthGrades > 0, "Slope witnesses never exercise a real north/south stair inside a connecting road");
|
||||||
|
Terrain empty = new Terrain((x, z) -> 0);
|
||||||
|
var fallback = LostCityPlanner21.plan(0, 0, 0, 512, true, empty);
|
||||||
|
require(fallback.connections().isEmpty() && fallback.reservation() != null && fallback.reservation().width() == 49,
|
||||||
|
"Void expanded the architectural fallback into a larger platform");
|
||||||
|
var continent = LostCityPlanner21.plan(0, 0, 0, 256, false, new Terrain((x, z) -> 220));
|
||||||
|
require(continent.connections().isEmpty() && continent.surface() != null, "Root extension policy leaked onto a continent");
|
||||||
|
System.out.println("LostCityPlanner21Smoke: preserved core, modest connected neighborhoods, bounded routes, terrain gaps, native bounds and no enlarged fallback passed.");
|
||||||
|
}
|
||||||
|
private static int verify(LostCityPlanner21.Result result, Terrain terrain) {
|
||||||
|
int northSouthGrades = 0;
|
||||||
|
require(result.columns() <= LostCityPlanner21.MAX_COLUMNS && result.volumeChecks() <= LostCityPlanner21.MAX_VOLUME_CHECKS, "Budget exceeded");
|
||||||
|
require(result.surface().districts().size() <= 3, "Town sprawls into more than three blocks");
|
||||||
|
if (!result.core().surface().isEmpty()) require(result.surface().districts().getFirst() == result.core().surface().districts().getFirst(), "Core was rewritten");
|
||||||
|
var columns = new HashMap<Long, Integer>(); var kinds = new HashMap<Long, LostCityPlan.Kind>();
|
||||||
|
var owners = new HashMap<Long, LostCityPlan.Parcel>();
|
||||||
|
for (var district : result.surface().districts()) for (var p : district.parcels()) {
|
||||||
|
require(p.width() <= 25 && p.depth() <= 25, "Road escaped parcel limits");
|
||||||
|
for (int z = p.minZ(); z <= p.maxZ(); z++) for (int x = p.minX(); x <= p.maxX(); x++) {
|
||||||
|
long key = key(x, z); Integer old = columns.putIfAbsent(key, p.surfaceY(x - p.minX(), z - p.minZ()));
|
||||||
|
require(old == null, "A road and cluster paint the same column twice"); kinds.put(key, p.kind()); owners.put(key, p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var link : result.connections()) {
|
||||||
|
require(link.districtIndex() > 0 && link.districtIndex() < result.surface().districts().size(), "Connection points to an absent district");
|
||||||
|
require(link.path().size() >= 14 && link.path().size() <= 192, "Route lacks the intended natural gap or exceeds reach");
|
||||||
|
for (int i = 0; i < link.path().size(); i++) {
|
||||||
|
var p = link.path().get(i); var key = key(p.x(), p.z());
|
||||||
|
require(columns.get(key) != null && columns.get(key) == p.floorY() && kinds.get(key) == LostCityPlan.Kind.STREET, "Connection centerline has no rendered floor");
|
||||||
|
require(!terrain.blocked.test(p.x(), p.floorY(), p.z()), "Connection crosses protected terrain");
|
||||||
|
if (i > 0) {
|
||||||
|
var a = link.path().get(i - 1);
|
||||||
|
require(Math.abs(p.x() - a.x()) + Math.abs(p.z() - a.z()) == 1 && Math.abs(p.floorY() - a.floorY()) <= 1, "Player route is discontinuous");
|
||||||
|
if (p.floorY() != a.floorY()) {
|
||||||
|
require(owners.get(key) == owners.get(key(a.x(), a.z())),
|
||||||
|
"A road grade crosses a parcel boundary, so the native renderer cannot build its connecting stair");
|
||||||
|
if (p.z() != a.z()) northSouthGrades++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return northSouthGrades;
|
||||||
|
}
|
||||||
|
private static String signature(LostCityPlanner21.Result r) {
|
||||||
|
var result = new StringBuilder().append(r.connections()).append(r.columns()).append(r.volumeChecks()).append(r.diagnostics());
|
||||||
|
for (var d : r.surface().districts()) for (var p : d.parcels()) result.append(p.kind()).append(p.minX()).append(p.minZ()).append(p.floorY()).append(java.util.Arrays.toString(p.surfaceHeights()));
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
private interface Protection { boolean test(int x, int y, int z); }
|
||||||
|
private static final class Terrain implements LostCityPlanner16.Terrain {
|
||||||
|
final IntBinaryOperator height; Protection blocked = (x, y, z) -> false;
|
||||||
|
Terrain(IntBinaryOperator height) { this.height = height; }
|
||||||
|
public boolean owns(int x, int z) { return Math.abs(x) < 600 && Math.abs(z) < 600; }
|
||||||
|
public LostCityPlanner.Ground ground(int x, int z) { int h = height.applyAsInt(x, z); return new LostCityPlanner.Ground(h, h > 0 ? 24 : 0, h > 0, blocked.test(x, h, z)); }
|
||||||
|
public boolean solid(int x, int y, int z) { return y > 0 && y <= height.applyAsInt(x, z); }
|
||||||
|
public boolean protectedAt(int x, int y, int z) { return blocked.test(x, y, z); }
|
||||||
|
}
|
||||||
|
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
|
||||||
|
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
|
||||||
|
}
|
||||||
+286
@@ -0,0 +1,286 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.city;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Kind;
|
||||||
|
import fr.koka.sanctuary.worldgen.city.LostCityPlan.Parcel;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.StairBlock;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.block.state.properties.BedPart;
|
||||||
|
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||||
|
import net.minecraft.world.level.storage.loot.LootTable;
|
||||||
|
|
||||||
|
/** Real block-state geometry, without creating a world: clipping, rotations and walkable floors. */
|
||||||
|
public final class LostCitySurface21Smoke {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
run();
|
||||||
|
System.out.println("LostCitySurface21Smoke: all exterior amenities, supported lamps, basements, contained pools, excavation access, carved park variants, grounded canopies, restrained oak palette and suspended tuff roads passed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Also callable from an already bootstrapped native GameTest. */
|
||||||
|
public static void run() {
|
||||||
|
var failures = new java.util.ArrayList<String>();
|
||||||
|
for (Kind kind : Kind.values()) {
|
||||||
|
if (kind.building()) continue;
|
||||||
|
for (int facing = 0; facing < 4; facing++) {
|
||||||
|
for (long seed : new long[] {0, 42, -61872}) {
|
||||||
|
try {
|
||||||
|
int floors = kind == Kind.HALL ? 2 : 3;
|
||||||
|
int foundation = kind == Kind.POOL || kind == Kind.EXCAVATION ? 8 : 4;
|
||||||
|
Parcel parcel = new Parcel(kind, 0, 0, 17, 17, 170, floors, facing, seed, foundation, new int[0]);
|
||||||
|
Capture result = render(parcel);
|
||||||
|
require(result.blocks.equals(render(parcel).blocks), "Renderer changed for identical saved parcel");
|
||||||
|
require(result.blocks.values().stream().noneMatch(state -> state.is(Blocks.SPAWNER)), "City introduced a spawner");
|
||||||
|
if (kind != Kind.STREET && kind != Kind.BRIDGE) {
|
||||||
|
LostCityInteriors21Smoke.verifyPalette(result.blocks, false, kind + " facing=" + facing);
|
||||||
|
LostCityInteriors21Smoke.verifyGroundedGeometry(result.blocks,
|
||||||
|
kind == Kind.EXCAVATION && foundation >= 8 ? -5 : kind == Kind.POOL && foundation >= 8 ? -4 : 0,
|
||||||
|
kind + " facing=" + facing + " seed=" + seed);
|
||||||
|
}
|
||||||
|
if (kind.building()) {
|
||||||
|
require(result.blocks.values().stream().anyMatch(state -> state.is(Blocks.STONE_BRICKS)), "Missing explicit stone palette");
|
||||||
|
require(result.blocks.values().stream().anyMatch(state -> state.is(Blocks.SMOOTH_STONE)), "Missing smooth stone palette");
|
||||||
|
Set<BlockPos> reached = walk(result, rotate(8, 1, 0, facing));
|
||||||
|
for (int floor = 1; floor <= floors; floor++) {
|
||||||
|
int elevation = floor * 5 + 1;
|
||||||
|
require(reached.stream().anyMatch(pos -> pos.getY() == elevation),
|
||||||
|
kind + " facing " + facing + " cannot reach floor/roof " + floor);
|
||||||
|
}
|
||||||
|
require(result.loot.size() == 1, kind + " must keep exactly one deferred, limited loot chest");
|
||||||
|
}
|
||||||
|
for (var entry : result.blocks.entrySet()) {
|
||||||
|
if (entry.getValue().is(Blocks.LANTERN)) {
|
||||||
|
var support = entry.getValue().getValue(BlockStateProperties.HANGING) ? entry.getKey().above() : entry.getKey().below();
|
||||||
|
require(result.block(support).isCollisionShapeFullBlock(net.minecraft.world.level.EmptyBlockGetter.INSTANCE, support), kind + " lantern lacks full support at " + entry.getKey());
|
||||||
|
}
|
||||||
|
BlockState state = entry.getValue();
|
||||||
|
if (state.is(Blocks.BED.white()) && state.getValue(BlockStateProperties.BED_PART) == BedPart.FOOT) {
|
||||||
|
Direction direction = state.getValue(BlockStateProperties.HORIZONTAL_FACING);
|
||||||
|
BlockState head = result.block(entry.getKey().relative(direction));
|
||||||
|
require(head.is(Blocks.BED.white()) && head.getValue(BlockStateProperties.BED_PART) == BedPart.HEAD,
|
||||||
|
"Rotated bedroom has an incomplete bed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (AssertionError | RuntimeException failure) {
|
||||||
|
failures.add(kind + " facing=" + facing + " seed=" + seed + ": " + failure.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int facing = 0; facing < 4; facing++) {
|
||||||
|
try {
|
||||||
|
Parcel garage = new Parcel(Kind.PARKING, 0, 0, 17, 17, 170, 1, facing, 9, 8, new int[0]);
|
||||||
|
Capture result = render(garage);
|
||||||
|
Set<BlockPos> reached = walk(result, rotate(8, 1, 0, facing));
|
||||||
|
require(reached.contains(rotate(8, -4, 10, facing)), "Underground garage is not reachable from its street ramp");
|
||||||
|
require(result.block(rotate(8, -6, 10, facing)).is(Blocks.STONE_BRICKS), "Garage excavation removed the certified rock below its floor");
|
||||||
|
for (int z = 0; z <= 4; z++) for (int x = 6; x <= 10; x++) for (int y = -5; y < -z; y++) {
|
||||||
|
var below = rotate(x, y, z, facing);
|
||||||
|
require(result.block(below).isCollisionShapeFullBlock(net.minecraft.world.level.EmptyBlockGetter.INSTANCE, below),
|
||||||
|
"Garage ramp is suspended above its floor instead of resting on its masonry wedge");
|
||||||
|
}
|
||||||
|
LostCityInteriors21Smoke.verifyGroundedGeometry(result.blocks, -5, "Underground garage facing=" + facing);
|
||||||
|
var threshold = rotate(8, 0, 0, facing);
|
||||||
|
require(result.block(threshold).isCollisionShapeFullBlock(net.minecraft.world.level.EmptyBlockGetter.INSTANCE, threshold)
|
||||||
|
&& result.block(threshold.above()).isAir() && result.block(threshold.above(2)).isAir(),
|
||||||
|
"Garage entry lost its level, clear ramp threshold");
|
||||||
|
} catch (AssertionError | RuntimeException failure) {
|
||||||
|
failures.add("Underground garage facing=" + facing + ": " + failure.getMessage());
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Parcel pit = new Parcel(Kind.EXCAVATION, 0, 0, 17, 17, 170, 0, facing, 9, 8, new int[0]);
|
||||||
|
Capture excavation = render(pit);
|
||||||
|
require(walk(excavation, rotate(8, 1, 0, facing)).contains(rotate(8, -4, 10, facing)),
|
||||||
|
"Excavation has no walkable stairs from the street");
|
||||||
|
require(excavation.block(rotate(8, -6, 10, facing)).is(Blocks.STONE_BRICKS), "Excavation removed its certified bottom support");
|
||||||
|
LostCityInteriors21Smoke.verifyGroundedGeometry(excavation.blocks, -5, "Excavation frame facing=" + facing);
|
||||||
|
for (int x : new int[]{3, 13}) for (int z : new int[]{10, 13}) {
|
||||||
|
for (int y = -4; y <= 3; y++) require(!excavation.block(rotate(x, y, z, facing)).isAir(),
|
||||||
|
"Excavation roof lacks an unbroken post to its real floor at " + x + "," + y + "," + z);
|
||||||
|
require(excavation.block(rotate(x, 4, z, facing)).is(Blocks.OAK_SLAB)
|
||||||
|
|| excavation.block(rotate(x, 4, z, facing)).is(Blocks.OAK_PLANKS),
|
||||||
|
"Excavation's only canopy is detached from its post");
|
||||||
|
}
|
||||||
|
} catch (AssertionError | RuntimeException failure) {
|
||||||
|
failures.add("Excavation facing=" + facing + ": " + failure.getMessage());
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Parcel pool = new Parcel(Kind.POOL, 0, 0, 17, 17, 170, 0, facing, 9, 8, new int[0]);
|
||||||
|
Capture water = render(pool);
|
||||||
|
sealedPool(water, facing);
|
||||||
|
for (int step = 1; step <= 3; step++) for (int x = 7; x <= 9; x++) {
|
||||||
|
var tread = rotate(x, -step, step + 4, facing);
|
||||||
|
require(water.block(tread).getBlock() instanceof StairBlock
|
||||||
|
&& water.block(tread).getValue(BlockStateProperties.WATERLOGGED),
|
||||||
|
"Pool lost its submerged stair tread");
|
||||||
|
for (int y = -4; y < -step; y++) {
|
||||||
|
var below = rotate(x, y, step + 4, facing);
|
||||||
|
require(water.block(below).isCollisionShapeFullBlock(net.minecraft.world.level.EmptyBlockGetter.INSTANCE, below),
|
||||||
|
"Pool tread is not carried continuously by its sealed basin floor");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require(walk(water, rotate(8, 1, 0, facing)).contains(rotate(8, -2, 7, facing)),
|
||||||
|
"Pool submerged steps cannot be reached from the deck");
|
||||||
|
} catch (AssertionError | RuntimeException failure) {
|
||||||
|
failures.add("Pool facing=" + facing + ": " + failure.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Every public type keeps a usable north approach, and its furniture does not consume the street threshold.
|
||||||
|
for (Kind kind : Kind.values()) if (!kind.building() && kind != Kind.STREET && kind != Kind.BRIDGE)
|
||||||
|
for (int facing = 0; facing < 4; facing++) {
|
||||||
|
try {
|
||||||
|
Capture c = render(new Parcel(kind, 0, 0, 17, 17, 170, 0, facing, 42, 8, new int[0]));
|
||||||
|
var reached = walk(c, rotate(8, 1, 0, facing));
|
||||||
|
require(reached.size() >= 100, kind + " lost its public circulation");
|
||||||
|
for (var chest : c.loot.keySet()) require(java.util.Arrays.stream(new Direction[]{Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST})
|
||||||
|
.anyMatch(d -> reached.contains(chest.relative(d))), kind + " loot is not approachable");
|
||||||
|
} catch (AssertionError | RuntimeException failure) {
|
||||||
|
failures.add(kind + " public access facing=" + facing + ": " + failure.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require(failures.isEmpty(), "City21 surface fixture failures (" + failures.size() + "):\n" + String.join("\n", failures));
|
||||||
|
exportPreviews();
|
||||||
|
Set<Map<BlockPos, BlockState>> parks = new HashSet<>();
|
||||||
|
for (Kind park : new Kind[] {Kind.PARK, Kind.PARK_GARDEN, Kind.PARK_PLAYGROUND, Kind.PARK_MEMORIAL}) {
|
||||||
|
parks.add(render(new Parcel(park, 0, 0, 17, 17, 170, 0, 0, 42, 4, new int[0])).blocks);
|
||||||
|
}
|
||||||
|
require(parks.size() == 4, "Park variants must have distinct geometry");
|
||||||
|
for (Kind kind : new Kind[] {Kind.POOL, Kind.EXCAVATION}) {
|
||||||
|
Capture fallback = render(new Parcel(kind, 0, 0, 17, 17, 170, 0, 0, 42, 4, new int[0]));
|
||||||
|
require(fallback.blocks.values().stream().noneMatch(state -> !state.getFluidState().isEmpty()),
|
||||||
|
"Uncertified ground produced a wet excavation");
|
||||||
|
require(fallback.block(new BlockPos(8, -1, 8)).is(Blocks.STONE_BRICKS), "Uncertified ground was excavated");
|
||||||
|
}
|
||||||
|
int[] heights = new int[17 * 5];
|
||||||
|
for (int z = 0; z < 17; z++) for (int x = 0; x < 5; x++) heights[z * 5 + x] = 170 + z / 4;
|
||||||
|
Parcel bridge = new Parcel(Kind.BRIDGE, 0, 0, 5, 17, 170, 0, 0, 33, 0, heights);
|
||||||
|
Capture result = render(bridge);
|
||||||
|
require(result.blocks.entrySet().stream().noneMatch(entry -> !entry.getValue().isAir()
|
||||||
|
&& entry.getKey().getY() < bridge.surfaceY(entry.getKey().getX(), entry.getKey().getZ()) - bridge.floorY()),
|
||||||
|
"Bridge grew a column below its suspended deck");
|
||||||
|
for (var e : result.blocks.entrySet()) if (e.getValue().is(Blocks.TUFF_BRICK_STAIRS)) {
|
||||||
|
var face = e.getValue().getValue(BlockStateProperties.HORIZONTAL_FACING);
|
||||||
|
var high = e.getKey().relative(face);
|
||||||
|
require(bridge.surfaceY(high.getX(), high.getZ()) - bridge.floorY() == e.getKey().getY(),
|
||||||
|
"Bridge stair fails to meet the higher deck");
|
||||||
|
}
|
||||||
|
Set<BlockPos> reached = walk(result, new BlockPos(2, 1, 0));
|
||||||
|
require(reached.contains(new BlockPos(2, 5, 16)), "Stepped bridge cannot be crossed");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void exportPreviews() {
|
||||||
|
String path = System.getProperty("sanctuary.city21.exteriorPreview");
|
||||||
|
if (path == null) return;
|
||||||
|
var examples = new java.util.ArrayList<Object>();
|
||||||
|
for (Kind kind : Kind.values()) if (!kind.building() && kind != Kind.STREET && kind != Kind.BRIDGE) {
|
||||||
|
var p = new Parcel(kind, 0, 0, 17, 17, 170, 0, 0, 42, 8, new int[0]); var c = render(p);
|
||||||
|
var blocks = new java.util.ArrayList<Object>();
|
||||||
|
c.blocks.forEach((at, state) -> { if (!state.isAir()) blocks.add(java.util.List.of(at.getX(), at.getY(), at.getZ(), state.toString())); });
|
||||||
|
examples.add(Map.of("kind", kind.name(), "seed", p.seed(), "facing", 0, "blocks", blocks));
|
||||||
|
}
|
||||||
|
try { var output = java.nio.file.Path.of(path); if (output.getParent() != null) java.nio.file.Files.createDirectories(output.getParent());
|
||||||
|
java.nio.file.Files.writeString(output, new com.google.gson.Gson().toJson(examples)); }
|
||||||
|
catch (java.io.IOException e) { throw new AssertionError("Cannot write requested geometry preview", e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Capture render(Parcel parcel) {
|
||||||
|
Capture result = new Capture(parcel);
|
||||||
|
LostCitySurface21.render(parcel, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Flood the cells a source can reach horizontally/downward, without permitting water to rise. */
|
||||||
|
private static void sealedPool(Capture result, int facing) {
|
||||||
|
Set<BlockPos> flooded = new HashSet<>();
|
||||||
|
ArrayDeque<BlockPos> queue = new ArrayDeque<>();
|
||||||
|
result.blocks.forEach((pos, state) -> {
|
||||||
|
if (!state.getFluidState().isEmpty()) { flooded.add(pos); queue.add(pos); }
|
||||||
|
});
|
||||||
|
require(flooded.size() > 150, "Pool must contain a real basin of water");
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
BlockPos pos = queue.removeFirst();
|
||||||
|
require(pos.getY() >= -3 && pos.getY() <= -1, "Pool leaks vertically through its floor or coping");
|
||||||
|
BlockPos canonical = rotate(pos.getX(), pos.getY(), pos.getZ(), (4 - facing) % 4);
|
||||||
|
require(canonical.getX() >= 4 && canonical.getX() <= 12 && canonical.getZ() >= 5 && canonical.getZ() <= 12,
|
||||||
|
"Pool leaks outside its continuous side walls");
|
||||||
|
for (Direction direction : new Direction[] {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST, Direction.DOWN}) {
|
||||||
|
BlockPos next = pos.relative(direction);
|
||||||
|
BlockState state = result.block(next);
|
||||||
|
if ((state.isAir() || !state.getFluidState().isEmpty()) && flooded.add(next)) queue.addLast(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Conservative full-block walking with one-block steps and two blocks of clear headroom. */
|
||||||
|
private static Set<BlockPos> walk(Capture result, BlockPos start) {
|
||||||
|
require(standable(result, start), "Entrance is blocked: " + start);
|
||||||
|
Set<BlockPos> seen = new HashSet<>();
|
||||||
|
ArrayDeque<BlockPos> queue = new ArrayDeque<>();
|
||||||
|
queue.add(start);
|
||||||
|
seen.add(start);
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
BlockPos current = queue.removeFirst();
|
||||||
|
for (Direction direction : new Direction[] {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}) {
|
||||||
|
for (int dy = -1; dy <= 1; dy++) {
|
||||||
|
BlockPos next = current.relative(direction).offset(0, dy, 0);
|
||||||
|
if (standable(result, next) && seen.add(next)) queue.addLast(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return seen;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean standable(Capture result, BlockPos pos) {
|
||||||
|
if (pos.getX() < 0 || pos.getZ() < 0 || pos.getX() >= result.parcel.width() || pos.getZ() >= result.parcel.depth()) return false;
|
||||||
|
BlockState support = result.block(pos.below());
|
||||||
|
return (support.isSolid() || support.getBlock() instanceof StairBlock)
|
||||||
|
&& clear(result.block(pos)) && clear(result.block(pos.above()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean clear(BlockState state) { return state.isAir() || state.is(Blocks.WATER); }
|
||||||
|
|
||||||
|
private static BlockPos rotate(int x, int y, int z, int facing) {
|
||||||
|
return switch (facing) {
|
||||||
|
case 0 -> new BlockPos(x, y, z);
|
||||||
|
case 1 -> new BlockPos(16 - z, y, x);
|
||||||
|
case 2 -> new BlockPos(16 - x, y, 16 - z);
|
||||||
|
default -> new BlockPos(z, y, 16 - x);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Capture implements LostCityRenderer.BlockWriter {
|
||||||
|
private final Parcel parcel;
|
||||||
|
private final Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||||
|
private final Map<BlockPos, ResourceKey<LootTable>> loot = new HashMap<>();
|
||||||
|
Capture(Parcel parcel) { this.parcel = parcel; }
|
||||||
|
|
||||||
|
@Override public void set(int x, int y, int z, BlockState state) {
|
||||||
|
require(x >= 0 && z >= 0 && x < parcel.width() && z < parcel.depth()
|
||||||
|
&& y >= parcel.minY() - parcel.floorY() && y <= parcel.maxY() - parcel.floorY(),
|
||||||
|
parcel.kind() + " emitted a block outside its saved bounds at " + x + "," + y + "," + z);
|
||||||
|
blocks.put(new BlockPos(x, y, z), state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long lootSeed) {
|
||||||
|
set(x, y, z, Blocks.CHEST.defaultBlockState());
|
||||||
|
loot.put(new BlockPos(x, y, z), table);
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockState block(BlockPos pos) { return blocks.getOrDefault(pos, Blocks.AIR.defaultBlockState()); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void require(boolean condition, String message) {
|
||||||
|
if (!condition) throw new AssertionError(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
+85
@@ -0,0 +1,85 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.heritage;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.nbt.IntTag;
|
||||||
|
import net.minecraft.nbt.ListTag;
|
||||||
|
|
||||||
|
/** Exact, bounded independent schema21 persistence for catalogue sites and chunk-local abandoned routes. */
|
||||||
|
public final class HeritagePiece21Smoke {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); run();
|
||||||
|
System.out.println("HeritagePiece21Smoke: 14 kinds, 4 facades, every rail profile, missing rails, strict NBT and local native bounds passed.");
|
||||||
|
}
|
||||||
|
public static void run() {
|
||||||
|
for (var kind : HeritagePlan19.Kind.values()) for (int face = 0; face < 4; face++) {
|
||||||
|
int x = -32, z = 40, floor = 160;
|
||||||
|
var entry = new HeritagePlan19.Entry(face == 1 ? x + 17 : face == 3 ? x - 1 : x + 8,
|
||||||
|
floor + 1, face == 2 ? z + 17 : face == 0 ? z - 1 : z + 8, face);
|
||||||
|
var supportList = new ArrayList<HeritagePlan19.Support>();
|
||||||
|
supportList.add(new HeritagePlan19.Support(x + 1, 150, z + 1, 155));
|
||||||
|
var site = new HeritagePlan19.Site("site_" + kind.name(), kind, x, floor, z, 17, 17, 20, face,
|
||||||
|
Long.MIN_VALUE + kind.ordinal(), 4, supportList, entry);
|
||||||
|
supportList.clear();
|
||||||
|
var saved = HeritagePiece21.encodeSite(site);
|
||||||
|
require(HeritagePiece21.decodeSite(saved).equals(site) && HeritagePiece21.encodeSite(HeritagePiece21.decodeSite(saved)).equals(saved), "Site roundtrip changed geometry");
|
||||||
|
require(site.supports().size() == 1 && HeritagePiece21.bounds(site).minY() == 150, "Support input is mutable or excluded from native bounds");
|
||||||
|
require(HeritagePiece21.locator(site).getX() == entry.x() && HeritagePiece21.locator(site).getY() == entry.y()
|
||||||
|
&& HeritagePiece21.locator(site).getZ() == entry.z(), "Locate lost the rotated facade entry");
|
||||||
|
badSite(saved, t -> t.remove("Facing")); badSite(saved, t -> t.putInt("Schema", 17));
|
||||||
|
badSite(saved, t -> t.putString("Type", "ROUTE")); badSite(saved, t -> t.putString("Kind", "FUTURE"));
|
||||||
|
badSite(saved, t -> t.putDouble("FloorY", 160)); badSite(saved, t -> t.putInt("Seed", 1));
|
||||||
|
badSite(saved, t -> t.putInt("Future", 1)); badSite(saved, t -> t.putInt("MinX", Integer.MAX_VALUE));
|
||||||
|
badSite(saved, t -> t.putInt("Width", 18)); badSite(saved, t -> t.putInt("Facing", 4));
|
||||||
|
badSite(saved, t -> t.putInt("FoundationDepth", 0)); badSite(saved, t -> t.putString("Id", "x".repeat(129)));
|
||||||
|
badSite(saved, t -> t.getCompound("Entry").orElseThrow().putInt("X", entry.x() + 1));
|
||||||
|
badSite(saved, t -> t.getCompound("Entry").orElseThrow().putLong("Y", entry.y()));
|
||||||
|
badSite(saved, t -> t.getList("Supports").orElseThrow().getCompound(0).orElseThrow().putInt("MinY", 130));
|
||||||
|
badSite(saved, t -> t.getList("Supports").orElseThrow().getCompound(0).orElseThrow().putInt("X", site.maxX() + 1));
|
||||||
|
badSite(saved, t -> {
|
||||||
|
var list = t.getList("Supports").orElseThrow(); list.add(list.getFirst().copy());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var cells = new ArrayList<HeritagePlan19.Cell>();
|
||||||
|
int index = 0;
|
||||||
|
for (var rail : HeritagePlan19.Rail.values()) {
|
||||||
|
cells.add(new HeritagePlan19.Cell(-32 + index, 150 + index % 3, 32, 2 + index % 4, rail, rail != HeritagePlan19.Rail.NONE && index % 2 == 0)); index++;
|
||||||
|
}
|
||||||
|
var segment = new HeritagePlan19.Segment("freight/part0", "station", "mine", HeritagePlan19.RouteKind.FREIGHT, -2, 2, cells, Long.MAX_VALUE);
|
||||||
|
cells.clear();
|
||||||
|
var landing = new HeritagePlan19.Segment("station/landing", "station", "station", HeritagePlan19.RouteKind.PATH,
|
||||||
|
-2, 2, List.of(new HeritagePlan19.Cell(-32, 150, 32, 2, HeritagePlan19.Rail.NONE, false)), 42);
|
||||||
|
require(HeritagePiece21.decodeSegment(HeritagePiece21.encodeSegment(landing)).equals(landing), "Standalone entry landing lost its site binding");
|
||||||
|
var saved = HeritagePiece21.encodeSegment(segment);
|
||||||
|
require(HeritagePiece21.decodeSegment(saved).equals(segment) && HeritagePiece21.encodeSegment(HeritagePiece21.decodeSegment(saved)).equals(saved), "Route roundtrip changed shapes, supports or deliberate rail gaps");
|
||||||
|
require(segment.cells().size() == HeritagePlan19.Rail.values().length, "External route list mutation changed its saved cells");
|
||||||
|
require(HeritagePiece21.bounds(segment).intersectingChunks().count() == 1, "Segment escaped its one-chunk native start");
|
||||||
|
badRoute(saved, t -> t.putInt("Schema", 20)); badRoute(saved, t -> t.putString("Type", "SITE"));
|
||||||
|
badRoute(saved, t -> t.remove("FromId")); badRoute(saved, t -> t.putString("ToId", "station"));
|
||||||
|
badRoute(saved, t -> t.putString("Id", " ")); badRoute(saved, t -> t.putInt("StartChunkX", -1));
|
||||||
|
badRoute(saved, t -> t.putDouble("Seed", 42)); badRoute(saved, t -> t.put("Cells", new ListTag()));
|
||||||
|
badRoute(saved, t -> cell(t).putInt("RailPresent", 1)); badRoute(saved, t -> cell(t).putByte("RailPresent", (byte) 2));
|
||||||
|
badRoute(saved, t -> cell(t).putString("Rail", "FUTURE")); badRoute(saved, t -> cell(t).putBoolean("RailPresent", true));
|
||||||
|
badRoute(saved, t -> cell(t).putInt("X", -16)); badRoute(saved, t -> cell(t).putInt("FoundationDepth", 13));
|
||||||
|
badRoute(saved, t -> cell(t).putInt("FloorY", 2)); badRoute(saved, t -> cell(t).putInt("Extra", 0));
|
||||||
|
badRoute(saved, t -> { var list = t.getList("Cells").orElseThrow(); list.add(list.getFirst().copy()); });
|
||||||
|
badRoute(saved, t -> { var list = new ListTag(); list.add(IntTag.valueOf(1)); t.put("Cells", list); });
|
||||||
|
badRoute(saved, t -> { var list = t.getList("Cells").orElseThrow(); while (list.size() <= 256) list.add(list.getFirst().copy()); });
|
||||||
|
}
|
||||||
|
private static CompoundTag cell(CompoundTag tag) { return tag.getList("Cells").orElseThrow().getCompound(0).orElseThrow(); }
|
||||||
|
private static void badSite(CompoundTag original, Consumer<CompoundTag> mutation) {
|
||||||
|
var value = original.copy(); mutation.accept(value);
|
||||||
|
try { HeritagePiece21.decodeSite(value); } catch (IllegalArgumentException expected) { return; }
|
||||||
|
throw new AssertionError("Malformed saved site accepted: " + value);
|
||||||
|
}
|
||||||
|
private static void badRoute(CompoundTag original, Consumer<CompoundTag> mutation) {
|
||||||
|
var value = original.copy(); mutation.accept(value);
|
||||||
|
try { HeritagePiece21.decodeSegment(value); } catch (IllegalArgumentException expected) { return; }
|
||||||
|
throw new AssertionError("Malformed saved route accepted: " + value);
|
||||||
|
}
|
||||||
|
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
|
||||||
|
}
|
||||||
+217
@@ -0,0 +1,217 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.heritage;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.*;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.IntBinaryOperator;
|
||||||
|
|
||||||
|
/** Pure geography contract: full catalogue, actual destinations, independent entrances and supported axes. */
|
||||||
|
public final class HeritagePlanner21Smoke {
|
||||||
|
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
|
||||||
|
public static void main(String[] args) {
|
||||||
|
var catalogue = EnumSet.noneOf(Kind.class);
|
||||||
|
int freight = 0, paths = 0, missingRails = 0, islandAxes = 0;
|
||||||
|
for (int diameter : new int[] {512, 724, 1024}) for (long seed : new long[] {0, 1, 42, -1}) {
|
||||||
|
Fixture terrain = new Fixture((x, z) -> 220);
|
||||||
|
var plan = HeritagePlanner21.plan(seed, 0, 0, diameter, terrain);
|
||||||
|
verify(plan, terrain);
|
||||||
|
require(plan.sites().size() >= 8, "Flat starter loses most of its small physical catalogue: " + plan.diagnostics());
|
||||||
|
require(plan.links().size() >= 2, "Flat starter has no useful inter-destination axes");
|
||||||
|
plan.sites().forEach(site -> catalogue.add(site.kind()));
|
||||||
|
freight += (int) plan.links().stream().filter(l -> l.kind() == RouteKind.FREIGHT).count();
|
||||||
|
islandAxes += (int) plan.links().stream().filter(l -> {
|
||||||
|
Point first = l.path().getFirst(), last = l.path().getLast();
|
||||||
|
return Math.max(Math.abs(first.x() - last.x()), Math.abs(first.z() - last.z())) >= diameter / 4;
|
||||||
|
}).count();
|
||||||
|
paths += (int) plan.links().stream().filter(l -> l.kind() == RouteKind.PATH).count();
|
||||||
|
missingRails += (int) plan.segments().stream().flatMap(s -> s.cells().stream())
|
||||||
|
.filter(c -> c.rail() != Rail.NONE && !c.railPresent()).count();
|
||||||
|
var repeat = HeritagePlanner21.plan(seed, 0, 0, diameter, new Fixture((x, z) -> 220));
|
||||||
|
require(plan.sites().equals(repeat.sites()) && plan.links().equals(repeat.links())
|
||||||
|
&& plan.segments().equals(repeat.segments()) && plan.samples() == repeat.samples()
|
||||||
|
&& plan.diagnostics().equals(repeat.diagnostics()), "Planning depends on generation order or mutable state");
|
||||||
|
}
|
||||||
|
require(catalogue.equals(EnumSet.allOf(Kind.class)), "A requested heritage family never appears: " + catalogue);
|
||||||
|
require(islandAxes > 0, "All published networks remain local; no axis reaches island scale");
|
||||||
|
require(freight > 0 && paths > 0 && missingRails > 0, "Both networks and repairable missing rails must be represented");
|
||||||
|
var flat = HeritagePlanner21.plan(0, 0, 0, 724, new Fixture((x, z) -> 220));
|
||||||
|
var oldFlat = HeritagePlanner19.plan(0, 0, 0, 724, new Fixture((x, z) -> 220));
|
||||||
|
var reached21 = new HashSet<String>(); flat.links().forEach(l -> { reached21.add(l.fromId()); reached21.add(l.toId()); });
|
||||||
|
var reached19 = new HashSet<String>(); oldFlat.links().forEach(l -> { reached19.add(l.fromId()); reached19.add(l.toId()); });
|
||||||
|
require(reached21.size() >= reached19.size(), "Proximity graph connects fewer real destinations than19 on its flat witness");
|
||||||
|
// Expensive duplicate edges must not consume the search reserved for a still isolated destination.
|
||||||
|
var components = new HashMap<String, String>();
|
||||||
|
flat.sites().forEach(site -> components.put(site.id(), site.id()));
|
||||||
|
for (var link : flat.links()) {
|
||||||
|
String from = root(components, link.fromId()), to = root(components, link.toId());
|
||||||
|
require(!from.equals(to), "An already connected flat destination receives a redundant budget-consuming cycle");
|
||||||
|
components.put(to, from);
|
||||||
|
}
|
||||||
|
require(flat.links().size() <= 24 && flat.segments().size() <= 512, "Route graph exceeded saved model limits");
|
||||||
|
require(!flat.sites().equals(HeritagePlanner21.plan(1, 0, 0, 724, new Fixture((x, z) -> 220)).sites()), "Seed does not affect sites");
|
||||||
|
|
||||||
|
Fixture wet = new Fixture((x, z) -> 220); wet.protection = (x, y, z) -> true;
|
||||||
|
var blocked = HeritagePlanner21.plan(0, 0, 0, 724, wet); verify(blocked, wet);
|
||||||
|
require(blocked.isEmpty() && blocked.links().isEmpty(), "Protected water or earlier city is overwritten");
|
||||||
|
Fixture voidTerrain = new Fixture((x, z) -> -1);
|
||||||
|
var absent = HeritagePlanner21.plan(0, 0, 0, 724, voidTerrain); verify(absent, voidTerrain);
|
||||||
|
require(absent.isEmpty() && absent.segments().isEmpty(), "Void receives unsupported structures or fictional routes");
|
||||||
|
Fixture thin = new Fixture((x, z) -> 220); thin.rock = (x, y, z) -> y >= 219 && y <= 220;
|
||||||
|
var thinPlan = HeritagePlanner21.plan(0, 0, 0, 724, thin); verify(thinPlan, thin);
|
||||||
|
require(thinPlan.isEmpty(), "Thin shelves receive buildings without anchored footings");
|
||||||
|
|
||||||
|
// Every site keeps its local access even when a protected ring rejects every inter-site alignment.
|
||||||
|
Set<Long> allowed = new HashSet<>();
|
||||||
|
for (var site : flat.sites()) for (int x = site.minX(); x <= site.maxX(); x++) for (int z = site.minZ(); z <= site.maxZ(); z++) allowed.add(key(x, z));
|
||||||
|
for (var segment : flat.segments()) if (segment.fromId().equals(segment.toId())) for (var c : segment.cells()) allowed.add(key(c.x(), c.z()));
|
||||||
|
Fixture isolated = new Fixture((x, z) -> 220); isolated.protection = (x, y, z) -> !allowed.contains(key(x, z));
|
||||||
|
var islands = HeritagePlanner21.plan(0, 0, 0, 724, isolated); verify(islands, isolated);
|
||||||
|
require(!islands.sites().isEmpty(), "Independent access fixture has no retained site");
|
||||||
|
require(islands.sites().stream().anyMatch(site -> islands.links().stream().noneMatch(l -> l.fromId().equals(site.id()) || l.toId().equals(site.id()))),
|
||||||
|
"The isolated destination witness unexpectedly has every long route");
|
||||||
|
|
||||||
|
// Historical landmark feet are read-only and have no mask bypass, even when a route reaches them.
|
||||||
|
Site station = flat.sites().stream().filter(s -> s.kind() == Kind.FREIGHT_STATION).findFirst().orElseThrow();
|
||||||
|
Entry historical = new Entry(station.entry().x() + DX[station.facing()] * 30,
|
||||||
|
221, station.entry().z() + DZ[station.facing()] * 30, (station.facing() + 2) & 3);
|
||||||
|
Fixture landmark = new Fixture((x, z) -> 220);
|
||||||
|
landmark.landmarks = List.of(new Landmark("old_mine", LandmarkKind.MINE, historical));
|
||||||
|
landmark.protection = (x, y, z) -> x == historical.x() && z == historical.z();
|
||||||
|
var linked = HeritagePlanner21.plan(0, 0, 0, 724, landmark); verify(linked, landmark);
|
||||||
|
require(!linked.contains(historical.x(), historical.y(), historical.z()), "A historical endpoint receives route painting");
|
||||||
|
|
||||||
|
Landmark left = new Landmark("left", LandmarkKind.CITY, new Entry(0, 101, 0, 1));
|
||||||
|
Landmark right = new Landmark("right", LandmarkKind.MINE, new Entry(1, 101, 0, 3));
|
||||||
|
Landmark unused = new Landmark("unused", LandmarkKind.UTILITY, new Entry(30, 101, 0, 0));
|
||||||
|
var dependency = new HeritagePlan19(List.of(), List.of(left, right, unused),
|
||||||
|
List.of(new Link("existing_access", "left", "right", RouteKind.PATH, List.of(new Point(0, 100, 0), new Point(1, 100, 0)))),
|
||||||
|
List.of(), 0, 0, Map.of());
|
||||||
|
for (int z = -1; z <= 1; z++) for (int y = 99; y <= 103; y++) {
|
||||||
|
require(dependency.protects(0, y, z) && dependency.intersectsProtection(0, y, z, 0, y, z), "Used historical access lost its rock/headroom dependency");
|
||||||
|
require(!dependency.contains(0, y, z), "Read-only historical access received writing permission");
|
||||||
|
}
|
||||||
|
require(!dependency.protects(30, 101, 0), "Unused historical landmark reserves unrelated terrain");
|
||||||
|
|
||||||
|
// Smooth slopes exercise certified cut/fill and route grade without allowing an unsupported plateau.
|
||||||
|
Fixture sloped = new Fixture((x, z) -> 220 + Math.floorDiv(z, 12));
|
||||||
|
var slopes = HeritagePlanner21.plan(42, 0, 0, 724, sloped); verify(slopes, sloped);
|
||||||
|
require(slopes.sites().size() >= 5, "A modest slope makes the entire catalogue inadmissible");
|
||||||
|
Fixture shifted = new Fixture((x, z) -> 220);
|
||||||
|
var translated = HeritagePlanner21.plan(42, -1024, 512, 724, shifted); verify(translated, shifted);
|
||||||
|
require(!translated.isEmpty(), "Negative chunk coordinates prevent generation");
|
||||||
|
require(flat.diagnostics().size() == Kind.values().length && blocked.diagnostics().size() == Kind.values().length,
|
||||||
|
"Missing kinds have no explicit diagnostic");
|
||||||
|
expectInvalid(() -> new Link("self", "a", "a", RouteKind.PATH, List.of(new Point(0, 100, 0), new Point(1, 100, 0))));
|
||||||
|
expectInvalid(() -> new Segment("self", "a", "a", RouteKind.FREIGHT, 0, 0, List.of(new Cell(0, 100, 0, 1, Rail.NONE, false)), 0));
|
||||||
|
expectInvalid(() -> new Cell(0, 100, 0, 13, Rail.NONE, false));
|
||||||
|
System.out.println("HeritagePlanner21Smoke passed: fourteen families, deterministic placements, persisted isolated entrances, real route destinations, bounded slopes and supported rails, unique chunk-local cells, indexed protection and budgets");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String root(Map<String, String> components, String id) {
|
||||||
|
String parent = components.get(id);
|
||||||
|
while (!parent.equals(id)) { id = parent; parent = components.get(id); }
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
private static void verify(HeritagePlan19 plan, Fixture terrain) {
|
||||||
|
require(plan.samples() == terrain.samples && plan.samples() <= HeritagePlan19.MAX_SAMPLES, "Probe accounting differs from native calls");
|
||||||
|
require(plan.columns() == terrain.columns.size() && plan.columns() <= HeritagePlan19.MAX_COLUMNS, "Column budget exceeded");
|
||||||
|
Map<Long, Cell> cells = new HashMap<>(); Map<String, Entry> destinations = new HashMap<>();
|
||||||
|
Set<Long> footprints = new HashSet<>();
|
||||||
|
for (var site : plan.sites()) {
|
||||||
|
destinations.put(site.id(), site.entry());
|
||||||
|
require(site.foundationDepth() <= 8 && site.maxY() <= 380, "Architecture exceeds its bounded envelope");
|
||||||
|
require(Math.abs((site.minX() >> 4) - site.startChunkX()) <= 7 && Math.abs((site.maxX() >> 4) - site.startChunkX()) <= 7
|
||||||
|
&& Math.abs((site.minZ() >> 4) - site.startChunkZ()) <= 7 && Math.abs((site.maxZ() >> 4) - site.startChunkZ()) <= 7, "Site exceeds native reference radius");
|
||||||
|
for (int z = site.minZ(); z <= site.maxZ(); z++) for (int x = site.minX(); x <= site.maxX(); x++) {
|
||||||
|
require(footprints.add(key(x, z)), "Two buildings overlap");
|
||||||
|
require(terrain.height.applyAsInt(x, z) - site.floorY() <= HeritagePlanner21.MAX_SITE_CUT, "Site cuts an excessive mound");
|
||||||
|
for (int y = site.minY(); y <= site.maxY(); y++) require(!terrain.protection.test(x, y, z), "Architecture intersects protected water/old structure");
|
||||||
|
require(plan.contains(x, site.floorY(), z) && plan.protects(x, site.maxY(), z)
|
||||||
|
&& plan.intersectsProtection(x, site.maxY(), z, x, site.maxY(), z), "Site is absent from indexed protection");
|
||||||
|
}
|
||||||
|
for (int dx : new int[] {1, 8, 15}) for (int dz : new int[] {1, 8, 15}) {
|
||||||
|
int x = site.minX() + dx, z = site.minZ() + dz;
|
||||||
|
var support = site.supports().stream().filter(s -> s.x() == x && s.z() == z).findFirst();
|
||||||
|
int rock = support.isPresent() ? support.get().minY() - 1 : site.minY() - 1;
|
||||||
|
require(terrain.rock.test(x, rock, z) && terrain.rock.test(x, rock - 1, z), "Site has no two-block rock bearing");
|
||||||
|
}
|
||||||
|
for (var support : site.supports()) {
|
||||||
|
require(support.maxY() - support.minY() < 12 && support.maxY() == site.minY() - 1, "Unbounded/disconnected architectural pier");
|
||||||
|
require(plan.protects(support.x(), support.minY() - 2, support.z()), "Pier bearing exposed to decoration");
|
||||||
|
for (int y = support.minY(); y <= support.maxY(); y++) require(!terrain.protection.test(support.x(), y, support.z()), "Pier overwrites protected terrain");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plan.landmarks().forEach(l -> destinations.put(l.id(), l.entry()));
|
||||||
|
for (var segment : plan.segments()) {
|
||||||
|
require(destinations.containsKey(segment.fromId()) && destinations.containsKey(segment.toId()), "Segment has a fictional destination");
|
||||||
|
for (Cell cell : segment.cells()) {
|
||||||
|
require(cells.put(key(cell.x(), cell.z()), cell) == null, "Two segments write the same column");
|
||||||
|
require(!footprints.contains(key(cell.x(), cell.z())), "Route crosses a building envelope");
|
||||||
|
require((cell.x() >> 4) == segment.startChunkX() && (cell.z() >> 4) == segment.startChunkZ(), "Route piece is not chunk-local");
|
||||||
|
require(terrain.rock.test(cell.x(), cell.minY() - 1, cell.z()), "Route ballast floats without bearing");
|
||||||
|
require(terrain.height.applyAsInt(cell.x(), cell.z()) - cell.floorY() <= HeritagePlanner21.MAX_ROUTE_CUT, "Route makes excessive terrain cuts");
|
||||||
|
require(cell.floorY() - terrain.height.applyAsInt(cell.x(), cell.z()) <= HeritagePlanner21.MAX_ROUTE_FILL,
|
||||||
|
"Route uses an unbounded fill to force a connection");
|
||||||
|
for (int y = cell.minY(); y <= cell.maxY(); y++) require(!terrain.protection.test(cell.x(), y, cell.z()), "Route overwrites protected water/old structure");
|
||||||
|
require(plan.contains(cell.x(), cell.floorY(), cell.z()) && plan.protects(cell.x(), cell.minY() - 1, cell.z())
|
||||||
|
&& plan.intersectsProtection(cell.x(), cell.minY() - 1, cell.z(), cell.x(), cell.minY() - 1, cell.z()), "Route/ballast missing from protection index");
|
||||||
|
require(!cell.railPresent() || cell.rail() != Rail.NONE, "Present rail has no saved geometry");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var site : plan.sites()) {
|
||||||
|
Entry e = site.entry();
|
||||||
|
for (int cross = -1; cross <= 1; cross++) {
|
||||||
|
Cell cell = cells.get(key(e.x() + DZ[e.facing()] * cross, e.z() - DX[e.facing()] * cross));
|
||||||
|
require(cell != null && cell.floorY() == e.y() - 1, "Unlinked entrance has no independently painted three-wide landing");
|
||||||
|
}
|
||||||
|
// A five-row access profile must meet natural terrain; never stop on an isolated retaining ledge.
|
||||||
|
var reached = new HashSet<Long>(); var queue = new ArrayDeque<Long>();
|
||||||
|
queue.add(key(e.x(), e.z())); reached.add(key(e.x(), e.z())); boolean meetsGround = false;
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
long key = queue.remove(); Cell c = cells.get(key);
|
||||||
|
if (Math.abs(c.x() - e.x()) + Math.abs(c.z() - e.z()) >= 4
|
||||||
|
&& Math.abs(c.floorY() - terrain.height.applyAsInt(c.x(), c.z())) <= 1) meetsGround = true;
|
||||||
|
for (int d = 0; d < 4; d++) {
|
||||||
|
long next = key(c.x() + DX[d], c.z() + DZ[d]); Cell n = cells.get(next);
|
||||||
|
if (n != null && Math.abs(n.floorY() - c.floorY()) <= 1 && reached.add(next)) queue.add(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require(meetsGround, "An entrance platform has no traversable return to the sampled terrain");
|
||||||
|
}
|
||||||
|
for (var link : plan.links()) {
|
||||||
|
Entry from = destinations.get(link.fromId()), to = destinations.get(link.toId());
|
||||||
|
require(link.path().getFirst().equals(new Point(from.x(), from.y() - 1, from.z()))
|
||||||
|
&& link.path().getLast().equals(new Point(to.x(), to.y() - 1, to.z())), "Route does not actually join its promised entrances");
|
||||||
|
for (int i = 0; i < link.path().size(); i++) {
|
||||||
|
Point p = link.path().get(i); Cell c = cells.get(key(p.x(), p.z()));
|
||||||
|
boolean historical = plan.landmarks().stream().anyMatch(l -> l.entry().x() == p.x() && l.entry().z() == p.z() && l.entry().y() == p.floorY() + 1);
|
||||||
|
require(c != null && c.floorY() == p.floorY() || historical, "A complete route contains an unpainted gap");
|
||||||
|
if (i > 0) {
|
||||||
|
Point a = link.path().get(i - 1);
|
||||||
|
require(Math.abs(a.x() - p.x()) + Math.abs(a.z() - p.z()) == 1 && Math.abs(a.floorY() - p.floorY()) <= 1, "Route has a jump or excessive grade");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require(!plan.protects(100_000, 200, 100_000) && !plan.intersectsProtection(100_000, 0, 100_000, 100_002, 383, 100_002), "Protection escaped accepted constructions");
|
||||||
|
}
|
||||||
|
@FunctionalInterface private interface Volume { boolean test(int x, int y, int z); }
|
||||||
|
private static final class Fixture implements HeritagePlanner21.Terrain {
|
||||||
|
final IntBinaryOperator height; final Set<Long> columns = new HashSet<>(); int samples;
|
||||||
|
Volume protection = (x, y, z) -> false;
|
||||||
|
Volume rock; List<Landmark> landmarks = List.of();
|
||||||
|
Fixture(IntBinaryOperator height) { this.height = height; rock = (x, y, z) -> y >= 8 && y <= height.applyAsInt(x, z); }
|
||||||
|
public boolean owns(int x, int z) { return true; }
|
||||||
|
public int surfaceY(int x, int z) { samples++; columns.add(key(x, z)); return height.applyAsInt(x, z); }
|
||||||
|
public boolean solid(int x, int y, int z) { samples++; return rock.test(x, y, z); }
|
||||||
|
public boolean protectedAt(int x, int y, int z) { samples++; return protection.test(x, y, z); }
|
||||||
|
public List<Landmark> landmarks() { return landmarks; }
|
||||||
|
}
|
||||||
|
private static void expectInvalid(Runnable action) { boolean rejected = false; try { action.run(); } catch (IllegalArgumentException expected) { rejected = true; } require(rejected, "Invalid saved geometry was accepted"); }
|
||||||
|
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
|
||||||
|
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
|
||||||
|
}
|
||||||
+506
@@ -0,0 +1,506 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.heritage;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.world.level.EmptyBlockGetter;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.DoorBlock;
|
||||||
|
import net.minecraft.world.level.block.Rotation;
|
||||||
|
import net.minecraft.world.level.block.TrapDoorBlock;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.block.state.properties.BedPart;
|
||||||
|
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||||
|
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
|
||||||
|
import net.minecraft.world.level.block.state.properties.Half;
|
||||||
|
import net.minecraft.world.level.storage.loot.LootTable;
|
||||||
|
import net.minecraft.world.phys.AABB;
|
||||||
|
|
||||||
|
/** Native shapes and final geometry: distinguish real rooms, reachable secrets and supported routes. */
|
||||||
|
public final class HeritageRenderer21Smoke {
|
||||||
|
private HeritageRenderer21Smoke() {}
|
||||||
|
private static final Direction[] DIRECTIONS = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); run();
|
||||||
|
System.out.println("HeritageRenderer21Smoke passed: fourteen restrained generation21 palettes, four rotations, supported single roofs and daylight wells, reachable original targets, retained loot and connected block details");
|
||||||
|
}
|
||||||
|
public static void run() {
|
||||||
|
Set<Set<BlockPos>> silhouettes = new HashSet<>();
|
||||||
|
for (var kind : HeritagePlan19.Kind.values()) for (long seed : new long[] {0, 43}) {
|
||||||
|
var canonical = render(site(kind, 0, seed));
|
||||||
|
if (seed == 0) {
|
||||||
|
export(canonical);
|
||||||
|
Set<BlockPos> shape = new HashSet<>(); canonical.blocks.forEach((p, b) -> { if (p.getY() > 0 && !b.isAir()) shape.add(p); });
|
||||||
|
require(silhouettes.add(shape), kind + " only repaints another catalogue building");
|
||||||
|
}
|
||||||
|
for (int facing = 0; facing < 4; facing++) {
|
||||||
|
var c = facing == 0 ? canonical : render(site(kind, facing, seed));
|
||||||
|
var repeat = render(c.site);
|
||||||
|
require(c.blocks.equals(repeat.blocks) && c.loot.equals(repeat.loot), label(c) + " is not deterministic");
|
||||||
|
rotate(canonical, c);
|
||||||
|
var targets = HeritageRenderer21.targets(c.site);
|
||||||
|
var reached = walk(c, at(targets.getFirst()));
|
||||||
|
for (var target : targets) require(reached.contains(at(target)), label(c) + " cannot reach " + target.name() + " at " + at(target));
|
||||||
|
require(targets.stream().anyMatch(t -> t.name().equals("secondary_entry")), "Missing secondary physical approach");
|
||||||
|
var secondary = targets.stream().filter(t -> t.name().equals("secondary_entry")).findFirst().orElseThrow();
|
||||||
|
Set<BlockPos> returnPath = walk(c, at(secondary));
|
||||||
|
require(returnPath.contains(at(targets.getFirst())), label(c) + " service exit cannot return through the certified courtyard");
|
||||||
|
require(c.loot.size() == 1, label(c) + " must have one shared, deferred discovery chest");
|
||||||
|
var chest = c.loot.entrySet().iterator().next();
|
||||||
|
require(chest.getValue().table().identifier().toString().equals("sanctuary:chests/heritage19/" + kind.name().toLowerCase(java.util.Locale.ROOT)), "Loot table is not generation19-specific");
|
||||||
|
require(approachable(chest.getKey(), reached), label(c) + " hidden chest cannot be opened from a reachable standing cell");
|
||||||
|
for (var t : targets) if (t.name().equals("loot_approach")) require(at(t).distManhattan(chest.getKey()) == 1, "Secret witness is not next to the actual chest");
|
||||||
|
furnishings(c, reached);
|
||||||
|
refugeShaft(c, reached);
|
||||||
|
railConnection(c);
|
||||||
|
fluids(c);
|
||||||
|
require(c.blocks.values().stream().noneMatch(b -> b.is(Blocks.SPAWNER) || b.is(Blocks.LAVA)), "Heritage added an unrequested spawner or lava");
|
||||||
|
identity(c);
|
||||||
|
architecture21(c);
|
||||||
|
daylightAndCrown21(c);
|
||||||
|
roofBearings21(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require(silhouettes.size() == 14, "Catalogue does not contain fourteen physical architectures");
|
||||||
|
routes(); chunkPartition(); materials21();
|
||||||
|
}
|
||||||
|
private static HeritagePlan19.Site site(HeritagePlan19.Kind kind, int facing, long seed) {
|
||||||
|
int height = switch (kind) {
|
||||||
|
case COMMUNAL_DEPOT, VOID_DOCK, CONSTRUCTION_REFUGE -> 14;
|
||||||
|
case EXPEDITION_OFFICE, IMPOSSIBLE_SERVICE -> 12;
|
||||||
|
case OBSERVATORY -> 20; case PUMP_STATION -> 18; case RADIO_RELAY -> 22;
|
||||||
|
default -> 10;
|
||||||
|
};
|
||||||
|
int x = -43, y = 180, z = 59;
|
||||||
|
var entry = new HeritagePlan19.Entry(facing == 1 ? x + 17 : facing == 3 ? x - 1 : x + 8, y + 1,
|
||||||
|
facing == 2 ? z + 17 : facing == 0 ? z - 1 : z + 8, facing);
|
||||||
|
return new HeritagePlan19.Site("test", kind, x, y, z, 17, 17, height, facing, seed, 3,
|
||||||
|
List.of(new HeritagePlan19.Support(x, y - 7, z, y - 4)), entry);
|
||||||
|
}
|
||||||
|
private static String label(Capture c) { return c.site.kind() + " facing=" + c.site.facing() + " seed=" + c.site.seed(); }
|
||||||
|
private static Capture render(HeritagePlan19.Site site) { var c = new Capture(site); HeritageRenderer21.render(site, c); return c; }
|
||||||
|
private static BlockPos at(HeritageRenderer21.Target t) { return new BlockPos(t.x(), t.y(), t.z()); }
|
||||||
|
private static BlockPos rotate(BlockPos p, int facing) {
|
||||||
|
return switch (facing) { case 1 -> new BlockPos(16 - p.getZ(), p.getY(), p.getX());
|
||||||
|
case 2 -> new BlockPos(16 - p.getX(), p.getY(), 16 - p.getZ());
|
||||||
|
case 3 -> new BlockPos(p.getZ(), p.getY(), 16 - p.getX()); default -> p; };
|
||||||
|
}
|
||||||
|
private static void rotate(Capture base, Capture other) {
|
||||||
|
int facing = other.site.facing();
|
||||||
|
Rotation rotation = switch (facing) { case 1 -> Rotation.CLOCKWISE_90; case 2 -> Rotation.CLOCKWISE_180;
|
||||||
|
case 3 -> Rotation.COUNTERCLOCKWISE_90; default -> Rotation.NONE; };
|
||||||
|
for (var e : base.blocks.entrySet()) {
|
||||||
|
// Saved footings are already in world coordinates; they do not rotate with the facade.
|
||||||
|
if (e.getKey().getY() < -base.site.foundationDepth() || base.loot.containsKey(e.getKey())) continue;
|
||||||
|
require(other.block(rotate(e.getKey(), facing)).equals(e.getValue().rotate(rotation)), label(other) + " rotates shape and block state differently at " + e.getKey());
|
||||||
|
}
|
||||||
|
for (var e : base.loot.entrySet()) require(e.getValue().equals(other.loot.get(rotate(e.getKey(), facing))), "Rotating a site changes its deferred treasure");
|
||||||
|
}
|
||||||
|
private static void furnishings(Capture c, Set<BlockPos> reached) {
|
||||||
|
for (var e : c.blocks.entrySet()) {
|
||||||
|
var p = e.getKey(); var b = e.getValue();
|
||||||
|
if (b.getBlock() instanceof DoorBlock) {
|
||||||
|
var half = b.getValue(BlockStateProperties.DOUBLE_BLOCK_HALF);
|
||||||
|
var partner = half == DoubleBlockHalf.LOWER ? p.above() : p.below();
|
||||||
|
require(c.block(partner).equals(b.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF,
|
||||||
|
half == DoubleBlockHalf.LOWER ? DoubleBlockHalf.UPPER : DoubleBlockHalf.LOWER)), label(c) + " has a truncated door at " + p);
|
||||||
|
if (half == DoubleBlockHalf.LOWER) {
|
||||||
|
require(full(c.block(p.below()), p.below()), "Door has no full threshold");
|
||||||
|
Direction normal = b.getValue(BlockStateProperties.HORIZONTAL_FACING);
|
||||||
|
require(reached.contains(p.relative(normal)) && reached.contains(p.relative(normal.getOpposite())),
|
||||||
|
label(c) + " door does not connect two usable sides at " + p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (b.is(Blocks.LADDER)) require(full(c.block(p.relative(b.getValue(BlockStateProperties.HORIZONTAL_FACING).getOpposite())), p),
|
||||||
|
label(c) + " floating ladder at " + p);
|
||||||
|
if (b.is(Blocks.LANTERN)) require(b.getValue(BlockStateProperties.HANGING) && full(c.block(p.above()), p.above()),
|
||||||
|
label(c) + " lantern does not hang from a complete ceiling at " + p);
|
||||||
|
if (b.is(Blocks.LEVER) || b.is(Blocks.GRINDSTONE)) require(full(c.block(p.below()), p.below()), "Usable fixture has no full support");
|
||||||
|
if ((b.is(Blocks.OAK_STAIRS) || b.is(Blocks.BIRCH_STAIRS)) && p.getY() == 1) {
|
||||||
|
var front = p.relative(b.getValue(BlockStateProperties.HORIZONTAL_FACING).getOpposite());
|
||||||
|
require(reached.contains(front), label(c) + " seat faces an obstruction at " + p);
|
||||||
|
}
|
||||||
|
if (b.hasProperty(BlockStateProperties.BED_PART)) {
|
||||||
|
Direction direction = b.getValue(BlockStateProperties.HORIZONTAL_FACING);
|
||||||
|
boolean head = b.getValue(BlockStateProperties.BED_PART) == BedPart.HEAD;
|
||||||
|
var partner = p.relative(head ? direction.getOpposite() : direction);
|
||||||
|
require(c.block(partner).equals(b.setValue(BlockStateProperties.BED_PART, head ? BedPart.FOOT : BedPart.HEAD)), "Bed halves disagree");
|
||||||
|
if (head) {
|
||||||
|
for (int y = 0; y < 3; y++) require(full(c.block(p.relative(direction).above(y)), p), "Bed head is not against its real wall");
|
||||||
|
boolean side = false;
|
||||||
|
for (Direction d : new Direction[] {direction.getClockWise(), direction.getCounterClockWise()})
|
||||||
|
side |= reached.contains(p.relative(d)) && reached.contains(partner.relative(d));
|
||||||
|
require(side, "Bed has no reachable full lateral approach");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (b.is(Blocks.CRAFTING_TABLE) || b.is(Blocks.CARTOGRAPHY_TABLE) || b.is(Blocks.FURNACE)
|
||||||
|
|| b.is(Blocks.GRINDSTONE) || b.is(Blocks.LECTERN))
|
||||||
|
require(approachable(p, reached), label(c) + " functional furnishing cannot be approached at " + p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static boolean full(BlockState b, BlockPos p) {
|
||||||
|
return b.getFluidState().isEmpty() && b.getCollisionShape(EmptyBlockGetter.INSTANCE, p).toAabbs().stream()
|
||||||
|
.anyMatch(a -> a.minX == 0 && a.minY == 0 && a.minZ == 0 && a.maxX == 1 && a.maxY == 1 && a.maxZ == 1);
|
||||||
|
}
|
||||||
|
private static boolean approachable(BlockPos p, Set<BlockPos> reached) {
|
||||||
|
for (var d : DIRECTIONS) if (reached.contains(p.relative(d))) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
private static boolean clear(Capture c, BlockPos p) {
|
||||||
|
var body = new AABB(p.getX() + .2, p.getY(), p.getZ() + .2, p.getX() + .8, p.getY() + 1.8, p.getZ() + .8);
|
||||||
|
for (var at : new BlockPos[] {p, p.above()}) {
|
||||||
|
var b = opened(c.block(at)); if (!b.getFluidState().isEmpty()) return false;
|
||||||
|
for (var shape : b.getCollisionShape(EmptyBlockGetter.INSTANCE, at).toAabbs())
|
||||||
|
if (shape.move(at.getX(), at.getY(), at.getZ()).intersects(body)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
private static boolean stand(Capture c, BlockPos p) {
|
||||||
|
if (p.getX() < 0 || p.getX() > 16 || p.getZ() < 0 || p.getZ() > 16
|
||||||
|
|| p.getY() < -c.site.foundationDepth() + 1 || p.getY() >= c.site.height() || !clear(c, p)) return false;
|
||||||
|
if (climb(c.block(p)) || climb(c.block(p.below()))) return true;
|
||||||
|
return opened(c.block(p.below())).getCollisionShape(EmptyBlockGetter.INSTANCE, p.below()).toAabbs().stream()
|
||||||
|
.anyMatch(a -> a.maxY >= .5 && a.minX <= .5 && a.maxX >= .5 && a.minZ <= .5 && a.maxZ >= .5);
|
||||||
|
}
|
||||||
|
private static BlockState opened(BlockState state) {
|
||||||
|
return state.getBlock() instanceof DoorBlock || state.getBlock() instanceof TrapDoorBlock
|
||||||
|
? state.setValue(BlockStateProperties.OPEN, true) : state;
|
||||||
|
}
|
||||||
|
private static void refugeShaft(Capture c, Set<BlockPos> reached) {
|
||||||
|
if (c.site.kind() != HeritagePlan19.Kind.CONSTRUCTION_REFUGE) return;
|
||||||
|
var hatch = rotate(new BlockPos(10, 5, 11), c.site.facing());
|
||||||
|
require(c.block(hatch).is(Blocks.OAK_TRAPDOOR)
|
||||||
|
&& c.block(hatch).getValue(BlockStateProperties.HALF) == Half.BOTTOM,
|
||||||
|
label(c) + " hatch is not at the climbable upper lip");
|
||||||
|
var direction = c.block(hatch).getValue(BlockStateProperties.HORIZONTAL_FACING);
|
||||||
|
require(stand(c, hatch) && reached.contains(hatch), label(c) + " open hatch has no reachable climbable support");
|
||||||
|
for (int y = 1; y <= 4; y++) {
|
||||||
|
var rung = new BlockPos(hatch.getX(), y, hatch.getZ());
|
||||||
|
require(c.block(rung).is(Blocks.LADDER)
|
||||||
|
&& c.block(rung).getValue(BlockStateProperties.HORIZONTAL_FACING) == direction,
|
||||||
|
label(c) + " hatch descent loses a matching ladder at " + rung);
|
||||||
|
require(stand(c, rung) && reached.contains(rung) && clear(c, rung.above()),
|
||||||
|
label(c) + " hatch descent cannot traverse vertically at " + rung);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static boolean climb(BlockState b) { return b.is(Blocks.LADDER); }
|
||||||
|
private static Set<BlockPos> walk(Capture c, BlockPos start) {
|
||||||
|
require(stand(c, start), label(c) + " starts without a standing surface at " + start);
|
||||||
|
Set<BlockPos> seen = new HashSet<>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
var p = queue.removeFirst();
|
||||||
|
for (var d : DIRECTIONS) for (int dy : new int[] {0, 1, -1}) {
|
||||||
|
var q = p.relative(d).offset(0, dy, 0);
|
||||||
|
if (dy > 0 && !clear(c, p.above())) continue;
|
||||||
|
if (stand(c, q) && seen.add(q)) queue.add(q);
|
||||||
|
}
|
||||||
|
for (int dy : new int[] {-1, 1}) {
|
||||||
|
var q = p.offset(0, dy, 0);
|
||||||
|
if ((climb(c.block(p)) || climb(c.block(q)) || climb(c.block(p.below()))) && stand(c, q) && seen.add(q)) queue.add(q);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return seen;
|
||||||
|
}
|
||||||
|
private static void fluids(Capture c) {
|
||||||
|
Set<BlockPos> water = new HashSet<>(); c.blocks.forEach((p, b) -> { if (!b.getFluidState().isEmpty()) water.add(p); });
|
||||||
|
int expected = c.site.kind() == HeritagePlan19.Kind.PUMP_STATION ? 60 : c.site.kind() == HeritagePlan19.Kind.GREENHOUSE ? 2 : 0;
|
||||||
|
require(water.size() == expected, label(c) + " has unexpected water quantity: " + water.size());
|
||||||
|
for (var p : water) {
|
||||||
|
require(c.block(p).is(Blocks.WATER), "Only explicitly contained water is allowed");
|
||||||
|
for (var d : new Direction[] {Direction.DOWN, Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}) {
|
||||||
|
var q = p.relative(d);
|
||||||
|
require(water.contains(q) || !c.block(q).isAir(), label(c) + " lets tank/irrigation water escape into air at " + q);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static void identity(Capture c) {
|
||||||
|
switch (c.site.kind()) {
|
||||||
|
case FREIGHT_STATION -> { has(c, Blocks.RAIL); has(c, Blocks.LEVER); }
|
||||||
|
case POST_OFFICE -> { has(c, Blocks.BARREL); has(c, Blocks.CARTOGRAPHY_TABLE); }
|
||||||
|
case COMMUNAL_DEPOT -> { has(c, Blocks.BARREL); has(c, Blocks.LECTERN); }
|
||||||
|
case EXPEDITION_OFFICE -> { has(c, Blocks.CARTOGRAPHY_TABLE); has(c, Blocks.BOOKSHELF); }
|
||||||
|
case OBSERVATORY -> { has(c, Blocks.CARTOGRAPHY_TABLE); has(c, Blocks.IRON_BARS); }
|
||||||
|
case RAIL_WORKSHOP -> { has(c, Blocks.GRINDSTONE); has(c, Blocks.RAIL); has(c, Blocks.FURNACE); }
|
||||||
|
case PUMP_STATION -> { has(c, Blocks.DISPENSER); has(c, Blocks.WATER); }
|
||||||
|
case SUBSTATION -> { has(c, Blocks.LEVER); has(c, Blocks.IRON_BARS); }
|
||||||
|
case GREENHOUSE -> { has(c, Blocks.WHEAT); has(c, Blocks.FARMLAND); has(c, Blocks.COMPOSTER); }
|
||||||
|
case VOID_DOCK -> { has(c, Blocks.IRON_CHAIN); has(c, Blocks.STRIPPED_OAK_LOG); }
|
||||||
|
case RADIO_RELAY -> { has(c, Blocks.BED.white()); has(c, Blocks.IRON_BARS); }
|
||||||
|
case PHOTO_CLUB -> { has(c, Blocks.WATER_CAULDRON); has(c, Blocks.IRON_CHAIN); }
|
||||||
|
case IMPOSSIBLE_SERVICE -> { has(c, Blocks.CALCITE); has(c, Blocks.WATER_CAULDRON); }
|
||||||
|
case CONSTRUCTION_REFUGE -> { has(c, Blocks.BED.white()); has(c, Blocks.LADDER); has(c, Blocks.OAK_TRAPDOOR); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static void architecture21(Capture c) {
|
||||||
|
if (c.site.kind() != HeritagePlan19.Kind.GREENHOUSE) has(c, Blocks.SMOOTH_STONE);
|
||||||
|
has(c, Blocks.STONE_BRICKS);
|
||||||
|
for (var e : c.blocks.entrySet()) {
|
||||||
|
String name = net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(e.getValue().getBlock()).getPath();
|
||||||
|
require(e.getKey().getY() <= 0 || !name.contains("tuff"), label(c) + " uses tuff above ground: " + e.getKey());
|
||||||
|
boolean greenhouse = c.site.kind() == HeritagePlan19.Kind.GREENHOUSE;
|
||||||
|
for (String wood : List.of("dark_oak", "oak", "birch", "spruce", "jungle", "acacia", "mangrove", "cherry", "pale_oak")) {
|
||||||
|
boolean woodBlock = name.startsWith(wood + "_") || name.startsWith("stripped_" + wood + "_");
|
||||||
|
require(!woodBlock || wood.equals(greenhouse ? "birch" : "oak"),
|
||||||
|
label(c) + " mixes timber species with " + name + " at " + e.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The working destinations keep exactly their historical entrances and service witnesses.
|
||||||
|
var previous = HeritageRenderer19.targets(c.site).stream().map(t -> t.name() + ":" + t.x() + ":" + t.y() + ":" + t.z()).toList();
|
||||||
|
var current = HeritageRenderer21.targets(c.site).stream().map(t -> t.name() + ":" + t.x() + ":" + t.y() + ":" + t.z()).toList();
|
||||||
|
require(previous.equals(current), label(c) + " changes the saved functional access contract");
|
||||||
|
for (var e : c.blocks.entrySet()) if (e.getKey().getY() < 0 && e.getKey().getY() >= -c.site.foundationDepth()) {
|
||||||
|
// Workshop/trench cuts are deliberate; the exterior foundation always remains brick masonry.
|
||||||
|
int x = e.getKey().getX(), z = e.getKey().getZ();
|
||||||
|
if (x == 0 || x == 16 || z == 0 || z == 16) require(e.getValue().is(Blocks.STONE_BRICKS), "Visible foundations lost the stone-brick construction");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static void daylightAndCrown21(Capture c) {
|
||||||
|
if (c.site.kind() == HeritagePlan19.Kind.FREIGHT_STATION) {
|
||||||
|
for (int x : new int[] {7, 9}) for (int z : new int[] {6, 8}) {
|
||||||
|
require(c.block(rotate(new BlockPos(x, 9, z), c.site.facing())).is(Blocks.GLASS),
|
||||||
|
label(c) + " passenger skylight is open to the sky instead of glazed");
|
||||||
|
clearLightWell(c, x, z, 6, c.site.height());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (c.site.kind() == HeritagePlan19.Kind.COMMUNAL_DEPOT) {
|
||||||
|
for (int x : new int[] {5, 9}) for (int z : new int[] {3, 7, 11}) {
|
||||||
|
require(c.block(rotate(new BlockPos(x, 11, z), c.site.facing())).is(Blocks.GLASS),
|
||||||
|
label(c) + " warehouse roof light is no longer glazed");
|
||||||
|
require(c.block(rotate(new BlockPos(x, 9, z), c.site.facing())).isAir(),
|
||||||
|
label(c) + " warehouse kept its obsolete flat ceiling beneath the new roof");
|
||||||
|
clearLightWell(c, x, z, 9, c.site.height());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (c.site.kind() == HeritagePlan19.Kind.RAIL_WORKSHOP || c.site.kind() == HeritagePlan19.Kind.GREENHOUSE) {
|
||||||
|
int x = c.site.kind() == HeritagePlan19.Kind.GREENHOUSE ? 5 : 8, z = 3;
|
||||||
|
require(c.block(rotate(new BlockPos(x, 6, z), c.site.facing())).isAir(),
|
||||||
|
label(c) + " old ceiling remains beneath the vaulted roof");
|
||||||
|
require(c.block(rotate(new BlockPos(x, 9, z), c.site.facing())).is(Blocks.GLASS),
|
||||||
|
label(c) + " single vaulted roof lost its glazing");
|
||||||
|
clearLightWell(c, x, z, 6, c.site.height());
|
||||||
|
}
|
||||||
|
if (c.site.kind() == HeritagePlan19.Kind.RADIO_RELAY || c.site.kind() == HeritagePlan19.Kind.CONSTRUCTION_REFUGE) {
|
||||||
|
int x = c.site.kind() == HeritagePlan19.Kind.RADIO_RELAY ? 7 : 11;
|
||||||
|
// Sample between the radio's trusses (z6,7,10), not through its real middle tie beam.
|
||||||
|
int z = 9;
|
||||||
|
int former = c.site.kind() == HeritagePlan19.Kind.RADIO_RELAY ? 6 : 10;
|
||||||
|
require(c.block(rotate(new BlockPos(x, former, z), c.site.facing())).isAir(),
|
||||||
|
label(c) + " shelter kept two separate opaque roofs");
|
||||||
|
int runs = 0; boolean previous = false;
|
||||||
|
for (int y = former; y <= c.site.height(); y++) {
|
||||||
|
boolean occupied = !c.block(rotate(new BlockPos(x, y, z), c.site.facing())).isAir();
|
||||||
|
if (occupied && !previous) runs++;
|
||||||
|
previous = occupied;
|
||||||
|
}
|
||||||
|
require(runs == 1, label(c) + " shelter has an interrupted or duplicated roof profile");
|
||||||
|
}
|
||||||
|
if (c.site.kind() == HeritagePlan19.Kind.OBSERVATORY) {
|
||||||
|
// The inset crown must be joined through face-adjacent masonry to the tower roof.
|
||||||
|
// This detects a detached decorative ring even when every individual voxel is in bounds.
|
||||||
|
Set<BlockPos> connected = new HashSet<>(); var pending = new java.util.ArrayDeque<BlockPos>();
|
||||||
|
c.blocks.forEach((p, block) -> {
|
||||||
|
if (p.getY() == 12 && !block.isAir()) { connected.add(p); pending.add(p); }
|
||||||
|
});
|
||||||
|
while (!pending.isEmpty()) {
|
||||||
|
var p = pending.removeFirst();
|
||||||
|
for (Direction direction : Direction.values()) {
|
||||||
|
var next = p.relative(direction);
|
||||||
|
if (next.getX() < 0 || next.getX() > 16 || next.getZ() < 0 || next.getZ() > 16
|
||||||
|
|| next.getY() < 12 || next.getY() > 17 || c.block(next).isAir()) continue;
|
||||||
|
if (connected.add(next)) pending.addLast(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.blocks.forEach((p, block) -> {
|
||||||
|
if (p.getY() == 17 && block.is(Blocks.SMOOTH_STONE_SLAB))
|
||||||
|
require(connected.contains(p), label(c) + " observatory crown floats above its buttresses at " + p);
|
||||||
|
});
|
||||||
|
require(c.block(rotate(new BlockPos(8, 17, 8), c.site.facing())).isAir(),
|
||||||
|
label(c) + " crown connection closes the sky-viewing slot");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static void clearLightWell(Capture c, int x, int z, int low, int high) {
|
||||||
|
for (int y = low; y <= high; y++) {
|
||||||
|
var p = rotate(new BlockPos(x, y, z), c.site.facing());
|
||||||
|
var block = c.block(p);
|
||||||
|
require(block.isAir() || block.is(Blocks.GLASS) || block.is(Blocks.GLASS_PANE),
|
||||||
|
label(c) + " skylight is blocked by an opaque roof or ceiling at " + p + ": " + block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static void roofBearings21(Capture c) {
|
||||||
|
int roofLine = switch (c.site.kind()) {
|
||||||
|
case COMMUNAL_DEPOT -> 9;
|
||||||
|
case OBSERVATORY -> 12;
|
||||||
|
case CONSTRUCTION_REFUGE, IMPOSSIBLE_SERVICE -> 10;
|
||||||
|
case VOID_DOCK, SUBSTATION -> 5;
|
||||||
|
default -> 6;
|
||||||
|
};
|
||||||
|
Set<BlockPos> supported = new HashSet<>(); var queue = new ArrayDeque<BlockPos>();
|
||||||
|
for (int x = 0; x < 17; x++) for (int z = 0; z < 17; z++) {
|
||||||
|
var at = new BlockPos(x, 0, z);
|
||||||
|
if (bearing(c.block(at))) { supported.add(at); queue.addLast(at); }
|
||||||
|
}
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
var at = queue.removeFirst();
|
||||||
|
for (var direction : Direction.values()) {
|
||||||
|
var next = at.relative(direction);
|
||||||
|
if (next.getX() < 0 || next.getX() > 16 || next.getZ() < 0 || next.getZ() > 16
|
||||||
|
|| next.getY() < 0 || next.getY() > c.site.height() || !bearing(c.block(next))) continue;
|
||||||
|
if (supported.add(next)) queue.addLast(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Shutters and cabinet screens may hang from a wall or each other, but not hover
|
||||||
|
// over a counter. The playable floor hatch has its separately verified ladder/lip contract.
|
||||||
|
queue.addAll(supported);
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
var at = queue.removeFirst();
|
||||||
|
for (var direction : Direction.values()) {
|
||||||
|
var next = at.relative(direction);
|
||||||
|
if (c.block(next).getBlock() instanceof TrapDoorBlock && supported.add(next)) queue.addLast(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var e : c.blocks.entrySet()) if (e.getKey().getY() > 0 && e.getValue().getBlock() instanceof TrapDoorBlock) {
|
||||||
|
boolean floorHatch = c.site.kind() == HeritagePlan19.Kind.CONSTRUCTION_REFUGE
|
||||||
|
&& e.getKey().equals(rotate(new BlockPos(10, 5, 11), c.site.facing()));
|
||||||
|
require(floorHatch || supported.contains(e.getKey()), label(c) + " decorative screen has no frame or counter at " + e.getKey());
|
||||||
|
}
|
||||||
|
long witnesses = 0;
|
||||||
|
for (var e : c.blocks.entrySet()) if (e.getKey().getY() > 0 && bearing(e.getValue())) {
|
||||||
|
if (e.getKey().getY() >= roofLine) witnesses++;
|
||||||
|
require(supported.contains(e.getKey()), label(c) + " construction or ornament is detached from all grounded walls/frames at " + e.getKey());
|
||||||
|
}
|
||||||
|
require(witnesses > 0, label(c) + " roof-bearing test has no physical roof witness");
|
||||||
|
}
|
||||||
|
private static boolean bearing(BlockState block) {
|
||||||
|
return !block.isAir() && block.getFluidState().isEmpty() && !block.is(Blocks.LANTERN)
|
||||||
|
&& !block.is(Blocks.LEVER) && !block.is(Blocks.LADDER) && !block.is(Blocks.RAIL)
|
||||||
|
&& !(block.getBlock() instanceof DoorBlock) && !(block.getBlock() instanceof TrapDoorBlock)
|
||||||
|
&& !block.hasProperty(BlockStateProperties.BED_PART);
|
||||||
|
}
|
||||||
|
private static void export(Capture c) {
|
||||||
|
String directory = System.getProperty("sanctuary.test.heritage21.export");
|
||||||
|
if (directory == null || directory.isBlank()) return;
|
||||||
|
var blocks = c.blocks.entrySet().stream().filter(e -> !e.getValue().isAir())
|
||||||
|
.sorted(java.util.Comparator.<Map.Entry<BlockPos, BlockState>>comparingInt(e -> e.getKey().getY())
|
||||||
|
.thenComparingInt(e -> e.getKey().getZ()).thenComparingInt(e -> e.getKey().getX()))
|
||||||
|
.map(e -> List.of(e.getKey().getX(), e.getKey().getY(), e.getKey().getZ(), e.getValue().toString())).toList();
|
||||||
|
var targets = HeritageRenderer21.targets(c.site).stream().map(t -> List.of(t.name(), t.x(), t.y(), t.z())).toList();
|
||||||
|
var document = Map.of("kind", c.site.kind().name(), "facing", c.site.facing(), "seed", c.site.seed(),
|
||||||
|
"width", 17, "depth", 17, "height", c.site.height(), "floor_y", 0, "blocks", blocks, "targets", targets);
|
||||||
|
try {
|
||||||
|
var root = java.nio.file.Path.of(directory); java.nio.file.Files.createDirectories(root);
|
||||||
|
java.nio.file.Files.writeString(root.resolve(c.site.kind().name().toLowerCase(java.util.Locale.ROOT) + ".json"),
|
||||||
|
new com.google.gson.GsonBuilder().setPrettyPrinting().create().toJson(document));
|
||||||
|
} catch (java.io.IOException error) { throw new AssertionError("Cannot export the requested architecture capture", error); }
|
||||||
|
}
|
||||||
|
private static void materials21() {
|
||||||
|
Map<Block, Long> counts = new HashMap<>();
|
||||||
|
for (var kind : HeritagePlan19.Kind.values()) render(site(kind, 0, 0)).blocks.forEach((p, b) -> {
|
||||||
|
if (p.getY() > 0 && !b.isAir()) counts.merge(b.getBlock(), 1L, Long::sum);
|
||||||
|
});
|
||||||
|
for (Block block : new Block[] {Blocks.SMOOTH_STONE, Blocks.STONE, Blocks.STONE_BRICKS,
|
||||||
|
Blocks.CRACKED_STONE_BRICKS, Blocks.MOSSY_STONE_BRICKS, Blocks.ANDESITE,
|
||||||
|
Blocks.OAK_WOOD, Blocks.BIRCH_WOOD, Blocks.OAK_PLANKS, Blocks.BIRCH_PLANKS})
|
||||||
|
require(counts.getOrDefault(block, 0L) > 0, "The restrained generation21 palette omits " + block);
|
||||||
|
require(counts.get(Blocks.SMOOTH_STONE) > counts.getOrDefault(Blocks.STONE_BRICKS, 0L)
|
||||||
|
&& counts.get(Blocks.SMOOTH_STONE) > counts.getOrDefault(Blocks.ANDESITE, 0L),
|
||||||
|
"Smooth stone is no longer the principal masonry of the catalogue");
|
||||||
|
}
|
||||||
|
private static void has(Capture c, Block b) { require(c.blocks.values().stream().anyMatch(v -> v.is(b)), label(c) + " loses its " + b); }
|
||||||
|
private static void railConnection(Capture c) {
|
||||||
|
BlockPos destination = switch (c.site.kind()) {
|
||||||
|
case FREIGHT_STATION -> new BlockPos(2, 1, 8);
|
||||||
|
case RAIL_WORKSHOP -> new BlockPos(5, 1, 7);
|
||||||
|
case COMMUNAL_DEPOT -> new BlockPos(8, 1, 7);
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
if (destination == null) return;
|
||||||
|
var start = rotate(new BlockPos(8, 1, 0), c.site.facing());
|
||||||
|
destination = rotate(destination, c.site.facing());
|
||||||
|
Set<BlockPos> seen = new HashSet<>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
var p = queue.removeFirst(); var b = c.block(p);
|
||||||
|
require(b.is(Blocks.RAIL), label(c) + " freight entry has no supported rail");
|
||||||
|
for (var d : railEnds(b)) {
|
||||||
|
var next = p.relative(d); var state = c.block(next);
|
||||||
|
if (state.is(Blocks.RAIL) && railEnds(state).contains(d.getOpposite()) && seen.add(next)) queue.add(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require(seen.contains(destination), label(c) + " interior rail does not connect to its real external freight entry");
|
||||||
|
}
|
||||||
|
private static List<Direction> railEnds(BlockState b) {
|
||||||
|
return switch (b.getValue(BlockStateProperties.RAIL_SHAPE)) {
|
||||||
|
case NORTH_SOUTH, ASCENDING_NORTH, ASCENDING_SOUTH -> List.of(Direction.NORTH, Direction.SOUTH);
|
||||||
|
case EAST_WEST, ASCENDING_EAST, ASCENDING_WEST -> List.of(Direction.EAST, Direction.WEST);
|
||||||
|
case SOUTH_EAST -> List.of(Direction.SOUTH, Direction.EAST);
|
||||||
|
case SOUTH_WEST -> List.of(Direction.SOUTH, Direction.WEST);
|
||||||
|
case NORTH_WEST -> List.of(Direction.NORTH, Direction.WEST);
|
||||||
|
case NORTH_EAST -> List.of(Direction.NORTH, Direction.EAST);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
private static void routes() {
|
||||||
|
var cells = new ArrayList<HeritagePlan19.Cell>();
|
||||||
|
int x = -15;
|
||||||
|
for (var rail : HeritagePlan19.Rail.values()) {
|
||||||
|
cells.add(new HeritagePlan19.Cell(x++, 160, 2, 2, rail, rail != HeritagePlan19.Rail.NONE));
|
||||||
|
}
|
||||||
|
cells.add(new HeritagePlan19.Cell(-1, 161, 2, 3, HeritagePlan19.Rail.ASCENDING_EAST, false));
|
||||||
|
var segment = new HeritagePlan19.Segment("route", "a", "b", HeritagePlan19.RouteKind.FREIGHT, -1, 0, cells, 7);
|
||||||
|
Map<BlockPos, BlockState> result = new HashMap<>();
|
||||||
|
HeritageRenderer21.renderSegment(segment, new LostCityRenderer.BlockWriter() {
|
||||||
|
@Override public void set(int x, int y, int z, BlockState b) {
|
||||||
|
var p = new BlockPos(segment.minX() + x, segment.minY() + y, segment.minZ() + z);
|
||||||
|
var cell = cells.stream().filter(c -> c.x() == p.getX() && c.z() == p.getZ()).findFirst().orElseThrow();
|
||||||
|
require(p.getY() >= cell.minY() && p.getY() <= cell.maxY(), "Route escaped its saved column mask"); result.put(p, b);
|
||||||
|
}
|
||||||
|
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { throw new AssertionError("A route generated treasure"); }
|
||||||
|
});
|
||||||
|
for (var c : cells) {
|
||||||
|
var p = new BlockPos(c.x(), c.floorY() + 1, c.z());
|
||||||
|
require(full(result.get(p.below()), p.below()), "Rail or missing-rail witness lost its ballast support");
|
||||||
|
if (c.railPresent()) require(result.get(p).is(Blocks.RAIL)
|
||||||
|
&& result.get(p).getValue(BlockStateProperties.RAIL_SHAPE).name().equals(c.rail().name()), "Saved rail shape was replaced by a straight track");
|
||||||
|
else require(result.get(p).isAir(), "An intentional abandoned-track gap was repaired by rendering");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static void chunkPartition() {
|
||||||
|
var s = site(HeritagePlan19.Kind.CONSTRUCTION_REFUGE, 3, 43); var expected = render(s); var parts = new Capture(s);
|
||||||
|
for (int cz = s.maxZ() >> 4; cz >= s.minZ() >> 4; cz--) for (int cx = s.maxX() >> 4; cx >= s.minX() >> 4; cx--) {
|
||||||
|
final int chunkX = cx, chunkZ = cz;
|
||||||
|
HeritageRenderer21.render(s, new LostCityRenderer.BlockWriter() {
|
||||||
|
boolean owns(int x, int z) { return ((s.minX() + x) >> 4) == chunkX && ((s.minZ() + z) >> 4) == chunkZ; }
|
||||||
|
@Override public void set(int x, int y, int z, BlockState state) { if (owns(x, z)) parts.set(x, y, z, state); }
|
||||||
|
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { if (owns(x, z)) parts.chest(x, y, z, table, seed); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
require(parts.blocks.equals(expected.blocks) && parts.loot.equals(expected.loot), "Native chunk order changes a site");
|
||||||
|
}
|
||||||
|
private record Loot(ResourceKey<LootTable> table, long seed) {}
|
||||||
|
private static final class Capture implements LostCityRenderer.BlockWriter {
|
||||||
|
final HeritagePlan19.Site site;
|
||||||
|
final Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||||
|
final Map<BlockPos, Loot> loot = new HashMap<>();
|
||||||
|
Capture(HeritagePlan19.Site site) { this.site = site; }
|
||||||
|
@Override public void set(int x, int y, int z, BlockState state) {
|
||||||
|
int wy = y + site.floorY();
|
||||||
|
boolean main = x >= 0 && x <= 16 && z >= 0 && z <= 16 && wy >= site.minY() && wy <= site.maxY();
|
||||||
|
boolean footing = site.supports().stream().anyMatch(s -> s.x() == x + site.minX() && s.z() == z + site.minZ() && wy >= s.minY() && wy <= s.maxY());
|
||||||
|
require(main || footing, label(this) + " writes outside its exact body and footing mask"); blocks.put(new BlockPos(x, y, z), state);
|
||||||
|
}
|
||||||
|
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
|
||||||
|
require(loot.put(new BlockPos(x, y, z), new Loot(table, seed)) == null, "Duplicate shared treasure"); set(x, y, z, Blocks.CHEST.defaultBlockState());
|
||||||
|
}
|
||||||
|
BlockState block(BlockPos p) { return blocks.getOrDefault(p, Blocks.AIR.defaultBlockState()); }
|
||||||
|
}
|
||||||
|
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.transit;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.worldgen.city.LostCityInteriors21Smoke;
|
||||||
|
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.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
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;
|
||||||
|
import net.minecraft.world.phys.AABB;
|
||||||
|
|
||||||
|
/** Native block-shape paths across the entire planned route, plus refusal and exact saved geometry. */
|
||||||
|
public final class Transit21Smoke {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); run();
|
||||||
|
System.out.println("Transit21Smoke: bounded planning, two surface stair exits, through-road and hall, supported roofs, strict saved modules and all interior targets passed.");
|
||||||
|
}
|
||||||
|
public static void run() {
|
||||||
|
var terrain = new Plateau();
|
||||||
|
for (int diameter : new int[]{512, 724, 1024}) {
|
||||||
|
var plan = TransitPlanner21.plan(0, 0, 0, diameter, terrain);
|
||||||
|
require(!plan.isEmpty(), "Continuous native rock rejected its through-route: " + plan.diagnostic());
|
||||||
|
var accesses = plan.modules().stream().filter(m -> m.kind() == Kind.ACCESS).toList();
|
||||||
|
require(accesses.size() == 2, "Route lacks two surface stair exits");
|
||||||
|
int span = Math.abs(accesses.get(0).centerX() - accesses.get(1).centerX())
|
||||||
|
+ Math.abs(accesses.get(0).centerZ() - accesses.get(1).centerZ());
|
||||||
|
require(span >= diameter * .4, "Route became a short unconnected cellar");
|
||||||
|
require(plan.modules().stream().anyMatch(m -> m.kind() == Kind.HALL), "Available space produced no industrial hall");
|
||||||
|
require(plan.equals(TransitPlanner21.plan(0, 0, 0, diameter, terrain)), "Identical input changed final saved geometry");
|
||||||
|
require(plan.samples() <= TransitPlan21.MAX_SAMPLES, "Planning exceeded its work bound");
|
||||||
|
for (var access : accesses) {
|
||||||
|
require(plan.protects(access.centerX(), access.surfaceY() + 2, access.minZ() - 1), "Outside access approach can be decorated shut");
|
||||||
|
require(!plan.contains(access.centerX(), access.surfaceY(), access.minZ() - 1), "Read-only approach became a painted platform");
|
||||||
|
}
|
||||||
|
if (diameter == 724) verifyWorld(plan);
|
||||||
|
for (var m : plan.modules()) {
|
||||||
|
var saved = TransitPiece21.encodeModule(m);
|
||||||
|
require(TransitPiece21.decodeModule(saved).equals(m), "Module changed its persisted geometry");
|
||||||
|
var foreign = saved.copy(); foreign.putInt("Foreign", 1); reject(foreign);
|
||||||
|
var wrongType = saved.copy(); wrongType.putLong("Width", m.width()); reject(wrongType);
|
||||||
|
var missing = saved.copy(); missing.remove("Supports"); reject(missing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var blocked = TransitPlanner21.plan(0, 0, 0, 724, new Plateau() {
|
||||||
|
@Override public boolean protectedAt(int x, int y, int z) { return true; }
|
||||||
|
});
|
||||||
|
require(blocked.isEmpty(), "Transit destroyed protected construction/hydrology");
|
||||||
|
var voidFloor = TransitPlanner21.plan(0, 0, 0, 724, new Plateau() {
|
||||||
|
@Override public boolean solid(int x, int y, int z) { return y >= 245 && y <= 252; }
|
||||||
|
});
|
||||||
|
require(voidFloor.isEmpty(), "Transit built an unsupported island-wide bridge");
|
||||||
|
var shortGap = TransitPlanner21.plan(0, 0, 0, 512, new Plateau() {
|
||||||
|
@Override public boolean solid(int x, int y, int z) { return super.solid(x, y, z) && !(y >= 199 && y <= 214); }
|
||||||
|
});
|
||||||
|
require(!shortGap.isEmpty() && shortGap.modules().stream().flatMap(m -> m.supports().stream())
|
||||||
|
.anyMatch(s -> s.maxY() - s.minY() + 1 == 16), "Certified sixteen-block piers should bridge a bounded natural cavity");
|
||||||
|
boolean longPierRejected = false;
|
||||||
|
try { new TransitPlan21.Support(0, 200, 0, 216); } catch (IllegalArgumentException expected) { longPierRejected = true; }
|
||||||
|
require(longPierRejected, "A seventeen-block pier escaped the foundation contract");
|
||||||
|
var oneStepApproach = TransitPlanner21.plan(0, 0, 0, 512, new Plateau() {
|
||||||
|
@Override public int surfaceY(int x, int z) { return (x & 1) == 0 ? 251 : 252; }
|
||||||
|
});
|
||||||
|
require(!oneStepApproach.isEmpty() && oneStepApproach.modules().stream().filter(m -> m.kind() == Kind.ACCESS)
|
||||||
|
.allMatch(m -> m.surfaceY() == 252), "A one-step native approach should use its highest cell without levelling it");
|
||||||
|
// Different surface heights terminate a partial flight, not just multiples of five steps.
|
||||||
|
for (int height : new int[]{249, 250, 251, 252, 253, 254, 255, 256, 257, 258}) {
|
||||||
|
var p = TransitPlanner21.plan(3, 0, 0, 512, new Plateau() {
|
||||||
|
@Override public int surfaceY(int x, int z) { return height; }
|
||||||
|
});
|
||||||
|
require(!p.isEmpty(), "Admissible irregular stair rise was rejected");
|
||||||
|
for (var access : p.modules()) if (access.kind() == Kind.ACCESS) verifySingle(access);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static void reject(CompoundTag tag) {
|
||||||
|
boolean rejected = false;
|
||||||
|
try { TransitPiece21.decodeModule(tag); } catch (IllegalArgumentException expected) { rejected = true; }
|
||||||
|
require(rejected, "Saved module accepted missing/foreign/wrong-typed geometry");
|
||||||
|
}
|
||||||
|
private static void verifySingle(Module m) {
|
||||||
|
var capture = new Capture(); renderInto(m, capture);
|
||||||
|
var entrance = world(m, TransitRenderer21.targets(m).stream().filter(t -> t.name().equals("surface_portico")).findFirst().orElseThrow());
|
||||||
|
var reached = walk(capture, entrance);
|
||||||
|
for (var target : TransitRenderer21.targets(m)) require(reached.contains(world(m, target)),
|
||||||
|
"Stair access rise=" + (m.surfaceY() - m.floorY()) + " cannot reach " + target.name() + " " + world(m, target));
|
||||||
|
}
|
||||||
|
private static void verifyWorld(TransitPlan21 plan) {
|
||||||
|
var capture = new Capture();
|
||||||
|
for (var module : plan.modules()) renderInto(module, capture);
|
||||||
|
var first = plan.modules().stream().filter(m -> m.kind() == Kind.ACCESS).findFirst().orElseThrow();
|
||||||
|
var entrance = new BlockPos(first.centerX(), first.surfaceY() + 1, first.minZ());
|
||||||
|
var reached = walk(capture, entrance);
|
||||||
|
for (var module : plan.modules()) for (var target : TransitRenderer21.targets(module))
|
||||||
|
require(reached.contains(world(module, target)), "Through-route cannot reach " + module.kind() + "/" + target.name() + " at " + world(module, target));
|
||||||
|
for (var chest : capture.loot) require(java.util.Arrays.stream(new Direction[]{Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST})
|
||||||
|
.anyMatch(d -> reached.contains(chest.relative(d))), "Tools chest has no reachable approach");
|
||||||
|
require(capture.loot.size() == 1, "Route must keep one modest shared tool chest");
|
||||||
|
}
|
||||||
|
private static void renderInto(Module m, Capture into) {
|
||||||
|
var local = new Capture();
|
||||||
|
TransitRenderer21.render(m, new LostCityRenderer.BlockWriter() {
|
||||||
|
@Override public void set(int x, int y, int z, BlockState state) {
|
||||||
|
require(x >= 0 && x < m.width() && z >= 0 && z < m.depth() && y >= m.boundsMinY() - m.minY() && y < m.height(),
|
||||||
|
"Transit renderer escaped its saved native bounds");
|
||||||
|
local.set(x, y, z, state);
|
||||||
|
}
|
||||||
|
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { local.chest(x, y, z, table, seed); }
|
||||||
|
});
|
||||||
|
LostCityInteriors21Smoke.verifyGroundedGeometry(local.blocks, 0, "Transit " + m.kind() + " floor=" + m.floorY());
|
||||||
|
local.blocks.forEach((at, state) -> {
|
||||||
|
var position = at.offset(m.minX(), m.minY(), m.minZ());
|
||||||
|
require(into.blocks.putIfAbsent(position, state) == null, "Neighboring native modules overwrite each other");
|
||||||
|
require(!state.is(Blocks.SPAWNER) && state.getFluidState().isEmpty(), "Transit introduced a spawner or uncontrolled fluid");
|
||||||
|
});
|
||||||
|
for (var chest : local.loot) into.loot.add(chest.offset(m.minX(), m.minY(), m.minZ()));
|
||||||
|
}
|
||||||
|
private static BlockPos world(Module m, TransitRenderer21.Target t) { return new BlockPos(m.minX() + t.x(), m.minY() + t.y(), m.minZ() + t.z()); }
|
||||||
|
private static Set<BlockPos> walk(Capture capture, BlockPos first) {
|
||||||
|
require(standable(capture, first), "Blocked transit entrance " + first);
|
||||||
|
var seen = new HashSet<BlockPos>(); var queue = new ArrayDeque<BlockPos>(); seen.add(first); queue.add(first);
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
var at = queue.removeFirst();
|
||||||
|
for (Direction direction : new Direction[]{Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST})
|
||||||
|
for (int dy = -1; dy <= 1; dy++) {
|
||||||
|
var next = at.relative(direction).offset(0, dy, 0);
|
||||||
|
if (dy > 0 && (!clear(capture, at.above()) || !ascending(capture, next.below(), direction))) continue;
|
||||||
|
if (dy < 0 && (!clear(capture, next.above()) || !ascending(capture, at.below(), direction.getOpposite()))) continue;
|
||||||
|
if (standable(capture, next) && seen.add(next)) queue.addLast(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return seen;
|
||||||
|
}
|
||||||
|
private static boolean ascending(Capture c, BlockPos at, Direction direction) {
|
||||||
|
var state = c.block(at);
|
||||||
|
return state.getBlock() instanceof net.minecraft.world.level.block.StairBlock
|
||||||
|
&& state.getValue(net.minecraft.world.level.block.state.properties.BlockStateProperties.HORIZONTAL_FACING) == direction
|
||||||
|
&& state.getValue(net.minecraft.world.level.block.state.properties.BlockStateProperties.HALF)
|
||||||
|
== net.minecraft.world.level.block.state.properties.Half.BOTTOM;
|
||||||
|
}
|
||||||
|
private static boolean standable(Capture c, BlockPos at) {
|
||||||
|
if (!c.blocks.containsKey(at) || !clear(c, at)) return false;
|
||||||
|
return c.block(at.below()).getCollisionShape(EmptyBlockGetter.INSTANCE, at.below()).toAabbs().stream()
|
||||||
|
.anyMatch(box -> box.maxY >= .99 && box.minX < .8 && box.maxX > .2 && box.minZ < .8 && box.maxZ > .2);
|
||||||
|
}
|
||||||
|
private static boolean clear(Capture c, BlockPos at) {
|
||||||
|
var body = new AABB(.2, 0, .2, .8, 1.8, .8);
|
||||||
|
for (int up = 0; up < 2; up++) for (var box : c.block(at.above(up)).getCollisionShape(EmptyBlockGetter.INSTANCE, at.above(up)).toAabbs())
|
||||||
|
if (body.intersects(box.move(0, up, 0))) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
private static class Plateau implements TransitPlanner21.Terrain {
|
||||||
|
@Override public boolean owns(int x, int z) { return Math.abs(x) < 600 && Math.abs(z) < 600; }
|
||||||
|
@Override public boolean solid(int x, int y, int z) { return y >= 80 && y <= 252; }
|
||||||
|
@Override public boolean protectedAt(int x, int y, int z) { return false; }
|
||||||
|
@Override public int surfaceY(int x, int z) { return 252; }
|
||||||
|
}
|
||||||
|
private static final class Capture implements LostCityRenderer.BlockWriter {
|
||||||
|
final Map<BlockPos, BlockState> blocks = new HashMap<>(); final Set<BlockPos> loot = new HashSet<>();
|
||||||
|
@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()); loot.add(new BlockPos(x, y, z)); }
|
||||||
|
BlockState block(BlockPos at) { return blocks.getOrDefault(at, Blocks.AIR.defaultBlockState()); }
|
||||||
|
}
|
||||||
|
private static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); }
|
||||||
|
}
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
name = "Sanctuary"
|
name = "Sanctuary"
|
||||||
author = "KOKA99CAB"
|
author = "KOKA99CAB"
|
||||||
version = "0.1.0-alpha.20"
|
version = "0.1.0-alpha.21"
|
||||||
pack-format = "packwiz:1.1.0"
|
pack-format = "packwiz:1.1.0"
|
||||||
|
|
||||||
[index]
|
[index]
|
||||||
|
|||||||
Reference in New Issue
Block a user