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