feat(worldgen): add reversible alpha19 heritage structures and routes
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -1,5 +1,27 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.0-alpha.19 — 2026-09-09
|
||||
|
||||
- Quatorze nouvelles infrastructures abandonnées sur l’île initiale, avec pièces,
|
||||
accès, mobilier, secrets et outils vanilla associés. La présence varie selon
|
||||
la graine et le relief ; les quatorze types ont été rencontrés en tests natifs.
|
||||
- Voies de fret aux rails incomplets et chemins entre destinations réelles.
|
||||
Le témoin Moyen, graine 0, contient treize lieux et sept liaisons, dont un
|
||||
fret de 499 blocs entre dépôt et pompage. Tracés enregistrés par chunk.
|
||||
- Option FR/EN « Structures expérimentales » dans le bouton natif Personnaliser,
|
||||
trois tailles conservées. Codec `sanctuary:island_v19` et journal 19 ; les
|
||||
anciens mondes gardent leurs plans et futurs chunks. Un nouveau monde avec
|
||||
l’option désactivée conserve le socle urbain sans les expériences 17/19.
|
||||
- Commande opérateur `/sanctuary structures` pour relever les entrées.
|
||||
La ville et ses sous-sols sont conservés ; les systèmes de la bêta restent
|
||||
à construire.
|
||||
|
||||
`check build assemblePack` passe, dont les huit GameTests généraux. Les trois
|
||||
formats sont vérifiés nativement ; le témoin moyen se rouvre avec ses 112 pièces,
|
||||
13 coffres, 68 cibles accessibles et la modification témoin. Les identités 17
|
||||
restent identiques à la release publiée. JAR, MRpack et ZIP Prism vérifiés ;
|
||||
publication en préparation. Voir [les preuves et limites](docs/testing-alpha19.md).
|
||||
|
||||
## 0.1.0-alpha.17 — 2026-09-09
|
||||
|
||||
- Prototype de trois réseaux oubliés sur l’île initiale : mines et ateliers,
|
||||
|
||||
@@ -20,6 +20,20 @@ trois familles de réseaux oubliés expérimentaux rejoignent la ville et ses gr
|
||||
sous-sols sur les nouvelles îles de départ. Le canal packwiz et la même instance
|
||||
Prism sont à jour ; voir [Distribution](docs/packwiz.md).
|
||||
|
||||
## Alpha.19 — anciennes infrastructures, livraison préparée
|
||||
|
||||
Le chantier ajoute une première passe des quatorze lieux du
|
||||
[catalogue](docs/structures-audit.md), avec leurs accès, des petits butins vanilla
|
||||
et des voies de fret ou chemins abandonnés entre destinations réelles.
|
||||
L’option **Structures expérimentales** reste enregistrée à la création du monde.
|
||||
La nouvelle génération garde les anciennes sauvegardes intactes. Aucun système
|
||||
de progression, mailbox, caméra ou réacteur n’est activé.
|
||||
Les trois tailles sont vérifiées en génération native ; création et réouverture
|
||||
conservent les structures et les coffres. `check build assemblePack` passe.
|
||||
Pour voir le grand fret : **Moyen, graine 0**, option activée ;
|
||||
`/sanctuary structures` liste les entrées. Voir [les preuves et la carte](docs/testing-alpha19.md).
|
||||
La publication et la synchronisation Prism sont en cours.
|
||||
|
||||
## Alpha.17 — prototype des réseaux oubliés
|
||||
|
||||
La nouvelle génération expérimentale recherche trois petits réseaux sur l’île
|
||||
@@ -409,7 +423,7 @@ sera une mise à jour explicite, avec vérification des API et des sauvegardes.
|
||||
| Fabric API | 0.160.0+26.3 |
|
||||
| Fabric Loom | 1.17.20 |
|
||||
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
|
||||
| Sanctuary / pack | 0.1.0-alpha.17 |
|
||||
| Sanctuary / pack | 0.1.0-alpha.19 |
|
||||
|
||||
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
|
||||
uniquement la bibliothèque standard et repère aussi une installation Python
|
||||
@@ -438,7 +452,7 @@ décrits dans [Validation](docs/testing.md).
|
||||
|
||||
Résultats :
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.17.jar` : mod à installer avec
|
||||
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.19.jar` : mod à installer avec
|
||||
Fabric API sur la version Minecraft indiquée.
|
||||
- `build/packwiz/` : pack de développement complet, avec le mod construit et
|
||||
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
|
||||
|
||||
@@ -717,6 +717,39 @@ Aucune structure, règle de butin, version binaire ou instance Prism modifiée.
|
||||
La sélection des lieux et la réalisation des axes feront l’objet du chantier
|
||||
suivant ; ce catalogue n’est pas une liste d’ajouts tous validés.
|
||||
|
||||
### WG-22 — Anciennes infrastructures et liaisons — alpha.19
|
||||
|
||||
**Branche :** `codex/heritage-worldgen-alpha19`.
|
||||
|
||||
**Demande validée :** première passe de toutes les nouvelles structures du
|
||||
catalogue WG-21 et de leurs liaisons avant la bêta, en conservant la possibilité
|
||||
de revenir à une génération sans ce chantier.
|
||||
|
||||
- Ajouter les quatorze lieux avec architectures, entrées, pièces, mobilier et
|
||||
petits butins vanilla associés à leur usage. Présence selon le terrain ;
|
||||
relever les lieux réellement générés et donner les coordonnées des témoins.
|
||||
- Planifier des voies de fret et chemins entre destinations réelles, avec
|
||||
sections abandonnées lisibles, appuis et passages vérifiés. Rechercher les
|
||||
raccords aux anciennes installations dans un budget borné.
|
||||
- Garder une majorité naturelle à l’île ; conserver le terrain, l’hydrologie,
|
||||
la ville et les réseaux existants comme base. Première île uniquement.
|
||||
- Nouveau codec 19, option expérimentale sauvegardée à la création du monde.
|
||||
Option désactivée : socle urbain seul. Les mondes 17 et antérieurs gardent
|
||||
leur génération, leurs pièces et leurs futurs chunks. Aucune conversion,
|
||||
suppression de structures ou rétrogradation d’une sauvegarde 19.
|
||||
- Les bâtiments préparent les futurs services ; mailbox, deposit boxes,
|
||||
caméra, réacteur, progression et récompenses personnelles restent pour la
|
||||
bêta. Les coffres de cette passe sont partagés et utilisent du butin vanilla.
|
||||
- Vérifier créations/réouvertures, option désactivée, anciens mondes, supports,
|
||||
circulations et vrais placements. `check build assemblePack`, artefacts,
|
||||
publication packwiz et mise à jour de la même instance Prism.
|
||||
|
||||
**État : code et artefacts vérifiés, publication en cours.** Voir
|
||||
[les essais natifs et la carte des lieux](testing-alpha19.md). Version 19 demandée
|
||||
explicitement ; aucune version 18 publiée
|
||||
dans ce dépôt à l’ouverture du chantier. Le catalogue est maintenant accepté
|
||||
pour une première passe, sans imposer sa présence exhaustive sur chaque graine.
|
||||
|
||||
## Réserve de thèmes futurs
|
||||
|
||||
Ces thèmes servent à retrouver la vision, pas à demander leur implémentation immédiate. On en extrait un ticket seulement lorsqu'il devient utile au prochain incrément jouable.
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1,169 @@
|
||||
# Alpha.19 — anciennes infrastructures de Sanctuary
|
||||
|
||||
**État : code et artefacts vérifiés, publication en préparation.** Ticket WG-22, branche
|
||||
`codex/heritage-worldgen-alpha19`. Minecraft 26.3-pre-2, mêmes dépendances que
|
||||
l’alpha.17. La numérotation 19 est demandée explicitement ; aucune 18 n’est
|
||||
intercalée dans ce dépôt.
|
||||
|
||||
## Périmètre et réversibilité
|
||||
|
||||
Première passe des quatorze lieux du [catalogue](structures-audit.md) : gare de
|
||||
marchandises, centre postal, dépôt communal, cartothèque, observatoire,
|
||||
maintenance ferroviaire, pompage, sous-station, serre, quai d’expédition,
|
||||
relais radio, club photo, vestiaires/salles étranges et refuge de chantier.
|
||||
Les plans recherchent des sites admissibles et des voies de fret ou chemins
|
||||
entre destinations réelles ; ils ne garantissent pas tous les lieux sur chaque
|
||||
graine. Les mesures et coordonnées des essais figureront ci-dessous.
|
||||
|
||||
Créer un **nouveau monde Sanctuary** et utiliser **Personnaliser** :
|
||||
Petit 512, Moyen 724 par défaut ou Grand 1 024, avec **Structures expérimentales**.
|
||||
L’option est activée par défaut et enregistrée avec le générateur 19. La désactiver
|
||||
pour un autre nouveau monde conserve le socle urbain sans les réseaux
|
||||
expérimentaux 17 et les infrastructures 19. Elle ne retire aucun bloc ni ne change
|
||||
les règles d’une sauvegarde existante.
|
||||
|
||||
Les mondes 17 et antérieurs gardent leurs codecs, plans, pièces et futurs chunks.
|
||||
Le preset 17 public est conservé octet par octet sous un identifiant historique
|
||||
caché à la création. Le générateur 19 et son journal sont distincts. Il n’existe
|
||||
aucune conversion ou rétrogradation de sauvegarde 19 vers un ancien binaire.
|
||||
L’ancienne release reste disponible pour ses propres mondes, et l’option d’un
|
||||
nouveau monde permet de comparer les deux configurations du nouveau binaire.
|
||||
|
||||
Les nouveaux lieux sont réservés à l’île initiale. La ville et les anciens
|
||||
réseaux servent de base ; leurs protections et celles de l’hydrologie restent
|
||||
respectées. Les liaisons vers de futurs continents seront construites après
|
||||
leur apparition, sans présumer leurs emplacements dans cette génération.
|
||||
|
||||
## Objets et systèmes
|
||||
|
||||
Les bâtiments suggèrent leurs usages futurs par leurs pièces et aménagements.
|
||||
Mailbox, deposit boxes, caméra, réacteur, progression et récompenses
|
||||
personnelles ne sont pas activés. Chaque nouveau lieu dispose d’une table
|
||||
de coffre propre à la génération 19, avec un outil vanilla lié à son usage et
|
||||
quelques fournitures. Les coffres sont partagés et ne se renouvellent pas.
|
||||
|
||||
La valeur des rails, équipements et blocs récupérables du décor s’ajoute au
|
||||
butin. Leur distribution doit être constatée sur les témoins, sans transformer
|
||||
cette première passe en une garantie d’équilibrage économique pour la bêta.
|
||||
|
||||
## Vérifications prévues
|
||||
|
||||
- Placement des quatorze architectures, ouvertures, circulation, orientation
|
||||
et appuis du mobilier ; accès réel au coffre et retour possible.
|
||||
- Plans bornés sur les trois tailles, terrain non occupé, supports, continuité
|
||||
des tracés et distinction entre ballast praticable et rails retirés.
|
||||
- Pièces natives et références locales, y compris pour les longues voies ;
|
||||
géométrie, orientation et rails sauvegardés dans un schéma strict.
|
||||
- Création et réouverture d’un monde 19 avec modification témoin ; conservation
|
||||
des starts, butins différés, accès, routes et supports.
|
||||
- Option désactivée, paramètres sauvegardés, génération 17 comparée aux preuves
|
||||
de la version publiée, refus des migrations implicites de journaux.
|
||||
- `./gradlew check build assemblePack`, artefacts et index, publication immuable
|
||||
puis canal packwiz et deux synchronisations de la même instance Prism.
|
||||
|
||||
Les smokes des quatorze architectures, de leurs quatre orientations, des
|
||||
plans sur les trois tailles et des pièces natives passent. Le mode désactivé
|
||||
est vérifié dans un monde 19 neuf : aucun site ni liaison expérimental.
|
||||
`./gradlew check build assemblePack` passe, avec les huit GameTests généraux,
|
||||
125 chunks FULL, 5 923 blocs d’eau conservés et 1 800 ticks de fluides.
|
||||
Le JAR, le MRpack, les hashes du pack et le ZIP Prism sont vérifiés.
|
||||
La publication et la synchronisation de l’instance restent en cours. Aucun essai client ou
|
||||
Windows n’est encore effectué.
|
||||
|
||||
## Témoin natif Moyen, graine 0
|
||||
|
||||
Créer un nouveau monde avec **Structures expérimentales activées**.
|
||||
`/sanctuary structures` liste les entrées prévues sans charger de chunks ;
|
||||
`/locate structure sanctuary:heritage_networks` retrouve l’entrée d’un site.
|
||||
Le relevé ci-dessous provient des placements et parcours vérifiés par le moteur.
|
||||
|
||||

|
||||
|
||||
Treize des quatorze lieux apparaissent. **La cartothèque est absente de cette
|
||||
graine** : la recherche n’a pas trouvé d’emplacement admissible. La ville et les
|
||||
trois réseaux souterrains 17 sont conservés. Les raccords recherchés vers la
|
||||
ville et l’ancienne mine ne sont pas admis sur ce témoin.
|
||||
|
||||
Les sept liaisons comprennent un tracé de fret **dépôt → pompage de 499 blocs**,
|
||||
d’une étendue de 245 blocs sur X et 181 sur Z. Son ballast est praticable ;
|
||||
478 rails sont présents et 21 sont volontairement manquants. Les autres voies
|
||||
et chemins relient gare, dépôt, atelier, poste, club photo, serre et vestiaires.
|
||||
Le quai, l’observatoire, le relais, la sous-station et le refuge gardent leurs
|
||||
propres accès au terrain, même sans liaison avec ce réseau.
|
||||
|
||||
La création et la réouverture dans deux JVM distinctes passent chacune les deux
|
||||
GameTests : **112 pièces natives, 111 chunks FULL, 80 487 blocs contrôlés,
|
||||
2 882 contrôles de portance, 13 coffres différés et 68 cibles accessibles**.
|
||||
Trois tirages de chacune des quatorze tables de butin sont vérifiés
|
||||
séparément, sans ouvrir les coffres persistants. Une modification témoin à
|
||||
`-204 / 255 / -13` est conservée à la réouverture.
|
||||
|
||||
Empreinte stable du plan :
|
||||
`7f476c2ecca703a05bf88a550c61cc5cbeff2594024c43ccd9f3620bbc2ad3de`.
|
||||
JVM de création `36366`, de réouverture `36481`. Reçus locaux ignorés :
|
||||
`build/alpha19-evidence/heritage19-create.json` et `heritage19-reload.json`.
|
||||
|
||||
La planification supplémentaire utilise 15 144 colonnes natives, dans un plafond
|
||||
de 16 000, en 20,7 s sur le Mac de développement. Ce temps concerne ce plan,
|
||||
pas le chargement total du jeu. Les routes sont découpées par chunk ; aucun
|
||||
start ne couvre toute leur longueur ni ne charge une destination lointaine.
|
||||
|
||||
La prospection 19 consulte les plans hydrologiques de toute l’enveloppe de
|
||||
départ, avant de publier les sites. Le contrôle général utilise donc pour 19
|
||||
une limite géographique calculée : 5 régions possibles pour Petit, 9 pour
|
||||
Moyen/Grand, avec les budgets de colonnes et de sondes contrôlés séparément.
|
||||
Le témoin Moyen en consulte 5 ; les quatre régions périphériques représentent
|
||||
3,38 s de calcul hydrologique supplémentaire dans le relevé du build.
|
||||
Ces consultations ne matérialisent aucun chunk. Les générations historiques
|
||||
conservent le contrôle initial de 4 régions maximum autour du spawn.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryHeritage19Tests=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryHeritage19Tests=true -PsanctuaryExpansionReload=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false
|
||||
```
|
||||
|
||||
## Autres tailles et couverture du catalogue
|
||||
|
||||
Les deux GameTests natifs passent aussi sur Petit et Grand, graine 42.
|
||||
Les quatorze architectures ont ainsi chacune été rencontrées et parcourues
|
||||
sur le terrain réel, en complément des rotations vérifiées par les smokes.
|
||||
|
||||
| Taille et graine | Lieux | Liaisons | Pièces natives | Chunks FULL |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| Petit 512, 42 | 10 | 2 | 35 | 44 |
|
||||
| Moyen 724, 0 | 13 | 7 | 112 | 111 |
|
||||
| Grand 1 024, 42 | 13 | 4 | 47 | 61 |
|
||||
|
||||
Pour visiter la cartothèque : **Petit, graine 42**, entrée
|
||||
`-101 / 251 / -25`, ou **Grand, graine 42**, entrée `-252 / 249 / 146`.
|
||||
Le Petit testé n’a ni poste, ni observatoire, ni atelier ferroviaire, ni quai.
|
||||
Le Grand testé n’a pas de quai. Aucun grand axe n’est admis sur ces deux témoins :
|
||||
les portées maximales de leurs liaisons sont respectivement de 32 et 55 blocs.
|
||||
Le témoin Moyen, graine 0, reste celui conseillé pour découvrir le grand fret.
|
||||
Ces résultats rendent explicite la variation de cette première passe ; ils ne
|
||||
constituent pas une garantie de présence ou de connectivité pour chaque graine.
|
||||
|
||||
Reçus locaux ignorés : `build/alpha19-evidence/small42-create.json` (JVM 36591)
|
||||
et `large42-create.json` (JVM 36669).
|
||||
|
||||
## Compatibilité 17 vérifiée
|
||||
|
||||
Deux JVM de développement distinctes créent puis rouvrent le témoin 17
|
||||
**Moyen, graine 0**, avec le nouveau binaire. Les deux GameTests passent dans
|
||||
chaque exécution. Le codec, les starts natifs, les butins différés et les
|
||||
parcours restent identiques aux preuves de l’alpha.17 publiée. Le bloc d’or
|
||||
placé par le test est conservé à la réouverture.
|
||||
|
||||
Empreinte du plan 17 :
|
||||
`7358ecd7841c03be1977a7f45f37c0c3ef83a32dc27e8d3ec4f0d85d6ee5d3ef`.
|
||||
Les trois starts restent ceux documentés dans [le témoin 17](testing-alpha17.md).
|
||||
Reçus locaux ignorés : `build/alpha19-evidence/legacy17-create.json` et
|
||||
`legacy17-reload.json`.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryRuins17Tests=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryRuins17Tests=true -PsanctuaryExpansionReload=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false
|
||||
```
|
||||
|
||||
Ces commandes visent uniquement le monde de développement ignoré. La première
|
||||
le recrée ; la seconde conserve ce même monde et lit ses preuves précédentes.
|
||||
+2
-2
@@ -8,6 +8,6 @@ loader_version=0.19.5
|
||||
loom_version=1.17.20
|
||||
fabric_api_version=0.160.0+26.3
|
||||
|
||||
mod_version=0.1.0-alpha.17
|
||||
pack_version=0.1.0-alpha.17
|
||||
mod_version=0.1.0-alpha.19
|
||||
pack_version=0.1.0-alpha.19
|
||||
maven_group=fr.koka.sanctuary
|
||||
|
||||
@@ -40,13 +40,14 @@ tasks.named('runGameTest') {
|
||||
systemProperty('sanctuary.test.unified', providers.gradleProperty('sanctuaryUnifiedTests').getOrElse('true'))
|
||||
systemProperty('sanctuary.test.diameter', providers.gradleProperty('sanctuaryTestDiameter').getOrElse('724'))
|
||||
systemProperty('sanctuary.test.generation', providers.gradleProperty('sanctuaryTestGeneration').getOrElse(
|
||||
providers.gradleProperty('sanctuaryLostCityTests').map { it.toBoolean() }.getOrElse(false) ? '14' : providers.gradleProperty('sanctuaryLostCity15Tests').map { it.toBoolean() }.getOrElse(false) ? '15' : providers.gradleProperty('sanctuaryLostCity16Tests').map { it.toBoolean() }.getOrElse(false) ? '16' : '17'))
|
||||
providers.gradleProperty('sanctuaryLostCityTests').map { it.toBoolean() }.getOrElse(false) ? '14' : providers.gradleProperty('sanctuaryLostCity15Tests').map { it.toBoolean() }.getOrElse(false) ? '15' : providers.gradleProperty('sanctuaryLostCity16Tests').map { it.toBoolean() }.getOrElse(false) ? '16' : providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false) ? '17' : '19'))
|
||||
systemProperty('sanctuary.test.expansion', providers.gradleProperty('sanctuaryExpansionTests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.expansionReload', providers.gradleProperty('sanctuaryExpansionReload').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.lostCity', providers.gradleProperty('sanctuaryLostCityTests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.lostCity15', providers.gradleProperty('sanctuaryLostCity15Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.lostCity16', providers.gradleProperty('sanctuaryLostCity16Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.ruins17', providers.gradleProperty('sanctuaryRuins17Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.heritage19', providers.gradleProperty('sanctuaryHeritage19Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.experimentalStructures', providers.gradleProperty('sanctuaryExperimentalStructures').getOrElse('true'))
|
||||
systemProperty('sanctuary.test.cityMatrix', providers.gradleProperty('sanctuaryCityMatrix').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.cityMatrixOnly', providers.gradleProperty('sanctuaryCityMatrixOnly').getOrElse('false'))
|
||||
@@ -69,10 +70,12 @@ tasks.named('processGametestResources') {
|
||||
inputs.property('lostCity16', lostCity16)
|
||||
def ruins17 = providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('ruins17', ruins17)
|
||||
def heritage19 = providers.gradleProperty('sanctuaryHeritage19Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('heritage19', heritage19)
|
||||
def voidReuse = providers.gradleProperty('sanctuaryVoidReuseTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('voidReuse', voidReuse)
|
||||
filesMatching('fabric.mod.json') {
|
||||
expand(gametest_entrypoint: ruins17 ? 'fr.koka.sanctuary.gametest.Ruins17WorldGameTests' : voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark
|
||||
expand(gametest_entrypoint: heritage19 ? 'fr.koka.sanctuary.gametest.Heritage19WorldGameTests' : ruins17 ? 'fr.koka.sanctuary.gametest.Ruins17WorldGameTests' : voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark
|
||||
? 'fr.koka.sanctuary.gametest.WorldgenPerformanceGameTests'
|
||||
: unified ? 'fr.koka.sanctuary.gametest.UnifiedWorldGameTests'
|
||||
: 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests')
|
||||
@@ -415,3 +418,14 @@ tasks.named('check') { dependsOn('lostCityFoundation16Smoke') }
|
||||
}
|
||||
tasks.named('check') { dependsOn(smoke) }
|
||||
}
|
||||
|
||||
// Local native starts keep the long heritage routes bounded by Minecraft's reference range.
|
||||
['Planner', 'Renderer', 'Piece'].each { component ->
|
||||
def smoke = tasks.register("heritage19${component}Smoke", JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka.sanctuary.worldgen.heritage.Heritage${component}19Smoke"
|
||||
}
|
||||
tasks.named('check') { dependsOn(smoke) }
|
||||
}
|
||||
|
||||
+452
@@ -0,0 +1,452 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritagePiece19;
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19;
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritageRenderer19;
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritageStructures19;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.DoorBlock;
|
||||
import net.minecraft.world.level.block.TrapDoorBlock;
|
||||
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import net.minecraft.world.level.storage.loot.LootParams;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
|
||||
import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Native creation/reload proof confined to a disposable generation19 starter island. */
|
||||
public final class Heritage19WorldGameTests {
|
||||
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
|
||||
@GameTest(maxTicks = 12000)
|
||||
public void heritagePersistsWithRealDestinations(GameTestHelper helper) {
|
||||
helper.assertTrue(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator,
|
||||
"The heritage proof requires a Sanctuary generator");
|
||||
var generator = (SanctuaryChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
helper.assertTrue(generator.rootGeneration() == 19, "Use a fresh generation19 test world; do not convert an old save");
|
||||
new Scenario(helper, generator, Boolean.getBoolean("sanctuary.test.expansionReload")).advance();
|
||||
}
|
||||
private static final class Scenario {
|
||||
final GameTestHelper helper;
|
||||
final ServerLevel level;
|
||||
final SanctuaryChunkGenerator generator;
|
||||
final HeritagePlan19 plan;
|
||||
final boolean reload;
|
||||
final Map<String, Object> report = new LinkedHashMap<>();
|
||||
final List<ChunkPos> chunks = new ArrayList<>();
|
||||
final List<StructureStart> starts = new ArrayList<>();
|
||||
JsonObject previous;
|
||||
BlockPos gold;
|
||||
int nextChunk;
|
||||
boolean prepared;
|
||||
Scenario(GameTestHelper helper, SanctuaryChunkGenerator generator, boolean reload) {
|
||||
this.helper = helper; this.level = helper.getLevel(); this.generator = generator; this.reload = reload;
|
||||
this.plan = generator.heritagePlan19();
|
||||
report.put("seed", level.getSeed()); report.put("size", generator.initialDiameter());
|
||||
report.put("process_id", ProcessHandle.current().pid()); report.put("world_directory", world(level).toString());
|
||||
report.put("experimental_structures", generator.experimentalStructures());
|
||||
report.put("structures_enabled", level.structureManager().shouldGenerateStructures());
|
||||
report.put("scope", "Native local starts, saved sites and routes, player-sized entrances, supports and unopened chests on the starter island. "
|
||||
+ "All14 tool tables are sampled separately without consuming a saved chest. Door states are restored after traversal. "
|
||||
+ "No personal save or expansion is opened; one disposable outer-wall edit is checked in a second JVM. "
|
||||
+ "Missing rails are intentional; this proof checks walking and saved rail shapes, not an uninterrupted minecart journey.");
|
||||
}
|
||||
void advance() {
|
||||
try {
|
||||
if (!prepared) { report.put("stage", "prepare"); prepare(); prepared = true; if (plan.isEmpty()) { finishDisabled(); return; } }
|
||||
if (nextChunk < chunks.size()) {
|
||||
report.put("stage", "load_full_chunks"); var pos = chunks.get(nextChunk++);
|
||||
helper.assertTrue(level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true) instanceof LevelChunk,
|
||||
"Heritage footprint must reach native FULL: " + pos);
|
||||
if (nextChunk % 20 == 0) SanctuaryMod.LOGGER.info("Heritage19 loaded {}/{} footprint chunks", nextChunk, chunks.size());
|
||||
helper.runAtTickTime(helper.getTick() + 1, this::advance); return;
|
||||
}
|
||||
report.put("stage", "verify_native_geometry"); verifyPlaced(); report.put("passed", true);
|
||||
if (!reload) {
|
||||
helper.assertTrue(gold != null && level.getBlockState(gold).isCollisionShapeFullBlock(level, gold), "Gold witness must replace a solid wall away from circulation");
|
||||
level.setBlock(gold, Blocks.GOLD_BLOCK.defaultBlockState(), 3); report.put("gold_witness", coords(gold));
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush native starts, unopened loot and the wall edit");
|
||||
report.put("world_flushed_before_shutdown", true);
|
||||
} else {
|
||||
helper.assertTrue(level.getBlockState(gold).is(Blocks.GOLD_BLOCK), "Reload replaced the player's wall edit");
|
||||
report.put("gold_witness", coords(gold)); report.put("gold_witness_preserved", true);
|
||||
}
|
||||
write(level, report, reload); helper.succeed();
|
||||
} catch (Exception failure) {
|
||||
report.put("passed", false); report.put("failure", failure.toString());
|
||||
try { write(level, report, reload); } catch (IOException ignored) { }
|
||||
throw new IllegalStateException("Heritage19 native " + (reload ? "reload" : "creation") + " failed", failure);
|
||||
}
|
||||
}
|
||||
void prepare() throws IOException {
|
||||
var ops = RegistryOps.create(JsonOps.INSTANCE, level.registryAccess());
|
||||
var encoded = ChunkGenerator.CODEC.encodeStart(ops, generator).getOrThrow();
|
||||
var decoded = ChunkGenerator.CODEC.parse(ops, encoded).getOrThrow();
|
||||
helper.assertTrue(decoded instanceof SanctuaryChunkGenerator copy && copy.rootGeneration() == 19
|
||||
&& copy.experimentalStructures() == generator.experimentalStructures(), "Generation19 codec lost its experimental switch");
|
||||
report.put("generator_codec", encoded); report.put("plan_sha256", planHash(plan));
|
||||
report.put("planned_sites", plan.sites().stream().map(site -> Map.of("id", site.id(), "kind", site.kind().name(),
|
||||
"entry", coords(HeritagePiece19.locator(site)), "floor_y", site.floorY(), "foundation_depth", site.foundationDepth(), "footings", site.supports().size())).toList());
|
||||
report.put("planned_links", plan.links().stream().map(link -> Map.of("id", link.id(), "from", link.fromId(), "to", link.toId(), "kind", link.kind().name(), "length", link.path().size(), "path", pathCoords(link))).toList());
|
||||
report.put("planning_samples", plan.samples()); report.put("planning_columns", plan.columns()); report.put("planning_diagnostics", plan.diagnostics());
|
||||
helper.assertTrue(plan.samples() <= HeritagePlan19.MAX_SAMPLES && plan.columns() <= HeritagePlan19.MAX_COLUMNS, "Heritage planning exceeded its budget");
|
||||
verifyLootTables(helper, report);
|
||||
if (reload) {
|
||||
previous = JsonParser.parseString(Files.readString(proof(level))).getAsJsonObject();
|
||||
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("world_flushed_before_shutdown").getAsBoolean(), "Reload needs a successful flushed creation proof");
|
||||
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload must use a second JVM");
|
||||
helper.assertTrue(previous.get("seed").getAsLong() == level.getSeed() && previous.get("size").getAsInt() == generator.initialDiameter()
|
||||
&& previous.get("world_directory").getAsString().equals(world(level).toString()), "Reload selected another world");
|
||||
helper.assertTrue(previous.get("generator_codec").equals(encoded) && previous.get("plan_sha256").getAsString().equals(planHash(plan)), "Saved settings or planned identities changed on reload");
|
||||
if (previous.has("gold_witness")) gold = readPos(previous.getAsJsonArray("gold_witness"));
|
||||
}
|
||||
if (!generator.experimentalStructures() || !level.structureManager().shouldGenerateStructures()) {
|
||||
helper.assertTrue(plan.isEmpty() && plan.segments().isEmpty() && plan.links().isEmpty(), "Disabled generation published heritage geometry"); return;
|
||||
}
|
||||
helper.assertTrue(!plan.isEmpty(), "Enabled reference island has no heritage site");
|
||||
report.put("present_site_kinds", plan.sites().stream().map(s -> s.kind().name()).distinct().sorted().toList());
|
||||
report.put("missing_site_kinds", java.util.Arrays.stream(HeritagePlan19.Kind.values()).filter(k -> plan.sites().stream().noneMatch(s -> s.kind() == k)).map(Enum::name).toList());
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
|
||||
var origins = new HashSet<ChunkPos>(); var uniqueChunks = new HashSet<ChunkPos>();
|
||||
plan.sites().forEach(s -> origins.add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
|
||||
plan.segments().forEach(s -> origins.add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
|
||||
for (var origin : origins.stream().sorted(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList()) {
|
||||
var chunk = level.getChunk(origin.x(), origin.z(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
var start = chunk.getStartForStructure(structure.value());
|
||||
helper.assertTrue(start != null && start.isValid(), "Accepted site/route has no native start at " + origin); starts.add(start);
|
||||
for (var piece : start.getPieces()) {
|
||||
helper.assertTrue(piece instanceof HeritagePiece19, "Heritage loaded a foreign piece type");
|
||||
var box = piece.getBoundingBox();
|
||||
helper.assertTrue(Math.abs((long) (box.minX() >> 4) - origin.x()) <= 7 && Math.abs((long) (box.maxX() >> 4) - origin.x()) <= 7
|
||||
&& Math.abs((long) (box.minZ() >> 4) - origin.z()) <= 7 && Math.abs((long) (box.maxZ() >> 4) - origin.z()) <= 7, "Native start references a distant route endpoint");
|
||||
box.intersectingChunks().forEach(uniqueChunks::add);
|
||||
}
|
||||
}
|
||||
for (var site : plan.sites()) uniqueChunks.add(ChunkPos.containing(HeritagePiece19.locator(site)));
|
||||
for (var link : plan.links()) for (var point : link.path()) uniqueChunks.add(new ChunkPos(point.x() >> 4, point.z() >> 4));
|
||||
helper.assertTrue(uniqueChunks.size() <= 1024, "Bounded proof footprint exceeds1024 chunks");
|
||||
chunks.addAll(uniqueChunks.stream().sorted(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList());
|
||||
}
|
||||
void finishDisabled() throws IOException {
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
|
||||
var start = level.getChunk(0, 0, ChunkStatus.STRUCTURE_STARTS, true).getStartForStructure(structure.value());
|
||||
helper.assertTrue(start == null || !start.isValid(), "Disabled heritage emitted a native start");
|
||||
helper.assertTrue(generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false) == null, "Disabled heritage is locatable");
|
||||
report.put("empty_plan_verified", true); report.put("passed", true);
|
||||
if (!reload) { helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save disabled experiment setting"); report.put("world_flushed_before_shutdown", true); }
|
||||
write(level, report, reload); helper.succeed();
|
||||
}
|
||||
void verifyPlaced() {
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
|
||||
var context = StructurePieceSerializationContext.fromLevel(level); var summaries = new ArrayList<Map<String, Object>>();
|
||||
int blocks = 0, chests = 0, pieces = 0, supports = 0;
|
||||
for (var start : starts) {
|
||||
var origin = start.getChunkPos(); var saved = start.createTag(context, origin); String nativeHash = hash(saved.toString());
|
||||
var copy = StructureStart.loadStaticStart(context, saved, level.getSeed());
|
||||
helper.assertTrue(copy != null && copy.isValid() && copy.createTag(context, origin).equals(saved), "Native start failed an exact NBT roundtrip");
|
||||
var expectedPayloads = new HashSet<net.minecraft.nbt.CompoundTag>();
|
||||
plan.sites().stream().filter(s -> s.startChunkX() == origin.x() && s.startChunkZ() == origin.z()).forEach(s -> expectedPayloads.add(HeritagePiece19.encodeSite(s)));
|
||||
plan.segments().stream().filter(s -> s.startChunkX() == origin.x() && s.startChunkZ() == origin.z()).forEach(s -> expectedPayloads.add(HeritagePiece19.encodeSegment(s)));
|
||||
var actualPayloads = new HashSet<net.minecraft.nbt.CompoundTag>();
|
||||
if (reload) {
|
||||
JsonObject prior = null;
|
||||
for (var value : previous.getAsJsonArray("starts")) { var item = value.getAsJsonObject(); var at = item.getAsJsonArray("start_chunk"); if (at.get(0).getAsInt() == origin.x() && at.get(1).getAsInt() == origin.z()) prior = item; }
|
||||
helper.assertTrue(prior != null && prior.get("native_start_sha256").getAsString().equals(nativeHash), "Reload changed saved geometry or native reference count");
|
||||
}
|
||||
var loot = new ArrayList<Map<String, Object>>();
|
||||
for (var value : start.getPieces()) {
|
||||
var piece = (HeritagePiece19) value; pieces++;
|
||||
var tag = piece.createTag(context); var restored = new HeritagePiece19(context, tag);
|
||||
helper.assertTrue(restored.createTag(context).equals(tag), "Saved piece parameters changed on native load");
|
||||
actualPayloads.add(tag.getCompound("SanctuaryHeritage").orElseThrow());
|
||||
var corrupt = tag.copy(); corrupt.getCompound("SanctuaryHeritage").orElseThrow().putInt("Schema", 20);
|
||||
boolean rejected = false; try { new HeritagePiece19(context, corrupt); } catch (IllegalArgumentException expected) { rejected = true; }
|
||||
helper.assertTrue(rejected, "Unknown schema must fail closed");
|
||||
piece.getBoundingBox().intersectingChunks().forEach(at -> helper.assertTrue(level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true)
|
||||
.getReferencesForStructure(structure.value()).contains(origin.pack()), "FULL chunk lost its native structure reference"));
|
||||
var expected = rendered(piece); blocks += expected.blocks().size();
|
||||
for (var entry : expected.blocks().entrySet()) {
|
||||
var at = entry.getKey(); var wanted = entry.getValue(); helper.assertTrue(piece.getBoundingBox().isInside(at), "Renderer escaped saved bounds");
|
||||
if (reload && at.equals(gold)) continue;
|
||||
helper.assertTrue(level.getBlockState(at).is(wanted.getBlock()), "Clipping or decoration changed heritage at " + at + ": expected " + wanted + " got " + level.getBlockState(at));
|
||||
helper.assertTrue(!wanted.is(Blocks.SPAWNER), "Heritage must not create spawners");
|
||||
}
|
||||
if (piece.isSite()) {
|
||||
supports += verifySiteBearing(helper, generator, plan, piece.site());
|
||||
if (!reload && gold == null) {
|
||||
gold = expected.blocks().entrySet().stream().filter(e -> isOuterWall(piece.site(), e.getKey()) && e.getValue().isCollisionShapeFullBlock(level, e.getKey()))
|
||||
.map(Map.Entry::getKey).sorted(Comparator.<BlockPos>comparingInt(p -> p.getY()).thenComparingInt(p -> p.getX()).thenComparingInt(p -> p.getZ())).findFirst().orElse(null);
|
||||
}
|
||||
} else for (var cell : piece.segment().cells()) { supports++; assertRock(helper, new BlockPos(cell.x(), cell.minY() - 1, cell.z())); }
|
||||
for (var chest : expected.chests()) {
|
||||
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Deferred chest is missing");
|
||||
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
|
||||
helper.assertTrue(entity.getLootTable() != null && entity.getLootTable().equals(chest.table()) && entity.getLootTableSeed() == chest.seed(), "Deferred loot table/seed was lost or consumed");
|
||||
if (piece.isSite()) helper.assertTrue(chest.table().equals(lootKey(piece.site().kind())), "Site uses another kind's tool table");
|
||||
loot.add(Map.of("position", coords(chest.position()), "table", chest.table().identifier().toString(), "seed", chest.seed())); chests++;
|
||||
}
|
||||
}
|
||||
helper.assertTrue(actualPayloads.equals(expectedPayloads) && start.getPieces().size() == expectedPayloads.size(), "Native start lost or duplicated planned pieces");
|
||||
summaries.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", nativeHash, "pieces", start.getPieces().size(), "unopened_chests", loot));
|
||||
}
|
||||
report.put("starts", summaries); report.put("native_pieces", pieces); report.put("native_blocks_checked", blocks);
|
||||
report.put("full_chunks_checked", chunks.size()); report.put("rock_supports_checked", supports); report.put("unopened_deferred_chests", chests);
|
||||
report.put("stage", "verify_site_routes"); var sites = new ArrayList<Map<String, Object>>(); for (var site : plan.sites()) sites.add(verifyAccess(helper, site)); report.put("native_sites", sites);
|
||||
report.put("stage", "verify_read_only_landings");
|
||||
report.put("read_only_landmarks", verifyReadOnlyLandings(helper, generator, plan));
|
||||
report.put("stage", "verify_island_routes"); report.put("native_links", verifyLinks(helper, plan));
|
||||
int greatestSpan = plan.links().stream().mapToInt(Heritage19WorldGameTests::routeSpan).max().orElse(0);
|
||||
report.put("greatest_single_route_span", greatestSpan);
|
||||
if (level.getSeed() == 0 && generator.initialDiameter() == 724) {
|
||||
int requiredSpan = generator.initialDiameter() / 4;
|
||||
report.put("reference_minimum_route_span", requiredSpan);
|
||||
helper.assertTrue(greatestSpan >= requiredSpan, "Seed0 Medium must demonstrate an island-scale route spanning at least "
|
||||
+ requiredSpan + " blocks on X or Z; got " + greatestSpan);
|
||||
}
|
||||
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
|
||||
helper.assertTrue(located != null && located.getSecond().is(HeritageStructures19.KEY) && plan.sites().stream().anyMatch(s -> HeritagePiece19.locator(s).equals(located.getFirst())), "Locate must lead to a real exterior site entry");
|
||||
report.put("locate", coords(located.getFirst()));
|
||||
}
|
||||
}
|
||||
private record Chest(BlockPos position, ResourceKey<LootTable> table, long seed) {}
|
||||
private record Rendered(Map<BlockPos, BlockState> blocks, List<Chest> chests) {}
|
||||
private static Rendered rendered(HeritagePiece19 piece) {
|
||||
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
|
||||
int x0 = piece.isSite() ? piece.site().minX() : piece.segment().minX();
|
||||
int y0 = piece.isSite() ? piece.site().floorY() : piece.segment().minY();
|
||||
int z0 = piece.isSite() ? piece.site().minZ() : piece.segment().minZ();
|
||||
var writer = new LostCityRenderer.BlockWriter() {
|
||||
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(new BlockPos(x0 + x, y0 + y, z0 + z), state); }
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
|
||||
var at = new BlockPos(x0 + x, y0 + y, z0 + z); blocks.put(at, Blocks.CHEST.defaultBlockState()); chests.add(new Chest(at, table, RandomSource.create(seed).nextLong()));
|
||||
}
|
||||
};
|
||||
if (piece.isSite()) HeritageRenderer19.render(piece.site(), writer); else HeritageRenderer19.renderSegment(piece.segment(), writer);
|
||||
return new Rendered(blocks, chests);
|
||||
}
|
||||
private static boolean isOuterWall(HeritagePlan19.Site site, BlockPos pos) {
|
||||
return pos.getY() >= site.floorY() + 2 && pos.getY() <= site.floorY() + 3
|
||||
&& (pos.getX() == site.minX() + 2 || pos.getX() == site.maxX() - 2 || pos.getZ() == site.minZ() + 2 || pos.getZ() == site.maxZ() - 2)
|
||||
&& Math.abs(pos.getX() - site.entry().x()) + Math.abs(pos.getZ() - site.entry().z()) > 6;
|
||||
}
|
||||
private static Map<String, Object> verifyAccess(GameTestHelper helper, HeritagePlan19.Site site) {
|
||||
var level = helper.getLevel(); var expected = rendered(new HeritagePiece19(site)); var doors = new HashMap<BlockPos, BlockState>();
|
||||
expected.blocks().forEach((at, state) -> {
|
||||
if ((state.getBlock() instanceof DoorBlock || state.getBlock() instanceof TrapDoorBlock) && state.hasProperty(BlockStateProperties.OPEN)) doors.put(at, level.getBlockState(at));
|
||||
});
|
||||
doors.forEach((at, state) -> level.setBlock(at, state.setValue(BlockStateProperties.OPEN, true), 2));
|
||||
try {
|
||||
var entry = HeritagePiece19.locator(site);
|
||||
Predicate<BlockPos> inside = p -> p.equals(entry) || p.getX() >= site.minX() && p.getX() <= site.maxX()
|
||||
&& p.getZ() >= site.minZ() && p.getZ() <= site.maxZ() && p.getY() >= site.floorY() - 4 && p.getY() < site.maxY();
|
||||
var reached = walk(helper, entry, inside); int targets = 0;
|
||||
for (var target : HeritageRenderer19.targets(site)) {
|
||||
var at = new BlockPos(site.minX() + target.x(), site.floorY() + target.y(), site.minZ() + target.z());
|
||||
helper.assertTrue(reached.contains(at), "Site " + site.id() + " cannot reach " + target.name() + " at " + at); targets++;
|
||||
}
|
||||
return Map.of("id", site.id(), "kind", site.kind().name(), "entry", coords(entry), "standing_cells", reached.size(), "walkable_targets", targets);
|
||||
} finally { doors.forEach((at, state) -> level.setBlock(at, state, 2)); }
|
||||
}
|
||||
private static List<Map<String, Object>> verifyLinks(GameTestHelper helper, HeritagePlan19 plan) {
|
||||
helper.assertTrue(!plan.links().isEmpty(), "Reference island needs a real route between accepted destinations");
|
||||
var entries = new HashMap<String, HeritagePlan19.Entry>(); plan.sites().forEach(s -> entries.put(s.id(), s.entry())); plan.landmarks().forEach(s -> entries.put(s.id(), s.entry()));
|
||||
var cells = new HashMap<Long, HeritagePlan19.Cell>(); plan.segments().forEach(s -> s.cells().forEach(c -> cells.put(column(c.x(), c.z()), c)));
|
||||
var result = new ArrayList<Map<String, Object>>();
|
||||
for (var link : plan.links()) {
|
||||
var from = entries.get(link.fromId()); var to = entries.get(link.toId());
|
||||
helper.assertTrue(from != null && to != null, "Route points to an absent destination");
|
||||
var first = link.path().getFirst(); var last = link.path().getLast();
|
||||
helper.assertTrue(first.x() == from.x() && first.z() == from.z() && first.floorY() + 1 == from.y()
|
||||
&& last.x() == to.x() && last.z() == to.z() && last.floorY() + 1 == to.y(), "Route does not meet its real entrances");
|
||||
int present = 0, missing = 0;
|
||||
for (int i = 0; i < link.path().size(); i++) {
|
||||
var point = link.path().get(i); var at = new BlockPos(point.x(), point.floorY() + 1, point.z());
|
||||
helper.assertTrue(standable(helper.getLevel(), at), "Ballast route " + link.id() + " is blocked at " + at);
|
||||
if (i > 0) { var prior = link.path().get(i - 1); if (point.floorY() > prior.floorY()) helper.assertTrue(clearBody(helper.getLevel(), new BlockPos(prior.x(), prior.floorY() + 2, prior.z())), "Route slope cannot be climbed by a player"); }
|
||||
var cell = cells.get(column(point.x(), point.z()));
|
||||
if (cell != null && cell.rail() != HeritagePlan19.Rail.NONE) {
|
||||
var actual = helper.getLevel().getBlockState(at);
|
||||
if (cell.railPresent()) {
|
||||
helper.assertTrue(actual.is(Blocks.RAIL), "Saved existing rail disappeared");
|
||||
helper.assertTrue(actual.getValue(BlockStateProperties.RAIL_SHAPE).name().equals(cell.rail().name()), "Native rail update changed the intended slope or curve at " + at); present++;
|
||||
} else { helper.assertTrue(!actual.is(Blocks.RAIL), "An intentionally missing rail regenerated"); missing++; }
|
||||
}
|
||||
}
|
||||
int spanX = link.path().stream().mapToInt(HeritagePlan19.Point::x).max().orElseThrow() - link.path().stream().mapToInt(HeritagePlan19.Point::x).min().orElseThrow();
|
||||
int spanZ = link.path().stream().mapToInt(HeritagePlan19.Point::z).max().orElseThrow() - link.path().stream().mapToInt(HeritagePlan19.Point::z).min().orElseThrow();
|
||||
result.add(Map.of("id", link.id(), "from", link.fromId(), "to", link.toId(), "kind", link.kind().name(), "length", link.path().size(),
|
||||
"span_x", spanX, "span_z", spanZ, "existing_rails", present, "deliberately_missing_rails", missing, "path", pathCoords(link)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private static int routeSpan(HeritagePlan19.Link link) {
|
||||
int spanX = link.path().stream().mapToInt(HeritagePlan19.Point::x).max().orElseThrow()
|
||||
- link.path().stream().mapToInt(HeritagePlan19.Point::x).min().orElseThrow();
|
||||
int spanZ = link.path().stream().mapToInt(HeritagePlan19.Point::z).max().orElseThrow()
|
||||
- link.path().stream().mapToInt(HeritagePlan19.Point::z).min().orElseThrow();
|
||||
return Math.max(spanX, spanZ);
|
||||
}
|
||||
private static List<List<Integer>> pathCoords(HeritagePlan19.Link link) {
|
||||
return link.path().stream().map(point -> List.of(point.x(), point.floorY(), point.z())).toList();
|
||||
}
|
||||
private static int verifySiteBearing(GameTestHelper helper, SanctuaryChunkGenerator generator, HeritagePlan19 plan, HeritagePlan19.Site site) {
|
||||
int checked = 0;
|
||||
// The planner certifies a nine-point bearing grid, either directly below its slab or below a saved short footing.
|
||||
for (int dx : new int[] {1, 8, 15}) for (int dz : new int[] {1, 8, 15}) {
|
||||
int x = site.minX() + dx, z = site.minZ() + dz;
|
||||
var support = site.supports().stream().filter(s -> s.x() == x && s.z() == z).findFirst();
|
||||
int bottom = support.map(HeritagePlan19.Support::minY).orElse(site.minY());
|
||||
for (int below = 1; below <= 2; below++) {
|
||||
var at = new BlockPos(x, bottom - below, z); assertRock(helper, at);
|
||||
helper.assertTrue(plan.protects(at.getX(), at.getY(), at.getZ()) && generator.protectsCityDecoration(at),
|
||||
"Site's certified bearing layer is not published in the decoration mask: " + site.id() + " at " + at);
|
||||
helper.assertTrue(!plan.contains(at.getX(), at.getY(), at.getZ()), "Original bearing rock became a writable heritage volume: " + at);
|
||||
checked++;
|
||||
}
|
||||
}
|
||||
return checked;
|
||||
}
|
||||
private static List<Map<String, Object>> verifyReadOnlyLandings(GameTestHelper helper, SanctuaryChunkGenerator generator, HeritagePlan19 plan) {
|
||||
var used = new HashSet<String>(); plan.links().forEach(link -> { used.add(link.fromId()); used.add(link.toId()); });
|
||||
var result = new ArrayList<Map<String, Object>>();
|
||||
for (var landmark : plan.landmarks()) if (used.contains(landmark.id())) {
|
||||
var e = landmark.entry(); int checked = 0;
|
||||
for (int side = -1; side <= 1; side++) {
|
||||
int x = e.x() + ((e.facing() & 1) == 0 ? side : 0), z = e.z() + ((e.facing() & 1) == 1 ? side : 0);
|
||||
for (int y = e.y() - 2; y <= e.y() + 2; y++) {
|
||||
var at = new BlockPos(x, y, z);
|
||||
helper.assertTrue(plan.protects(x, y, z) && generator.protectsCityDecoration(at), "Read-only arrival lost its decoration mask: " + landmark.id() + " at " + at);
|
||||
helper.assertTrue(!plan.contains(x, y, z), "Heritage gained write ownership of a historical arrival: " + landmark.id() + " at " + at);
|
||||
if (y == e.y() - 1) assertRock(helper, at);
|
||||
if (y >= e.y()) helper.assertTrue(helper.getLevel().getBlockState(at).isAir(), "Decoration obstructed a certified historical arrival: " + at);
|
||||
checked++;
|
||||
}
|
||||
}
|
||||
result.add(Map.of("id", landmark.id(), "entry", List.of(e.x(), e.y(), e.z()), "read_only_cells", checked));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private static void assertRock(GameTestHelper helper, BlockPos at) {
|
||||
helper.assertTrue(helper.getLevel().getBlockState(at).isSolid() && helper.getLevel().getFluidState(at).isEmpty(), "Lost original rock bearing at " + at);
|
||||
}
|
||||
private static void verifyLootTables(GameTestHelper helper, Map<String, Object> report) {
|
||||
var result = new ArrayList<Map<String, Object>>(); var level = helper.getLevel();
|
||||
var params = new LootParams.Builder(level).withParameter(LootContextParams.ORIGIN, new Vec3(0, 160, 0)).create(LootContextParamSets.CHEST);
|
||||
for (var kind : HeritagePlan19.Kind.values()) {
|
||||
var table = level.getServer().reloadableRegistries().getLootTable(lootKey(kind));
|
||||
helper.assertTrue(table != LootTable.EMPTY, "Heritage tool table did not load: " + kind);
|
||||
for (long seed : new long[] {0, 42, -1}) helper.assertTrue(table.getRandomItems(params, seed).stream().anyMatch(stack -> stack.is(tool(kind))), "Tool table is empty or lacks its expected primary tool: " + kind);
|
||||
result.add(Map.of("kind", kind.name(), "table", lootKey(kind).identifier().toString(), "sampled_seeds", List.of(0, 42, -1)));
|
||||
}
|
||||
report.put("tool_tables_sampled_separately", result);
|
||||
}
|
||||
private static ResourceKey<LootTable> lootKey(HeritagePlan19.Kind kind) { return ResourceKey.create(Registries.LOOT_TABLE, SanctuaryMod.id("chests/heritage19/" + kind.name().toLowerCase(Locale.ROOT))); }
|
||||
private static Item tool(HeritagePlan19.Kind kind) {
|
||||
return switch (kind) {
|
||||
case OBSERVATORY -> Items.SPYGLASS;
|
||||
case POST_OFFICE, EXPEDITION_OFFICE, RADIO_RELAY -> Items.COMPASS;
|
||||
case SUBSTATION, IMPOSSIBLE_SERVICE -> Items.CLOCK;
|
||||
case FREIGHT_STATION -> Items.MINECART;
|
||||
case COMMUNAL_DEPOT -> Items.IRON_AXE;
|
||||
case RAIL_WORKSHOP -> Items.IRON_PICKAXE;
|
||||
case PUMP_STATION -> Items.BUCKET;
|
||||
case GREENHOUSE -> Items.IRON_HOE;
|
||||
case VOID_DOCK -> Items.LEAD;
|
||||
case PHOTO_CLUB -> Items.BOOK;
|
||||
case CONSTRUCTION_REFUGE -> Items.IRON_SHOVEL;
|
||||
};
|
||||
}
|
||||
private static Set<BlockPos> walk(GameTestHelper helper, BlockPos start, Predicate<BlockPos> inside) {
|
||||
var level = helper.getLevel(); helper.assertTrue(inside.test(start) && standable(level, start), "Blocked or unsupported exterior entrance: " + start);
|
||||
var seen = new HashSet<BlockPos>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
|
||||
while (!queue.isEmpty()) {
|
||||
var pos = queue.removeFirst();
|
||||
for (var direction : HORIZONTAL) for (int dy = -1; dy <= 1; dy++) {
|
||||
var next = pos.relative(direction).offset(0, dy, 0);
|
||||
if (!inside.test(next) || dy > 0 && !clearBody(level, pos.above())) continue;
|
||||
if (standable(level, next) && seen.add(next)) queue.add(next);
|
||||
}
|
||||
for (int dy : new int[] {-1, 1}) {
|
||||
var next = pos.offset(0, dy, 0); if (!inside.test(next)) continue;
|
||||
if ((level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(next).is(Blocks.LADDER) || level.getBlockState(pos.below()).is(Blocks.LADDER))
|
||||
&& standable(level, next) && seen.add(next)) queue.add(next);
|
||||
}
|
||||
helper.assertTrue(seen.size() <= 50_000, "Native site traversal escaped its bounds");
|
||||
}
|
||||
return seen;
|
||||
}
|
||||
private static boolean standable(ServerLevel level, BlockPos pos) {
|
||||
if (!clearBody(level, pos)) return false;
|
||||
if (level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(pos.below()).is(Blocks.LADDER)) return true;
|
||||
var below = pos.below(); return level.getBlockState(below).getCollisionShape(level, below).toAabbs().stream()
|
||||
.anyMatch(b -> b.maxY >= .5 && b.minX <= .5 && b.maxX >= .5 && b.minZ <= .5 && b.maxZ >= .5);
|
||||
}
|
||||
private static boolean clearBody(ServerLevel level, BlockPos pos) {
|
||||
var body = new AABB(pos.getX() + .2, pos.getY(), pos.getZ() + .2, pos.getX() + .8, pos.getY() + 1.8, pos.getZ() + .8);
|
||||
for (var at : new BlockPos[] {pos, pos.above()}) {
|
||||
if (!level.getFluidState(at).isEmpty()) return false;
|
||||
for (var shape : level.getBlockState(at).getCollisionShape(level, at).toAabbs()) if (shape.move(at.getX(), at.getY(), at.getZ()).intersects(body)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private static long column(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
|
||||
private static List<Integer> coords(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
|
||||
private static BlockPos readPos(com.google.gson.JsonArray value) { return new BlockPos(value.get(0).getAsInt(), value.get(1).getAsInt(), value.get(2).getAsInt()); }
|
||||
private static Path world(ServerLevel level) { return level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize(); }
|
||||
private static Path proof(ServerLevel level) { return world(level).resolve("heritage19-create-proof.json"); }
|
||||
private static String planHash(HeritagePlan19 plan) { return hash(plan.sites().toString() + plan.landmarks() + plan.links() + plan.segments()); }
|
||||
private static void write(ServerLevel level, Map<String, Object> report, boolean reload) throws IOException {
|
||||
var path = Path.of("diagnostics", "heritage19-" + (reload ? "reload" : "create") + ".json"); Files.createDirectories(path.getParent());
|
||||
var value = new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n"; Files.writeString(path, value); if (!reload) Files.writeString(proof(level), value);
|
||||
}
|
||||
private static String hash(String value) {
|
||||
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8))); }
|
||||
catch (NoSuchAlgorithmException impossible) { throw new AssertionError(impossible); }
|
||||
}
|
||||
}
|
||||
+24
-2
@@ -210,8 +210,30 @@ public final class PopulationDiagnostics {
|
||||
}
|
||||
|
||||
void prepare() throws IOException {
|
||||
helper.assertTrue(regionsAtInitialSpawn > 0 && regionsAtInitialSpawn <= 4,
|
||||
"Initial spawn must plan only nearby regions, not materialize the large island: " + regionsAtInitialSpawn);
|
||||
boolean heritage = level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator sanctuary
|
||||
&& sanctuary.rootGeneration() == 19 && sanctuary.experimentalStructures();
|
||||
// Generation19 explicitly prospects destinations across the root island before publishing them.
|
||||
// A consulted hydrology region is a bounded immutable plan, not a loaded/generated chunk.
|
||||
// Historical generators retain their original near-spawn contract.
|
||||
int regionLimit = 4;
|
||||
if (heritage) {
|
||||
int extent = capacity.terrainLimit();
|
||||
var first = capacity.regionAt(-extent, -extent); var last = capacity.regionAt(extent, extent);
|
||||
regionLimit = 0;
|
||||
for (int x = first.x(); x <= last.x(); x++) for (int z = first.z(); z <= last.z(); z++) {
|
||||
var region = new IslandCapacity.Region(x, z);
|
||||
double nearestX = Math.max(0, Math.max(region.minX(), -region.maxX()));
|
||||
double nearestZ = Math.max(0, Math.max(region.minZ(), -region.maxZ()));
|
||||
if (Math.hypot(nearestX, nearestZ) < extent) regionLimit++;
|
||||
}
|
||||
helper.assertTrue(regionLimit <= 9, "Heritage starter hydrology must fit within nine local regions");
|
||||
var plan = ((SanctuaryChunkGenerator) level.getChunkSource().getGenerator()).heritagePlan19();
|
||||
helper.assertTrue(plan.columns() <= fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.MAX_COLUMNS
|
||||
&& plan.samples() <= fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.MAX_SAMPLES,
|
||||
"Island-wide heritage prospecting exceeded its independent column/probe budget");
|
||||
}
|
||||
helper.assertTrue(regionsAtInitialSpawn > 0 && regionsAtInitialSpawn <= regionLimit,
|
||||
"Initial hydrology exceeded its geographic planning scope (limit " + regionLimit + "): " + regionsAtInitialSpawn);
|
||||
List<IslandCapacity.Region> targets = new ArrayList<>();
|
||||
targets.add(new IslandCapacity.Region(0,0));
|
||||
if (capacity.players() == 20) targets.add(new IslandCapacity.Region(1,0));
|
||||
|
||||
+6
-3
@@ -45,8 +45,11 @@ final class UnifiedWorldSettingsDiagnostics {
|
||||
var historical16 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v16"))).value().createWorldDimensions();
|
||||
helper.assertTrue(historical16.overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 16
|
||||
&& old.source().generation() == 16 && !old.experimentalStructures(), "Historical16 never gains experimental structures");
|
||||
var historical17 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v17"))).value().createWorldDimensions();
|
||||
helper.assertTrue(historical17.overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 17
|
||||
&& old.source().generation() == 17 && old.experimentalStructures(), "Historical17 keeps its original experimental generator");
|
||||
var generator = (SanctuaryChunkGenerator) standard.overworld();
|
||||
helper.assertTrue(generator.rootGeneration() == 17 && generator.source().generation() == 17 && generator.experimentalStructures(), "The public preset uses generation17 with its new-world option");
|
||||
helper.assertTrue(generator.rootGeneration() == 19 && generator.source().generation() == 19 && generator.experimentalStructures(), "The public preset uses generation19 with its new-world option");
|
||||
helper.assertTrue(generator.initialDiameter() == 724, "The public preset defaults to Medium / 724 blocks");
|
||||
for (var tag : List.of(WorldPresetTags.NORMAL, WorldPresetTags.EXTENDED)) {
|
||||
var visible = presets.getOrThrow(tag).stream().map(holder -> holder.getRegisteredName())
|
||||
@@ -56,11 +59,11 @@ final class UnifiedWorldSettingsDiagnostics {
|
||||
}
|
||||
|
||||
var ops = RegistryOps.create(JsonOps.INSTANCE, access);
|
||||
var activeCodec = generator.rootGeneration() == 17 ? SanctuaryChunkGenerator.RUINS17_CODEC : generator.rootGeneration() == 16 ? SanctuaryChunkGenerator.CITY16_CODEC : generator.rootGeneration() == 15 ? SanctuaryChunkGenerator.CITY15_CODEC : generator.rootGeneration() == 14 ? SanctuaryChunkGenerator.CITY_CODEC : SanctuaryChunkGenerator.CODEC;
|
||||
var activeCodec = generator.rootGeneration() == 19 ? SanctuaryChunkGenerator.HERITAGE19_CODEC : generator.rootGeneration() == 17 ? SanctuaryChunkGenerator.RUINS17_CODEC : generator.rootGeneration() == 16 ? SanctuaryChunkGenerator.CITY16_CODEC : generator.rootGeneration() == 15 ? SanctuaryChunkGenerator.CITY15_CODEC : generator.rootGeneration() == 14 ? SanctuaryChunkGenerator.CITY_CODEC : SanctuaryChunkGenerator.CODEC;
|
||||
var serialized = activeCodec.codec().encodeStart(ops, generator).getOrThrow().getAsJsonObject();
|
||||
serialized.remove("experimental_structures");
|
||||
helper.assertTrue(activeCodec.codec().parse(ops, serialized).getOrThrow().experimentalStructures(),
|
||||
"Generation17 omitted experimental option defaults to enabled");
|
||||
"Generation19 omitted experimental option defaults to enabled");
|
||||
helper.assertTrue(!((SanctuaryChunkGenerator) historical16.overworld()).withExperimentalStructures(true).experimentalStructures(),
|
||||
"The new-world switch cannot activate experimental generation in a historical16 generator");
|
||||
serialized.remove("initial_diameter");
|
||||
|
||||
+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.
|
||||
@Inject(method = "waitUntilNextTick", at = @At("RETURN"))
|
||||
private void sanctuary$paceExpansionWorkers(CallbackInfo ci) {
|
||||
if (Boolean.getBoolean("sanctuary.test.ruins17") || Boolean.getBoolean("sanctuary.test.expansion") || Boolean.getBoolean("sanctuary.test.lostCity")
|
||||
if (Boolean.getBoolean("sanctuary.test.heritage19") || Boolean.getBoolean("sanctuary.test.ruins17") || Boolean.getBoolean("sanctuary.test.expansion") || Boolean.getBoolean("sanctuary.test.lostCity")
|
||||
|| Boolean.getBoolean("sanctuary.test.lostCity16") || Boolean.getBoolean("sanctuary.test.lostCity15") || Boolean.getBoolean("sanctuary.test.voidReuse")) {
|
||||
long until = System.nanoTime() + 20_000_000L;
|
||||
long remaining;
|
||||
@@ -46,9 +46,9 @@ public abstract class SanctuaryGameTestServerMixin {
|
||||
"Lnet/minecraft/world/level/levelgen/presets/WorldPresets;FLAT_ALL_DIMENSIONS:Lnet/minecraft/resources/ResourceKey;"))
|
||||
private static ResourceKey<WorldPreset> sanctuary$selectActualPreset() {
|
||||
if (Boolean.getBoolean("sanctuary.test.unified")) {
|
||||
int generation = Integer.getInteger("sanctuary.test.generation", 17);
|
||||
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17) throw new IllegalArgumentException("Unsupported test generation");
|
||||
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 17 ? "sanctuary" : "sanctuary_v" + generation));
|
||||
int generation = Integer.getInteger("sanctuary.test.generation", 19);
|
||||
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19) throw new IllegalArgumentException("Unsupported test generation");
|
||||
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 19 ? "sanctuary" : "sanctuary_v" + generation));
|
||||
}
|
||||
if (Boolean.getBoolean("sanctuary.test.expansion")) return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("expansion_lab"));
|
||||
int players = Integer.getInteger("sanctuary.test.players", 10);
|
||||
|
||||
@@ -48,6 +48,7 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("expansion_island"), fr.koka.sanctuary.expansion.ExpansionDensity.CODEC);
|
||||
fr.koka.sanctuary.expansion.ExpansionRuntime.register();
|
||||
fr.koka.sanctuary.expansion.ExpansionCommands.register();
|
||||
fr.koka.sanctuary.worldgen.heritage.HeritageCommands19.register();
|
||||
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("main_island"), MainIslandDensity.CODEC);
|
||||
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v13"), SanctuaryChunkGenerator.CODEC);
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v13"), SanctuaryBiomeSource.CODEC);
|
||||
@@ -60,6 +61,9 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v17"), SanctuaryChunkGenerator.RUINS17_CODEC);
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v17"), SanctuaryBiomeSource.RUINS17_CODEC);
|
||||
fr.koka.sanctuary.worldgen.city.LostCityStructures.register();
|
||||
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v19"), SanctuaryChunkGenerator.HERITAGE19_CODEC);
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v19"), SanctuaryBiomeSource.HERITAGE19_CODEC);
|
||||
fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.register();
|
||||
fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.register();
|
||||
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("population_island"), PopulationIslandDensity.CODEC);
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("population_island"), PopulationIslandBiomeSource.CODEC);
|
||||
|
||||
+2
-2
@@ -51,11 +51,11 @@ public final class SanctuaryWorldOptionsScreen extends Screen {
|
||||
contents.addChild(sizeButton);
|
||||
diameterHint = contents.addChild(new MultiLineTextWidget(diameterHint(), font)
|
||||
.setMaxWidth(310).setCentered(true));
|
||||
if (generator.rootGeneration() == 17) {
|
||||
if (generator.rootGeneration() == 17 || generator.rootGeneration() == 19) {
|
||||
contents.addChild(CycleButton.builder((Boolean enabled) -> enabled ? CommonComponents.OPTION_ON : CommonComponents.OPTION_OFF, experimental)
|
||||
.withValues(true, false).create(0, 0, 280, 20, Component.translatable("sanctuary.world_options.experimental"),
|
||||
(button, value) -> experimental = value));
|
||||
contents.addChild(new MultiLineTextWidget(Component.translatable("sanctuary.world_options.experimental_hint"), font)
|
||||
contents.addChild(new MultiLineTextWidget(Component.translatable(generator.rootGeneration() == 19 ? "sanctuary.world_options.heritage_hint" : "sanctuary.world_options.experimental_hint"), font)
|
||||
.setMaxWidth(310).setCentered(true));
|
||||
}
|
||||
layout.addToContents(contents);
|
||||
|
||||
@@ -122,6 +122,7 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
|
||||
ChunkAccess chunk, StructureTemplateManager templates, ResourceKey<Level> dimension) {
|
||||
fr.koka.sanctuary.worldgen.city.LostCityStructures.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||
fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||
fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||
ExpansionStructures.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||
}
|
||||
@Override public void spawnOriginalMobs(WorldGenRegion region) {
|
||||
|
||||
@@ -41,7 +41,7 @@ public record ExpansionIsland(String id, String parent, String direction, String
|
||||
if (!((spatialGeneration(generation) ? DIAMETERS : LEGACY_DIAMETERS).contains(diameter) || (id.equals("sanctuary") && (diameter == 724 || (generation == 13 || spatialGeneration(generation)) && diameter == 1024)))) throw new IllegalArgumentException("Unsupported nominal diameter");
|
||||
if (Math.abs((long) centerX) > 100_000 || Math.abs((long) centerZ) > 100_000 || (centerX & 15) != 0 || (centerZ & 15) != 0) throw new IllegalArgumentException("Invalid island centre");
|
||||
if (!List.of("reserved", "ready").contains(status) || (generation != GENERATION && !spatialGeneration(generation) && !(generation == 13 && id.equals("sanctuary")))) throw new IllegalArgumentException("Unsupported island state or generation");
|
||||
if (climate.equals("lost_city") && (!spatialGeneration(generation) || id.equals("sanctuary"))) throw new IllegalArgumentException("Lost City requires a generation 14, 15, 16 or 17 expansion");
|
||||
if (climate.equals("lost_city") && (!spatialGeneration(generation) || id.equals("sanctuary"))) throw new IllegalArgumentException("Lost City requires a generation 14, 15, 16, 17 or 19 expansion");
|
||||
if (reuseToken == null || !reuseToken.isEmpty() && (!spatialGeneration(generation) || id.equals("sanctuary") || !reuseToken.matches("[0-9a-f]{64}")))
|
||||
throw new IllegalArgumentException("Invalid void reuse certificate token");
|
||||
}
|
||||
@@ -53,7 +53,7 @@ public record ExpansionIsland(String id, String parent, String direction, String
|
||||
return new ExpansionIsland("sanctuary", "sanctuary", "north", "temperate", diameter, 0, 0, seed, "natural", "ready", generation);
|
||||
}
|
||||
|
||||
public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16 || generation == 17; }
|
||||
public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16 || generation == 17 || generation == 19; }
|
||||
public boolean origin() { return id.equals("sanctuary"); }
|
||||
public double referenceDiameter() { return origin() && diameter == 724 ? Math.sqrt(2) * 512 : 512; }
|
||||
public double horizontalScale() { return origin() ? 1.0 : diameter / referenceDiameter(); }
|
||||
|
||||
+3
-2
@@ -25,7 +25,8 @@ public abstract class UnifiedDecorationProtectionMixin {
|
||||
var destination = generator.islandForChunk(pos.getX() >> 4, pos.getZ() >> 4);
|
||||
if (owner == null || destination == null || !owner.id().equals(destination.id())
|
||||
|| !fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
|
||||
&& !fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.isPlacing() && generator.protectsCityDecoration(pos))
|
||||
&& !fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.isPlacing()
|
||||
&& !fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.isPlacing() && generator.protectsCityDecoration(pos))
|
||||
callback.setReturnValue(false);
|
||||
}
|
||||
|
||||
@@ -40,7 +41,7 @@ public abstract class UnifiedDecorationProtectionMixin {
|
||||
if (state.is(Blocks.SUGAR_CANE)) return;
|
||||
// Generation16 reserves its architectural volume before decoration. Only its own
|
||||
// native pieces may replace water there; published generations retain the old rule.
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17)
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19)
|
||||
&& fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
|
||||
&& generator.protectsCityDecoration(pos)) return;
|
||||
if (generator.protectsDecoration(pos) && !region.getBlockState(pos).equals(state)) {
|
||||
|
||||
@@ -42,6 +42,10 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
|
||||
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
|
||||
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 17)));
|
||||
|
||||
public static final MapCodec<SanctuaryBiomeSource> HERITAGE19_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
|
||||
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
|
||||
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 19)));
|
||||
|
||||
private final List<Holder<Biome>> allBiomes;
|
||||
private final PopulationIslandBiomeSource population;
|
||||
private final boolean cities;
|
||||
@@ -75,7 +79,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
|
||||
cityPlans = java.util.Map.copyOf(plans);
|
||||
}
|
||||
|
||||
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; }
|
||||
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; }
|
||||
|
||||
@Override public BiomeResolver createResolver(Climate.Sampler sampler) {
|
||||
BiomeResolver woodlands = population.createResolver(sampler);
|
||||
|
||||
+46
-15
@@ -45,9 +45,17 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
|
||||
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 17, experimental)));
|
||||
|
||||
public static final MapCodec<SanctuaryChunkGenerator> HERITAGE19_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
|
||||
SanctuaryBiomeSource.HERITAGE19_CODEC.fieldOf("biome_source").forGetter(SanctuaryChunkGenerator::source),
|
||||
DIAMETER_CODEC.optionalFieldOf("initial_diameter", 724).forGetter(SanctuaryChunkGenerator::initialDiameter),
|
||||
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
|
||||
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 19, experimental)));
|
||||
|
||||
private final boolean experimentalStructures;
|
||||
private volatile fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17 = fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.EMPTY;
|
||||
private boolean ruinsPrepared;
|
||||
private volatile fr.koka.sanctuary.worldgen.heritage.HeritagePlan19 heritagePlan19 = fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.EMPTY;
|
||||
private boolean heritagePrepared;
|
||||
private volatile Context originContext;
|
||||
private final int generation;
|
||||
private net.minecraft.world.level.LevelHeightAccessor heightAccessor;
|
||||
@@ -83,12 +91,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
}
|
||||
private SanctuaryChunkGenerator(SanctuaryBiomeSource source, int initialDiameter, int generation, boolean experimental) {
|
||||
super(source, initialDiameter, true);
|
||||
this.experimentalStructures = generation == 17 && experimental;
|
||||
this.experimentalStructures = (generation == 17 || generation == 19) && experimental;
|
||||
this.generation = generation;
|
||||
if (generation != source.generation()) throw new IllegalArgumentException("Sanctuary generator and biome source versions must agree");
|
||||
}
|
||||
@Override public SanctuaryBiomeSource source() { return (SanctuaryBiomeSource) super.source(); }
|
||||
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; }
|
||||
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; }
|
||||
public SanctuaryChunkGenerator withInitialDiameter(int diameter) {
|
||||
return new SanctuaryChunkGenerator(source().copy(), diameter, generation, experimentalStructures);
|
||||
}
|
||||
@@ -97,8 +105,10 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
return new SanctuaryChunkGenerator(source().copy(), initialDiameter(), generation, enabled);
|
||||
}
|
||||
public fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17() { return ruinsPlan17; }
|
||||
public fr.koka.sanctuary.worldgen.heritage.HeritagePlan19 heritagePlan19() { return heritagePlan19; }
|
||||
public boolean intersectsExperimentalStructures(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||
return generation == 17 && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ);
|
||||
return (generation == 17 || generation == 19) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||
|| generation == 19 && heritagePlan19.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ);
|
||||
}
|
||||
public IslandCapacity capacity() {
|
||||
return switch (initialDiameter()) {
|
||||
@@ -134,11 +144,13 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
context(islands.getFirst());
|
||||
prepareCities(islands);
|
||||
prepareRuins(islands);
|
||||
prepareHeritage(islands);
|
||||
}
|
||||
@Override public void setIslands(List<ExpansionIsland> islands) {
|
||||
// Complete and publish city geometry before workers can observe the new owner list.
|
||||
prepareCities(islands);
|
||||
prepareRuins(islands);
|
||||
prepareHeritage(islands);
|
||||
super.setIslands(islands);
|
||||
}
|
||||
private void prepareCities(List<ExpansionIsland> islands) {
|
||||
@@ -149,7 +161,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
source().setCityPlans(java.util.Map.of());
|
||||
return;
|
||||
}
|
||||
if ((generation == 16 || generation == 17) && !cityStructuresEnabled) {
|
||||
if ((generation == 16 || generation == 17 || generation == 19) && !cityStructuresEnabled) {
|
||||
cityProtection16 = fr.koka.sanctuary.worldgen.city.LostCityProtection16.EMPTY;
|
||||
activeCityPlans = java.util.Map.of();
|
||||
source().setCityPlans(java.util.Map.of());
|
||||
@@ -165,7 +177,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
}
|
||||
cityProtection15 = protection.build();
|
||||
}
|
||||
if (generation == 16 || generation == 17) {
|
||||
if (generation == 16 || generation == 17 || generation == 19) {
|
||||
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
|
||||
for (var island : islands) {
|
||||
protection.add(published.get(island.id()), undergroundPlan16(island));
|
||||
@@ -180,7 +192,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
source().setCityPlans(published);
|
||||
}
|
||||
private void prepareRuins(List<ExpansionIsland> islands) {
|
||||
if (generation != 17 || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return;
|
||||
if ((generation != 17 && generation != 19) || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return;
|
||||
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
||||
var terrain = context(root);
|
||||
var sampled = new fr.koka.sanctuary.worldgen.ruins.RuinsTerrain17(root, terrain.generator(), terrain.random(),
|
||||
@@ -192,6 +204,22 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
SanctuaryMod.LOGGER.info("Experimental ruins17 seed {}: {} networks, {} probes, {} noise columns in {} ms",
|
||||
root.seed(), plan.networks().size(), plan.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000);
|
||||
}
|
||||
private void prepareHeritage(List<ExpansionIsland> islands) {
|
||||
if (generation != 19 || !experimentalStructures || !cityStructuresEnabled || heritagePrepared) return;
|
||||
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
||||
var terrain = context(root);
|
||||
var sampled = new fr.koka.sanctuary.worldgen.heritage.HeritageTerrain19(root, terrain.generator(), terrain.random(),
|
||||
heightAccessor, pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()));
|
||||
long start = System.nanoTime();
|
||||
sampled.discoverLandmarks(lostCityPlan(root), ruinsPlan17);
|
||||
var plan = fr.koka.sanctuary.worldgen.heritage.HeritagePlanner19.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
|
||||
heritagePlan19 = plan;
|
||||
heritagePrepared = true;
|
||||
SanctuaryMod.LOGGER.info("Heritage19 seed {}: {} sites, {} routes, {} segments, {} probes, {} native columns in {} ms; {}",
|
||||
root.seed(), plan.sites().size(), plan.links().size(), plan.segments().size(), plan.samples(), sampled.columnsUsed(),
|
||||
(System.nanoTime() - start) / 1_000_000, plan.diagnostics());
|
||||
}
|
||||
private static ExpansionIsland cityIdentity(ExpansionIsland island) {
|
||||
// State and receipt do not change the terrain geometry used by a cached plan.
|
||||
var ready = island.status().equals("ready") ? island : island.ready();
|
||||
@@ -217,7 +245,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
return cityPlans.computeIfAbsent(cityIdentity(island), ignored -> {
|
||||
long start = System.nanoTime();
|
||||
var terrain = context(island);
|
||||
if (generation == 16 || generation == 17) {
|
||||
if (generation == 16 || generation == 17 || generation == 19) {
|
||||
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain16(island, terrain.generator(), terrain.random(),
|
||||
heightAccessor, pos -> island.origin() && protectsDecoration(pos));
|
||||
var placement = LostCityPlanner16.plan(island.seed(), island.centerX(), island.centerZ(),
|
||||
@@ -288,8 +316,9 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
});
|
||||
}
|
||||
public boolean protectsCityDecoration(net.minecraft.core.BlockPos pos) {
|
||||
if (generation == 16 || generation == 17) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| generation == 17 && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ());
|
||||
if (generation == 16 || generation == 17 || generation == 19) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| (generation == 17 || generation == 19) && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| generation == 19 && heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ());
|
||||
if (generation == 15) return cityProtection15.contains(pos.getX(), pos.getY(), pos.getZ());
|
||||
if (generation != 14) return false;
|
||||
var island = islandAt(pos.getX(), pos.getZ());
|
||||
@@ -311,20 +340,20 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
return citySupports.getOrDefault(cityIdentity(island), List.of());
|
||||
}
|
||||
public boolean intersectsCityConstruction16(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||
return (generation == 16 || generation == 17) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
|
||||
return (generation == 16 || generation == 17 || generation == 19) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
|
||||
}
|
||||
public fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan undergroundPlan16(ExpansionIsland island) {
|
||||
if (generation != 16 && generation != 17) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
|
||||
if (generation != 16 && generation != 17 && generation != 19) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
|
||||
lostCityPlan(island);
|
||||
return undergroundPlans16.getOrDefault(cityIdentity(island), new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0));
|
||||
}
|
||||
public List<LostCityPlanner16.Support> citySupports16(ExpansionIsland island) {
|
||||
if (generation != 16 && generation != 17) return List.of();
|
||||
if (generation != 16 && generation != 17 && generation != 19) return List.of();
|
||||
lostCityPlan(island);
|
||||
return citySupports16.getOrDefault(cityIdentity(island), List.of());
|
||||
}
|
||||
public LostCityPlanner16.Fallback cityReservation16(ExpansionIsland island) {
|
||||
if (generation != 16 && generation != 17) return null;
|
||||
if (generation != 16 && generation != 17 && generation != 19) return null;
|
||||
lostCityPlan(island);
|
||||
return cityReservations16.get(cityIdentity(island));
|
||||
}
|
||||
@@ -336,8 +365,10 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
net.minecraft.core.BlockPos origin, int radius, boolean skipReferenced) {
|
||||
if (structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.city.LostCityStructures.KEY))
|
||||
return fr.koka.sanctuary.worldgen.city.LostCityStructures.findNearest(level, structures, origin, radius, skipReferenced);
|
||||
if (generation == 17 && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.KEY))
|
||||
if ((generation == 17 || generation == 19) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.KEY))
|
||||
return fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.findNearest(level, structures, origin, radius, skipReferenced);
|
||||
if (generation == 19 && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.KEY))
|
||||
return fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.findNearest(level, structures, origin, radius, skipReferenced);
|
||||
return super.findNearestMapStructure(level, structures, origin, radius, skipReferenced);
|
||||
}
|
||||
@Override protected Context context(ExpansionIsland island) {
|
||||
@@ -365,7 +396,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
@Override protected void afterTerrain(ExpansionIsland island, ChunkAccess chunk) {
|
||||
// Root water/lava are already applied by the Population hook on the named delegate.
|
||||
if (!island.origin()) super.afterTerrain(island, chunk);
|
||||
if ((generation == 16 || generation == 17) && cityStructuresEnabled) {
|
||||
if ((generation == 16 || generation == 17 || generation == 19) && cityStructuresEnabled) {
|
||||
var reserved = cityReservations16.get(cityIdentity(island));
|
||||
if (reserved != null) {
|
||||
var mutable = new net.minecraft.core.BlockPos.MutableBlockPos();
|
||||
|
||||
@@ -29,17 +29,17 @@ public final class LostCityStructure extends Structure {
|
||||
throw new IllegalStateException("Lost City district exceeds native structure reference range");
|
||||
}
|
||||
return Optional.of(new GenerationStub(LostCityStructures.locator(district), pieces -> {
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17) && generator.cityReservation16(island) != null)
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19) && generator.cityReservation16(island) != null)
|
||||
pieces.addPiece(new LostCityFoundationPiece16(generator.cityReservation16(island), district.seed()));
|
||||
if (generator.rootGeneration() == 15) for (var support : generator.citySupports(island)) {
|
||||
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
|
||||
pieces.addPiece(new LostCitySupportPiece15(support));
|
||||
}
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17)) for (var support : generator.citySupports16(island)) {
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19)) for (var support : generator.citySupports16(island)) {
|
||||
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
|
||||
pieces.addPiece(new LostCitySupportPiece16(support));
|
||||
}
|
||||
district.parcels().forEach(parcel -> pieces.addPiece((generator.rootGeneration() == 16 || generator.rootGeneration() == 17) ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
|
||||
district.parcels().forEach(parcel -> pieces.addPiece((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19) ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
|
||||
? new LostCityPiece15(parcel) : new LostCityPiece(parcel)));
|
||||
if (generator.rootGeneration() == 15) for (var network : generator.undergroundPlan(island).networks()) {
|
||||
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
|
||||
@@ -49,7 +49,7 @@ public final class LostCityStructure extends Structure {
|
||||
network.modules().stream().filter(m -> m.kind() == LostCityUnderground15.Kind.ACCESS)
|
||||
.forEach(m -> pieces.addPiece(new LostCityUndergroundPiece15(m)));
|
||||
}
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17)) for (var network : generator.undergroundPlan16(island).networks()) {
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19)) for (var network : generator.undergroundPlan16(island).networks()) {
|
||||
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
|
||||
// Surface comes first, rooms/corridors next, shafts last so every entrance remains open.
|
||||
network.modules().stream().filter(m -> m.kind() != LostCityUnderground16.Kind.ACCESS)
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package fr.koka.sanctuary.worldgen.heritage;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.util.Locale;
|
||||
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** Read-only operator catalogue. Inspecting a plan neither loads chunks nor activates an expansion. */
|
||||
public final class HeritageCommands19 {
|
||||
private HeritageCommands19() {}
|
||||
public static void register() {
|
||||
CommandRegistrationCallback.EVENT.register((dispatcher, registries, environment) ->
|
||||
dispatcher.register(Commands.literal("sanctuary")
|
||||
.requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS))
|
||||
.then(Commands.literal("structures").executes(context -> {
|
||||
var source = context.getSource();
|
||||
var level = source.getLevel();
|
||||
if (!Level.OVERWORLD.equals(level.dimension())
|
||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| generator.rootGeneration() != 19 || !generator.experimentalStructures()) {
|
||||
source.sendFailure(Component.translatable("sanctuary.heritage.unavailable"));
|
||||
return 0;
|
||||
}
|
||||
var plan = generator.heritagePlan19();
|
||||
source.sendSuccess(() -> Component.translatable("sanctuary.heritage.header", plan.sites().size(), plan.links().size()), false);
|
||||
for (var site : plan.sites()) source.sendSuccess(() -> Component.translatable("sanctuary.heritage.entry",
|
||||
Component.translatable("sanctuary.heritage.kind." + site.kind().name().toLowerCase(Locale.ROOT)),
|
||||
site.entry().x(), site.entry().y(), site.entry().z()), false);
|
||||
return plan.sites().size();
|
||||
}))));
|
||||
}
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
package fr.koka.sanctuary.worldgen.heritage;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Set;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
|
||||
/** Self-contained schema19 geometry. Reload never needs the planner or a distant route destination. */
|
||||
public final class HeritagePiece19 extends StructurePiece {
|
||||
private static final Set<String> SITE_KEYS = Set.of("Schema", "Type", "Id", "Kind", "MinX", "FloorY", "MinZ",
|
||||
"Width", "Depth", "Height", "Facing", "Seed", "FoundationDepth", "Supports", "Entry");
|
||||
private static final Set<String> ROUTE_KEYS = Set.of("Schema", "Type", "Id", "FromId", "ToId", "Kind",
|
||||
"StartChunkX", "StartChunkZ", "Cells", "Seed");
|
||||
private static final Set<String> SUPPORT_KEYS = Set.of("X", "MinY", "Z", "MaxY");
|
||||
private static final Set<String> ENTRY_KEYS = Set.of("X", "Y", "Z", "Facing");
|
||||
private static final Set<String> CELL_KEYS = Set.of("X", "FloorY", "Z", "FoundationDepth", "Rail", "RailPresent");
|
||||
private final HeritagePlan19.Site site;
|
||||
private final HeritagePlan19.Segment segment;
|
||||
|
||||
public HeritagePiece19(HeritagePlan19.Site site) {
|
||||
super(HeritageStructures19.PIECE, 0, bounds(site));
|
||||
this.site = site; this.segment = null; setOrientation(null);
|
||||
}
|
||||
public HeritagePiece19(HeritagePlan19.Segment segment) {
|
||||
super(HeritageStructures19.PIECE, 0, bounds(segment));
|
||||
this.site = null; this.segment = segment; setOrientation(null);
|
||||
}
|
||||
public HeritagePiece19(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||
super(HeritageStructures19.PIECE, tag);
|
||||
var payload = tag.getCompound("SanctuaryHeritage").orElseThrow(() -> new IllegalArgumentException("Missing saved heritage geometry"));
|
||||
String type = string(payload, "Type");
|
||||
if (type.equals("SITE")) { site = decodeSite(payload); segment = null; }
|
||||
else if (type.equals("ROUTE")) { site = null; segment = decodeSegment(payload); }
|
||||
else throw new IllegalArgumentException("Unknown heritage piece type");
|
||||
if (!getBoundingBox().equals(isSite() ? bounds(site) : bounds(segment)))
|
||||
throw new IllegalArgumentException("Heritage geometry disagrees with its saved native bounds");
|
||||
setOrientation(null);
|
||||
}
|
||||
public boolean isSite() { return site != null; }
|
||||
public HeritagePlan19.Site site() { if (site == null) throw new IllegalStateException("This heritage piece is a route"); return site; }
|
||||
public HeritagePlan19.Segment segment() { if (segment == null) throw new IllegalStateException("This heritage piece is a site"); return segment; }
|
||||
@Override public BlockPos getLocatorPosition() {
|
||||
if (isSite()) return locator(site);
|
||||
var cell = segment.cells().getFirst(); return new BlockPos(cell.x(), cell.floorY() + 1, cell.z());
|
||||
}
|
||||
public static BlockPos locator(HeritagePlan19.Site site) { return new BlockPos(site.entry().x(), site.entry().y(), site.entry().z()); }
|
||||
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||
tag.put("SanctuaryHeritage", isSite() ? encodeSite(site) : encodeSegment(segment));
|
||||
}
|
||||
public static CompoundTag encodeSite(HeritagePlan19.Site site) {
|
||||
var tag = header("SITE", site.id(), site.kind().name(), site.seed());
|
||||
tag.putInt("MinX", site.minX()); tag.putInt("FloorY", site.floorY()); tag.putInt("MinZ", site.minZ());
|
||||
tag.putInt("Width", site.width()); tag.putInt("Depth", site.depth()); tag.putInt("Height", site.height());
|
||||
tag.putInt("Facing", site.facing()); tag.putInt("FoundationDepth", site.foundationDepth());
|
||||
var entry = new CompoundTag(); entry.putInt("X", site.entry().x()); entry.putInt("Y", site.entry().y());
|
||||
entry.putInt("Z", site.entry().z()); entry.putInt("Facing", site.entry().facing()); tag.put("Entry", entry);
|
||||
var supports = new ListTag();
|
||||
for (var support : site.supports()) {
|
||||
var value = new CompoundTag(); value.putInt("X", support.x()); value.putInt("MinY", support.minY());
|
||||
value.putInt("Z", support.z()); value.putInt("MaxY", support.maxY()); supports.add(value);
|
||||
}
|
||||
tag.put("Supports", supports); return tag;
|
||||
}
|
||||
public static CompoundTag encodeSegment(HeritagePlan19.Segment segment) {
|
||||
var tag = header("ROUTE", segment.id(), segment.kind().name(), segment.seed());
|
||||
tag.putString("FromId", segment.fromId()); tag.putString("ToId", segment.toId());
|
||||
tag.putInt("StartChunkX", segment.startChunkX()); tag.putInt("StartChunkZ", segment.startChunkZ());
|
||||
var cells = new ListTag();
|
||||
for (var cell : segment.cells()) {
|
||||
var value = new CompoundTag(); value.putInt("X", cell.x()); value.putInt("FloorY", cell.floorY());
|
||||
value.putInt("Z", cell.z()); value.putInt("FoundationDepth", cell.foundationDepth());
|
||||
value.putString("Rail", cell.rail().name()); value.putBoolean("RailPresent", cell.railPresent()); cells.add(value);
|
||||
}
|
||||
tag.put("Cells", cells); return tag;
|
||||
}
|
||||
public static HeritagePlan19.Site decodeSite(CompoundTag tag) {
|
||||
schema(tag, "SITE", SITE_KEYS);
|
||||
var entry = compound(tag, "Entry", ENTRY_KEYS);
|
||||
var saved = list(tag, "Supports", 9); var supports = new ArrayList<HeritagePlan19.Support>();
|
||||
for (var value : saved) {
|
||||
var support = element(value, SUPPORT_KEYS);
|
||||
supports.add(new HeritagePlan19.Support(coordinate(support, "X"), integer(support, "MinY"),
|
||||
coordinate(support, "Z"), integer(support, "MaxY")));
|
||||
}
|
||||
return new HeritagePlan19.Site(id(tag, "Id"), HeritagePlan19.Kind.valueOf(string(tag, "Kind")),
|
||||
coordinate(tag, "MinX"), integer(tag, "FloorY"), coordinate(tag, "MinZ"), integer(tag, "Width"),
|
||||
integer(tag, "Depth"), integer(tag, "Height"), integer(tag, "Facing"), seed(tag), integer(tag, "FoundationDepth"),
|
||||
supports, new HeritagePlan19.Entry(coordinate(entry, "X"), integer(entry, "Y"), coordinate(entry, "Z"), integer(entry, "Facing")));
|
||||
}
|
||||
public static HeritagePlan19.Segment decodeSegment(CompoundTag tag) {
|
||||
schema(tag, "ROUTE", ROUTE_KEYS);
|
||||
var saved = list(tag, "Cells", 256); var cells = new ArrayList<HeritagePlan19.Cell>();
|
||||
for (var value : saved) {
|
||||
var cell = element(value, CELL_KEYS);
|
||||
type(cell, "RailPresent", Tag.TAG_BYTE);
|
||||
byte present = cell.getByte("RailPresent").orElseThrow();
|
||||
if (present != 0 && present != 1) throw new IllegalArgumentException("Invalid saved rail presence");
|
||||
cells.add(new HeritagePlan19.Cell(coordinate(cell, "X"), integer(cell, "FloorY"), coordinate(cell, "Z"),
|
||||
integer(cell, "FoundationDepth"), HeritagePlan19.Rail.valueOf(string(cell, "Rail")), present == 1));
|
||||
}
|
||||
String from = id(tag, "FromId"), to = id(tag, "ToId");
|
||||
var kind = HeritagePlan19.RouteKind.valueOf(string(tag, "Kind"));
|
||||
if (from.equals(to) && kind != HeritagePlan19.RouteKind.PATH)
|
||||
throw new IllegalArgumentException("Only a local pedestrian landing may join a destination to itself");
|
||||
return new HeritagePlan19.Segment(id(tag, "Id"), from, to, kind,
|
||||
integer(tag, "StartChunkX"), integer(tag, "StartChunkZ"), cells, seed(tag));
|
||||
}
|
||||
private static CompoundTag header(String type, String id, String kind, long seed) {
|
||||
var value = new CompoundTag(); value.putInt("Schema", 19); value.putString("Type", type);
|
||||
value.putString("Id", id); value.putString("Kind", kind); value.putLong("Seed", seed); return value;
|
||||
}
|
||||
private static void schema(CompoundTag tag, String expected, Set<String> keys) {
|
||||
if (!tag.keySet().equals(keys) || integer(tag, "Schema") != 19 || !string(tag, "Type").equals(expected))
|
||||
throw new IllegalArgumentException("Unsupported or incomplete saved heritage schema");
|
||||
}
|
||||
private static CompoundTag compound(CompoundTag tag, String key, Set<String> keys) {
|
||||
type(tag, key, Tag.TAG_COMPOUND); return element(tag.get(key), keys);
|
||||
}
|
||||
private static CompoundTag element(Tag tag, Set<String> keys) {
|
||||
if (!(tag instanceof CompoundTag value) || !value.keySet().equals(keys))
|
||||
throw new IllegalArgumentException("Unsupported or incomplete saved heritage element");
|
||||
return value;
|
||||
}
|
||||
private static ListTag list(CompoundTag tag, String key, int maximum) {
|
||||
type(tag, key, Tag.TAG_LIST); var value = tag.getList(key).orElseThrow();
|
||||
if (value.size() > maximum) throw new IllegalArgumentException("Oversized saved heritage list"); return value;
|
||||
}
|
||||
private static long seed(CompoundTag tag) { type(tag, "Seed", Tag.TAG_LONG); return tag.getLong("Seed").orElseThrow(); }
|
||||
private static String id(CompoundTag tag, String key) {
|
||||
var value = string(tag, key);
|
||||
if (value.isBlank() || value.length() > 128) throw new IllegalArgumentException("Invalid saved heritage identifier"); return value;
|
||||
}
|
||||
private static String string(CompoundTag tag, String key) { type(tag, key, Tag.TAG_STRING); return tag.getString(key).orElseThrow(); }
|
||||
private static int coordinate(CompoundTag tag, String key) {
|
||||
int value = integer(tag, key);
|
||||
if (value < -30_000_000 || value > 30_000_000) throw new IllegalArgumentException("Heritage coordinate outside world bounds"); return value;
|
||||
}
|
||||
private static int integer(CompoundTag tag, String key) { type(tag, key, Tag.TAG_INT); return tag.getInt(key).orElseThrow(); }
|
||||
private static void type(CompoundTag tag, String key, byte id) {
|
||||
if (tag.get(key) == null || tag.get(key).getId() != id) throw new IllegalArgumentException("Invalid saved heritage field: " + key);
|
||||
}
|
||||
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
|
||||
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
|
||||
final int x0 = isSite() ? site.minX() : segment.minX();
|
||||
final int y0 = isSite() ? site.floorY() : segment.minY();
|
||||
final int z0 = isSite() ? site.minZ() : segment.minZ();
|
||||
var writer = new LostCityRenderer.BlockWriter() {
|
||||
private BlockPos world(int x, int y, int z) { return new BlockPos(Math.addExact(x0, x), Math.addExact(y0, y), Math.addExact(z0, z)); }
|
||||
@Override public void set(int x, int y, int z, BlockState block) {
|
||||
var position = world(x, y, z);
|
||||
if (getBoundingBox().isInside(position) && clip.isInside(position))
|
||||
placeBlock(level, block, position.getX(), position.getY(), position.getZ(), clip);
|
||||
}
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
|
||||
var position = world(x, y, z);
|
||||
if (getBoundingBox().isInside(position) && clip.isInside(position))
|
||||
createChest(level, clip, RandomSource.create(seed), position, loot, null);
|
||||
}
|
||||
};
|
||||
HeritageStructures19.placing(() -> {
|
||||
if (isSite()) HeritageRenderer19.render(site, writer);
|
||||
else HeritageRenderer19.renderSegment(segment, writer);
|
||||
});
|
||||
}
|
||||
public static BoundingBox bounds(HeritagePlan19.Site site) {
|
||||
return new BoundingBox(site.minX(), site.boundsMinY(), site.minZ(), site.maxX(), site.boundsMaxY(), site.maxZ());
|
||||
}
|
||||
public static BoundingBox bounds(HeritagePlan19.Segment segment) {
|
||||
return new BoundingBox(segment.minX(), segment.boundsMinY(), segment.minZ(), segment.maxX(), segment.boundsMaxY(), segment.maxZ());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package fr.koka.sanctuary.worldgen.heritage;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Immutable generation19 catalogue and real-destination routes. Pieces save geometry, not terrain queries. */
|
||||
public final class HeritagePlan19 {
|
||||
private final List<Site> sites;
|
||||
private final List<Landmark> landmarks;
|
||||
private final List<Link> links;
|
||||
private final List<Segment> segments;
|
||||
private final int samples, columns;
|
||||
private final Map<Kind, String> diagnostics;
|
||||
private final Map<Long, List<Range>> protectedColumns;
|
||||
private final Map<Long, List<Box>> protectedChunks;
|
||||
public static final int MAX_COLUMNS = 16_000, MAX_SAMPLES = 800_000;
|
||||
public static final HeritagePlan19 EMPTY = new HeritagePlan19(List.of(), List.of(), List.of(), List.of(), 0, 0, Map.of());
|
||||
public enum Kind { FREIGHT_STATION, POST_OFFICE, COMMUNAL_DEPOT, EXPEDITION_OFFICE, OBSERVATORY,
|
||||
RAIL_WORKSHOP, PUMP_STATION, SUBSTATION, GREENHOUSE, VOID_DOCK, RADIO_RELAY, PHOTO_CLUB,
|
||||
IMPOSSIBLE_SERVICE, CONSTRUCTION_REFUGE }
|
||||
public enum RouteKind { FREIGHT, PATH }
|
||||
public enum LandmarkKind { CITY, MINE, UTILITY }
|
||||
/** NONE is a side column or path; a missing rail retains its intended shape with railPresent=false. */
|
||||
public enum Rail { NONE, NORTH_SOUTH, EAST_WEST, ASCENDING_EAST, ASCENDING_WEST, ASCENDING_NORTH,
|
||||
ASCENDING_SOUTH, SOUTH_EAST, SOUTH_WEST, NORTH_WEST, NORTH_EAST }
|
||||
|
||||
/** Coordinates of the player's feet, immediately outside the facade. Facing0=N,1=E,2=S,3=W. */
|
||||
public record Entry(int x, int y, int z, int facing) {
|
||||
public Entry { if (!coordinate(x) || !coordinate(z) || y < 2 || y > 380 || facing < 0 || facing > 3) throw new IllegalArgumentException("Invalid heritage entry"); }
|
||||
}
|
||||
public record Support(int x, int minY, int z, int maxY) {
|
||||
public Support { if (!coordinate(x) || !coordinate(z) || minY < 2 || maxY < minY || maxY - minY >= 12 || maxY > 370) throw new IllegalArgumentException("Invalid heritage support"); }
|
||||
}
|
||||
public record Site(String id, Kind kind, int minX, int floorY, int minZ, int width, int depth, int height,
|
||||
int facing, long seed, int foundationDepth, List<Support> supports, Entry entry) {
|
||||
public Site {
|
||||
supports = List.copyOf(supports);
|
||||
if (!identifier(id) || !coordinate(minX) || !coordinate(minZ) || kind == null || width != 17 || depth != 17 || height < 7 || height > 24
|
||||
|| floorY < 32 || floorY + height > 380 || facing < 0 || facing > 3
|
||||
|| foundationDepth < 3 || foundationDepth > 8 || supports.size() > 9 || entry == null)
|
||||
throw new IllegalArgumentException("Invalid heritage site");
|
||||
int ex = facing == 1 ? minX + width : facing == 3 ? minX - 1 : minX + width / 2;
|
||||
int ez = facing == 2 ? minZ + depth : facing == 0 ? minZ - 1 : minZ + depth / 2;
|
||||
if (entry.x() != ex || entry.z() != ez || entry.y() != floorY + 1 || entry.facing() != facing)
|
||||
throw new IllegalArgumentException("Heritage entry does not meet its rotated facade");
|
||||
var positions = new HashSet<Long>();
|
||||
for (var support : supports) if (support.x() < minX || support.x() >= minX + width
|
||||
|| support.z() < minZ || support.z() >= minZ + depth || support.maxY() != floorY - foundationDepth - 1
|
||||
|| !positions.add(key(support.x(), support.z()))) throw new IllegalArgumentException("Invalid site footing");
|
||||
}
|
||||
public int maxX() { return minX + width - 1; }
|
||||
public int maxZ() { return minZ + depth - 1; }
|
||||
public int minY() { return floorY - foundationDepth; }
|
||||
public int maxY() { return floorY + height; }
|
||||
public int boundsMinY() { return supports.stream().mapToInt(Support::minY).min().orElse(minY()); }
|
||||
public int boundsMaxY() { return maxY(); }
|
||||
public int centerX() { return minX + width / 2; }
|
||||
public int centerZ() { return minZ + depth / 2; }
|
||||
public int startChunkX() { return centerX() >> 4; }
|
||||
public int startChunkZ() { return centerZ() >> 4; }
|
||||
}
|
||||
/** A pre-existing, read-only destination. Its coordinate is never granted a mask bypass. */
|
||||
public record Landmark(String id, LandmarkKind kind, Entry entry) {
|
||||
public Landmark { if (!identifier(id) || kind == null || entry == null) throw new IllegalArgumentException("Invalid heritage landmark"); }
|
||||
}
|
||||
/** Floor/block-support Y, not player feet. Endpoints may be read-only landmark coordinates. */
|
||||
public record Point(int x, int floorY, int z) {
|
||||
public Point { if (!coordinate(x) || !coordinate(z) || floorY < 2 || floorY > 370) throw new IllegalArgumentException("Invalid route point"); }
|
||||
}
|
||||
public record Link(String id, String fromId, String toId, RouteKind kind, List<Point> path) {
|
||||
public Link {
|
||||
path = List.copyOf(path);
|
||||
if (!identifier(id) || !identifier(fromId) || !identifier(toId) || fromId.equals(toId)
|
||||
|| kind == null || path.size() < 2 || path.size() > 1024) throw new IllegalArgumentException("Invalid heritage link");
|
||||
for (int i = 1; i < path.size(); i++) {
|
||||
var a = path.get(i - 1); var b = path.get(i);
|
||||
if (Math.abs(a.x() - b.x()) + Math.abs(a.z() - b.z()) != 1 || Math.abs(a.floorY() - b.floorY()) > 1)
|
||||
throw new IllegalArgumentException("Heritage route has a discontinuous centerline");
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Each written ground column belongs to exactly one Segment. Clear local y1..3 before rail placement. */
|
||||
public record Cell(int x, int floorY, int z, int foundationDepth, Rail rail, boolean railPresent) {
|
||||
public Cell {
|
||||
if (!coordinate(x) || !coordinate(z) || floorY < 14 || floorY > 350 || foundationDepth < 1 || foundationDepth > 12 || rail == null
|
||||
|| rail == Rail.NONE && railPresent) throw new IllegalArgumentException("Invalid heritage route cell");
|
||||
}
|
||||
public int minY() { return floorY - foundationDepth; }
|
||||
public int maxY() { return floorY + 3; }
|
||||
}
|
||||
/** Chunk-local route piece: stable ownership prevents neighboring links painting a shared column twice. */
|
||||
public record Segment(String id, String fromId, String toId, RouteKind kind, int startChunkX, int startChunkZ,
|
||||
List<Cell> cells, long seed) {
|
||||
public Segment {
|
||||
cells = List.copyOf(cells);
|
||||
if (!identifier(id) || !identifier(fromId) || !identifier(toId) || fromId.equals(toId) && kind != RouteKind.PATH || kind == null || cells.isEmpty() || cells.size() > 256)
|
||||
throw new IllegalArgumentException("Invalid heritage segment");
|
||||
var positions = new HashSet<Long>();
|
||||
for (var cell : cells) if ((cell.x() >> 4) != startChunkX || (cell.z() >> 4) != startChunkZ
|
||||
|| !positions.add(key(cell.x(), cell.z()))) throw new IllegalArgumentException("Route cells escaped their owning chunk");
|
||||
}
|
||||
public int minX() { return cells.stream().mapToInt(Cell::x).min().orElseThrow(); }
|
||||
public int maxX() { return cells.stream().mapToInt(Cell::x).max().orElseThrow(); }
|
||||
public int minZ() { return cells.stream().mapToInt(Cell::z).min().orElseThrow(); }
|
||||
public int maxZ() { return cells.stream().mapToInt(Cell::z).max().orElseThrow(); }
|
||||
public int minY() { return cells.stream().mapToInt(Cell::minY).min().orElseThrow(); }
|
||||
public int maxY() { return cells.stream().mapToInt(Cell::maxY).max().orElseThrow(); }
|
||||
public int boundsMinY() { return minY(); }
|
||||
public int boundsMaxY() { return maxY(); }
|
||||
}
|
||||
|
||||
public HeritagePlan19(List<Site> sites, List<Landmark> landmarks, List<Link> links, List<Segment> segments,
|
||||
int samples, int columns, Map<Kind, String> diagnostics) {
|
||||
sites = List.copyOf(sites); landmarks = List.copyOf(landmarks); links = List.copyOf(links); segments = List.copyOf(segments);
|
||||
diagnostics = Map.copyOf(diagnostics);
|
||||
if (sites.size() > 14 || landmarks.size() > 16 || links.size() > 24 || segments.size() > 512
|
||||
|| samples < 0 || samples > MAX_SAMPLES || columns < 0 || columns > MAX_COLUMNS)
|
||||
throw new IllegalArgumentException("Invalid heritage plan budget");
|
||||
var destinations = new HashSet<String>();
|
||||
for (var site : sites) if (!destinations.add(site.id())) throw new IllegalArgumentException("Duplicate heritage destination");
|
||||
for (var landmark : landmarks) if (!destinations.add(landmark.id())) throw new IllegalArgumentException("Duplicate heritage destination");
|
||||
for (var link : links) if (!destinations.contains(link.fromId()) || !destinations.contains(link.toId()))
|
||||
throw new IllegalArgumentException("A heritage route promises a nonexistent destination");
|
||||
var occupied = new HashSet<Long>();
|
||||
for (var segment : segments) {
|
||||
if (!destinations.contains(segment.fromId()) || !destinations.contains(segment.toId()))
|
||||
throw new IllegalArgumentException("A heritage segment promises a nonexistent destination");
|
||||
for (var cell : segment.cells()) if (!occupied.add(key(cell.x(), cell.z())))
|
||||
throw new IllegalArgumentException("Heritage segments write the same column");
|
||||
}
|
||||
this.sites = sites; this.landmarks = landmarks; this.links = links; this.segments = segments;
|
||||
this.samples = samples; this.columns = columns; this.diagnostics = diagnostics;
|
||||
var columnIndex = new HashMap<Long, List<Range>>(); var chunkIndex = new HashMap<Long, List<Box>>();
|
||||
for (var site : sites) {
|
||||
index(columnIndex, chunkIndex, new Box(site.minX(), site.minY() - 2, site.minZ(), site.maxX(), site.maxY(), site.maxZ()));
|
||||
for (var support : site.supports()) index(columnIndex, chunkIndex,
|
||||
new Box(support.x(), support.minY() - 2, support.z(), support.x(), support.maxY(), support.z()));
|
||||
}
|
||||
for (var segment : segments) for (var cell : segment.cells()) index(columnIndex, chunkIndex,
|
||||
new Box(cell.x(), cell.minY() - 1, cell.z(), cell.x(), cell.maxY(), cell.z()));
|
||||
var usedDestinations = new HashSet<String>();
|
||||
for (var link : links) { usedDestinations.add(link.fromId()); usedDestinations.add(link.toId()); }
|
||||
for (var landmark : landmarks) if (usedDestinations.contains(landmark.id())) {
|
||||
Entry e = landmark.entry();
|
||||
for (int side = -1; side <= 1; side++) {
|
||||
int x = e.x() + ((e.facing() & 1) == 0 ? side : 0);
|
||||
int z = e.z() + ((e.facing() & 1) == 1 ? side : 0);
|
||||
// Read-only dependencies: these cells never enter contains() or any native writer.
|
||||
index(columnIndex, chunkIndex, new Box(x, e.y() - 2, z, x, e.y() + 2, z));
|
||||
}
|
||||
}
|
||||
columnIndex.replaceAll((key, values) -> List.copyOf(values)); chunkIndex.replaceAll((key, values) -> List.copyOf(values));
|
||||
protectedColumns = Map.copyOf(columnIndex); protectedChunks = Map.copyOf(chunkIndex);
|
||||
}
|
||||
public List<Site> sites() { return sites; }
|
||||
public List<Landmark> landmarks() { return landmarks; }
|
||||
public List<Link> links() { return links; }
|
||||
public List<Segment> segments() { return segments; }
|
||||
public int samples() { return samples; }
|
||||
public int columns() { return columns; }
|
||||
public Map<Kind, String> diagnostics() { return diagnostics; }
|
||||
public boolean isEmpty() { return sites.isEmpty(); }
|
||||
public boolean contains(int x, int y, int z) {
|
||||
for (var site : sites) {
|
||||
if (x < site.minX() || x > site.maxX() || z < site.minZ() || z > site.maxZ()) continue;
|
||||
if (y >= site.minY() && y <= site.maxY()) return true;
|
||||
for (var support : site.supports()) if (x == support.x() && z == support.z() && y >= support.minY() && y <= support.maxY()) return true;
|
||||
}
|
||||
for (var segment : segments) if ((x >> 4) == segment.startChunkX() && (z >> 4) == segment.startChunkZ())
|
||||
for (var cell : segment.cells()) if (cell.x() == x && cell.z() == z && y >= cell.minY() && y <= cell.maxY()) return true;
|
||||
return false;
|
||||
}
|
||||
/** Protect the original rock below slabs as well as every written volume. */
|
||||
public boolean protects(int x, int y, int z) {
|
||||
for (var range : protectedColumns.getOrDefault(key(x, z), List.of())) if (y >= range.minY() && y <= range.maxY()) return true;
|
||||
return false;
|
||||
}
|
||||
public boolean intersectsProtection(int x0, int y0, int z0, int x1, int y1, int z1) {
|
||||
if (x0 > x1 || y0 > y1 || z0 > z1) throw new IllegalArgumentException("Reversed heritage query");
|
||||
for (int cx = x0 >> 4; cx <= x1 >> 4; cx++) for (int cz = z0 >> 4; cz <= z1 >> 4; cz++)
|
||||
for (var box : protectedChunks.getOrDefault(key(cx, cz), List.of()))
|
||||
if (x0 <= box.maxX() && x1 >= box.minX() && z0 <= box.maxZ() && z1 >= box.minZ()
|
||||
&& y0 <= box.maxY() && y1 >= box.minY()) return true;
|
||||
return false;
|
||||
}
|
||||
private record Range(int minY, int maxY) {}
|
||||
private record Box(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {}
|
||||
private static void index(Map<Long, List<Range>> columns, Map<Long, List<Box>> chunks, Box box) {
|
||||
for (int x = box.minX(); x <= box.maxX(); x++) for (int z = box.minZ(); z <= box.maxZ(); z++)
|
||||
columns.computeIfAbsent(key(x, z), ignored -> new ArrayList<>()).add(new Range(box.minY(), box.maxY()));
|
||||
for (int x = box.minX() >> 4; x <= box.maxX() >> 4; x++) for (int z = box.minZ() >> 4; z <= box.maxZ() >> 4; z++)
|
||||
chunks.computeIfAbsent(key(x, z), ignored -> new ArrayList<>()).add(box);
|
||||
}
|
||||
private static boolean coordinate(int value) { return Math.abs((long) value) <= 29_999_000; }
|
||||
private static boolean identifier(String value) { return value != null && !value.isBlank() && value.length() <= 128; }
|
||||
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
|
||||
}
|
||||
+545
@@ -0,0 +1,545 @@
|
||||
package fr.koka.sanctuary.worldgen.heritage;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/** Generation19 only. A finite catalogue is admitted before complete, repairable routes are published.
|
||||
* Everything samples native columns; no chunk, player object or future continent is requested. */
|
||||
public final class HeritagePlanner19 {
|
||||
public static final int MAX_COLUMNS = HeritagePlan19.MAX_COLUMNS, MAX_SAMPLES = HeritagePlan19.MAX_SAMPLES;
|
||||
public static final int MAX_SITE_CUT = 6, MAX_ROUTE_CUT = 3;
|
||||
private static final int SITE_COLUMNS = 11_000, SITE_SAMPLES = 540_000;
|
||||
private static final int INVALID = Integer.MIN_VALUE;
|
||||
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
|
||||
private HeritagePlanner19() {}
|
||||
public interface Terrain {
|
||||
boolean owns(int x, int z);
|
||||
int surfaceY(int x, int z);
|
||||
boolean solid(int x, int y, int z);
|
||||
boolean protectedAt(int x, int y, int z);
|
||||
default List<Landmark> landmarks() { return List.of(); }
|
||||
}
|
||||
|
||||
public static HeritagePlan19 plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
|
||||
if (!List.of(512, 724, 1024).contains(diameter)) throw new IllegalArgumentException("Heritage19 requires a starter size");
|
||||
var probe = new Probe(terrain, cx, cz, diameter);
|
||||
List<Landmark> landmarks = List.copyOf(terrain.landmarks()).stream().limit(16).toList();
|
||||
var sites = new ArrayList<Site>(); var cells = new LinkedHashMap<Long, OwnedCell>(); var diagnostics = new EnumMap<Kind, String>(Kind.class);
|
||||
for (var kind : Kind.values()) {
|
||||
int remaining = Kind.values().length - kind.ordinal() - 1;
|
||||
probe.columnLimit = SITE_COLUMNS - remaining * (SITE_COLUMNS / 14);
|
||||
probe.sampleLimit = SITE_SAMPLES - remaining * (SITE_SAMPLES / 14);
|
||||
SiteDraft admitted = null; int attempts = 0;
|
||||
var reasons = new java.util.TreeMap<String, Integer>(); probe.reasons = reasons;
|
||||
try {
|
||||
for (var candidate : candidates(seed, kind, cx, cz, diameter, sites, landmarks, probe)) {
|
||||
attempts++;
|
||||
admitted = site(seed, kind, candidate, sites, landmarks, cells, probe);
|
||||
if (admitted != null) break;
|
||||
}
|
||||
} catch (BudgetExceeded exhausted) { reasons.merge(exhausted.columns ? "column-budget" : "probe-budget", 1, Integer::sum); }
|
||||
if (admitted != null) {
|
||||
sites.add(admitted.site());
|
||||
Request landing = new Request(admitted.site().id() + "_landing", admitted.site().id(), admitted.site().id(), RouteKind.PATH);
|
||||
for (var cell : admitted.landings()) cells.put(key(cell.x(), cell.z()), new OwnedCell(landing, cell));
|
||||
}
|
||||
diagnostics.put(kind, (admitted == null ? "not admitted" : "admitted") + "; candidates=" + attempts + "; " + reasons);
|
||||
}
|
||||
probe.columnLimit = MAX_COLUMNS; probe.sampleLimit = MAX_SAMPLES;
|
||||
var links = new ArrayList<Link>();
|
||||
var routeResults = new LinkedHashMap<String, String>();
|
||||
var routeReasons = new java.util.TreeMap<String, Integer>(); probe.reasons = routeReasons;
|
||||
Map<String, Endpoint> endpoints = new LinkedHashMap<>();
|
||||
for (var site : sites) endpoints.put(site.id(), new Endpoint(site.id(), site.entry(), false));
|
||||
for (var landmark : landmarks) if (!endpoints.containsKey(landmark.id()))
|
||||
endpoints.put(landmark.id(), new Endpoint(landmark.id(), landmark.entry(), true));
|
||||
List<Request> requests = requests(sites, landmarks).stream()
|
||||
.filter(r -> endpoints.containsKey(r.from()) && endpoints.containsKey(r.to())).toList();
|
||||
requests = new ArrayList<>(requests);
|
||||
requests.sort(Comparator.comparingInt((Request r) -> r.kind() == RouteKind.FREIGHT ? 0
|
||||
: span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry()) >= diameter / 4 ? 1 : 2)
|
||||
.thenComparingInt(r -> span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry())));
|
||||
int adaptiveLong = 0;
|
||||
for (int requestIndex = 0; requestIndex < requests.size(); requestIndex++) {
|
||||
var request = requests.get(requestIndex);
|
||||
int remaining = requests.size() - requestIndex - 1;
|
||||
// Failed alignments must not starve every later destination. Unused allowances carry forward.
|
||||
boolean longAxis = span(endpoints.get(request.from()).entry(), endpoints.get(request.to()).entry()) >= diameter / 4;
|
||||
int allowance = longAxis && adaptiveLong++ < 2 ? 4_500 : 1_400;
|
||||
probe.columnLimit = Math.min(MAX_COLUMNS - remaining * 120, probe.columns.size() + allowance);
|
||||
probe.sampleLimit = Math.min(MAX_SAMPLES - remaining * 4_000, probe.samples + (longAxis ? 150_000 : 75_000));
|
||||
Endpoint from = endpoints.get(request.from()), to = endpoints.get(request.to());
|
||||
if (from == null || to == null) continue;
|
||||
int beforeRouteColumns = probe.columns.size();
|
||||
try {
|
||||
RouteDraft route = route(seed, request, from, to, sites, landmarks, cells, probe);
|
||||
if (route == null) { probe.reject("no-complete-route"); routeResults.put(request.id(), "refused; columns=" + (probe.columns.size() - beforeRouteColumns)); continue; }
|
||||
links.add(new Link(request.id(), request.from(), request.to(), request.kind(), route.path()));
|
||||
routeResults.put(request.id(), "admitted; span=" + span(from.entry(), to.entry()) + "; length=" + route.path().size() + "; columns=" + (probe.columns.size() - beforeRouteColumns));
|
||||
for (var cell : route.cells().values()) {
|
||||
long key = key(cell.x(), cell.z());
|
||||
OwnedCell old = cells.get(key);
|
||||
if (old == null) cells.put(key, new OwnedCell(request, cell));
|
||||
else if (cell.rail() != Rail.NONE && old.cell().rail() == Rail.NONE)
|
||||
cells.put(key, new OwnedCell(old.request(), cell));
|
||||
else if (cell.rail() != Rail.NONE && old.cell().rail() != Rail.NONE && cell.rail() != old.cell().rail()) {
|
||||
// A missing switch is intentional; never emit a rail with contradictory connections.
|
||||
var c = old.cell(); cells.put(key, new OwnedCell(old.request(),
|
||||
new Cell(c.x(), c.floorY(), c.z(), c.foundationDepth(), c.rail(), false)));
|
||||
}
|
||||
}
|
||||
} catch (BudgetExceeded exhausted) { probe.reject(exhausted.columns ? "route-column-budget" : "route-probe-budget"); routeResults.put(request.id(), "budget; columns=" + (probe.columns.size() - beforeRouteColumns)); }
|
||||
}
|
||||
var segments = segments(seed, cells);
|
||||
if (Boolean.getBoolean("sanctuary.test.heritage19") || Boolean.getBoolean("sanctuary.heritage.plannerDiagnostics"))
|
||||
System.getLogger(HeritagePlanner19.class.getName()).log(System.Logger.Level.INFO,
|
||||
"Heritage19 seed=" + seed + " diameter=" + diameter + " sites=" + sites.stream().map(Site::kind).toList()
|
||||
+ " links=" + links.size() + " segments=" + segments.size() + " columns=" + probe.columns.size()
|
||||
+ " samples=" + probe.samples + " catalogue=" + diagnostics + " routes=" + routeReasons + " routeResults=" + routeResults);
|
||||
return new HeritagePlan19(sites, landmarks, links, segments, probe.samples, probe.columns.size(), diagnostics);
|
||||
}
|
||||
|
||||
private static List<Candidate> candidates(long seed, Kind kind, int cx, int cz, int diameter,
|
||||
List<Site> sites, List<Landmark> landmarks, Probe probe) {
|
||||
double angle = unit(mix(seed ^ 0x19ACEL)) * Math.PI * 2 + kind.ordinal() * 2.399963229728653;
|
||||
double radius = diameter * (kind == Kind.VOID_DOCK ? .40 : kind == Kind.OBSERVATORY || kind == Kind.RADIO_RELAY ? .31 : .22);
|
||||
int x = cx + (int) Math.round(Math.cos(angle) * radius), z = cz + (int) Math.round(Math.sin(angle) * radius);
|
||||
Kind parent = switch (kind) {
|
||||
case POST_OFFICE, COMMUNAL_DEPOT, RAIL_WORKSHOP -> Kind.FREIGHT_STATION;
|
||||
case EXPEDITION_OFFICE, PHOTO_CLUB -> Kind.POST_OFFICE;
|
||||
case SUBSTATION, CONSTRUCTION_REFUGE -> Kind.RAIL_WORKSHOP;
|
||||
case GREENHOUSE, IMPOSSIBLE_SERVICE -> Kind.PUMP_STATION;
|
||||
default -> null;
|
||||
};
|
||||
Site neighbor = parent == null ? null : find(sites, parent);
|
||||
if (neighbor != null) {
|
||||
double local = unit(mix(seed ^ kind.ordinal() * 179L)) * Math.PI * 2;
|
||||
int distance = kind == Kind.RAIL_WORKSHOP ? 65 : 40;
|
||||
x = neighbor.centerX() + (int) Math.round(Math.cos(local) * distance);
|
||||
z = neighbor.centerZ() + (int) Math.round(Math.sin(local) * distance);
|
||||
}
|
||||
var candidates = new ArrayList<Candidate>();
|
||||
for (int i = 0; i < 16; i++) {
|
||||
double turn = angle + i * Math.PI / 3;
|
||||
int offset = i == 0 ? 0 : i < 5 ? 24 : i < 8 ? 48 : i < 12 ? 72 : 96;
|
||||
int px = x + (int) Math.round(Math.cos(turn) * offset), pz = z + (int) Math.round(Math.sin(turn) * offset);
|
||||
if (Math.hypot((double) px - cx, (double) pz - cz) > diameter / 2.0 - 18) continue;
|
||||
if (overlapsSites(px - 15, pz - 15, px + 15, pz + 15, sites)) continue;
|
||||
int low = 384, high = -1; boolean dry = true;
|
||||
for (int dz : new int[] {-8, 0, 8}) for (int dx : new int[] {-8, 0, 8}) {
|
||||
int top = probe.surface(px + dx, pz + dz);
|
||||
if (top < 48 || top > 330 || !probe.solid(px + dx, top, pz + dz)
|
||||
|| probe.protectedAt(px + dx, top, pz + dz)) { dry = false; break; }
|
||||
low = Math.min(low, top); high = Math.max(high, top);
|
||||
}
|
||||
if (!dry || high - low > 13) { probe.reject("coarse-terrain"); continue; }
|
||||
int facing = direction(px, pz, neighbor == null ? cx : neighbor.centerX(), neighbor == null ? cz : neighbor.centerZ());
|
||||
if (kind == Kind.VOID_DOCK) facing = (direction(px, pz, cx, cz) + 2) & 3;
|
||||
int score = (i >= 8 ? 1_000 : 0) + high - low + (kind == Kind.OBSERVATORY || kind == Kind.RADIO_RELAY ? (300 - high) / 4 : 0);
|
||||
candidates.add(new Candidate(px - 8, pz - 8, facing, score, mix(seed ^ kind.ordinal() * 463L ^ i)));
|
||||
}
|
||||
candidates.sort(Comparator.comparingInt(Candidate::score).thenComparingLong(Candidate::seed));
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private static SiteDraft site(long seed, Kind kind, Candidate candidate, List<Site> previous,
|
||||
List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
|
||||
int x = candidate.x(), z = candidate.z(), low = 384, high = -1;
|
||||
for (var landmark : landmarks) if (landmark.entry().x() >= x - 2 && landmark.entry().x() <= x + 18
|
||||
&& landmark.entry().z() >= z - 2 && landmark.entry().z() <= z + 18) { probe.reject("existing-entry"); return null; }
|
||||
var heights = new ArrayList<Integer>();
|
||||
for (int dz = 0; dz < 17; dz++) for (int dx = 0; dx < 17; dx++) {
|
||||
int top = probe.surface(x + dx, z + dz);
|
||||
if (top < 48 || top > 330 || !probe.solid(x + dx, top, z + dz)) { probe.reject("dry-footprint"); return null; }
|
||||
heights.add(top); low = Math.min(low, top); high = Math.max(high, top);
|
||||
}
|
||||
int minimum = Math.max(48, high - MAX_SITE_CUT), maximum = Math.min(330, low + 7);
|
||||
if (minimum > maximum) { probe.reject("site-slope"); return null; }
|
||||
heights.sort(Integer::compareTo);
|
||||
int floor = Math.clamp(heights.get(heights.size() / 2), minimum, maximum);
|
||||
int foundation = Math.max(3, floor - low + 1), height = height(kind);
|
||||
int base = floor - foundation;
|
||||
for (int dz = 0; dz < 17; dz++) for (int dx = 0; dx < 17; dx++)
|
||||
for (int y = base; y <= floor + height; y++) if (probe.protectedAt(x + dx, y, z + dz)) {
|
||||
probe.reject("site-protected-volume"); return null;
|
||||
}
|
||||
var supports = new ArrayList<Support>();
|
||||
for (int dz : new int[] {1, 8, 15}) for (int dx : new int[] {1, 8, 15}) {
|
||||
int bottom = base - 1, rock = bottom;
|
||||
while (rock >= Math.max(2, base - 13) && (!probe.solid(x + dx, rock, z + dz)
|
||||
|| !probe.solid(x + dx, rock - 1, z + dz))) rock--;
|
||||
if (rock < Math.max(2, base - 13)) { probe.reject("site-no-bearing"); return null; }
|
||||
for (int y = rock - 1; y <= bottom; y++) if (probe.protectedAt(x + dx, y, z + dz)) { probe.reject("support-protected"); return null; }
|
||||
if (rock < bottom) supports.add(new Support(x + dx, rock + 1, z + dz, bottom));
|
||||
}
|
||||
int ex = candidate.facing() == 1 ? x + 17 : candidate.facing() == 3 ? x - 1 : x + 8;
|
||||
int ez = candidate.facing() == 2 ? z + 17 : candidate.facing() == 0 ? z - 1 : z + 8;
|
||||
Entry entry = new Entry(ex, floor + 1, ez, candidate.facing());
|
||||
Site result = new Site(id(kind), kind, x, floor, z, 17, 17, height, candidate.facing(),
|
||||
mix(seed ^ candidate.seed()), foundation, supports, entry);
|
||||
var landings = landing(result.entry(), previous, probe);
|
||||
if (landings == null) { probe.reject("entry-landing"); return null; }
|
||||
for (var cell : landings) {
|
||||
OwnedCell old = existing.get(key(cell.x(), cell.z()));
|
||||
if (old != null && old.cell().floorY() != cell.floorY()) { probe.reject("landing-collision"); return null; }
|
||||
}
|
||||
return new SiteDraft(result, landings);
|
||||
}
|
||||
|
||||
private static int height(Kind kind) {
|
||||
return switch (kind) {
|
||||
case FREIGHT_STATION, POST_OFFICE, RAIL_WORKSHOP, SUBSTATION, GREENHOUSE, PHOTO_CLUB -> 10;
|
||||
case EXPEDITION_OFFICE, IMPOSSIBLE_SERVICE -> 12;
|
||||
case COMMUNAL_DEPOT, VOID_DOCK, CONSTRUCTION_REFUGE -> 14;
|
||||
case PUMP_STATION -> 18; case OBSERVATORY -> 20; case RADIO_RELAY -> 22;
|
||||
};
|
||||
}
|
||||
private static List<Request> requests(List<Site> sites, List<Landmark> landmarks) {
|
||||
var result = new ArrayList<Request>();
|
||||
request(result, Kind.FREIGHT_STATION, Kind.COMMUNAL_DEPOT, RouteKind.FREIGHT);
|
||||
request(result, Kind.FREIGHT_STATION, Kind.RAIL_WORKSHOP, RouteKind.FREIGHT);
|
||||
request(result, Kind.COMMUNAL_DEPOT, Kind.PUMP_STATION, RouteKind.FREIGHT);
|
||||
Site workshop = find(sites, Kind.RAIL_WORKSHOP);
|
||||
for (var landmark : landmarks) if (landmark.kind() == LandmarkKind.MINE && workshop != null)
|
||||
result.add(new Request("mine_freight", landmark.id(), workshop.id(), RouteKind.FREIGHT));
|
||||
request(result, Kind.FREIGHT_STATION, Kind.POST_OFFICE, RouteKind.PATH);
|
||||
request(result, Kind.POST_OFFICE, Kind.EXPEDITION_OFFICE, RouteKind.PATH);
|
||||
request(result, Kind.EXPEDITION_OFFICE, Kind.OBSERVATORY, RouteKind.PATH);
|
||||
request(result, Kind.COMMUNAL_DEPOT, Kind.VOID_DOCK, RouteKind.PATH);
|
||||
request(result, Kind.RAIL_WORKSHOP, Kind.SUBSTATION, RouteKind.PATH);
|
||||
request(result, Kind.SUBSTATION, Kind.PUMP_STATION, RouteKind.PATH);
|
||||
request(result, Kind.PUMP_STATION, Kind.GREENHOUSE, RouteKind.PATH);
|
||||
request(result, Kind.POST_OFFICE, Kind.PHOTO_CLUB, RouteKind.PATH);
|
||||
request(result, Kind.PUMP_STATION, Kind.IMPOSSIBLE_SERVICE, RouteKind.PATH);
|
||||
request(result, Kind.OBSERVATORY, Kind.RADIO_RELAY, RouteKind.PATH);
|
||||
request(result, Kind.RAIL_WORKSHOP, Kind.CONSTRUCTION_REFUGE, RouteKind.PATH);
|
||||
Site post = find(sites, Kind.POST_OFFICE);
|
||||
for (var landmark : landmarks) if (landmark.kind() == LandmarkKind.CITY && post != null) {
|
||||
result.add(new Request("city_post_" + result.size(), landmark.id(), post.id(), RouteKind.PATH)); break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private static void request(List<Request> requests, Kind from, Kind to, RouteKind kind) {
|
||||
requests.add(new Request(id(from) + "_" + id(to), id(from), id(to), kind));
|
||||
}
|
||||
|
||||
/** Always persisted, including for an isolated destination: five steps return to natural terrain. */
|
||||
private static List<Cell> landing(Entry entry, List<Site> sites, Probe probe) {
|
||||
int facing = entry.facing(), floor = entry.y() - 1;
|
||||
int endX = entry.x() + DX[facing] * 4, endZ = entry.z() + DZ[facing] * 4;
|
||||
int target = probe.surface(endX, endZ);
|
||||
if (target < 14 || target > 350 || Math.abs(target - floor) > 3) return null;
|
||||
var result = new ArrayList<Cell>();
|
||||
for (int step = 0; step < 5; step++) {
|
||||
// Keep the first two rows flat for the facade and the first rail turn.
|
||||
int y = floor + (int) Math.round((target - floor) * Math.max(0, step - 1) / 3.0);
|
||||
for (int cross = -1; cross <= 1; cross++) {
|
||||
int x = entry.x() + DX[facing] * step + DZ[facing] * cross;
|
||||
int z = entry.z() + DZ[facing] * step - DX[facing] * cross;
|
||||
if (overlapsSites(x, z, x, z, sites)) return null;
|
||||
Cell cell = routeCell(x, y, z, Rail.NONE, false, probe);
|
||||
if (cell == null) return null;
|
||||
result.add(cell);
|
||||
if (step == 4) {
|
||||
int beyond = probe.surface(x + DX[facing], z + DZ[facing]);
|
||||
if (Math.abs(beyond - y) > 1 || !probe.solid(x + DX[facing], beyond, z + DZ[facing])) return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private static RouteDraft route(long seed, Request request, Endpoint from, Endpoint to, List<Site> sites,
|
||||
List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
|
||||
Entry a = from.entry(), b = to.entry();
|
||||
XZ start = new XZ(a.x(), a.z()), end = new XZ(b.x(), b.z());
|
||||
XZ outA = start.move(a.facing(), 6), outB = end.move(b.facing(), 6);
|
||||
int middleX = (outA.x() + outB.x()) / 2, middleZ = (outA.z() + outB.z()) / 2;
|
||||
var alternatives = List.of(
|
||||
List.of(outA, new XZ(outB.x(), outA.z()), outB),
|
||||
List.of(outA, new XZ(outA.x(), outB.z()), outB),
|
||||
List.of(outA, new XZ(middleX, outA.z()), new XZ(middleX, outB.z()), outB),
|
||||
List.of(outA, new XZ(outA.x(), middleZ), new XZ(outB.x(), middleZ), outB),
|
||||
List.of(outA, new XZ(outA.x(), middleZ - 16), new XZ(outB.x(), middleZ - 16), outB),
|
||||
List.of(outA, new XZ(middleX + 16, outA.z()), new XZ(middleX + 16, outB.z()), outB));
|
||||
for (var waypoints : alternatives) {
|
||||
var points = new ArrayList<XZ>(); points.add(start);
|
||||
boolean okay = true;
|
||||
for (var waypoint : waypoints) if (!line(points, waypoint)) { okay = false; break; }
|
||||
if (!okay || !line(points, end) || points.size() > 1024 || new HashSet<>(points).size() != points.size()) continue;
|
||||
RouteDraft result = profile(seed, request, from, to, points, sites, landmarks, existing, probe);
|
||||
if (result != null) return result;
|
||||
}
|
||||
return adaptive(seed, request, from, to, start, end, outA, outB, sites, landmarks, existing, probe);
|
||||
}
|
||||
|
||||
/** Adaptive fallback. The coarse graph only proposes an alignment; it grants no writing permission. */
|
||||
private static RouteDraft adaptive(long seed, Request request, Endpoint from, Endpoint to, XZ start, XZ end,
|
||||
XZ outA, XZ outB, List<Site> sites, List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
|
||||
int stepSize = span(from.entry(), to.entry()) >= 160 ? 8 : 4;
|
||||
int certificateReserve = Math.min(2_200, distance(outA, outB) * 3 + 80);
|
||||
var excluded = new HashSet<XZ>();
|
||||
for (int attempt = 0; attempt < 3; attempt++) {
|
||||
var open = new java.util.PriorityQueue<SearchNode>(Comparator.comparingDouble(SearchNode::priority)
|
||||
.thenComparingDouble(SearchNode::cost).thenComparingInt(n -> n.position().x()).thenComparingInt(n -> n.position().z()));
|
||||
var best = new HashMap<XZ, Double>(); var parents = new HashMap<XZ, XZ>(); var closed = new HashSet<XZ>();
|
||||
var terrainHeights = new HashMap<XZ, Integer>();
|
||||
int begin = coarseRoute(outA, sites, terrainHeights, probe);
|
||||
if (begin == INVALID) return null;
|
||||
open.add(new SearchNode(outA, 0, distance(outA, outB))); best.put(outA, 0.0);
|
||||
List<XZ> found = null;
|
||||
int searchColumns = Math.max(probe.columns.size() + 150, probe.columnLimit - certificateReserve);
|
||||
while (!open.isEmpty() && closed.size() < 500 && probe.columns.size() < searchColumns) {
|
||||
var current = open.remove(); XZ p = current.position();
|
||||
if (!closed.add(p) || current.cost() > best.getOrDefault(p, Double.POSITIVE_INFINITY)) continue;
|
||||
if (distance(p, outB) <= stepSize * 2) {
|
||||
var reversed = new ArrayList<XZ>(); XZ ancestor = p;
|
||||
while (ancestor != null) { reversed.add(ancestor); ancestor = parents.get(ancestor); }
|
||||
java.util.Collections.reverse(reversed);
|
||||
for (int order = 0; order < 2 && found == null; order++) {
|
||||
var candidate = new ArrayList<XZ>(); candidate.add(start); boolean valid = true;
|
||||
for (XZ step : reversed) if (!line(candidate, step)) { valid = false; break; }
|
||||
XZ turn = order == 0 ? new XZ(outB.x(), p.z()) : new XZ(p.x(), outB.z());
|
||||
if (valid && line(candidate, turn) && line(candidate, outB) && line(candidate, end)
|
||||
&& new HashSet<>(candidate).size() == candidate.size()) found = candidate;
|
||||
}
|
||||
if (found != null) break;
|
||||
}
|
||||
int top = terrainHeights.get(p); XZ parent = parents.get(p);
|
||||
for (int d = 0; d < 4; d++) {
|
||||
if (parent == null && d == ((from.entry().facing() + 2) & 3)) continue;
|
||||
XZ next = p.move(d, stepSize);
|
||||
if (closed.contains(next) || excluded.contains(next)
|
||||
|| Math.min(distance(next, outA), distance(next, outB)) > distance(outA, outB) + 64) continue;
|
||||
int height = coarseRoute(next, sites, terrainHeights, probe);
|
||||
if (height == INVALID || Math.abs(height - top) > stepSize + 3) continue;
|
||||
double turnCost = parent == null || parent.x() == next.x() || parent.z() == next.z() ? 0 : 1.5;
|
||||
double cost = current.cost() + stepSize + Math.abs(height - top) * 1.1 + turnCost;
|
||||
if (cost >= best.getOrDefault(next, Double.POSITIVE_INFINITY)) continue;
|
||||
best.put(next, cost); parents.put(next, p);
|
||||
open.add(new SearchNode(next, cost, cost + distance(next, outB) * 1.6));
|
||||
}
|
||||
}
|
||||
if (found == null) { probe.reject("adaptive-no-alignment"); return null; }
|
||||
RouteDraft certified = profile(seed, request, from, to, found, sites, landmarks, existing, probe);
|
||||
if (certified != null) { probe.reject("adaptive-admitted"); return certified; }
|
||||
if (probe.routeBlock == null) return null;
|
||||
// A failed exact corridor removes only nearby coarse nodes on the next proposal.
|
||||
XZ bad = probe.routeBlock;
|
||||
int gx = outA.x() + (int) Math.round((bad.x() - outA.x()) / (double) stepSize) * stepSize;
|
||||
int gz = outA.z() + (int) Math.round((bad.z() - outA.z()) / (double) stepSize) * stepSize;
|
||||
excluded.add(new XZ(gx, gz));
|
||||
for (int d = 0; d < 4; d++) excluded.add(new XZ(gx, gz).move(d, stepSize));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private record SearchNode(XZ position, double cost, double priority) {}
|
||||
private static int distance(XZ a, XZ b) { return Math.abs(a.x() - b.x()) + Math.abs(a.z() - b.z()); }
|
||||
private static int coarseRoute(XZ p, List<Site> sites, Map<XZ, Integer> cache, Probe probe) {
|
||||
Integer old = cache.get(p); if (old != null) return old;
|
||||
if (overlapsSites(p.x() - 2, p.z() - 2, p.x() + 2, p.z() + 2, sites)) { cache.put(p, INVALID); return INVALID; }
|
||||
int top = probe.surface(p.x(), p.z());
|
||||
if (top < 14 || top > 350 || !probe.solid(p.x(), top, p.z()) || probe.protectedAt(p.x(), top, p.z())) {
|
||||
cache.put(p, INVALID); return INVALID;
|
||||
}
|
||||
for (int d = 0; d < 4; d++) {
|
||||
XZ side = p.move(d, 1); int sideTop = probe.surface(side.x(), side.z());
|
||||
if (sideTop < 14 || sideTop > 350 || Math.abs(sideTop - top) > 4 || probe.protectedAt(side.x(), sideTop, side.z())) {
|
||||
cache.put(p, INVALID); return INVALID;
|
||||
}
|
||||
}
|
||||
cache.put(p, top); return top;
|
||||
}
|
||||
|
||||
/** Finite orthogonal alignments, each certified at full block resolution before publication. */
|
||||
private static boolean line(List<XZ> points, XZ destination) {
|
||||
XZ p = points.getLast();
|
||||
if (p.x() != destination.x() && p.z() != destination.z()) return false;
|
||||
int dx = Integer.signum(destination.x() - p.x()), dz = Integer.signum(destination.z() - p.z());
|
||||
while (!p.equals(destination)) {
|
||||
p = new XZ(p.x() + dx, p.z() + dz); points.add(p);
|
||||
if (points.size() > 1024) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static RouteDraft profile(long seed, Request request, Endpoint from, Endpoint to, List<XZ> points,
|
||||
List<Site> sites, List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
|
||||
int size = points.size(); probe.routeBlock = null;
|
||||
int[] lo = new int[size], hi = new int[size], desired = new int[size], group = new int[size];
|
||||
boolean[] flat = new boolean[size - 1];
|
||||
for (int i = 1; i < size - 1; i++) if (turn(points, i)) { flat[i - 1] = true; flat[i] = true; }
|
||||
for (int i = 0; i < size; i++) {
|
||||
XZ point = points.get(i); probe.routeBlock = point; lo[i] = 14; hi[i] = 350;
|
||||
desired[i] = probe.surface(point.x(), point.z());
|
||||
boolean readOnly = i == 0 && from.readOnly() || i == size - 1 && to.readOnly();
|
||||
for (XZ column : strip(points, i)) {
|
||||
if (overlapsSites(column.x(), column.z(), column.x(), column.z(), sites)) { probe.reject("route-building"); return null; }
|
||||
int top = probe.surface(column.x(), column.z());
|
||||
if (top < 14 || top > 350 || !probe.solid(column.x(), top, column.z())) { probe.reject("route-dry-ground"); return null; }
|
||||
lo[i] = Math.max(lo[i], top - MAX_ROUTE_CUT); hi[i] = Math.min(hi[i], top + 4);
|
||||
OwnedCell old = existing.get(key(column.x(), column.z()));
|
||||
if (old != null) { lo[i] = Math.max(lo[i], old.cell().floorY()); hi[i] = Math.min(hi[i], old.cell().floorY()); }
|
||||
if (readOnly && top != (i == 0 ? from.entry().y() : to.entry().y()) - 1) return null;
|
||||
}
|
||||
if (i == 0 || i == size - 1) {
|
||||
int exact = (i == 0 ? from.entry().y() : to.entry().y()) - 1;
|
||||
lo[i] = Math.max(lo[i], exact); hi[i] = Math.min(hi[i], exact);
|
||||
}
|
||||
if (lo[i] > hi[i]) { probe.reject("route-grade-interval"); return null; }
|
||||
}
|
||||
// Curves and both adjacent rails share one elevation; a Minecraft curved rail cannot slope.
|
||||
int groups = 1;
|
||||
for (int i = 1; i < size; i++) { if (!flat[i - 1]) groups++; group[i] = groups - 1; }
|
||||
int[] gl = new int[groups], gh = new int[groups], gd = new int[groups], y = new int[groups];
|
||||
java.util.Arrays.fill(gl, 14); java.util.Arrays.fill(gh, 350);
|
||||
for (int i = 0; i < size; i++) { int g = group[i]; gl[g] = Math.max(gl[g], lo[i]); gh[g] = Math.min(gh[g], hi[i]); gd[g] = desired[i]; }
|
||||
for (int g = 1; g < groups; g++) { gl[g] = Math.max(gl[g], gl[g - 1] - 1); gh[g] = Math.min(gh[g], gh[g - 1] + 1); }
|
||||
for (int g = groups - 2; g >= 0; g--) { gl[g] = Math.max(gl[g], gl[g + 1] - 1); gh[g] = Math.min(gh[g], gh[g + 1] + 1); }
|
||||
for (int g = 0; g < groups; g++) {
|
||||
if (gl[g] > gh[g]) { probe.reject("route-grade"); return null; }
|
||||
int lower = g == 0 ? gl[g] : Math.max(gl[g], y[g - 1] - 1), upper = g == 0 ? gh[g] : Math.min(gh[g], y[g - 1] + 1);
|
||||
y[g] = Math.clamp(gd[g], lower, upper);
|
||||
}
|
||||
var path = new ArrayList<Point>(); var floors = new LinkedHashMap<Long, Integer>();
|
||||
for (int i = 0; i < size; i++) {
|
||||
XZ point = points.get(i); int floor = y[group[i]]; path.add(new Point(point.x(), floor, point.z()));
|
||||
if (i == 0 && from.readOnly() || i == size - 1 && to.readOnly()) continue;
|
||||
for (XZ column : strip(points, i)) {
|
||||
long key = key(column.x(), column.z()); Integer old = floors.putIfAbsent(key, floor);
|
||||
if (old != null && old != floor) { probe.reject("route-self-grade"); return null; }
|
||||
// Historical endpoints and their lateral landing are read-only, even at another nearby turn.
|
||||
for (var landmark : landmarks) if (landingColumn(landmark.entry(), column)) {
|
||||
boolean actualEnd = from.readOnly() && landmark.id().equals(from.id())
|
||||
|| to.readOnly() && landmark.id().equals(to.id());
|
||||
if (!actualEnd || floor != landmark.entry().y() - 1) { probe.reject("route-historical-entry"); return null; }
|
||||
floors.remove(key); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
var cells = new LinkedHashMap<Long, Cell>();
|
||||
for (var entry : floors.entrySet()) {
|
||||
int x = (int) (entry.getKey() >> 32), z = (int) (long) entry.getKey();
|
||||
probe.routeBlock = new XZ(x, z);
|
||||
Cell cell = routeCell(x, entry.getValue(), z, Rail.NONE, false, probe);
|
||||
if (cell == null) { probe.reject("route-column-certificate"); return null; }
|
||||
cells.put(entry.getKey(), cell);
|
||||
}
|
||||
if (request.kind() == RouteKind.FREIGHT) for (int i = 0; i < path.size(); i++) {
|
||||
Point point = path.get(i); long key = key(point.x(), point.z()); Cell cell = cells.get(key);
|
||||
if (cell == null) continue;
|
||||
Rail rail = rail(path, i);
|
||||
// A one-block valley cannot carry two ascending slopes on the same rail. Keep a repairable rail gap.
|
||||
boolean intact = rail != Rail.NONE && Math.floorMod(mix(seed ^ key ^ request.id().hashCode()), 19) != 0;
|
||||
if (rail == Rail.NONE) rail = points.get(Math.max(0, i - 1)).x() == points.get(Math.min(size - 1, i + 1)).x() ? Rail.NORTH_SOUTH : Rail.EAST_WEST;
|
||||
cells.put(key, new Cell(cell.x(), cell.floorY(), cell.z(), cell.foundationDepth(), rail, intact));
|
||||
}
|
||||
return new RouteDraft(List.copyOf(path), cells);
|
||||
}
|
||||
|
||||
private static boolean landingColumn(Entry entry, XZ column) {
|
||||
int dx = column.x() - entry.x(), dz = column.z() - entry.z();
|
||||
return DX[entry.facing()] * dx + DZ[entry.facing()] * dz == 0 && Math.abs(dx) + Math.abs(dz) <= 1;
|
||||
}
|
||||
private static List<XZ> strip(List<XZ> points, int i) {
|
||||
XZ p = points.get(i); var result = new ArrayList<XZ>();
|
||||
if (turn(points, i)) {
|
||||
for (int dx = -1; dx <= 1; dx++) for (int dz = -1; dz <= 1; dz++) result.add(new XZ(p.x() + dx, p.z() + dz));
|
||||
} else {
|
||||
XZ other = points.get(i == points.size() - 1 ? i - 1 : i + 1);
|
||||
int dx = Integer.signum(other.x() - p.x()), dz = Integer.signum(other.z() - p.z());
|
||||
for (int cross = -1; cross <= 1; cross++) result.add(new XZ(p.x() + dz * cross, p.z() - dx * cross));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private static boolean turn(List<XZ> points, int i) {
|
||||
if (i == 0 || i == points.size() - 1) return false;
|
||||
XZ a = points.get(i - 1), b = points.get(i + 1);
|
||||
return a.x() != b.x() && a.z() != b.z();
|
||||
}
|
||||
private static Rail rail(List<Point> path, int i) {
|
||||
Point p = path.get(i), a = path.get(Math.max(0, i - 1)), b = path.get(Math.min(path.size() - 1, i + 1));
|
||||
if (a.floorY() > p.floorY() && b.floorY() > p.floorY()) return Rail.NONE;
|
||||
Point high = a.floorY() > p.floorY() ? a : b.floorY() > p.floorY() ? b : null;
|
||||
if (high != null) return switch (direction(p.x(), p.z(), high.x(), high.z())) {
|
||||
case 0 -> Rail.ASCENDING_NORTH; case 1 -> Rail.ASCENDING_EAST; case 2 -> Rail.ASCENDING_SOUTH; default -> Rail.ASCENDING_WEST;
|
||||
};
|
||||
if (a.x() == b.x()) return Rail.NORTH_SOUTH;
|
||||
if (a.z() == b.z()) return Rail.EAST_WEST;
|
||||
boolean n = a.z() < p.z() || b.z() < p.z(), s = a.z() > p.z() || b.z() > p.z();
|
||||
boolean e = a.x() > p.x() || b.x() > p.x();
|
||||
return n ? e ? Rail.NORTH_EAST : Rail.NORTH_WEST : s && e ? Rail.SOUTH_EAST : Rail.SOUTH_WEST;
|
||||
}
|
||||
private static Cell routeCell(int x, int floor, int z, Rail rail, boolean present, Probe probe) {
|
||||
int top = probe.surface(x, z);
|
||||
if (floor < 14 || floor > 350 || top < floor - 4 || top > floor + MAX_ROUTE_CUT) return null;
|
||||
int foundation = Math.max(1, floor - top + 1), base = floor - foundation;
|
||||
if (!probe.solid(x, base - 1, z) || !probe.solid(x, base, z)) return null;
|
||||
for (int y = base - 1; y <= floor + 3; y++) if (probe.protectedAt(x, y, z)) return null;
|
||||
return new Cell(x, floor, z, foundation, rail, present);
|
||||
}
|
||||
private record XZ(int x, int z) { XZ move(int facing, int distance) { return new XZ(x + DX[facing] * distance, z + DZ[facing] * distance); } }
|
||||
private record SegmentKey(Request request, int cx, int cz) {}
|
||||
private static List<Segment> segments(long seed, Map<Long, OwnedCell> cells) {
|
||||
var groups = new LinkedHashMap<SegmentKey, List<Cell>>();
|
||||
for (var owned : cells.values()) {
|
||||
Cell c = owned.cell(); var key = new SegmentKey(owned.request(), c.x() >> 4, c.z() >> 4);
|
||||
groups.computeIfAbsent(key, ignored -> new ArrayList<>()).add(c);
|
||||
}
|
||||
var result = new ArrayList<Segment>();
|
||||
for (var entry : groups.entrySet()) {
|
||||
var k = entry.getKey(); var r = k.request();
|
||||
// IDs are stable, bounded and independent of native generation order.
|
||||
String id = "piece_" + r.id() + "_" + k.cx() + "_" + k.cz();
|
||||
result.add(new Segment(id, r.from(), r.to(), r.kind(), k.cx(), k.cz(), entry.getValue(), mix(seed ^ key(k.cx(), k.cz()) ^ r.id().hashCode())));
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
|
||||
private static int span(Entry a, Entry b) { return Math.max(Math.abs(a.x() - b.x()), Math.abs(a.z() - b.z())); }
|
||||
private static Site find(List<Site> sites, Kind kind) { return sites.stream().filter(site -> site.kind() == kind).findFirst().orElse(null); }
|
||||
private static String id(Kind kind) { return kind.name().toLowerCase(Locale.ROOT); }
|
||||
private static int direction(int x, int z, int tx, int tz) { return Math.abs(tx - x) >= Math.abs(tz - z) ? tx > x ? 1 : 3 : tz > z ? 2 : 0; }
|
||||
private static boolean overlapsSites(int x0, int z0, int x1, int z1, List<Site> sites) {
|
||||
for (var site : sites) if (x0 <= site.maxX() && x1 >= site.minX() && z0 <= site.maxZ() && z1 >= site.minZ()) return true;
|
||||
return false;
|
||||
}
|
||||
private record SiteDraft(Site site, List<Cell> landings) {}
|
||||
private record Candidate(int x, int z, int facing, int score, long seed) {}
|
||||
private record Endpoint(String id, Entry entry, boolean readOnly) {}
|
||||
private record Request(String id, String from, String to, RouteKind kind) {}
|
||||
private record OwnedCell(Request request, Cell cell) {}
|
||||
private record RouteDraft(List<Point> path, Map<Long, Cell> cells) {}
|
||||
private static final class BudgetExceeded extends RuntimeException {
|
||||
final boolean columns; BudgetExceeded(boolean columns) { this.columns = columns; }
|
||||
}
|
||||
private static final class Probe {
|
||||
final Terrain terrain; final int cx, cz; final double radius;
|
||||
final Map<Long, Integer> columns = new HashMap<>();
|
||||
XZ routeBlock; Map<String, Integer> reasons = new HashMap<>(); int samples, columnLimit, sampleLimit;
|
||||
Probe(Terrain terrain, int cx, int cz, int diameter) { this.terrain = java.util.Objects.requireNonNull(terrain); this.cx = cx; this.cz = cz; radius = diameter / 2.0 - 2; }
|
||||
int surface(int x, int z) {
|
||||
if (Math.hypot((double) x - cx, (double) z - cz) > radius || !terrain.owns(x, z)) return INVALID;
|
||||
long key = key(x, z); var cached = columns.get(key); if (cached != null) return cached;
|
||||
if (columns.size() >= columnLimit) throw new BudgetExceeded(true);
|
||||
count(); int top = terrain.surfaceY(x, z); columns.put(key, top); return top;
|
||||
}
|
||||
boolean solid(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) == INVALID) return false; count(); return terrain.solid(x, y, z); }
|
||||
boolean protectedAt(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) == INVALID) return true; count(); return terrain.protectedAt(x, y, z); }
|
||||
void count() { if (samples >= sampleLimit) throw new BudgetExceeded(false); samples++; }
|
||||
void reject(String reason) { reasons.merge(reason, 1, Integer::sum); }
|
||||
}
|
||||
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
|
||||
private static double unit(long value) { return (value >>> 11) * 0x1.0p-53; }
|
||||
private static long mix(long value) { value = (value ^ value >>> 30) * 0xbf58476d1ce4e5b9L; value = (value ^ value >>> 27) * 0x94d049bb133111ebL; return value ^ value >>> 31; }
|
||||
}
|
||||
+361
@@ -0,0 +1,361 @@
|
||||
package fr.koka.sanctuary.worldgen.heritage;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityPalette;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.AttachFace;
|
||||
import net.minecraft.world.level.block.state.properties.BedPart;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.block.state.properties.DoorHingeSide;
|
||||
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
|
||||
import net.minecraft.world.level.block.state.properties.Half;
|
||||
import net.minecraft.world.level.block.state.properties.RailShape;
|
||||
|
||||
/** Fourteen original, physical generation19 places. The saved site controls all geometry and loot.
|
||||
* Site writes are local to minX/floorY/minZ; route writes are local to the segment's minimum bounds. */
|
||||
public final class HeritageRenderer19 {
|
||||
private HeritageRenderer19() {}
|
||||
public record Target(String name, int x, int y, int z) {}
|
||||
|
||||
/** These are already rotated local standing positions, not world coordinates. */
|
||||
public static List<Target> targets(HeritagePlan19.Site site) {
|
||||
var b = new Builder(site, null); b.layout(); return List.copyOf(b.targets);
|
||||
}
|
||||
public static void render(HeritagePlan19.Site site, LostCityRenderer.BlockWriter writer) {
|
||||
var b = new Builder(site, writer); b.ground(); b.layout();
|
||||
// Footings have absolute saved coordinates and must not undergo a second facade rotation.
|
||||
for (var support : site.supports()) for (int y = support.minY(); y <= support.maxY(); y++)
|
||||
writer.set(support.x() - site.minX(), y - site.floorY(), support.z() - site.minZ(),
|
||||
y == support.minY() ? Blocks.SMOOTH_STONE.defaultBlockState() : Blocks.STONE_BRICKS.defaultBlockState());
|
||||
}
|
||||
public static void renderSegment(HeritagePlan19.Segment segment, LostCityRenderer.BlockWriter writer) {
|
||||
int x0 = segment.minX(), y0 = segment.minY(), z0 = segment.minZ();
|
||||
for (var cell : segment.cells()) {
|
||||
int x = cell.x() - x0, z = cell.z() - z0, floor = cell.floorY() - y0;
|
||||
for (int y = cell.minY() - y0; y <= floor; y++) {
|
||||
Block material = Blocks.STONE;
|
||||
if (y == floor) material = segment.kind() == HeritagePlan19.RouteKind.FREIGHT
|
||||
? cell.rail() != HeritagePlan19.Rail.NONE && Math.floorMod(cell.x() + cell.z(), 3) == 0
|
||||
? Blocks.DARK_OAK_PLANKS : Blocks.GRAVEL
|
||||
: LostCityPalette.roll(segment.seed(), cell.x(), y, cell.z(), 4) == 0 ? Blocks.ANDESITE : Blocks.STONE;
|
||||
writer.set(x, y, z, material.defaultBlockState());
|
||||
}
|
||||
for (int y = floor + 1; y <= floor + 3; y++) writer.set(x, y, z, Blocks.AIR.defaultBlockState());
|
||||
if (cell.railPresent()) writer.set(x, floor + 1, z, Blocks.RAIL.defaultBlockState()
|
||||
.setValue(BlockStateProperties.RAIL_SHAPE, RailShape.valueOf(cell.rail().name())));
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Builder {
|
||||
final HeritagePlan19.Site s;
|
||||
final LostCityRenderer.BlockWriter out;
|
||||
final List<Target> targets = new ArrayList<>();
|
||||
final Rotation rotation;
|
||||
Builder(HeritagePlan19.Site s, LostCityRenderer.BlockWriter out) {
|
||||
this.s = s; this.out = out;
|
||||
rotation = switch (s.facing()) { case 1 -> Rotation.CLOCKWISE_90; case 2 -> Rotation.CLOCKWISE_180;
|
||||
case 3 -> Rotation.COUNTERCLOCKWISE_90; default -> Rotation.NONE; };
|
||||
}
|
||||
int rx(int x, int z) { return switch (s.facing()) { case 1 -> 16 - z; case 2 -> 16 - x; case 3 -> z; default -> x; }; }
|
||||
int rz(int x, int z) { return switch (s.facing()) { case 1 -> x; case 2 -> 16 - z; case 3 -> 16 - x; default -> z; }; }
|
||||
void set(int x, int y, int z, BlockState state) {
|
||||
if (x < 0 || x > 16 || z < 0 || z > 16 || y < -s.foundationDepth() || y > s.height())
|
||||
throw new IllegalArgumentException("Heritage " + s.kind() + " renderer escaped saved site: " + x + "/" + y + "/" + z);
|
||||
if (out != null) out.set(rx(x, z), y, rz(x, z), state.rotate(rotation));
|
||||
}
|
||||
void set(int x, int y, int z, Block b) { set(x, y, z, b.defaultBlockState()); }
|
||||
void box(int x0, int y0, int z0, int x1, int y1, int z1, Block b) {
|
||||
for (int y = y0; y <= y1; y++) for (int z = z0; z <= z1; z++) for (int x = x0; x <= x1; x++) set(x, y, z, b);
|
||||
}
|
||||
void target(String name, int x, int y, int z) { targets.add(new Target(name, rx(x, z), y, rz(x, z))); }
|
||||
void ground() {
|
||||
for (int y = -s.foundationDepth(); y <= s.height(); y++) for (int z = 0; z <= 16; z++) for (int x = 0; x <= 16; x++)
|
||||
set(x, y, z, y < 0 ? Blocks.STONE.defaultBlockState() : y == 0
|
||||
? LostCityPalette.pavement(s.seed(), x, z) : Blocks.AIR.defaultBlockState());
|
||||
}
|
||||
void layout() {
|
||||
target("main_entry", 8, 1, 0);
|
||||
switch (s.kind()) {
|
||||
case FREIGHT_STATION -> station();
|
||||
case POST_OFFICE -> post();
|
||||
case COMMUNAL_DEPOT -> depot();
|
||||
case EXPEDITION_OFFICE -> expedition();
|
||||
case OBSERVATORY -> observatory();
|
||||
case RAIL_WORKSHOP -> railway();
|
||||
case PUMP_STATION -> pumping();
|
||||
case SUBSTATION -> substation();
|
||||
case GREENHOUSE -> greenhouse();
|
||||
case VOID_DOCK -> dock();
|
||||
case RADIO_RELAY -> radio();
|
||||
case PHOTO_CLUB -> photo();
|
||||
case IMPOSSIBLE_SERVICE -> impossible();
|
||||
case CONSTRUCTION_REFUGE -> refuge();
|
||||
}
|
||||
}
|
||||
void shell(int x0, int floor, int z0, int x1, int ceiling, int z1) {
|
||||
for (int y = floor; y <= ceiling; y++) for (int z = z0; z <= z1; z++) for (int x = x0; x <= x1; x++) {
|
||||
boolean wall = x == x0 || x == x1 || z == z0 || z == z1;
|
||||
set(x, y, z, y == floor || y == ceiling ? Blocks.SMOOTH_STONE.defaultBlockState()
|
||||
: wall ? LostCityPalette.masonry(s.seed(), x, y, z) : Blocks.AIR.defaultBlockState());
|
||||
}
|
||||
}
|
||||
void gable(int x0, int z0, int x1, int z1, int y) {
|
||||
int rise = Math.min(3, (x1 - x0) / 2);
|
||||
for (int step = 0; step <= rise; step++)
|
||||
box(x0 + step, y + step, z0, x1 - step, y + step, z1, step == rise ? Blocks.STONE_BRICK_SLAB : Blocks.STONE_BRICKS);
|
||||
}
|
||||
void door(int x, int y, int z, Direction facing) { door(x, y, z, facing, false); }
|
||||
void door(int x, int y, int z, Direction facing, boolean right) {
|
||||
BlockState state = Blocks.DARK_OAK_DOOR.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing)
|
||||
.setValue(BlockStateProperties.DOOR_HINGE, right ? DoorHingeSide.RIGHT : DoorHingeSide.LEFT)
|
||||
.setValue(BlockStateProperties.OPEN, false).setValue(BlockStateProperties.POWERED, false);
|
||||
set(x, y, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.LOWER));
|
||||
set(x, y + 1, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.UPPER));
|
||||
}
|
||||
void doubleDoor(int x, int y, int z) { door(x, y, z, Direction.SOUTH, true); door(x + 1, y, z, Direction.SOUTH, false); }
|
||||
void northWindows(int x0, int x1, int z, int y) { box(x0, y, z, x1, y + 1, z, Blocks.GLASS_PANE); }
|
||||
void hang(int x, int ceiling, int z) { set(x, ceiling - 1, z, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true)); }
|
||||
void seat(int x, int y, int z, Direction view) {
|
||||
set(x, y, z, Blocks.DARK_OAK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, view.getOpposite()));
|
||||
}
|
||||
void lever(int x, int y, int z, Direction facing) {
|
||||
set(x, y, z, Blocks.LEVER.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR)
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, facing));
|
||||
}
|
||||
void rail(int x, int z, RailShape shape) { set(x, 1, z, Blocks.RAIL.defaultBlockState().setValue(BlockStateProperties.RAIL_SHAPE, shape)); }
|
||||
void loot(int x, int y, int z, int approachX, int approachZ) {
|
||||
target("loot_approach", approachX, y, approachZ);
|
||||
if (out != null) out.chest(rx(x, z), y, rz(x, z), ResourceKey.create(Registries.LOOT_TABLE,
|
||||
SanctuaryMod.id("chests/heritage19/" + s.kind().name().toLowerCase(Locale.ROOT))), s.seed() ^ 0x19CA7A10L);
|
||||
}
|
||||
void stairs(int x, int z, int base, int rise) {
|
||||
for (int step = 1; step <= rise; step++) for (int xx = x; xx <= x + 1; xx++) {
|
||||
box(xx, base + step + 1, z + step - 1, xx, base + step + 3, z + step - 1, Blocks.AIR);
|
||||
set(xx, base + step - 1, z + step - 1, Blocks.STONE_BRICKS);
|
||||
set(xx, base + step, z + step - 1, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
|
||||
}
|
||||
}
|
||||
void bed(int x, int y, int z, Direction facing) {
|
||||
BlockState state = Blocks.BED.white().defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing);
|
||||
set(x, y, z, state.setValue(BlockStateProperties.BED_PART, BedPart.FOOT));
|
||||
set(x + facing.getStepX(), y, z + facing.getStepZ(), state.setValue(BlockStateProperties.BED_PART, BedPart.HEAD));
|
||||
int wx = x + facing.getStepX() * 2, wz = z + facing.getStepZ() * 2;
|
||||
Direction along = facing.getClockWise();
|
||||
for (int side = -1; side <= 1; side++) for (int up = 0; up <= 2; up++)
|
||||
set(wx + along.getStepX() * side, y + up, wz + along.getStepZ() * side, Blocks.SMOOTH_STONE);
|
||||
}
|
||||
|
||||
void station() {
|
||||
shell(4, 0, 3, 12, 6, 11); gable(4, 3, 12, 11, 6);
|
||||
door(8, 1, 3, Direction.SOUTH); northWindows(5, 6, 3, 2); northWindows(10, 11, 3, 2);
|
||||
box(5, 1, 5, 6, 1, 5, Blocks.SMOOTH_STONE); set(5, 1, 8, Blocks.BARREL); seat(10, 1, 5, Direction.WEST);
|
||||
for (int z = 2; z <= 14; z++) rail(2, z, RailShape.NORTH_SOUTH);
|
||||
set(2, 1, 15, Blocks.DARK_OAK_FENCE);
|
||||
rail(8, 0, RailShape.NORTH_SOUTH); rail(8, 1, RailShape.NORTH_WEST);
|
||||
for (int x = 3; x <= 7; x++) rail(x, 1, RailShape.EAST_WEST);
|
||||
rail(2, 1, RailShape.SOUTH_EAST);
|
||||
box(0, 5, 5, 3, 5, 11, Blocks.DARK_OAK_PLANKS);
|
||||
for (int z : new int[] {5, 11}) box(1, 1, z, 1, 4, z, Blocks.STRIPPED_OAK_LOG);
|
||||
shell(9, 0, 11, 15, 4, 15); door(10, 1, 11, Direction.SOUTH); door(12, 1, 15, Direction.NORTH);
|
||||
set(10, 1, 13, Blocks.SMOOTH_STONE); lever(10, 2, 13, Direction.EAST);
|
||||
northWindows(13, 14, 15, 2); hang(8, 6, 7); hang(12, 4, 12);
|
||||
target("ticket_counter", 8, 1, 5); target("freight_platform", 3, 1, 9); target("signal_office", 12, 1, 12);
|
||||
target("secondary_entry", 12, 1, 16); loot(14, 1, 14, 13, 14);
|
||||
}
|
||||
void post() {
|
||||
shell(2, 0, 2, 14, 6, 14); door(8, 1, 2, Direction.SOUTH);
|
||||
northWindows(3, 6, 2, 2); northWindows(10, 13, 2, 2);
|
||||
box(3, 1, 7, 13, 5, 7, Blocks.SMOOTH_STONE); door(8, 1, 7, Direction.SOUTH);
|
||||
box(4, 1, 6, 6, 1, 6, Blocks.DARK_OAK_PLANKS);
|
||||
for (int z = 9; z <= 11; z++) { set(3, 1, z, Blocks.BARREL); set(3, 2, z, Blocks.BARREL); }
|
||||
set(6, 1, 10, Blocks.CRAFTING_TABLE); set(8, 1, 12, Blocks.BARREL);
|
||||
shell(10, 0, 10, 14, 6, 14); door(10, 1, 12, Direction.EAST); door(14, 1, 12, Direction.WEST);
|
||||
set(11, 1, 11, Blocks.CARTOGRAPHY_TABLE); hang(8, 6, 4); hang(7, 6, 10); hang(12, 6, 12);
|
||||
target("public_hall", 8, 1, 4); target("sorting_aisle", 7, 1, 10); target("factor_office", 12, 1, 12);
|
||||
target("secondary_entry", 15, 1, 12); loot(13, 1, 13, 12, 13);
|
||||
}
|
||||
void depot() {
|
||||
shell(1, 0, 2, 15, 9, 14); gable(1, 2, 15, 14, 9);
|
||||
box(7, 1, 2, 9, 3, 2, Blocks.AIR); door(4, 1, 2, Direction.SOUTH);
|
||||
for (int z : new int[] {5, 8}) {
|
||||
box(4, 1, z, 5, 1, z + 1, Blocks.DARK_OAK_PLANKS); set(4, 2, z, Blocks.BARREL);
|
||||
box(11, 1, z, 13, 1, z + 1, Blocks.DARK_OAK_PLANKS); set(13, 2, z + 1, Blocks.BARREL);
|
||||
}
|
||||
box(2, 5, 10, 14, 5, 13, Blocks.SMOOTH_STONE);
|
||||
for (int x = 5; x <= 14; x++) set(x, 6, 10, Blocks.IRON_BARS);
|
||||
shell(10, 5, 10, 14, 9, 14); door(10, 6, 12, Direction.EAST);
|
||||
set(11, 6, 11, Blocks.LECTERN.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
|
||||
stairs(2, 5, 0, 5); door(15, 1, 12, Direction.WEST); hang(8, 9, 6); hang(12, 9, 12);
|
||||
for (int z = 0; z <= 8; z++) rail(8, z, RailShape.NORTH_SOUTH);
|
||||
set(8, 1, 9, Blocks.DARK_OAK_FENCE);
|
||||
target("unloading_bay", 8, 1, 5); target("inventory_mezzanine", 5, 6, 12);
|
||||
target("secondary_entry", 16, 1, 12); loot(13, 6, 13, 12, 13);
|
||||
}
|
||||
void expedition() {
|
||||
shell(2, 0, 2, 12, 6, 12); door(8, 1, 2, Direction.SOUTH);
|
||||
northWindows(3, 5, 2, 2); northWindows(10, 11, 2, 2);
|
||||
set(4, 1, 4, Blocks.CARTOGRAPHY_TABLE); set(5, 1, 4, Blocks.LECTERN.defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH)); set(3, 1, 6, Blocks.BARREL);
|
||||
shell(2, 0, 8, 6, 6, 12); door(6, 1, 10, Direction.WEST); set(3, 1, 9, Blocks.BOOKSHELF);
|
||||
box(3, 0, 13, 14, 0, 15, Blocks.DARK_OAK_PLANKS);
|
||||
box(3, 1, 15, 14, 1, 15, Blocks.DARK_OAK_FENCE); door(10, 1, 12, Direction.NORTH);
|
||||
seat(6, 1, 13, Direction.SOUTH); hang(8, 6, 5); hang(4, 6, 10);
|
||||
target("expedition_table", 5, 1, 5); target("balcony", 10, 1, 14);
|
||||
target("secondary_entry", 10, 1, 13); loot(3, 1, 11, 4, 11);
|
||||
}
|
||||
boolean tower(int x, int z) { return x >= 3 && x <= 13 && z >= 3 && z <= 13 && Math.abs(x - 8) + Math.abs(z - 8) <= 8; }
|
||||
void observatory() {
|
||||
for (int y = 0; y <= 12; y++) for (int z = 3; z <= 13; z++) for (int x = 3; x <= 13; x++) if (tower(x, z)) {
|
||||
boolean wall = !tower(x - 1, z) || !tower(x + 1, z) || !tower(x, z - 1) || !tower(x, z + 1);
|
||||
set(x, y, z, y == 0 || y == 6 || y == 12 ? Blocks.SMOOTH_STONE.defaultBlockState()
|
||||
: wall ? LostCityPalette.masonry(s.seed(), x, y, z) : Blocks.AIR.defaultBlockState());
|
||||
if (y == 12 && wall) set(x, 13, z, Blocks.IRON_BARS);
|
||||
}
|
||||
door(8, 1, 3, Direction.SOUTH); door(8, 1, 13, Direction.NORTH);
|
||||
northWindows(7, 9, 3, 8); box(13, 8, 7, 13, 9, 9, Blocks.GLASS_PANE);
|
||||
shell(9, 0, 9, 12, 4, 12); door(9, 1, 10, Direction.EAST); set(10, 1, 11, Blocks.BARREL);
|
||||
set(10, 7, 5, Blocks.CARTOGRAPHY_TABLE); set(11, 7, 8, Blocks.SMOOTH_STONE); lever(11, 8, 8, Direction.WEST);
|
||||
stairs(4, 5, 0, 6); stairs(4, 5, 6, 6);
|
||||
box(4, 12, 11, 6, 12, 11, Blocks.SMOOTH_STONE); box(4, 13, 11, 6, 15, 11, Blocks.AIR);
|
||||
box(10, 13, 9, 10, 18, 9, Blocks.DARK_OAK_FENCE); box(9, 18, 9, 11, 18, 9, Blocks.IRON_BARS);
|
||||
hang(8, 6, 6); hang(10, 4, 11);
|
||||
target("instruments", 9, 7, 8); target("sky_platform", 8, 13, 8);
|
||||
target("secondary_entry", 8, 1, 14); loot(11, 1, 11, 11, 10);
|
||||
}
|
||||
void railway() {
|
||||
shell(1, 0, 2, 15, 6, 14); gable(1, 2, 15, 14, 6); doubleDoor(7, 1, 2);
|
||||
for (int z = 3; z <= 13; z++) for (int x : new int[] {5, 11}) set(x, 1, z, Blocks.RAIL);
|
||||
box(4, 1, 2, 6, 3, 2, Blocks.AIR); rail(8, 0, RailShape.NORTH_SOUTH); rail(8, 1, RailShape.NORTH_WEST);
|
||||
rail(7, 1, RailShape.EAST_WEST); rail(6, 1, RailShape.EAST_WEST); rail(5, 1, RailShape.SOUTH_EAST); rail(5, 2, RailShape.NORTH_SOUTH);
|
||||
box(7, -2, 5, 9, -2, 11, Blocks.SMOOTH_STONE); box(7, -1, 5, 9, 0, 11, Blocks.AIR);
|
||||
for (int step = 0; step <= 2; step++) set(8, -step, 4 + step, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
|
||||
for (int z = 6; z <= 11; z++) { set(6, 1, z, Blocks.IRON_BARS); set(10, 1, z, Blocks.IRON_BARS); }
|
||||
set(2, 1, 4, Blocks.CRAFTING_TABLE); set(3, 1, 4, Blocks.FURNACE.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
|
||||
set(2, 1, 6, Blocks.GRINDSTONE.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR));
|
||||
shell(1, 0, 10, 5, 5, 14); door(5, 1, 12, Direction.WEST); door(8, 1, 14, Direction.NORTH);
|
||||
set(5, 1, 9, Blocks.DARK_OAK_FENCE);
|
||||
set(2, 1, 11, Blocks.ANVIL); hang(8, 6, 8); hang(3, 5, 12);
|
||||
target("maintenance_pit", 8, -1, 9); target("workbench", 3, 1, 5);
|
||||
target("secondary_entry", 8, 1, 15); loot(2, 1, 13, 3, 13);
|
||||
}
|
||||
void pumping() {
|
||||
shell(2, 0, 3, 9, 6, 12); door(8, 1, 3, Direction.SOUTH);
|
||||
set(3, 1, 5, Blocks.DISPENSER.defaultBlockState().setValue(BlockStateProperties.FACING, Direction.EAST));
|
||||
set(4, 1, 5, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 3));
|
||||
set(3, 2, 5, Blocks.POLISHED_ANDESITE); box(3, 3, 5, 3, 5, 8, Blocks.IRON_BARS);
|
||||
for (int x : new int[] {10, 15}) for (int z : new int[] {5, 11}) box(x, 1, z, x, 7, z, Blocks.POLISHED_ANDESITE);
|
||||
shell(10, 8, 5, 15, 13, 11); box(11, 9, 6, 14, 11, 10, Blocks.WATER);
|
||||
box(10, 3, 6, 14, 3, 13, Blocks.SMOOTH_STONE); stairs(10, 3, 0, 3);
|
||||
shell(2, 0, 10, 6, 5, 15); door(5, 1, 10, Direction.SOUTH); door(5, 1, 15, Direction.NORTH);
|
||||
set(3, 1, 11, Blocks.BARREL); hang(7, 6, 7); hang(4, 5, 12);
|
||||
target("pump_room", 6, 1, 6); target("reservoir_walkway", 13, 4, 12);
|
||||
target("secondary_entry", 5, 1, 16); loot(3, 1, 14, 4, 14);
|
||||
}
|
||||
void substation() {
|
||||
for (int z = 2; z <= 14; z++) { set(1, 1, z, Blocks.IRON_BARS); set(15, 1, z, Blocks.IRON_BARS); }
|
||||
for (int x = 1; x <= 15; x++) if (x < 7 || x > 9) set(x, 1, 2, Blocks.IRON_BARS);
|
||||
for (int z : new int[] {5, 8}) {
|
||||
box(3, 1, z, 4, 2, z + 1, Blocks.POLISHED_ANDESITE); box(3, 3, z, 4, 3, z, Blocks.IRON_BARS);
|
||||
}
|
||||
shell(10, 0, 4, 15, 5, 10); door(10, 1, 6, Direction.EAST); northWindows(12, 14, 4, 2);
|
||||
set(12, 1, 5, Blocks.SMOOTH_STONE); lever(12, 2, 5, Direction.SOUTH); set(14, 1, 5, Blocks.CRAFTING_TABLE);
|
||||
shell(10, 0, 10, 15, 5, 15); door(12, 1, 10, Direction.SOUTH); door(15, 1, 13, Direction.WEST);
|
||||
box(6, -2, 10, 7, -2, 13, Blocks.SMOOTH_STONE); box(6, -1, 10, 7, 0, 13, Blocks.AIR);
|
||||
for (int step = 0; step <= 2; step++) set(6, -step, 9 + step, Blocks.STONE_BRICK_STAIRS.defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
|
||||
hang(13, 5, 7); hang(12, 5, 13); target("distribution_yard", 8, 1, 6); target("cable_trench", 6, -1, 12);
|
||||
target("secondary_entry", 16, 1, 13); loot(14, 1, 14, 13, 14);
|
||||
}
|
||||
void greenhouse() {
|
||||
shell(1, 0, 2, 11, 6, 14);
|
||||
for (int y = 2; y <= 5; y++) for (int z = 3; z <= 13; z++) { set(1, y, z, Blocks.GLASS); set(11, y, z, Blocks.GLASS); }
|
||||
northWindows(2, 5, 2, 2); northWindows(7, 10, 2, 2); door(6, 1, 2, Direction.SOUTH);
|
||||
for (int x = 2; x <= 10; x++) for (int z = 3; z <= 13; z++) set(x, 6, z, x == 6 ? Blocks.SMOOTH_STONE : Blocks.GLASS);
|
||||
for (int x : new int[] {2, 3, 4, 8, 9, 10}) for (int z = 4; z <= 10; z++) {
|
||||
set(x, 0, z, Blocks.FARMLAND.defaultBlockState().setValue(BlockStateProperties.MOISTURE, 7));
|
||||
set(x, 1, z, Blocks.WHEAT.defaultBlockState().setValue(BlockStateProperties.AGE_7,
|
||||
2 + LostCityPalette.roll(s.seed(), x, 1, z, 6)));
|
||||
}
|
||||
set(3, 0, 6, Blocks.WATER); set(9, 0, 6, Blocks.WATER);
|
||||
set(3, 1, 6, Blocks.AIR); set(9, 1, 6, Blocks.AIR);
|
||||
shell(11, 0, 9, 15, 5, 15); door(11, 1, 12, Direction.EAST); door(13, 1, 15, Direction.NORTH);
|
||||
set(12, 1, 10, Blocks.CRAFTING_TABLE); set(13, 1, 10, Blocks.FLOWER_POT); set(9, 1, 12, Blocks.COMPOSTER);
|
||||
hang(6, 6, 5); hang(13, 5, 12); target("glasshouse_aisle", 6, 1, 7); target("potting_shed", 13, 1, 12);
|
||||
target("secondary_entry", 13, 1, 16); loot(14, 1, 14, 13, 14);
|
||||
}
|
||||
void dock() {
|
||||
box(0, 0, 0, 16, 0, 16, Blocks.DARK_OAK_PLANKS); box(0, 1, 16, 16, 1, 16, Blocks.DARK_OAK_FENCE);
|
||||
box(0, 1, 1, 0, 1, 15, Blocks.DARK_OAK_FENCE); box(16, 1, 1, 16, 1, 15, Blocks.DARK_OAK_FENCE);
|
||||
shell(2, 0, 3, 7, 5, 10); door(7, 1, 6, Direction.WEST); seat(3, 1, 5, Direction.EAST);
|
||||
shell(2, 0, 10, 7, 5, 15); door(5, 1, 10, Direction.SOUTH); door(7, 1, 13, Direction.WEST);
|
||||
box(12, 1, 5, 12, 11, 5, Blocks.STRIPPED_OAK_LOG); box(8, 11, 5, 14, 11, 5, Blocks.STRIPPED_OAK_LOG);
|
||||
box(14, 7, 5, 14, 10, 5, Blocks.IRON_CHAIN); hang(10, 11, 5); hang(5, 5, 6); hang(5, 5, 12);
|
||||
target("departure_deck", 8, 1, 14); target("waiting_room", 5, 1, 6); target("secondary_entry", 8, 1, 13);
|
||||
loot(3, 1, 14, 4, 14);
|
||||
}
|
||||
void radio() {
|
||||
shell(2, 0, 3, 10, 6, 12); door(8, 1, 3, Direction.SOUTH); northWindows(3, 5, 3, 2);
|
||||
bed(4, 1, 5, Direction.NORTH); set(8, 1, 5, Blocks.CARTOGRAPHY_TABLE);
|
||||
shell(2, 0, 8, 6, 6, 12); door(6, 1, 10, Direction.WEST); door(2, 1, 10, Direction.EAST);
|
||||
box(11, 5, 6, 15, 5, 10, Blocks.SMOOTH_STONE);
|
||||
for (int x : new int[] {11, 15}) for (int z : new int[] {6, 10}) box(x, 1, z, x, 4, z, Blocks.POLISHED_ANDESITE);
|
||||
box(13, 1, 8, 13, 18, 8, Blocks.POLISHED_ANDESITE); box(11, 17, 8, 15, 17, 8, Blocks.IRON_BARS);
|
||||
box(13, 19, 8, 13, 21, 8, Blocks.IRON_BARS); stairs(11, 1, 0, 5);
|
||||
hang(7, 6, 6); hang(4, 6, 10);
|
||||
target("caretaker_room", 7, 1, 6); target("antenna_platform", 12, 6, 9); target("secondary_entry", 1, 1, 10);
|
||||
loot(3, 1, 11, 4, 11);
|
||||
}
|
||||
void photo() {
|
||||
shell(2, 0, 2, 14, 6, 14); door(8, 1, 2, Direction.SOUTH); northWindows(3, 6, 2, 2); northWindows(10, 13, 2, 2);
|
||||
box(3, 1, 8, 13, 5, 8, Blocks.SMOOTH_STONE); door(4, 1, 8, Direction.SOUTH);
|
||||
box(8, 1, 9, 8, 5, 13, Blocks.SMOOTH_STONE); door(8, 1, 11, Direction.EAST);
|
||||
box(10, 1, 5, 12, 1, 5, Blocks.DARK_OAK_PLANKS); set(3, 1, 4, Blocks.BOOKSHELF);
|
||||
for (int z : new int[] {10, 12}) set(3, 1, z, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 3));
|
||||
box(11, 4, 10, 11, 5, 12, Blocks.IRON_CHAIN); set(12, 1, 10, Blocks.CRAFTING_TABLE);
|
||||
door(12, 1, 14, Direction.NORTH); hang(8, 6, 4); hang(5, 6, 11); hang(12, 6, 12);
|
||||
target("club_room", 8, 1, 4); target("darkroom", 5, 1, 11); target("drying_room", 10, 1, 11);
|
||||
target("secondary_entry", 12, 1, 15); loot(13, 1, 13, 12, 13);
|
||||
}
|
||||
void impossible() {
|
||||
shell(3, 0, 2, 11, 4, 7); door(8, 1, 2, Direction.SOUTH);
|
||||
for (int z = 3; z <= 5; z++) { set(4, 1, z, Blocks.BARREL); set(4, 2, z, Blocks.BARREL); }
|
||||
seat(10, 1, 4, Direction.WEST);
|
||||
shell(1, 0, 7, 15, 10, 15); door(8, 1, 7, Direction.SOUTH);
|
||||
for (int x = 2; x <= 14; x++) for (int z = 8; z <= 14; z++) set(x, 0, z,
|
||||
((x + z) & 1) == 0 ? Blocks.CALCITE : Blocks.POLISHED_ANDESITE);
|
||||
for (int x : new int[] {3, 6, 10, 13}) box(x, 1, 11, x, 9, 11, Blocks.CALCITE);
|
||||
shell(1, 0, 12, 5, 5, 15); door(5, 1, 13, Direction.WEST); door(3, 1, 15, Direction.NORTH);
|
||||
set(2, 2, 12, Blocks.GLASS); set(2, 1, 13, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 2));
|
||||
hang(8, 4, 4); hang(8, 10, 11); hang(3, 5, 13);
|
||||
target("lockers", 8, 1, 4); target("oversized_tiled_hall", 8, 1, 11);
|
||||
target("secondary_entry", 3, 1, 16); loot(2, 1, 14, 3, 14);
|
||||
}
|
||||
void refuge() {
|
||||
box(3, 4, 5, 14, 4, 15, Blocks.DARK_OAK_PLANKS);
|
||||
for (int x : new int[] {3, 14}) for (int z : new int[] {5, 15}) box(x, 1, z, x, 3, z, Blocks.STONE_BRICKS);
|
||||
shell(5, 4, 6, 13, 10, 14); gable(5, 6, 13, 14, 10); door(8, 5, 6, Direction.SOUTH);
|
||||
bed(6, 5, 8, Direction.NORTH); set(12, 5, 8, Blocks.CRAFTING_TABLE);
|
||||
shell(6, 0, 9, 11, 4, 14); door(6, 1, 11, Direction.EAST);
|
||||
// Carry the ladder through the floor so opening the hatch leaves a climbable lip.
|
||||
for (int y = 1; y <= 4; y++) set(10, y, 11, Blocks.LADDER.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST));
|
||||
set(10, 5, 11, Blocks.DARK_OAK_TRAPDOOR.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST)
|
||||
.setValue(BlockStateProperties.HALF, Half.BOTTOM));
|
||||
stairs(7, 1, 0, 4); box(2, 1, 3, 3, 11, 4, Blocks.STONE_BRICKS); box(2, 11, 3, 14, 11, 4, Blocks.STRIPPED_OAK_LOG);
|
||||
hang(10, 10, 8); hang(8, 4, 12);
|
||||
target("suspended_hut", 9, 5, 8); target("hidden_floor_hatch", 10, 5, 11); target("secondary_entry", 5, 1, 11);
|
||||
loot(7, 1, 13, 8, 13);
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package fr.koka.sanctuary.worldgen.heritage;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Optional;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureType;
|
||||
|
||||
/** Island-scale routes are split into small independent native starts before rendering. */
|
||||
public final class HeritageStructure19 extends Structure {
|
||||
public static final MapCodec<HeritageStructure19> CODEC = simpleCodec(HeritageStructure19::new);
|
||||
public HeritageStructure19(StructureSettings settings) { super(settings); }
|
||||
|
||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| generator.rootGeneration() != 19 || !generator.experimentalStructures()) return Optional.empty();
|
||||
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
|
||||
if (island == null || !island.origin() || island.generation() != 19) return Optional.empty();
|
||||
var plan = generator.heritagePlan19();
|
||||
int x = context.chunkPos().x(), z = context.chunkPos().z();
|
||||
var pieces = new ArrayList<HeritagePiece19>();
|
||||
// Local route segments render before buildings. No piece needs a distant link endpoint loaded.
|
||||
for (var segment : plan.segments())
|
||||
if (segment.startChunkX() == x && segment.startChunkZ() == z)
|
||||
pieces.add(new HeritagePiece19(segment));
|
||||
for (var site : plan.sites()) if (site.startChunkX() == x && site.startChunkZ() == z)
|
||||
pieces.add(new HeritagePiece19(site));
|
||||
if (pieces.isEmpty()) return Optional.empty();
|
||||
for (var piece : pieces) {
|
||||
var box = piece.getBoundingBox();
|
||||
if (Math.abs((long) Math.floorDiv(box.minX(), 16) - x) > 7
|
||||
|| Math.abs((long) Math.floorDiv(box.maxX(), 16) - x) > 7
|
||||
|| Math.abs((long) Math.floorDiv(box.minZ(), 16) - z) > 7
|
||||
|| Math.abs((long) Math.floorDiv(box.maxZ(), 16) - z) > 7)
|
||||
throw new IllegalStateException("Heritage piece exceeds its local native reference contract");
|
||||
}
|
||||
var locator = pieces.stream().filter(HeritagePiece19::isSite).findFirst().orElse(pieces.getFirst()).getLocatorPosition();
|
||||
return Optional.of(new GenerationStub(locator, builder -> pieces.forEach(builder::addPiece)));
|
||||
}
|
||||
@Override public StructureType<?> type() { return HeritageStructures19.TYPE; }
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package fr.koka.sanctuary.worldgen.heritage;
|
||||
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureType;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceType;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
|
||||
/** Experimental generation19 uses independent native starts, references and saved room pieces. */
|
||||
public final class HeritageStructures19 {
|
||||
public static final ResourceKey<Structure> KEY = ResourceKey.create(Registries.STRUCTURE, SanctuaryMod.id("heritage_networks"));
|
||||
public static final StructureType<HeritageStructure19> TYPE = Registry.register(BuiltInRegistries.STRUCTURE_TYPE,
|
||||
SanctuaryMod.id("heritage_networks_v19"), () -> HeritageStructure19.CODEC);
|
||||
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
|
||||
SanctuaryMod.id("heritage_piece_v19"), HeritagePiece19::new);
|
||||
private static final ThreadLocal<Integer> PLACEMENT_DEPTH = ThreadLocal.withInitial(() -> 0);
|
||||
|
||||
private HeritageStructures19() {}
|
||||
public static void register() { /* Initialize serializers before the dynamic structure registry is read. */ }
|
||||
public static boolean isPlacing() { return PLACEMENT_DEPTH.get() > 0; }
|
||||
static void placing(Runnable renderer) {
|
||||
int previous = PLACEMENT_DEPTH.get();
|
||||
PLACEMENT_DEPTH.set(previous + 1);
|
||||
try { renderer.run(); }
|
||||
finally {
|
||||
if (previous == 0) PLACEMENT_DEPTH.remove();
|
||||
else PLACEMENT_DEPTH.set(previous);
|
||||
}
|
||||
}
|
||||
public static void generate(ExpansionChunkGenerator generator, RegistryAccess registries,
|
||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || generator.rootGeneration() != 19
|
||||
|| !sanctuary.experimentalStructures()) return;
|
||||
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
||||
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
|
||||
var plan = sanctuary.heritagePlan19();
|
||||
if (!hasStart(plan, chunk.getPos().x(), chunk.getPos().z())) return;
|
||||
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||
Structure structure = holder.value();
|
||||
StructureStart previous = manager.getStartForStructure(structure, chunk);
|
||||
if (previous != null && previous.isValid()) return;
|
||||
var random = generator.randomState(island);
|
||||
var sampler = random.createClimateSampler(SamplerContext.builder().enableCaches().build());
|
||||
var start = structure.generate(holder, level, registries, generator, generator.getBiomeSource(), sampler,
|
||||
random, templates, island.seed(), chunk.getPos(), previous == null ? 0 : previous.getReferences(),
|
||||
chunk, structure.biomes()::contains);
|
||||
if (start.isValid()) manager.setStartForStructure(structure, start, chunk);
|
||||
}
|
||||
/** Starts are loaded at STRUCTURE_STARTS; locating never creates an expansion or rebuilds an existing start. */
|
||||
public static Pair<BlockPos, Holder<Structure>> findNearest(ServerLevel level, HolderSet<Structure> structures,
|
||||
BlockPos origin, int radius, boolean skipReferenced) {
|
||||
if (!Level.OVERWORLD.equals(level.dimension())
|
||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| generator.rootGeneration() != 19 || !generator.experimentalStructures()
|
||||
|| !level.structureManager().shouldGenerateStructures()) return null;
|
||||
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||
if (!structures.contains(holder)) return null;
|
||||
var candidates = new ArrayList<HeritagePlan19.Site>();
|
||||
int originChunkX = origin.getX() >> 4, originChunkZ = origin.getZ() >> 4;
|
||||
for (var network : generator.heritagePlan19().sites()) {
|
||||
var island = generator.islandForChunk(network.startChunkX(), network.startChunkZ());
|
||||
if (island == null || !island.origin() || island.generation() != generator.rootGeneration() || !island.status().equals("ready")) continue;
|
||||
if (Math.abs((long) network.startChunkX() - originChunkX) <= radius
|
||||
&& Math.abs((long) network.startChunkZ() - originChunkZ) <= radius) candidates.add(network);
|
||||
}
|
||||
candidates.sort(Comparator.comparingDouble(n -> origin.distSqr(locator(n))));
|
||||
var visited = new HashSet<Long>();
|
||||
for (var network : candidates) {
|
||||
long key = ((long) network.startChunkX() << 32) ^ (network.startChunkZ() & 0xffffffffL);
|
||||
if (!visited.add(key)) continue;
|
||||
var chunk = level.getChunk(network.startChunkX(), network.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
StructureStart start = level.structureManager().getStartForStructure(holder.value(), chunk);
|
||||
if (start == null || !start.isValid() || skipReferenced && !start.canBeReferenced()) continue;
|
||||
if (skipReferenced) level.structureManager().addReference(start);
|
||||
var entrance = start.getPieces().stream().filter(p -> p instanceof HeritagePiece19 site
|
||||
&& site.isSite()).findFirst().orElseThrow();
|
||||
return Pair.of(entrance.getLocatorPosition(), holder);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
static BlockPos locator(HeritagePlan19.Site site) { return HeritagePiece19.locator(site); }
|
||||
static boolean hasStart(HeritagePlan19 plan, int x, int z) {
|
||||
return plan.sites().stream().anyMatch(s -> s.startChunkX() == x && s.startChunkZ() == z)
|
||||
|| plan.segments().stream().anyMatch(s -> s.startChunkX() == x && s.startChunkZ() == z);
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package fr.koka.sanctuary.worldgen.heritage;
|
||||
|
||||
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityPlan;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
|
||||
/** Read-only pre-decoration terrain. No loaded chunks or player-edited blocks enter a plan. */
|
||||
public final class HeritageTerrain19 implements HeritagePlanner19.Terrain {
|
||||
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
|
||||
private final ExpansionIsland island;
|
||||
private final NoiseBasedChunkGenerator generator;
|
||||
private final RandomState random;
|
||||
private final LevelHeightAccessor height;
|
||||
private final Predicate<BlockPos> protection;
|
||||
private final Map<Long, NoiseColumn> columns = new HashMap<>();
|
||||
private List<HeritagePlan19.Landmark> landmarks = List.of();
|
||||
|
||||
public HeritageTerrain19(ExpansionIsland island, NoiseBasedChunkGenerator generator, RandomState random,
|
||||
LevelHeightAccessor height, Predicate<BlockPos> protection) {
|
||||
this.island = island; this.generator = generator; this.random = random;
|
||||
this.height = height; this.protection = protection;
|
||||
}
|
||||
private NoiseColumn column(int x, int z) {
|
||||
if (!owns(x, z)) return null;
|
||||
long key = ((long) x << 32) ^ (z & 0xffffffffL);
|
||||
var previous = columns.get(key);
|
||||
if (previous != null) return previous;
|
||||
if (columns.size() >= HeritagePlan19.MAX_COLUMNS) return null;
|
||||
var result = generator.getBaseColumn(x, z, height, random);
|
||||
columns.put(key, result);
|
||||
return result;
|
||||
}
|
||||
@Override public boolean owns(int x, int z) { return island.contains(x, z); }
|
||||
@Override public boolean solid(int x, int y, int z) {
|
||||
if (y < 0 || y >= 384) return false;
|
||||
var column = column(x, z);
|
||||
return column != null && column.getBlock(y).isSolid() && column.getBlock(y).getFluidState().isEmpty();
|
||||
}
|
||||
@Override public boolean protectedAt(int x, int y, int z) {
|
||||
if (y < 0 || y >= 384) return true;
|
||||
var column = column(x, z);
|
||||
return column == null || !column.getBlock(y).getFluidState().isEmpty()
|
||||
|| protection.test(new BlockPos(x, y, z));
|
||||
}
|
||||
@Override public int surfaceY(int x, int z) {
|
||||
var column = column(x, z);
|
||||
if (column == null) return -1;
|
||||
for (int y = 380; y >= 1; y--) {
|
||||
var state = column.getBlock(y);
|
||||
if (!state.isAir()) return state.isSolid() && state.getFluidState().isEmpty() ? y : -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@Override public List<HeritagePlan19.Landmark> landmarks() { return landmarks; }
|
||||
public int columnsUsed() { return columns.size(); }
|
||||
|
||||
/** Select actual dry ground abutting existing walkable platforms. Their protected blocks stay intact. */
|
||||
public void discoverLandmarks(LostCityPlan city, RuinsPlan17 ruins) {
|
||||
var result = new ArrayList<HeritagePlan19.Landmark>();
|
||||
for (var network : ruins.networks()) {
|
||||
var access = network.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS).findFirst().orElse(null);
|
||||
if (access == null) continue;
|
||||
int deck = access.maxY() - 2;
|
||||
for (int direction = 0; direction < 4; direction++) {
|
||||
var entry = dryEntry(access.centerX() + DX[direction] * 3, access.centerZ() + DZ[direction] * 3, deck, direction);
|
||||
if (entry == null) continue;
|
||||
var kind = network.family() == RuinsPlan17.Family.MINE ? HeritagePlan19.LandmarkKind.MINE : HeritagePlan19.LandmarkKind.UTILITY;
|
||||
result.add(new HeritagePlan19.Landmark("old_" + network.id(), kind, entry));
|
||||
break;
|
||||
}
|
||||
}
|
||||
int cityEntries = 0;
|
||||
outer: for (var district : city.districts()) for (var parcel : district.parcels()) {
|
||||
if (parcel.kind() != LostCityPlan.Kind.STREET) continue;
|
||||
boolean eastWest = parcel.width() > parcel.depth();
|
||||
for (int direction : eastWest ? new int[]{1, 3} : new int[]{0, 2}) {
|
||||
int x = direction == 1 ? parcel.width() - 1 : direction == 3 ? 0 : parcel.width() / 2;
|
||||
int z = direction == 2 ? parcel.depth() - 1 : direction == 0 ? 0 : parcel.depth() / 2;
|
||||
var entry = dryEntry(parcel.minX() + x + DX[direction], parcel.minZ() + z + DZ[direction], parcel.surfaceY(x, z), direction);
|
||||
if (entry == null) continue;
|
||||
result.add(new HeritagePlan19.Landmark("old_city_" + cityEntries++, HeritagePlan19.LandmarkKind.CITY, entry));
|
||||
if (cityEntries == 2) break outer;
|
||||
}
|
||||
}
|
||||
landmarks = List.copyOf(result);
|
||||
}
|
||||
private HeritagePlan19.Entry dryEntry(int x, int z, int deck, int direction) {
|
||||
int y = surfaceY(x, z);
|
||||
if (y < 32 || y > 348 || Math.abs(y - deck) > 1) return null;
|
||||
// A three-wide arrival can abut a platform without cutting its wall or footings.
|
||||
for (int side = -1; side <= 1; side++) {
|
||||
int px = x + DZ[direction] * side, pz = z + DX[direction] * side;
|
||||
var column = column(px, pz);
|
||||
if (column == null || surfaceY(px, pz) != y || !solid(px, y, pz)) return null;
|
||||
for (int by = y - 1; by <= y + 3; by++) if (protectedAt(px, by, pz)) return null;
|
||||
for (int by = y + 1; by <= y + 3; by++) if (!column.getBlock(by).isAir()) return null;
|
||||
}
|
||||
return new HeritagePlan19.Entry(x, y + 1, z, direction);
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,9 @@ public final class RuinsStructure17 extends Structure {
|
||||
|
||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| generator.rootGeneration() != 17 || !generator.experimentalStructures()) return Optional.empty();
|
||||
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19) || !generator.experimentalStructures()) return Optional.empty();
|
||||
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
|
||||
if (island == null || !island.origin() || island.generation() != 17) return Optional.empty();
|
||||
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return Optional.empty();
|
||||
var networks = generator.ruinsPlan17().networks().stream()
|
||||
.filter(n -> n.startChunkX() == context.chunkPos().x() && n.startChunkZ() == context.chunkPos().z()).toList();
|
||||
if (networks.isEmpty()) return Optional.empty();
|
||||
|
||||
+4
-4
@@ -53,10 +53,10 @@ public final class RuinsStructures17 {
|
||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || generator.rootGeneration() != 17
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 17 && generator.rootGeneration() != 19)
|
||||
|| !sanctuary.experimentalStructures()) return;
|
||||
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
||||
if (island == null || !island.origin() || island.generation() != 17) return;
|
||||
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
|
||||
var plan = sanctuary.ruinsPlan17();
|
||||
if (plan.networks().stream().noneMatch(n -> n.startChunkX() == chunk.getPos().x()
|
||||
&& n.startChunkZ() == chunk.getPos().z())) return;
|
||||
@@ -76,7 +76,7 @@ public final class RuinsStructures17 {
|
||||
BlockPos origin, int radius, boolean skipReferenced) {
|
||||
if (!Level.OVERWORLD.equals(level.dimension())
|
||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| generator.rootGeneration() != 17 || !generator.experimentalStructures()
|
||||
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19) || !generator.experimentalStructures()
|
||||
|| !level.structureManager().shouldGenerateStructures()) return null;
|
||||
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||
if (!structures.contains(holder)) return null;
|
||||
@@ -84,7 +84,7 @@ public final class RuinsStructures17 {
|
||||
int originChunkX = origin.getX() >> 4, originChunkZ = origin.getZ() >> 4;
|
||||
for (var network : generator.ruinsPlan17().networks()) {
|
||||
var island = generator.islandForChunk(network.startChunkX(), network.startChunkZ());
|
||||
if (island == null || !island.origin() || island.generation() != 17 || !island.status().equals("ready")) continue;
|
||||
if (island == null || !island.origin() || island.generation() != generator.rootGeneration() || !island.status().equals("ready")) continue;
|
||||
if (Math.abs((long) network.startChunkX() - originChunkX) <= radius
|
||||
&& Math.abs((long) network.startChunkZ() - originChunkZ) <= radius) candidates.add(network);
|
||||
}
|
||||
|
||||
@@ -127,5 +127,24 @@
|
||||
"sanctuary.expansion.error.void_reuse_incomplete": "Void reuse is incomplete. The reservation is retained; check the server log.",
|
||||
"sanctuary.world_options.experimental": "Experimental structures",
|
||||
"sanctuary.world_options.experimental_hint": "Mines, strange rooms and abandoned facilities. This choice is saved with the new world.",
|
||||
"structure.sanctuary.forgotten_networks": "Forgotten networks"
|
||||
"structure.sanctuary.forgotten_networks": "Forgotten networks",
|
||||
"sanctuary.world_options.heritage_hint": "Adds forgotten networks, buildings and abandoned routes to the new island. This choice is saved with the world.",
|
||||
"structure.sanctuary.heritage_networks": "Abandoned infrastructure",
|
||||
"sanctuary.heritage.kind.freight_station": "Freight station",
|
||||
"sanctuary.heritage.kind.post_office": "Post office",
|
||||
"sanctuary.heritage.kind.communal_depot": "Communal depot",
|
||||
"sanctuary.heritage.kind.expedition_office": "Expedition office and map archive",
|
||||
"sanctuary.heritage.kind.observatory": "Observatory and weather station",
|
||||
"sanctuary.heritage.kind.rail_workshop": "Rail workshop",
|
||||
"sanctuary.heritage.kind.pump_station": "Pump station",
|
||||
"sanctuary.heritage.kind.substation": "Utility substation",
|
||||
"sanctuary.heritage.kind.greenhouse": "Greenhouse and seed store",
|
||||
"sanctuary.heritage.kind.void_dock": "Expedition dock",
|
||||
"sanctuary.heritage.kind.radio_relay": "Radio relay",
|
||||
"sanctuary.heritage.kind.photo_club": "Photography club",
|
||||
"sanctuary.heritage.kind.impossible_service": "Changing rooms and strange service rooms",
|
||||
"sanctuary.heritage.kind.construction_refuge": "Construction refuge",
|
||||
"sanctuary.heritage.unavailable": "This survey requires a Sanctuary alpha.19 world with experimental structures enabled.",
|
||||
"sanctuary.heritage.header": "Abandoned infrastructure: %s planned sites and %s links. Entrance coordinates:",
|
||||
"sanctuary.heritage.entry": "%s : %s / %s / %s"
|
||||
}
|
||||
|
||||
@@ -127,5 +127,24 @@
|
||||
"sanctuary.expansion.error.void_reuse_incomplete": "La réutilisation du vide est incomplète. La réservation est conservée ; consultez le journal du serveur.",
|
||||
"sanctuary.world_options.experimental": "Structures expérimentales",
|
||||
"sanctuary.world_options.experimental_hint": "Mines, salles étranges et installations abandonnées. Ce choix reste attaché au nouveau monde.",
|
||||
"structure.sanctuary.forgotten_networks": "Réseaux oubliés"
|
||||
"structure.sanctuary.forgotten_networks": "Réseaux oubliés",
|
||||
"sanctuary.world_options.heritage_hint": "Ajoute les anciens réseaux, bâtiments et voies abandonnées sur la nouvelle île. Ce choix est enregistré avec le monde.",
|
||||
"structure.sanctuary.heritage_networks": "Anciennes infrastructures",
|
||||
"sanctuary.heritage.kind.freight_station": "Gare de marchandises",
|
||||
"sanctuary.heritage.kind.post_office": "Centre postal",
|
||||
"sanctuary.heritage.kind.communal_depot": "Dépôt communal",
|
||||
"sanctuary.heritage.kind.expedition_office": "Bureau des expéditions et cartothèque",
|
||||
"sanctuary.heritage.kind.observatory": "Observatoire et station météo",
|
||||
"sanctuary.heritage.kind.rail_workshop": "Atelier ferroviaire",
|
||||
"sanctuary.heritage.kind.pump_station": "Station de pompage",
|
||||
"sanctuary.heritage.kind.substation": "Sous-station",
|
||||
"sanctuary.heritage.kind.greenhouse": "Serre et graineterie",
|
||||
"sanctuary.heritage.kind.void_dock": "Quai d’expédition",
|
||||
"sanctuary.heritage.kind.radio_relay": "Relais radio",
|
||||
"sanctuary.heritage.kind.photo_club": "Club photo",
|
||||
"sanctuary.heritage.kind.impossible_service": "Vestiaires et salles étranges",
|
||||
"sanctuary.heritage.kind.construction_refuge": "Refuge de chantier",
|
||||
"sanctuary.heritage.unavailable": "Ce relevé nécessite un monde Sanctuary alpha.19 avec les structures expérimentales activées.",
|
||||
"sanctuary.heritage.header": "Anciennes infrastructures : %s lieux et %s liaisons prévus. Coordonnées des entrées :",
|
||||
"sanctuary.heritage.entry": "%s : %s / %s / %s"
|
||||
}
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:iron_axe"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:torch",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:bread",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:iron_shovel"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:bread",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:torch",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:compass"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:paper",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:map",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:minecart"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:rail",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:coal",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:iron_hoe"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:wheat_seeds",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:pumpkin_seeds",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:clock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:torch",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:string",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:spyglass"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:paper",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:book",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:book"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:paper",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:glass_pane",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:compass"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:paper",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:string",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:bucket"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:string",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:torch",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:compass"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:redstone",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:paper",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:iron_pickaxe"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:rail",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:coal",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:clock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:redstone",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:copper_ingot",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:lead"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:torch",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:bread",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"replace": false,
|
||||
"values": [
|
||||
"#minecraft:is_overworld",
|
||||
"sanctuary:cavern_bamboo_grove",
|
||||
"sanctuary:cavern_birch_forest",
|
||||
"sanctuary:cavern_clearing",
|
||||
"sanctuary:cavern_dark_grove",
|
||||
"sanctuary:cavern_dripstone_caves",
|
||||
"sanctuary:cavern_dry_woodland",
|
||||
"sanctuary:cavern_lush_caves",
|
||||
"sanctuary:cavern_oak_forest",
|
||||
"sanctuary:cavern_rocky_heath",
|
||||
"sanctuary:cavern_sulfur_depths",
|
||||
"sanctuary:layered_flower_meadow",
|
||||
"sanctuary:layered_lower_grove",
|
||||
"sanctuary:layered_meadow",
|
||||
"sanctuary:layered_mossy_terraces",
|
||||
"sanctuary:layered_rocky_terraces",
|
||||
"sanctuary:lost_city",
|
||||
"sanctuary:population_bamboo_grove",
|
||||
"sanctuary:population_birch_forest",
|
||||
"sanctuary:population_clearing",
|
||||
"sanctuary:population_dark_grove",
|
||||
"sanctuary:population_dripstone_caves",
|
||||
"sanctuary:population_dry_woodland",
|
||||
"sanctuary:population_lush_caves",
|
||||
"sanctuary:population_oak_forest",
|
||||
"sanctuary:population_rocky_heath",
|
||||
"sanctuary:population_sulfur_depths",
|
||||
"sanctuary:rift_bamboo_grove",
|
||||
"sanctuary:rift_birch_forest",
|
||||
"sanctuary:rift_clearing",
|
||||
"sanctuary:rift_dark_grove",
|
||||
"sanctuary:rift_dry_woodland",
|
||||
"sanctuary:rift_oak_forest",
|
||||
"sanctuary:rift_rocky_heath",
|
||||
"sanctuary:rift_sulfur_depths",
|
||||
"sanctuary:starter_forest",
|
||||
"sanctuary:starter_forest_hydrology",
|
||||
"sanctuary:temperate_birch_forest",
|
||||
"sanctuary:temperate_dappled_forest",
|
||||
"sanctuary:temperate_flower_forest",
|
||||
"sanctuary:temperate_forest",
|
||||
"sanctuary:temperate_plains",
|
||||
"sanctuary:woodland_bamboo_grove",
|
||||
"sanctuary:woodland_birch_forest",
|
||||
"sanctuary:woodland_clearing",
|
||||
"sanctuary:woodland_dark_grove",
|
||||
"sanctuary:woodland_dry_woodland",
|
||||
"sanctuary:woodland_oak_forest",
|
||||
"sanctuary:woodland_rocky_heath",
|
||||
"sanctuary:woodland_sulfur_depths"
|
||||
]
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"type": "sanctuary:heritage_networks_v19",
|
||||
"biomes": "#sanctuary:has_structure/heritage_networks",
|
||||
"step": "underground_structures",
|
||||
"terrain_adaptation": "none",
|
||||
"spawn_overrides": {}
|
||||
}
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"minecraft:overworld": {
|
||||
"type": "sanctuary:sanctuary_384",
|
||||
"generator": {
|
||||
"type": "sanctuary:island_v17",
|
||||
"type": "sanctuary:island_v19",
|
||||
"biome_source": {
|
||||
"biomes": [
|
||||
"minecraft:the_void",
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"dimensions": {
|
||||
"minecraft:overworld": {
|
||||
"type": "sanctuary:sanctuary_384",
|
||||
"generator": {
|
||||
"type": "sanctuary:island_v17",
|
||||
"biome_source": {
|
||||
"biomes": [
|
||||
"minecraft:the_void",
|
||||
"minecraft:forest",
|
||||
"minecraft:birch_forest",
|
||||
"minecraft:flower_forest",
|
||||
"minecraft:plains",
|
||||
"minecraft:meadow",
|
||||
"minecraft:taiga",
|
||||
"minecraft:snowy_taiga",
|
||||
"minecraft:snowy_plains",
|
||||
"minecraft:grove",
|
||||
"minecraft:ice_spikes",
|
||||
"minecraft:frozen_peaks",
|
||||
"minecraft:old_growth_spruce_taiga",
|
||||
"minecraft:dark_forest",
|
||||
"minecraft:swamp",
|
||||
"minecraft:pale_garden",
|
||||
"minecraft:jungle",
|
||||
"minecraft:sparse_jungle",
|
||||
"minecraft:bamboo_jungle",
|
||||
"minecraft:mangrove_swamp",
|
||||
"minecraft:savanna",
|
||||
"minecraft:savanna_plateau",
|
||||
"minecraft:desert",
|
||||
"minecraft:badlands",
|
||||
"minecraft:eroded_badlands",
|
||||
"minecraft:wooded_badlands",
|
||||
"minecraft:windswept_hills",
|
||||
"minecraft:windswept_gravelly_hills",
|
||||
"minecraft:stony_peaks",
|
||||
"minecraft:dappled_forest",
|
||||
"minecraft:cherry_grove",
|
||||
"minecraft:lush_caves",
|
||||
"minecraft:dripstone_caves",
|
||||
"minecraft:sulfur_caves",
|
||||
"sanctuary:population_oak_forest",
|
||||
"sanctuary:population_birch_forest",
|
||||
"sanctuary:population_clearing",
|
||||
"sanctuary:population_dry_woodland",
|
||||
"sanctuary:population_rocky_heath",
|
||||
"sanctuary:population_dark_grove",
|
||||
"sanctuary:population_bamboo_grove",
|
||||
"sanctuary:population_sulfur_depths",
|
||||
"sanctuary:population_lush_caves",
|
||||
"sanctuary:population_dripstone_caves",
|
||||
"sanctuary:lost_city"
|
||||
]
|
||||
},
|
||||
"initial_diameter": 724
|
||||
}
|
||||
},
|
||||
"minecraft:the_end": {
|
||||
"type": "minecraft:the_end",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": {
|
||||
"type": "minecraft:the_end"
|
||||
},
|
||||
"settings": "minecraft:end"
|
||||
}
|
||||
},
|
||||
"minecraft:the_nether": {
|
||||
"type": "minecraft:the_nether",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": {
|
||||
"type": "minecraft:multi_noise",
|
||||
"preset": "minecraft:nether"
|
||||
},
|
||||
"settings": "minecraft:nether"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,6 +158,21 @@ public final class ExpansionJournalSmoke {
|
||||
require(Arrays.equals(experimentBytes, Files.readAllBytes(experimentDirectory.resolve(ExpansionJournal.FILE_NAME)))
|
||||
&& Arrays.equals(ruinsBytes, Files.readAllBytes(ruinsDirectory.resolve(ExpansionJournal.FILE_NAME))),
|
||||
"Rejected cross-generation changes preserve experiment and published16 bytes");
|
||||
var heritageRoot = ExpansionIsland.origin(724, SEED, 19);
|
||||
var heritageDirectory = scratch.resolve("heritage-19");
|
||||
var heritageJournal = new ExpansionJournal(heritageDirectory, SEED, 724, 19);
|
||||
var heritageChild = ExpansionPlacement.candidates(SEED, List.of(heritageRoot), "heritage", "sanctuary", "east", "lost_city", 128, "natural", 0).getFirst()
|
||||
.withReuseToken("f".repeat(64));
|
||||
heritageJournal.commit(List.of(heritageRoot, heritageChild));
|
||||
require(new ExpansionJournal(heritageDirectory, SEED, 724, 19).islands().equals(List.of(heritageRoot, heritageChild)),
|
||||
"Generation19 preserves the new world and its expansions across journal reload");
|
||||
byte[] heritageBytes = Files.readAllBytes(heritageDirectory.resolve(ExpansionJournal.FILE_NAME));
|
||||
reject(() -> new ExpansionJournal(heritageDirectory, SEED, 724, 17), "No implicit downgrade from19 to17");
|
||||
reject(() -> new ExpansionJournal(experimentDirectory, SEED, 724, 19), "No implicit upgrade from17 to19");
|
||||
reject(() -> heritageJournal.commit(List.of(heritageRoot, experimentChild)), "No generation17 child in19");
|
||||
require(Arrays.equals(heritageBytes, Files.readAllBytes(heritageDirectory.resolve(ExpansionJournal.FILE_NAME)))
|
||||
&& Arrays.equals(experimentBytes, Files.readAllBytes(experimentDirectory.resolve(ExpansionJournal.FILE_NAME))),
|
||||
"Rejected migrations preserve both generation17 and19 journals");
|
||||
try {
|
||||
ExpansionPlacement.candidates(SEED, List.of(ExpansionIsland.origin(724, SEED, 13)), "city", "sanctuary", "east", "lost_city", 256, "natural", 0);
|
||||
throw new AssertionError("Lost City must not silently activate in a saved alpha.13 world");
|
||||
|
||||
+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 schema19 persistence for catalogue sites and chunk-local abandoned routes. */
|
||||
public final class HeritagePiece19Smoke {
|
||||
public static void main(String[] args) {
|
||||
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); run();
|
||||
System.out.println("HeritagePiece19Smoke: 14 kinds, 4 facades, every rail profile, missing rails, strict NBT and local native bounds passed.");
|
||||
}
|
||||
public static void run() {
|
||||
for (var kind : HeritagePlan19.Kind.values()) for (int face = 0; face < 4; face++) {
|
||||
int x = -32, z = 40, floor = 160;
|
||||
var entry = new HeritagePlan19.Entry(face == 1 ? x + 17 : face == 3 ? x - 1 : x + 8,
|
||||
floor + 1, face == 2 ? z + 17 : face == 0 ? z - 1 : z + 8, face);
|
||||
var supportList = new ArrayList<HeritagePlan19.Support>();
|
||||
supportList.add(new HeritagePlan19.Support(x + 1, 150, z + 1, 155));
|
||||
var site = new HeritagePlan19.Site("site_" + kind.name(), kind, x, floor, z, 17, 17, 20, face,
|
||||
Long.MIN_VALUE + kind.ordinal(), 4, supportList, entry);
|
||||
supportList.clear();
|
||||
var saved = HeritagePiece19.encodeSite(site);
|
||||
require(HeritagePiece19.decodeSite(saved).equals(site) && HeritagePiece19.encodeSite(HeritagePiece19.decodeSite(saved)).equals(saved), "Site roundtrip changed geometry");
|
||||
require(site.supports().size() == 1 && HeritagePiece19.bounds(site).minY() == 150, "Support input is mutable or excluded from native bounds");
|
||||
require(HeritagePiece19.locator(site).getX() == entry.x() && HeritagePiece19.locator(site).getY() == entry.y()
|
||||
&& HeritagePiece19.locator(site).getZ() == entry.z(), "Locate lost the rotated facade entry");
|
||||
badSite(saved, t -> t.remove("Facing")); badSite(saved, t -> t.putInt("Schema", 17));
|
||||
badSite(saved, t -> t.putString("Type", "ROUTE")); badSite(saved, t -> t.putString("Kind", "FUTURE"));
|
||||
badSite(saved, t -> t.putDouble("FloorY", 160)); badSite(saved, t -> t.putInt("Seed", 1));
|
||||
badSite(saved, t -> t.putInt("Future", 1)); badSite(saved, t -> t.putInt("MinX", Integer.MAX_VALUE));
|
||||
badSite(saved, t -> t.putInt("Width", 18)); badSite(saved, t -> t.putInt("Facing", 4));
|
||||
badSite(saved, t -> t.putInt("FoundationDepth", 0)); badSite(saved, t -> t.putString("Id", "x".repeat(129)));
|
||||
badSite(saved, t -> t.getCompound("Entry").orElseThrow().putInt("X", entry.x() + 1));
|
||||
badSite(saved, t -> t.getCompound("Entry").orElseThrow().putLong("Y", entry.y()));
|
||||
badSite(saved, t -> t.getList("Supports").orElseThrow().getCompound(0).orElseThrow().putInt("MinY", 130));
|
||||
badSite(saved, t -> t.getList("Supports").orElseThrow().getCompound(0).orElseThrow().putInt("X", site.maxX() + 1));
|
||||
badSite(saved, t -> {
|
||||
var list = t.getList("Supports").orElseThrow(); list.add(list.getFirst().copy());
|
||||
});
|
||||
}
|
||||
var cells = new ArrayList<HeritagePlan19.Cell>();
|
||||
int index = 0;
|
||||
for (var rail : HeritagePlan19.Rail.values()) {
|
||||
cells.add(new HeritagePlan19.Cell(-32 + index, 150 + index % 3, 32, 2 + index % 4, rail, rail != HeritagePlan19.Rail.NONE && index % 2 == 0)); index++;
|
||||
}
|
||||
var segment = new HeritagePlan19.Segment("freight/part0", "station", "mine", HeritagePlan19.RouteKind.FREIGHT, -2, 2, cells, Long.MAX_VALUE);
|
||||
cells.clear();
|
||||
var landing = new HeritagePlan19.Segment("station/landing", "station", "station", HeritagePlan19.RouteKind.PATH,
|
||||
-2, 2, List.of(new HeritagePlan19.Cell(-32, 150, 32, 2, HeritagePlan19.Rail.NONE, false)), 42);
|
||||
require(HeritagePiece19.decodeSegment(HeritagePiece19.encodeSegment(landing)).equals(landing), "Standalone entry landing lost its site binding");
|
||||
var saved = HeritagePiece19.encodeSegment(segment);
|
||||
require(HeritagePiece19.decodeSegment(saved).equals(segment) && HeritagePiece19.encodeSegment(HeritagePiece19.decodeSegment(saved)).equals(saved), "Route roundtrip changed shapes, supports or deliberate rail gaps");
|
||||
require(segment.cells().size() == HeritagePlan19.Rail.values().length, "External route list mutation changed its saved cells");
|
||||
require(HeritagePiece19.bounds(segment).intersectingChunks().count() == 1, "Segment escaped its one-chunk native start");
|
||||
badRoute(saved, t -> t.putInt("Schema", 20)); badRoute(saved, t -> t.putString("Type", "SITE"));
|
||||
badRoute(saved, t -> t.remove("FromId")); badRoute(saved, t -> t.putString("ToId", "station"));
|
||||
badRoute(saved, t -> t.putString("Id", " ")); badRoute(saved, t -> t.putInt("StartChunkX", -1));
|
||||
badRoute(saved, t -> t.putDouble("Seed", 42)); badRoute(saved, t -> t.put("Cells", new ListTag()));
|
||||
badRoute(saved, t -> cell(t).putInt("RailPresent", 1)); badRoute(saved, t -> cell(t).putByte("RailPresent", (byte) 2));
|
||||
badRoute(saved, t -> cell(t).putString("Rail", "FUTURE")); badRoute(saved, t -> cell(t).putBoolean("RailPresent", true));
|
||||
badRoute(saved, t -> cell(t).putInt("X", -16)); badRoute(saved, t -> cell(t).putInt("FoundationDepth", 13));
|
||||
badRoute(saved, t -> cell(t).putInt("FloorY", 2)); badRoute(saved, t -> cell(t).putInt("Extra", 0));
|
||||
badRoute(saved, t -> { var list = t.getList("Cells").orElseThrow(); list.add(list.getFirst().copy()); });
|
||||
badRoute(saved, t -> { var list = new ListTag(); list.add(IntTag.valueOf(1)); t.put("Cells", list); });
|
||||
badRoute(saved, t -> { var list = t.getList("Cells").orElseThrow(); while (list.size() <= 256) list.add(list.getFirst().copy()); });
|
||||
}
|
||||
private static CompoundTag cell(CompoundTag tag) { return tag.getList("Cells").orElseThrow().getCompound(0).orElseThrow(); }
|
||||
private static void badSite(CompoundTag original, Consumer<CompoundTag> mutation) {
|
||||
var value = original.copy(); mutation.accept(value);
|
||||
try { HeritagePiece19.decodeSite(value); } catch (IllegalArgumentException expected) { return; }
|
||||
throw new AssertionError("Malformed saved site accepted: " + value);
|
||||
}
|
||||
private static void badRoute(CompoundTag original, Consumer<CompoundTag> mutation) {
|
||||
var value = original.copy(); mutation.accept(value);
|
||||
try { HeritagePiece19.decodeSegment(value); } catch (IllegalArgumentException expected) { return; }
|
||||
throw new AssertionError("Malformed saved route accepted: " + value);
|
||||
}
|
||||
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
|
||||
}
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
package fr.koka.sanctuary.worldgen.heritage;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.*;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
|
||||
/** Pure geography contract: full catalogue, actual destinations, independent entrances and supported axes. */
|
||||
public final class HeritagePlanner19Smoke {
|
||||
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
|
||||
public static void main(String[] args) {
|
||||
var catalogue = EnumSet.noneOf(Kind.class);
|
||||
int freight = 0, paths = 0, missingRails = 0, islandAxes = 0;
|
||||
for (int diameter : new int[] {512, 724, 1024}) for (long seed : new long[] {0, 1, 42, -1}) {
|
||||
Fixture terrain = new Fixture((x, z) -> 220);
|
||||
var plan = HeritagePlanner19.plan(seed, 0, 0, diameter, terrain);
|
||||
verify(plan, terrain);
|
||||
require(plan.sites().size() >= 8, "Flat starter loses most of its small physical catalogue: " + plan.diagnostics());
|
||||
require(plan.links().size() >= 2, "Flat starter has no useful inter-destination axes");
|
||||
plan.sites().forEach(site -> catalogue.add(site.kind()));
|
||||
freight += (int) plan.links().stream().filter(l -> l.kind() == RouteKind.FREIGHT).count();
|
||||
islandAxes += (int) plan.links().stream().filter(l -> {
|
||||
Point first = l.path().getFirst(), last = l.path().getLast();
|
||||
return Math.max(Math.abs(first.x() - last.x()), Math.abs(first.z() - last.z())) >= diameter / 4;
|
||||
}).count();
|
||||
paths += (int) plan.links().stream().filter(l -> l.kind() == RouteKind.PATH).count();
|
||||
missingRails += (int) plan.segments().stream().flatMap(s -> s.cells().stream())
|
||||
.filter(c -> c.rail() != Rail.NONE && !c.railPresent()).count();
|
||||
var repeat = HeritagePlanner19.plan(seed, 0, 0, diameter, new Fixture((x, z) -> 220));
|
||||
require(plan.sites().equals(repeat.sites()) && plan.links().equals(repeat.links())
|
||||
&& plan.segments().equals(repeat.segments()) && plan.samples() == repeat.samples()
|
||||
&& plan.diagnostics().equals(repeat.diagnostics()), "Planning depends on generation order or mutable state");
|
||||
}
|
||||
require(catalogue.equals(EnumSet.allOf(Kind.class)), "A requested heritage family never appears: " + catalogue);
|
||||
require(islandAxes > 0, "All published networks remain local; no axis reaches island scale");
|
||||
require(freight > 0 && paths > 0 && missingRails > 0, "Both networks and repairable missing rails must be represented");
|
||||
var flat = HeritagePlanner19.plan(0, 0, 0, 724, new Fixture((x, z) -> 220));
|
||||
require(!flat.sites().equals(HeritagePlanner19.plan(1, 0, 0, 724, new Fixture((x, z) -> 220)).sites()), "Seed does not affect sites");
|
||||
|
||||
Fixture wet = new Fixture((x, z) -> 220); wet.protection = (x, y, z) -> true;
|
||||
var blocked = HeritagePlanner19.plan(0, 0, 0, 724, wet); verify(blocked, wet);
|
||||
require(blocked.isEmpty() && blocked.links().isEmpty(), "Protected water or earlier city is overwritten");
|
||||
Fixture voidTerrain = new Fixture((x, z) -> -1);
|
||||
var absent = HeritagePlanner19.plan(0, 0, 0, 724, voidTerrain); verify(absent, voidTerrain);
|
||||
require(absent.isEmpty() && absent.segments().isEmpty(), "Void receives unsupported structures or fictional routes");
|
||||
Fixture thin = new Fixture((x, z) -> 220); thin.rock = (x, y, z) -> y >= 219 && y <= 220;
|
||||
var thinPlan = HeritagePlanner19.plan(0, 0, 0, 724, thin); verify(thinPlan, thin);
|
||||
require(thinPlan.isEmpty(), "Thin shelves receive buildings without anchored footings");
|
||||
|
||||
// Every site keeps its local access even when a protected ring rejects every inter-site alignment.
|
||||
Set<Long> allowed = new HashSet<>();
|
||||
for (var site : flat.sites()) for (int x = site.minX(); x <= site.maxX(); x++) for (int z = site.minZ(); z <= site.maxZ(); z++) allowed.add(key(x, z));
|
||||
for (var segment : flat.segments()) if (segment.fromId().equals(segment.toId())) for (var c : segment.cells()) allowed.add(key(c.x(), c.z()));
|
||||
Fixture isolated = new Fixture((x, z) -> 220); isolated.protection = (x, y, z) -> !allowed.contains(key(x, z));
|
||||
var islands = HeritagePlanner19.plan(0, 0, 0, 724, isolated); verify(islands, isolated);
|
||||
require(!islands.sites().isEmpty(), "Independent access fixture has no retained site");
|
||||
require(islands.sites().stream().anyMatch(site -> islands.links().stream().noneMatch(l -> l.fromId().equals(site.id()) || l.toId().equals(site.id()))),
|
||||
"The isolated destination witness unexpectedly has every long route");
|
||||
|
||||
// Historical landmark feet are read-only and have no mask bypass, even when a route reaches them.
|
||||
Site station = flat.sites().stream().filter(s -> s.kind() == Kind.FREIGHT_STATION).findFirst().orElseThrow();
|
||||
Entry historical = new Entry(station.entry().x() + DX[station.facing()] * 30,
|
||||
221, station.entry().z() + DZ[station.facing()] * 30, (station.facing() + 2) & 3);
|
||||
Fixture landmark = new Fixture((x, z) -> 220);
|
||||
landmark.landmarks = List.of(new Landmark("old_mine", LandmarkKind.MINE, historical));
|
||||
landmark.protection = (x, y, z) -> x == historical.x() && z == historical.z();
|
||||
var linked = HeritagePlanner19.plan(0, 0, 0, 724, landmark); verify(linked, landmark);
|
||||
require(!linked.contains(historical.x(), historical.y(), historical.z()), "A historical endpoint receives route painting");
|
||||
|
||||
Landmark left = new Landmark("left", LandmarkKind.CITY, new Entry(0, 101, 0, 1));
|
||||
Landmark right = new Landmark("right", LandmarkKind.MINE, new Entry(1, 101, 0, 3));
|
||||
Landmark unused = new Landmark("unused", LandmarkKind.UTILITY, new Entry(30, 101, 0, 0));
|
||||
var dependency = new HeritagePlan19(List.of(), List.of(left, right, unused),
|
||||
List.of(new Link("existing_access", "left", "right", RouteKind.PATH, List.of(new Point(0, 100, 0), new Point(1, 100, 0)))),
|
||||
List.of(), 0, 0, Map.of());
|
||||
for (int z = -1; z <= 1; z++) for (int y = 99; y <= 103; y++) {
|
||||
require(dependency.protects(0, y, z) && dependency.intersectsProtection(0, y, z, 0, y, z), "Used historical access lost its rock/headroom dependency");
|
||||
require(!dependency.contains(0, y, z), "Read-only historical access received writing permission");
|
||||
}
|
||||
require(!dependency.protects(30, 101, 0), "Unused historical landmark reserves unrelated terrain");
|
||||
|
||||
// Smooth slopes exercise certified cut/fill and route grade without allowing an unsupported plateau.
|
||||
Fixture sloped = new Fixture((x, z) -> 220 + Math.floorDiv(z, 12));
|
||||
var slopes = HeritagePlanner19.plan(42, 0, 0, 724, sloped); verify(slopes, sloped);
|
||||
require(slopes.sites().size() >= 5, "A modest slope makes the entire catalogue inadmissible");
|
||||
Fixture shifted = new Fixture((x, z) -> 220);
|
||||
var translated = HeritagePlanner19.plan(42, -1024, 512, 724, shifted); verify(translated, shifted);
|
||||
require(!translated.isEmpty(), "Negative chunk coordinates prevent generation");
|
||||
require(flat.diagnostics().size() == Kind.values().length && blocked.diagnostics().size() == Kind.values().length,
|
||||
"Missing kinds have no explicit diagnostic");
|
||||
expectInvalid(() -> new Link("self", "a", "a", RouteKind.PATH, List.of(new Point(0, 100, 0), new Point(1, 100, 0))));
|
||||
expectInvalid(() -> new Segment("self", "a", "a", RouteKind.FREIGHT, 0, 0, List.of(new Cell(0, 100, 0, 1, Rail.NONE, false)), 0));
|
||||
expectInvalid(() -> new Cell(0, 100, 0, 13, Rail.NONE, false));
|
||||
System.out.println("HeritagePlanner19Smoke passed: fourteen families, deterministic placements, persisted isolated entrances, real route destinations, bounded slopes and supported rails, unique chunk-local cells, indexed protection and budgets");
|
||||
}
|
||||
|
||||
private static void verify(HeritagePlan19 plan, Fixture terrain) {
|
||||
require(plan.samples() == terrain.samples && plan.samples() <= HeritagePlan19.MAX_SAMPLES, "Probe accounting differs from native calls");
|
||||
require(plan.columns() == terrain.columns.size() && plan.columns() <= HeritagePlan19.MAX_COLUMNS, "Column budget exceeded");
|
||||
Map<Long, Cell> cells = new HashMap<>(); Map<String, Entry> destinations = new HashMap<>();
|
||||
Set<Long> footprints = new HashSet<>();
|
||||
for (var site : plan.sites()) {
|
||||
destinations.put(site.id(), site.entry());
|
||||
require(site.foundationDepth() <= 8 && site.maxY() <= 380, "Architecture exceeds its bounded envelope");
|
||||
require(Math.abs((site.minX() >> 4) - site.startChunkX()) <= 7 && Math.abs((site.maxX() >> 4) - site.startChunkX()) <= 7
|
||||
&& Math.abs((site.minZ() >> 4) - site.startChunkZ()) <= 7 && Math.abs((site.maxZ() >> 4) - site.startChunkZ()) <= 7, "Site exceeds native reference radius");
|
||||
for (int z = site.minZ(); z <= site.maxZ(); z++) for (int x = site.minX(); x <= site.maxX(); x++) {
|
||||
require(footprints.add(key(x, z)), "Two buildings overlap");
|
||||
require(terrain.height.applyAsInt(x, z) - site.floorY() <= HeritagePlanner19.MAX_SITE_CUT, "Site cuts an excessive mound");
|
||||
for (int y = site.minY(); y <= site.maxY(); y++) require(!terrain.protection.test(x, y, z), "Architecture intersects protected water/old structure");
|
||||
require(plan.contains(x, site.floorY(), z) && plan.protects(x, site.maxY(), z)
|
||||
&& plan.intersectsProtection(x, site.maxY(), z, x, site.maxY(), z), "Site is absent from indexed protection");
|
||||
}
|
||||
for (int dx : new int[] {1, 8, 15}) for (int dz : new int[] {1, 8, 15}) {
|
||||
int x = site.minX() + dx, z = site.minZ() + dz;
|
||||
var support = site.supports().stream().filter(s -> s.x() == x && s.z() == z).findFirst();
|
||||
int rock = support.isPresent() ? support.get().minY() - 1 : site.minY() - 1;
|
||||
require(terrain.rock.test(x, rock, z) && terrain.rock.test(x, rock - 1, z), "Site has no two-block rock bearing");
|
||||
}
|
||||
for (var support : site.supports()) {
|
||||
require(support.maxY() - support.minY() < 12 && support.maxY() == site.minY() - 1, "Unbounded/disconnected architectural pier");
|
||||
require(plan.protects(support.x(), support.minY() - 2, support.z()), "Pier bearing exposed to decoration");
|
||||
for (int y = support.minY(); y <= support.maxY(); y++) require(!terrain.protection.test(support.x(), y, support.z()), "Pier overwrites protected terrain");
|
||||
}
|
||||
}
|
||||
plan.landmarks().forEach(l -> destinations.put(l.id(), l.entry()));
|
||||
for (var segment : plan.segments()) {
|
||||
require(destinations.containsKey(segment.fromId()) && destinations.containsKey(segment.toId()), "Segment has a fictional destination");
|
||||
for (Cell cell : segment.cells()) {
|
||||
require(cells.put(key(cell.x(), cell.z()), cell) == null, "Two segments write the same column");
|
||||
require(!footprints.contains(key(cell.x(), cell.z())), "Route crosses a building envelope");
|
||||
require((cell.x() >> 4) == segment.startChunkX() && (cell.z() >> 4) == segment.startChunkZ(), "Route piece is not chunk-local");
|
||||
require(terrain.rock.test(cell.x(), cell.minY() - 1, cell.z()), "Route ballast floats without bearing");
|
||||
require(terrain.height.applyAsInt(cell.x(), cell.z()) - cell.floorY() <= HeritagePlanner19.MAX_ROUTE_CUT, "Route makes excessive terrain cuts");
|
||||
for (int y = cell.minY(); y <= cell.maxY(); y++) require(!terrain.protection.test(cell.x(), y, cell.z()), "Route overwrites protected water/old structure");
|
||||
require(plan.contains(cell.x(), cell.floorY(), cell.z()) && plan.protects(cell.x(), cell.minY() - 1, cell.z())
|
||||
&& plan.intersectsProtection(cell.x(), cell.minY() - 1, cell.z(), cell.x(), cell.minY() - 1, cell.z()), "Route/ballast missing from protection index");
|
||||
require(!cell.railPresent() || cell.rail() != Rail.NONE, "Present rail has no saved geometry");
|
||||
}
|
||||
}
|
||||
for (var site : plan.sites()) {
|
||||
Entry e = site.entry();
|
||||
for (int cross = -1; cross <= 1; cross++) {
|
||||
Cell cell = cells.get(key(e.x() + DZ[e.facing()] * cross, e.z() - DX[e.facing()] * cross));
|
||||
require(cell != null && cell.floorY() == e.y() - 1, "Unlinked entrance has no independently painted three-wide landing");
|
||||
}
|
||||
// A five-row access profile must meet natural terrain; never stop on an isolated retaining ledge.
|
||||
var reached = new HashSet<Long>(); var queue = new ArrayDeque<Long>();
|
||||
queue.add(key(e.x(), e.z())); reached.add(key(e.x(), e.z())); boolean meetsGround = false;
|
||||
while (!queue.isEmpty()) {
|
||||
long key = queue.remove(); Cell c = cells.get(key);
|
||||
if (Math.abs(c.x() - e.x()) + Math.abs(c.z() - e.z()) >= 4
|
||||
&& Math.abs(c.floorY() - terrain.height.applyAsInt(c.x(), c.z())) <= 1) meetsGround = true;
|
||||
for (int d = 0; d < 4; d++) {
|
||||
long next = key(c.x() + DX[d], c.z() + DZ[d]); Cell n = cells.get(next);
|
||||
if (n != null && Math.abs(n.floorY() - c.floorY()) <= 1 && reached.add(next)) queue.add(next);
|
||||
}
|
||||
}
|
||||
require(meetsGround, "An entrance platform has no traversable return to the sampled terrain");
|
||||
}
|
||||
for (var link : plan.links()) {
|
||||
Entry from = destinations.get(link.fromId()), to = destinations.get(link.toId());
|
||||
require(link.path().getFirst().equals(new Point(from.x(), from.y() - 1, from.z()))
|
||||
&& link.path().getLast().equals(new Point(to.x(), to.y() - 1, to.z())), "Route does not actually join its promised entrances");
|
||||
for (int i = 0; i < link.path().size(); i++) {
|
||||
Point p = link.path().get(i); Cell c = cells.get(key(p.x(), p.z()));
|
||||
boolean historical = plan.landmarks().stream().anyMatch(l -> l.entry().x() == p.x() && l.entry().z() == p.z() && l.entry().y() == p.floorY() + 1);
|
||||
require(c != null && c.floorY() == p.floorY() || historical, "A complete route contains an unpainted gap");
|
||||
if (i > 0) {
|
||||
Point a = link.path().get(i - 1);
|
||||
require(Math.abs(a.x() - p.x()) + Math.abs(a.z() - p.z()) == 1 && Math.abs(a.floorY() - p.floorY()) <= 1, "Route has a jump or excessive grade");
|
||||
}
|
||||
}
|
||||
}
|
||||
require(!plan.protects(100_000, 200, 100_000) && !plan.intersectsProtection(100_000, 0, 100_000, 100_002, 383, 100_002), "Protection escaped accepted constructions");
|
||||
}
|
||||
@FunctionalInterface private interface Volume { boolean test(int x, int y, int z); }
|
||||
private static final class Fixture implements HeritagePlanner19.Terrain {
|
||||
final IntBinaryOperator height; final Set<Long> columns = new HashSet<>(); int samples;
|
||||
Volume protection = (x, y, z) -> false;
|
||||
Volume rock; List<Landmark> landmarks = List.of();
|
||||
Fixture(IntBinaryOperator height) { this.height = height; rock = (x, y, z) -> y >= 8 && y <= height.applyAsInt(x, z); }
|
||||
public boolean owns(int x, int z) { return true; }
|
||||
public int surfaceY(int x, int z) { samples++; columns.add(key(x, z)); return height.applyAsInt(x, z); }
|
||||
public boolean solid(int x, int y, int z) { samples++; return rock.test(x, y, z); }
|
||||
public boolean protectedAt(int x, int y, int z) { samples++; return protection.test(x, y, z); }
|
||||
public List<Landmark> landmarks() { return landmarks; }
|
||||
}
|
||||
private static void expectInvalid(Runnable action) { boolean rejected = false; try { action.run(); } catch (IllegalArgumentException expected) { rejected = true; } require(rejected, "Invalid saved geometry was accepted"); }
|
||||
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
|
||||
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
|
||||
}
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
package fr.koka.sanctuary.worldgen.heritage;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.EmptyBlockGetter;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.DoorBlock;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.block.TrapDoorBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BedPart;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
|
||||
import net.minecraft.world.level.block.state.properties.Half;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
|
||||
/** Native shapes and final geometry: distinguish real rooms, reachable secrets and supported routes. */
|
||||
public final class HeritageRenderer19Smoke {
|
||||
private HeritageRenderer19Smoke() {}
|
||||
private static final Direction[] DIRECTIONS = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
|
||||
public static void main(String[] args) {
|
||||
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); run();
|
||||
System.out.println("HeritageRenderer19Smoke passed: fourteen distinct architectures, four rotations, actual doors/bed/seat/light supports, every target and secret reachable, contained water, immutable loot and shaped abandoned routes");
|
||||
}
|
||||
public static void run() {
|
||||
Set<Set<BlockPos>> silhouettes = new HashSet<>();
|
||||
for (var kind : HeritagePlan19.Kind.values()) for (long seed : new long[] {0, 43}) {
|
||||
var canonical = render(site(kind, 0, seed));
|
||||
if (seed == 0) {
|
||||
Set<BlockPos> shape = new HashSet<>(); canonical.blocks.forEach((p, b) -> { if (p.getY() > 0 && !b.isAir()) shape.add(p); });
|
||||
require(silhouettes.add(shape), kind + " only repaints another catalogue building");
|
||||
}
|
||||
for (int facing = 0; facing < 4; facing++) {
|
||||
var c = facing == 0 ? canonical : render(site(kind, facing, seed));
|
||||
var repeat = render(c.site);
|
||||
require(c.blocks.equals(repeat.blocks) && c.loot.equals(repeat.loot), label(c) + " is not deterministic");
|
||||
rotate(canonical, c);
|
||||
var targets = HeritageRenderer19.targets(c.site);
|
||||
var reached = walk(c, at(targets.getFirst()));
|
||||
for (var target : targets) require(reached.contains(at(target)), label(c) + " cannot reach " + target.name() + " at " + at(target));
|
||||
require(targets.stream().anyMatch(t -> t.name().equals("secondary_entry")), "Missing secondary physical approach");
|
||||
var secondary = targets.stream().filter(t -> t.name().equals("secondary_entry")).findFirst().orElseThrow();
|
||||
Set<BlockPos> returnPath = walk(c, at(secondary));
|
||||
require(returnPath.contains(at(targets.getFirst())), label(c) + " service exit cannot return through the certified courtyard");
|
||||
require(c.loot.size() == 1, label(c) + " must have one shared, deferred discovery chest");
|
||||
var chest = c.loot.entrySet().iterator().next();
|
||||
require(chest.getValue().table().identifier().toString().equals("sanctuary:chests/heritage19/" + kind.name().toLowerCase(java.util.Locale.ROOT)), "Loot table is not generation19-specific");
|
||||
require(approachable(chest.getKey(), reached), label(c) + " hidden chest cannot be opened from a reachable standing cell");
|
||||
for (var t : targets) if (t.name().equals("loot_approach")) require(at(t).distManhattan(chest.getKey()) == 1, "Secret witness is not next to the actual chest");
|
||||
furnishings(c, reached);
|
||||
refugeShaft(c, reached);
|
||||
railConnection(c);
|
||||
fluids(c);
|
||||
require(c.blocks.values().stream().noneMatch(b -> b.is(Blocks.SPAWNER) || b.is(Blocks.LAVA)), "Heritage added an unrequested spawner or lava");
|
||||
identity(c);
|
||||
}
|
||||
}
|
||||
require(silhouettes.size() == 14, "Catalogue does not contain fourteen physical architectures");
|
||||
routes(); chunkPartition();
|
||||
}
|
||||
private static HeritagePlan19.Site site(HeritagePlan19.Kind kind, int facing, long seed) {
|
||||
int height = switch (kind) {
|
||||
case COMMUNAL_DEPOT, VOID_DOCK, CONSTRUCTION_REFUGE -> 14;
|
||||
case EXPEDITION_OFFICE, IMPOSSIBLE_SERVICE -> 12;
|
||||
case OBSERVATORY -> 20; case PUMP_STATION -> 18; case RADIO_RELAY -> 22;
|
||||
default -> 10;
|
||||
};
|
||||
int x = -43, y = 180, z = 59;
|
||||
var entry = new HeritagePlan19.Entry(facing == 1 ? x + 17 : facing == 3 ? x - 1 : x + 8, y + 1,
|
||||
facing == 2 ? z + 17 : facing == 0 ? z - 1 : z + 8, facing);
|
||||
return new HeritagePlan19.Site("test", kind, x, y, z, 17, 17, height, facing, seed, 3,
|
||||
List.of(new HeritagePlan19.Support(x, y - 7, z, y - 4)), entry);
|
||||
}
|
||||
private static String label(Capture c) { return c.site.kind() + " facing=" + c.site.facing() + " seed=" + c.site.seed(); }
|
||||
private static Capture render(HeritagePlan19.Site site) { var c = new Capture(site); HeritageRenderer19.render(site, c); return c; }
|
||||
private static BlockPos at(HeritageRenderer19.Target t) { return new BlockPos(t.x(), t.y(), t.z()); }
|
||||
private static BlockPos rotate(BlockPos p, int facing) {
|
||||
return switch (facing) { case 1 -> new BlockPos(16 - p.getZ(), p.getY(), p.getX());
|
||||
case 2 -> new BlockPos(16 - p.getX(), p.getY(), 16 - p.getZ());
|
||||
case 3 -> new BlockPos(p.getZ(), p.getY(), 16 - p.getX()); default -> p; };
|
||||
}
|
||||
private static void rotate(Capture base, Capture other) {
|
||||
int facing = other.site.facing();
|
||||
Rotation rotation = switch (facing) { case 1 -> Rotation.CLOCKWISE_90; case 2 -> Rotation.CLOCKWISE_180;
|
||||
case 3 -> Rotation.COUNTERCLOCKWISE_90; default -> Rotation.NONE; };
|
||||
for (var e : base.blocks.entrySet()) {
|
||||
// Saved footings are already in world coordinates; they do not rotate with the facade.
|
||||
if (e.getKey().getY() < -base.site.foundationDepth() || base.loot.containsKey(e.getKey())) continue;
|
||||
require(other.block(rotate(e.getKey(), facing)).equals(e.getValue().rotate(rotation)), label(other) + " rotates shape and block state differently at " + e.getKey());
|
||||
}
|
||||
for (var e : base.loot.entrySet()) require(e.getValue().equals(other.loot.get(rotate(e.getKey(), facing))), "Rotating a site changes its deferred treasure");
|
||||
}
|
||||
private static void furnishings(Capture c, Set<BlockPos> reached) {
|
||||
for (var e : c.blocks.entrySet()) {
|
||||
var p = e.getKey(); var b = e.getValue();
|
||||
if (b.getBlock() instanceof DoorBlock) {
|
||||
var half = b.getValue(BlockStateProperties.DOUBLE_BLOCK_HALF);
|
||||
var partner = half == DoubleBlockHalf.LOWER ? p.above() : p.below();
|
||||
require(c.block(partner).equals(b.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF,
|
||||
half == DoubleBlockHalf.LOWER ? DoubleBlockHalf.UPPER : DoubleBlockHalf.LOWER)), label(c) + " has a truncated door at " + p);
|
||||
if (half == DoubleBlockHalf.LOWER) {
|
||||
require(full(c.block(p.below()), p.below()), "Door has no full threshold");
|
||||
Direction normal = b.getValue(BlockStateProperties.HORIZONTAL_FACING);
|
||||
require(reached.contains(p.relative(normal)) && reached.contains(p.relative(normal.getOpposite())),
|
||||
label(c) + " door does not connect two usable sides at " + p);
|
||||
}
|
||||
}
|
||||
if (b.is(Blocks.LADDER)) require(full(c.block(p.relative(b.getValue(BlockStateProperties.HORIZONTAL_FACING).getOpposite())), p),
|
||||
label(c) + " floating ladder at " + p);
|
||||
if (b.is(Blocks.LANTERN)) require(b.getValue(BlockStateProperties.HANGING) && full(c.block(p.above()), p.above()),
|
||||
label(c) + " lantern does not hang from a complete ceiling at " + p);
|
||||
if (b.is(Blocks.LEVER) || b.is(Blocks.GRINDSTONE)) require(full(c.block(p.below()), p.below()), "Usable fixture has no full support");
|
||||
if (b.is(Blocks.DARK_OAK_STAIRS)) {
|
||||
var front = p.relative(b.getValue(BlockStateProperties.HORIZONTAL_FACING).getOpposite());
|
||||
require(reached.contains(front), label(c) + " seat faces an obstruction at " + p);
|
||||
}
|
||||
if (b.hasProperty(BlockStateProperties.BED_PART)) {
|
||||
Direction direction = b.getValue(BlockStateProperties.HORIZONTAL_FACING);
|
||||
boolean head = b.getValue(BlockStateProperties.BED_PART) == BedPart.HEAD;
|
||||
var partner = p.relative(head ? direction.getOpposite() : direction);
|
||||
require(c.block(partner).equals(b.setValue(BlockStateProperties.BED_PART, head ? BedPart.FOOT : BedPart.HEAD)), "Bed halves disagree");
|
||||
if (head) {
|
||||
for (int y = 0; y < 3; y++) require(full(c.block(p.relative(direction).above(y)), p), "Bed head is not against its real wall");
|
||||
boolean side = false;
|
||||
for (Direction d : new Direction[] {direction.getClockWise(), direction.getCounterClockWise()})
|
||||
side |= reached.contains(p.relative(d)) && reached.contains(partner.relative(d));
|
||||
require(side, "Bed has no reachable full lateral approach");
|
||||
}
|
||||
}
|
||||
if (b.is(Blocks.CRAFTING_TABLE) || b.is(Blocks.CARTOGRAPHY_TABLE) || b.is(Blocks.FURNACE)
|
||||
|| b.is(Blocks.GRINDSTONE) || b.is(Blocks.LECTERN))
|
||||
require(approachable(p, reached), label(c) + " functional furnishing cannot be approached at " + p);
|
||||
}
|
||||
}
|
||||
private static boolean full(BlockState b, BlockPos p) {
|
||||
return b.getFluidState().isEmpty() && b.getCollisionShape(EmptyBlockGetter.INSTANCE, p).toAabbs().stream()
|
||||
.anyMatch(a -> a.minX == 0 && a.minY == 0 && a.minZ == 0 && a.maxX == 1 && a.maxY == 1 && a.maxZ == 1);
|
||||
}
|
||||
private static boolean approachable(BlockPos p, Set<BlockPos> reached) {
|
||||
for (var d : DIRECTIONS) if (reached.contains(p.relative(d))) return true;
|
||||
return false;
|
||||
}
|
||||
private static boolean clear(Capture c, BlockPos p) {
|
||||
var body = new AABB(p.getX() + .2, p.getY(), p.getZ() + .2, p.getX() + .8, p.getY() + 1.8, p.getZ() + .8);
|
||||
for (var at : new BlockPos[] {p, p.above()}) {
|
||||
var b = opened(c.block(at)); if (!b.getFluidState().isEmpty()) return false;
|
||||
for (var shape : b.getCollisionShape(EmptyBlockGetter.INSTANCE, at).toAabbs())
|
||||
if (shape.move(at.getX(), at.getY(), at.getZ()).intersects(body)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private static boolean stand(Capture c, BlockPos p) {
|
||||
if (p.getX() < 0 || p.getX() > 16 || p.getZ() < 0 || p.getZ() > 16
|
||||
|| p.getY() < -c.site.foundationDepth() + 1 || p.getY() >= c.site.height() || !clear(c, p)) return false;
|
||||
if (climb(c.block(p)) || climb(c.block(p.below()))) return true;
|
||||
return opened(c.block(p.below())).getCollisionShape(EmptyBlockGetter.INSTANCE, p.below()).toAabbs().stream()
|
||||
.anyMatch(a -> a.maxY >= .5 && a.minX <= .5 && a.maxX >= .5 && a.minZ <= .5 && a.maxZ >= .5);
|
||||
}
|
||||
private static BlockState opened(BlockState state) {
|
||||
return state.getBlock() instanceof DoorBlock || state.getBlock() instanceof TrapDoorBlock
|
||||
? state.setValue(BlockStateProperties.OPEN, true) : state;
|
||||
}
|
||||
private static void refugeShaft(Capture c, Set<BlockPos> reached) {
|
||||
if (c.site.kind() != HeritagePlan19.Kind.CONSTRUCTION_REFUGE) return;
|
||||
var hatch = rotate(new BlockPos(10, 5, 11), c.site.facing());
|
||||
require(c.block(hatch).is(Blocks.DARK_OAK_TRAPDOOR)
|
||||
&& c.block(hatch).getValue(BlockStateProperties.HALF) == Half.BOTTOM,
|
||||
label(c) + " hatch is not at the climbable upper lip");
|
||||
var direction = c.block(hatch).getValue(BlockStateProperties.HORIZONTAL_FACING);
|
||||
require(stand(c, hatch) && reached.contains(hatch), label(c) + " open hatch has no reachable climbable support");
|
||||
for (int y = 1; y <= 4; y++) {
|
||||
var rung = new BlockPos(hatch.getX(), y, hatch.getZ());
|
||||
require(c.block(rung).is(Blocks.LADDER)
|
||||
&& c.block(rung).getValue(BlockStateProperties.HORIZONTAL_FACING) == direction,
|
||||
label(c) + " hatch descent loses a matching ladder at " + rung);
|
||||
require(stand(c, rung) && reached.contains(rung) && clear(c, rung.above()),
|
||||
label(c) + " hatch descent cannot traverse vertically at " + rung);
|
||||
}
|
||||
}
|
||||
private static boolean climb(BlockState b) { return b.is(Blocks.LADDER); }
|
||||
private static Set<BlockPos> walk(Capture c, BlockPos start) {
|
||||
require(stand(c, start), label(c) + " starts without a standing surface at " + start);
|
||||
Set<BlockPos> seen = new HashSet<>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
|
||||
while (!queue.isEmpty()) {
|
||||
var p = queue.removeFirst();
|
||||
for (var d : DIRECTIONS) for (int dy : new int[] {0, 1, -1}) {
|
||||
var q = p.relative(d).offset(0, dy, 0);
|
||||
if (dy > 0 && !clear(c, p.above())) continue;
|
||||
if (stand(c, q) && seen.add(q)) queue.add(q);
|
||||
}
|
||||
for (int dy : new int[] {-1, 1}) {
|
||||
var q = p.offset(0, dy, 0);
|
||||
if ((climb(c.block(p)) || climb(c.block(q)) || climb(c.block(p.below()))) && stand(c, q) && seen.add(q)) queue.add(q);
|
||||
}
|
||||
}
|
||||
return seen;
|
||||
}
|
||||
private static void fluids(Capture c) {
|
||||
Set<BlockPos> water = new HashSet<>(); c.blocks.forEach((p, b) -> { if (!b.getFluidState().isEmpty()) water.add(p); });
|
||||
int expected = c.site.kind() == HeritagePlan19.Kind.PUMP_STATION ? 60 : c.site.kind() == HeritagePlan19.Kind.GREENHOUSE ? 2 : 0;
|
||||
require(water.size() == expected, label(c) + " has unexpected water quantity: " + water.size());
|
||||
for (var p : water) {
|
||||
require(c.block(p).is(Blocks.WATER), "Only explicitly contained water is allowed");
|
||||
for (var d : new Direction[] {Direction.DOWN, Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}) {
|
||||
var q = p.relative(d);
|
||||
require(water.contains(q) || !c.block(q).isAir(), label(c) + " lets tank/irrigation water escape into air at " + q);
|
||||
}
|
||||
}
|
||||
}
|
||||
private static void identity(Capture c) {
|
||||
switch (c.site.kind()) {
|
||||
case FREIGHT_STATION -> { has(c, Blocks.RAIL); has(c, Blocks.LEVER); }
|
||||
case POST_OFFICE -> { has(c, Blocks.BARREL); has(c, Blocks.CARTOGRAPHY_TABLE); }
|
||||
case COMMUNAL_DEPOT -> { has(c, Blocks.BARREL); has(c, Blocks.LECTERN); }
|
||||
case EXPEDITION_OFFICE -> { has(c, Blocks.CARTOGRAPHY_TABLE); has(c, Blocks.BOOKSHELF); }
|
||||
case OBSERVATORY -> { has(c, Blocks.CARTOGRAPHY_TABLE); has(c, Blocks.IRON_BARS); }
|
||||
case RAIL_WORKSHOP -> { has(c, Blocks.GRINDSTONE); has(c, Blocks.RAIL); has(c, Blocks.FURNACE); }
|
||||
case PUMP_STATION -> { has(c, Blocks.DISPENSER); has(c, Blocks.WATER); }
|
||||
case SUBSTATION -> { has(c, Blocks.LEVER); has(c, Blocks.IRON_BARS); }
|
||||
case GREENHOUSE -> { has(c, Blocks.WHEAT); has(c, Blocks.FARMLAND); has(c, Blocks.COMPOSTER); }
|
||||
case VOID_DOCK -> { has(c, Blocks.IRON_CHAIN); has(c, Blocks.STRIPPED_OAK_LOG); }
|
||||
case RADIO_RELAY -> { has(c, Blocks.BED.white()); has(c, Blocks.IRON_BARS); }
|
||||
case PHOTO_CLUB -> { has(c, Blocks.WATER_CAULDRON); has(c, Blocks.IRON_CHAIN); }
|
||||
case IMPOSSIBLE_SERVICE -> { has(c, Blocks.CALCITE); has(c, Blocks.WATER_CAULDRON); }
|
||||
case CONSTRUCTION_REFUGE -> { has(c, Blocks.BED.white()); has(c, Blocks.LADDER); has(c, Blocks.DARK_OAK_TRAPDOOR); }
|
||||
}
|
||||
}
|
||||
private static void has(Capture c, Block b) { require(c.blocks.values().stream().anyMatch(v -> v.is(b)), label(c) + " loses its " + b); }
|
||||
private static void railConnection(Capture c) {
|
||||
BlockPos destination = switch (c.site.kind()) {
|
||||
case FREIGHT_STATION -> new BlockPos(2, 1, 8);
|
||||
case RAIL_WORKSHOP -> new BlockPos(5, 1, 7);
|
||||
case COMMUNAL_DEPOT -> new BlockPos(8, 1, 7);
|
||||
default -> null;
|
||||
};
|
||||
if (destination == null) return;
|
||||
var start = rotate(new BlockPos(8, 1, 0), c.site.facing());
|
||||
destination = rotate(destination, c.site.facing());
|
||||
Set<BlockPos> seen = new HashSet<>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
|
||||
while (!queue.isEmpty()) {
|
||||
var p = queue.removeFirst(); var b = c.block(p);
|
||||
require(b.is(Blocks.RAIL), label(c) + " freight entry has no supported rail");
|
||||
for (var d : railEnds(b)) {
|
||||
var next = p.relative(d); var state = c.block(next);
|
||||
if (state.is(Blocks.RAIL) && railEnds(state).contains(d.getOpposite()) && seen.add(next)) queue.add(next);
|
||||
}
|
||||
}
|
||||
require(seen.contains(destination), label(c) + " interior rail does not connect to its real external freight entry");
|
||||
}
|
||||
private static List<Direction> railEnds(BlockState b) {
|
||||
return switch (b.getValue(BlockStateProperties.RAIL_SHAPE)) {
|
||||
case NORTH_SOUTH, ASCENDING_NORTH, ASCENDING_SOUTH -> List.of(Direction.NORTH, Direction.SOUTH);
|
||||
case EAST_WEST, ASCENDING_EAST, ASCENDING_WEST -> List.of(Direction.EAST, Direction.WEST);
|
||||
case SOUTH_EAST -> List.of(Direction.SOUTH, Direction.EAST);
|
||||
case SOUTH_WEST -> List.of(Direction.SOUTH, Direction.WEST);
|
||||
case NORTH_WEST -> List.of(Direction.NORTH, Direction.WEST);
|
||||
case NORTH_EAST -> List.of(Direction.NORTH, Direction.EAST);
|
||||
};
|
||||
}
|
||||
private static void routes() {
|
||||
var cells = new ArrayList<HeritagePlan19.Cell>();
|
||||
int x = -15;
|
||||
for (var rail : HeritagePlan19.Rail.values()) {
|
||||
cells.add(new HeritagePlan19.Cell(x++, 160, 2, 2, rail, rail != HeritagePlan19.Rail.NONE));
|
||||
}
|
||||
cells.add(new HeritagePlan19.Cell(-1, 161, 2, 3, HeritagePlan19.Rail.ASCENDING_EAST, false));
|
||||
var segment = new HeritagePlan19.Segment("route", "a", "b", HeritagePlan19.RouteKind.FREIGHT, -1, 0, cells, 7);
|
||||
Map<BlockPos, BlockState> result = new HashMap<>();
|
||||
HeritageRenderer19.renderSegment(segment, new LostCityRenderer.BlockWriter() {
|
||||
@Override public void set(int x, int y, int z, BlockState b) {
|
||||
var p = new BlockPos(segment.minX() + x, segment.minY() + y, segment.minZ() + z);
|
||||
var cell = cells.stream().filter(c -> c.x() == p.getX() && c.z() == p.getZ()).findFirst().orElseThrow();
|
||||
require(p.getY() >= cell.minY() && p.getY() <= cell.maxY(), "Route escaped its saved column mask"); result.put(p, b);
|
||||
}
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { throw new AssertionError("A route generated treasure"); }
|
||||
});
|
||||
for (var c : cells) {
|
||||
var p = new BlockPos(c.x(), c.floorY() + 1, c.z());
|
||||
require(full(result.get(p.below()), p.below()), "Rail or missing-rail witness lost its ballast support");
|
||||
if (c.railPresent()) require(result.get(p).is(Blocks.RAIL)
|
||||
&& result.get(p).getValue(BlockStateProperties.RAIL_SHAPE).name().equals(c.rail().name()), "Saved rail shape was replaced by a straight track");
|
||||
else require(result.get(p).isAir(), "An intentional abandoned-track gap was repaired by rendering");
|
||||
}
|
||||
}
|
||||
private static void chunkPartition() {
|
||||
var s = site(HeritagePlan19.Kind.CONSTRUCTION_REFUGE, 3, 43); var expected = render(s); var parts = new Capture(s);
|
||||
for (int cz = s.maxZ() >> 4; cz >= s.minZ() >> 4; cz--) for (int cx = s.maxX() >> 4; cx >= s.minX() >> 4; cx--) {
|
||||
final int chunkX = cx, chunkZ = cz;
|
||||
HeritageRenderer19.render(s, new LostCityRenderer.BlockWriter() {
|
||||
boolean owns(int x, int z) { return ((s.minX() + x) >> 4) == chunkX && ((s.minZ() + z) >> 4) == chunkZ; }
|
||||
@Override public void set(int x, int y, int z, BlockState state) { if (owns(x, z)) parts.set(x, y, z, state); }
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { if (owns(x, z)) parts.chest(x, y, z, table, seed); }
|
||||
});
|
||||
}
|
||||
require(parts.blocks.equals(expected.blocks) && parts.loot.equals(expected.loot), "Native chunk order changes a site");
|
||||
}
|
||||
private record Loot(ResourceKey<LootTable> table, long seed) {}
|
||||
private static final class Capture implements LostCityRenderer.BlockWriter {
|
||||
final HeritagePlan19.Site site;
|
||||
final Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
final Map<BlockPos, Loot> loot = new HashMap<>();
|
||||
Capture(HeritagePlan19.Site site) { this.site = site; }
|
||||
@Override public void set(int x, int y, int z, BlockState state) {
|
||||
int wy = y + site.floorY();
|
||||
boolean main = x >= 0 && x <= 16 && z >= 0 && z <= 16 && wy >= site.minY() && wy <= site.maxY();
|
||||
boolean footing = site.supports().stream().anyMatch(s -> s.x() == x + site.minX() && s.z() == z + site.minZ() && wy >= s.minY() && wy <= s.maxY());
|
||||
require(main || footing, label(this) + " writes outside its exact body and footing mask"); blocks.put(new BlockPos(x, y, z), state);
|
||||
}
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
|
||||
require(loot.put(new BlockPos(x, y, z), new Loot(table, seed)) == null, "Duplicate shared treasure"); set(x, y, z, Blocks.CHEST.defaultBlockState());
|
||||
}
|
||||
BlockState block(BlockPos p) { return blocks.getOrDefault(p, Blocks.AIR.defaultBlockState()); }
|
||||
}
|
||||
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "0.1.0-alpha.17"
|
||||
version = "0.1.0-alpha.19"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
Reference in New Issue
Block a user