Add underground village, branched rails and upper terrain for alpha.30.6
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -21,6 +21,18 @@ le relief et les souterrains de la 24, retire la montgolfière et travaille les
|
||||
marais souterrains et la restauration de vrais modèles d’épaves vanilla. Voir le
|
||||
[chantier et ses limites](docs/generation-alpha27.md).
|
||||
|
||||
## Alpha.30.6 — village souterrain et voies ramifiées
|
||||
|
||||
La déformation du terrain 30.5 est légèrement renforcée. Un village vanilla
|
||||
abandonné dispose d’une cavité locale adaptée ; les mines sont réservées à
|
||||
l’avance et les donjons natifs disposent de davantage de tentatives. Les voies
|
||||
abandonnées s’allongent et se ramifient entre les plateaux.
|
||||
Deux nouveaux morceaux de terrain naturel sur Petit/Moyen, trois sur Grand,
|
||||
occupent le ciel vers Y300–400. Le lieu envisagé à Y600 reste une idée future.
|
||||
|
||||
Voir [la génération](docs/generation-alpha30.6.md), [les essais](docs/testing-alpha30.6.md)
|
||||
et [l’atlas scientifique](docs/terrain-atlas.md). **Nouveau monde requis.**
|
||||
|
||||
## Alpha.30.5 — plateaux et atlas du terrain
|
||||
|
||||
L’étirement vertical de la 30.4 est retiré. Les plateaux issus de la 24/27
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# Alpha.30.6 — village souterrain et voies ramifiées
|
||||
|
||||
Cette passe prolonge le terrain 30.5 validé en jeu. Elle ne reprend ni
|
||||
l’étirement vertical 30.4 ni les grandes cavités artificielles de la 25.
|
||||
Créer un **nouveau monde** : le profil de prototype `sanctuary:island_v24`
|
||||
évolue avec l’accord du créateur pour ses mondes de test. Plafond Y640,
|
||||
Minecraft 26.3-pre-2, Petit 512 / Moyen 724 par défaut / Grand 1 024 inchangés.
|
||||
|
||||
## Relief
|
||||
|
||||
L’amplitude de la déformation 3D supérieure passe de 0,80 à 0,95 dans les
|
||||
mêmes grandes provinces. Leur répartition, les plateaux, les grottes basses,
|
||||
les rivières courbes et le filtrage des petits fragments restent en place.
|
||||
Il n’y a ni sommet imposé ni ajout de bruit de détail. L’atlas compare les
|
||||
champs 24/27, 30.1, 30.5 et 30.6 aux mêmes coordonnées.
|
||||
|
||||
Deux morceaux de terrain naturel sur Petit/Moyen, trois sur Grand, sont
|
||||
recherchés entre Y300 et Y400 au-dessus de l’île. Ce sont des volumes rocheux
|
||||
pleins érodés par un bruit 3D large, indépendants des deux îlots minéraux à
|
||||
coffre. Ils ne reçoivent aucun butin ni construction. Leurs positions sont
|
||||
séparées et recherchées en 24 essais au plus par masse ; le champ de terrain
|
||||
existant est sondé pour conserver un espace d’air dessous. Le filtrage des
|
||||
petits fragments s’applique également à ces hauteurs. La surface projetée
|
||||
reste minoritaire ; aucun semis de blocs flottants n’est ajouté.
|
||||
|
||||
L’idée d’un lieu exceptionnel vers **Y600** est conservée pour plus tard.
|
||||
Aucune structure, plateforme ou arène n’y est générée dans cette version.
|
||||
|
||||
## Village abandonné
|
||||
|
||||
Un village vanilla abandonné est recherché dans la roche de l’île initiale.
|
||||
**La cavité est aménagée spécialement pour lui**, à la demande du créateur :
|
||||
sol commun praticable, chambres qui se recouvrent autour du graphe complet,
|
||||
voûte irrégulière et accès vers les grottes voisines. Ce creusement reste
|
||||
local ; aucun nouveau champ de cavités ne s’étend à l’île entière.
|
||||
|
||||
- Identifiant : `sanctuary:underground_abandoned_village`.
|
||||
- Assemblage Jigsaw natif des plaines abandonnées, taille 6 et distance 80 ;
|
||||
ces paramètres ne fixent pas un nombre de maisons. Aucun pool Minecraft
|
||||
global n’est remplacé, aucun graphe n’est réduit pour le faire tenir.
|
||||
- Au moins six maisons pour accepter un graphe. Les templates natifs gardent
|
||||
leurs lits, coffres, toiles et zombie-villageois persistants et guérissables.
|
||||
- Les rues utilisent le même sol pendant la planification **et** la pose,
|
||||
sans être projetées sur le toit de l’île.
|
||||
- Au plus 60 positions × six altitudes possibles, au plus 32 000 colonnes mises en
|
||||
cache ; aucun chargement de chunks pour rechercher l’emplacement.
|
||||
- Le sous-sol doit offrir une majorité d’appuis et de couverture. Le plan
|
||||
local évite les fluides détectés, les secrets et les infrastructures ; les
|
||||
raccords sont contrôlés avec leur emprise complète. Les refus sont journalisés.
|
||||
- Les pièces enregistrent la cavité avec les maisons ; le locator et le
|
||||
registre observent le véritable départ. Le village ne se reconstitue pas
|
||||
dans un chunk déjà généré.
|
||||
|
||||
Le creusement laisse des fenêtres naturelles possibles. Ce n’est pas une
|
||||
nouvelle ville de surface, un laboratoire ou une restauration des Lost Cities.
|
||||
Les options expérimentales et générales de structures doivent être actives.
|
||||
|
||||
## Mines et donjons
|
||||
|
||||
Avant les premiers chunks, le générateur réserve des mines Minecraft complètes :
|
||||
objectif **une sur Petit/Moyen, deux sur Grand**. Il cherche dans au plus 192
|
||||
candidats natifs, avec les contrôles d’appuis et d’altitude existants. Le plan
|
||||
accepté est celui effectivement placé et localisé. Un échec reste explicite
|
||||
au journal ; les graines testées ne prouvent pas l’ensemble des graines possibles.
|
||||
Les mines restent indépendantes du commutateur expérimental ; l’option générale
|
||||
« Générer les structures » fait autorité.
|
||||
|
||||
Les salles à spawner restent la feature vanilla `monster_room`, avec ses
|
||||
contrôles, butins et mobs. Les tentatives explorent neuf colonnes par chunk
|
||||
au lieu de quatre, dans chaque chunk éligible, avec au plus 32 tentatives.
|
||||
Les pièces protégées et les fluides restent exclus. Les plantes dont le support
|
||||
est retiré par la pose tardive d’un donjon sont nettoyées dans son volume, sans
|
||||
modifier les plantes valides ni produire de drops. Cela s’applique aussi
|
||||
aux expansions du générateur courant ; aucune expansion n’est ouverte par
|
||||
la recherche de structures.
|
||||
|
||||
## Voies de surface
|
||||
|
||||
Les voies se développent en longues portions suivant les pentes admissibles,
|
||||
avec des bifurcations au milieu des parcours et des prolongements. Le ballast
|
||||
reste continu sur les portions retenues ; certains rails et aiguillages sont
|
||||
manquants et réparables. Aucune gare n’est nécessaire pour conserver une voie.
|
||||
L’arrivée naturelle reste protégée sur 48 blocs autour de l’origine.
|
||||
|
||||
Les **6–8 % évoqués sont un repère visuel, pas un quota imposé**. L’atlas des
|
||||
structures mesure les colonnes aménagées et les superpose au relief. Les voies
|
||||
s’arrêtent lorsqu’un terrain ou un raccord ne convient pas ; elles ne terrassent
|
||||
pas une autoroute à travers les montagnes ou le vide pour atteindre un quota.
|
||||
|
||||
## Registre et vérification
|
||||
|
||||
Une erreur d’arrêt révélée par les essais est corrigée : Minecraft peut exécuter
|
||||
une tâche immédiatement sur un worker lorsque le serveur s’arrête. Le registre
|
||||
ne tente plus de modifier son état depuis ce worker. Les chunks trop tardifs
|
||||
restent inconnus jusqu’à leur prochain chargement, sans inventer une absence.
|
||||
|
||||
Voir [les essais et limites](testing-alpha30.6.md) et [la méthode d’atlas](terrain-atlas.md).
|
||||
+25
-3
@@ -20,8 +20,8 @@ expansion n’est créé pour cet export.
|
||||
-PsanctuaryAtlasRevision=REVISION_DU_CODE
|
||||
python3.14 -m venv build/atlas-venv
|
||||
build/atlas-venv/bin/pip install -r scripts/terrain-atlas-requirements.txt
|
||||
build/atlas-venv/bin/python scripts/terrain_atlas.py --output build/atlas305 \
|
||||
mods/sanctuary/build/run/gameTest/diagnostics/atlas305/terrain-*.json
|
||||
build/atlas-venv/bin/python scripts/terrain_atlas.py --output build/atlas306 \
|
||||
mods/sanctuary/build/run/gameTest/diagnostics/atlas306/terrain-*.json
|
||||
```
|
||||
|
||||
Le tracé livré utilise Python 3.14 ; les versions de Matplotlib et NumPy
|
||||
@@ -40,7 +40,7 @@ avant d’en faire une archive. Aucune sauvegarde personnelle n’est utilisée.
|
||||
représente pas les blocs ou les biomes ; du blanc n’implique pas de neige.
|
||||
- Altitude : dernier solide rencontré, recherche verticale par pas de 2 blocs.
|
||||
Une cellule grise signifie qu’aucun solide n’a été échantillonné dans la colonne.
|
||||
- Épaisseur cumulée : nombre d’échantillons solides multiplié par 2 sur Y2..382.
|
||||
- Épaisseur cumulée : nombre d’échantillons solides multiplié par 2 sur Y2..418 dans la 30.6 (Y2..382 dans la 30.5).
|
||||
Ce n’est ni une densité physique, ni un volume exact de matériaux minables.
|
||||
- Roche continue : épaisseur depuis le sommet jusqu’au premier vide échantillonné.
|
||||
Un petit résultat peut signaler une voûte mince, un surplomb ou un fragment.
|
||||
@@ -78,3 +78,25 @@ Le libellé « 24 / 27 » désigne le champ de la 27 restauré depuis la 24, pas
|
||||
une simulation de tous les matériaux et structures historiques de la 24.
|
||||
Les samplers 30.1 et 30.4 sont conservés pour comparer des transformations
|
||||
exactes, et non des approximations dessinées pour illustrer un résultat attendu.
|
||||
|
||||
|
||||
## Comparaison 30.6 et infrastructures
|
||||
|
||||
L’export courant remplace la colonne 30.4 par la comparaison directe 30.5/30.6.
|
||||
`compileHistorical305` conserve exactement le coefficient 0,80 ; le champ livré
|
||||
utilise 0,95. Le renderer accepte également les anciens exports 30.5.
|
||||
Des coupes supplémentaires Y260..418 ciblent les morceaux de terrain supérieurs.
|
||||
Les JSON courants sont dans `diagnostics/atlas306/`.
|
||||
|
||||
`scripts/structure_atlas.py` superpose les plans ferroviaires au relief et trace
|
||||
les coupes des blocs du village effectivement générés par `Village306GameTests`.
|
||||
Le pourcentage est une estimation de surface projetée à partir du pas de
|
||||
12 blocs ; le numérateur inclut les colonnes de ballast et les chemins. Ce n’est
|
||||
ni une longueur de rails en pourcentage ni une part du volume de l’île.
|
||||
|
||||
```sh
|
||||
build/atlas-venv/bin/python scripts/structure_atlas.py \
|
||||
mods/sanctuary/build/run/gameTest/diagnostics/village306/724-0.json \
|
||||
mods/sanctuary/build/run/gameTest/diagnostics/atlas306/terrain-724-0.json \
|
||||
--output build/atlas306/structures
|
||||
```
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# Vérifications alpha.30.6
|
||||
|
||||
Essais natifs sur **Mac**, Minecraft 26.3-pre-2, Java 25. Aucun essai Windows
|
||||
natif n’est revendiqué. Un **nouveau monde** est nécessaire.
|
||||
|
||||
## Atlas final
|
||||
|
||||
Neuf couples : Petit 512 / Moyen 724 / Grand 1024 × graines 0, 42 et
|
||||
-7228211907433324401. Le serveur d’export plat passe ses deux tests ;
|
||||
l’acquisition finale dure 3 min 37 s, démarrage compris. Elle compile le champ
|
||||
natif avant eau, matériaux, végétation et structures, sans générer de chunks
|
||||
Sanctuary. Le rendu utilise Matplotlib/NumPy épinglés et aucune interpolation.
|
||||
|
||||
Les trois références (24/27, 30.1, 30.5) sont comparées aux neuf exports
|
||||
archivés de la 30.5 : colonnes et valeurs de densité des coupes **identiques**.
|
||||
L’augmentation du domaine d’affichage à Y420 ajoute seulement de l’air aux
|
||||
références ; le plafond constructible reste Y640.
|
||||
|
||||
| Diamètre | Graine | Sommet échantillonné, formes aériennes incluses | Hauts plateaux conservés à ±2 blocs |
|
||||
|---|---|---|---|
|
||||
| 1024 | `-7228211907433324401` | Y382 | 71.6 % |
|
||||
| 1024 | `0` | Y374 | 75.9 % |
|
||||
| 1024 | `42` | Y386 | 68.8 % |
|
||||
| 512 | `-7228211907433324401` | Y378 | 78.7 % |
|
||||
| 512 | `0` | Y376 | 78.8 % |
|
||||
| 512 | `42` | Y384 | 69.9 % |
|
||||
| 724 | `-7228211907433324401` | Y384 | 76.5 % |
|
||||
| 724 | `0` | Y376 | 77.6 % |
|
||||
| 724 | `42` | Y382 | 68.0 % |
|
||||
|
||||
La dernière colonne compare les colonnes de la base 24/27 atteignant Y200,
|
||||
sur un pas horizontal de 12 blocs et vertical de 2. Elle ne mesure pas toute
|
||||
la surface au bloc près. Les coupes supplémentaires ciblent les masses
|
||||
supérieures ; une coupe 2D ne démontre pas à elle seule une connexion en 3D.
|
||||
Le filtrage des composantes de 32 blocs ou moins reste actif dans le volume.
|
||||
|
||||
## Essais de structures
|
||||
|
||||
La passe complète du Grand sur la graine utilisateur a passé les 25 GameTests
|
||||
et `check build` (9 min 58 s). Le village contient 12 maisons natives,
|
||||
20 blocs de lits, 3 coffres, 852 blocs de rue et 9 zombie-villageois persistants,
|
||||
observés dans 84 chunks FULL. Deux départs de mines vanilla sont réservés.
|
||||
La recherche native par identifiants trouve une vraie mine au chunk [13,-2]
|
||||
sans ouvrir d’expansion ; ses corridors et leurs éléments sont inspectés.
|
||||
93 salles vanilla `monster_room` ont été observées dans l’ensemble de cette
|
||||
passe : ce n’est pas le nombre total de donjons de l’île ni une garantie par graine.
|
||||
|
||||
Ce témoin complet précède l’ultime sculpture des morceaux de terrain aérien.
|
||||
La génération sous Y300 n’a pas changé dans ce dernier ajustement. Les plans
|
||||
finaux du Grand et le témoin complet du Moyen sont vérifiés séparément ci-dessous.
|
||||
|
||||
La cavité et toutes les pièces font un aller-retour NBT : position, rotation,
|
||||
modèle, projection et jonctions conservés. Sur le Grand, Minecraft réduit la
|
||||
marge verticale de collision de 18 pièces Jigsaw à la boîte du template lors
|
||||
du chargement ; le test vérifie précisément cette normalisation native. Ce
|
||||
contrôle ne remplace pas un essai de pillage suivi d’un redémarrage complet.
|
||||
|
||||
Les premières passes de développement avaient aussi observé 17 maisons,
|
||||
3 coffres et 6 zombie-villageois sur Petit/42, et 15 maisons, 2 coffres et
|
||||
10 zombie-villageois sur Moyen/0. Elles ne sont pas présentées comme des
|
||||
essais complets du dernier binaire.
|
||||
|
||||
## Témoin final Moyen / 0
|
||||
|
||||
Le dernier `./gradlew check build`, après correction des plantes privées de
|
||||
support par les donjons, réussit en **7 min 20 s** : **25/25 GameTests**, puis
|
||||
tous les contrôles de build. Ce temps inclut le démarrage et les tests ; ce
|
||||
n’est pas une mesure du temps d’ouverture d’un monde chez un joueur.
|
||||
|
||||
Le village réellement généré comporte **15 maisons natives, 24 blocs de lits,
|
||||
2 coffres, 1 340 blocs de rue et 10 zombie-villageois persistants** dans
|
||||
104 chunks FULL. Entrée témoin : `-16, 112, 19`. Les 99 pièces natives et la
|
||||
cavité conservent leurs données à la sérialisation ; 29 pièces subissent la
|
||||
normalisation verticale native décrite plus haut. Une mine est réservée.
|
||||
Les tests contrôlent également le marais, les rivières, la traversée,
|
||||
les satellites et la localisation native.
|
||||
|
||||
Le plan de surface contient 11 parcours et 52 segments : **659 colonnes de
|
||||
voie**, 1 982 colonnes aménagées en incluant les chemins. Cela représente
|
||||
**0,90 %** de l’emprise terrestre projetée estimée (0,96 % au-dessus de Y180),
|
||||
et non les 6–8 % évoqués comme repère visuel. Le réseau est plus ramifié mais
|
||||
ne couvre pas toute l’île. Les coupes du village montrent les blocs observés
|
||||
après décoration ; les tracés ferroviaires représentent le plan préparé.
|
||||
|
||||
## Plans finaux Grand / graine utilisateur
|
||||
|
||||
Une nouvelle préparation native, après la dernière sculpture aérienne, passe
|
||||
**2/2 tests en 3 min 1 s**, démarrage compris. Le village (12 maisons, entrée
|
||||
`-59, 112, 336`) et les deux emprises de mines sont identiques au témoin
|
||||
complet précédent. Le plan de surface final possède 11 parcours, 66 segments,
|
||||
844 colonnes de voie et 2 538 colonnes aménagées, soit **0,48 %** de la surface
|
||||
terrestre projetée estimée. Ce dernier essai vérifie les plans et leur
|
||||
sérialisation, sans prétendre rejouer toute la génération FULL du Grand.
|
||||
|
||||
## Livraison et limites
|
||||
|
||||
L’assemblage utilise `./gradlew assemblePack -x :sanctuary:check` après le
|
||||
`check build` complet réussi, pour ne pas relancer une seconde fois tous les
|
||||
mêmes tests par la dépendance `assemblePack → build → check`. Le premier
|
||||
assemblage sans exclusion a été interrompu avant cette répétition native.
|
||||
L’assemblage réussit ; JAR, index packwiz, MRpack, dépendance Fabric API et
|
||||
amorçage Prism sont vérifiés localement avant publication.
|
||||
Le reçu de publication et de synchronisation figure dans `docs/packwiz.md`.
|
||||
L’archive fige les mesures, les images et l’empreinte du JAR de cette livraison.
|
||||
Les pourcentages de voies sont des mesures du plan ; aucun trajet en minecart
|
||||
sur l’intégralité du réseau n’est revendiqué. Les recherches bornées peuvent
|
||||
refuser une autre graine ; les témoins testés ne prouvent pas toutes les graines.
|
||||
Aucune arène ni plateforme à Y600 n’est implémentée.
|
||||
+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.30.5
|
||||
pack_version=0.1.0-alpha.30.5
|
||||
mod_version=0.1.0-alpha.30.6
|
||||
pack_version=0.1.0-alpha.30.6
|
||||
maven_group=fr.koka.sanctuary
|
||||
|
||||
@@ -36,6 +36,7 @@ tasks.register('cleanGameTestWorld', Delete) {
|
||||
}
|
||||
tasks.named('runGameTest') {
|
||||
dependsOn('cleanGameTestWorld')
|
||||
systemProperty('sanctuary.test.village306.planOnly', providers.gradleProperty('sanctuaryVillagePlanOnly').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.atlasOnly', providers.gradleProperty('sanctuaryAtlasOnly').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.atlasSeeds', providers.gradleProperty('sanctuaryAtlasSeeds').getOrElse('0,42,-7228211907433324401'))
|
||||
systemProperty('sanctuary.test.atlasRevision', providers.gradleProperty('sanctuaryAtlasRevision').getOrElse('working-tree'))
|
||||
@@ -87,6 +88,8 @@ tasks.named('runGameTest') {
|
||||
}
|
||||
|
||||
tasks.named('processGametestResources') {
|
||||
def villageOnly = providers.gradleProperty('sanctuaryVillageOnly').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('villageOnly', villageOnly)
|
||||
def atlasOnly = providers.gradleProperty('sanctuaryAtlasOnly').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('atlasOnly', atlasOnly)
|
||||
def benchmark = providers.gradleProperty('sanctuaryBenchmark').map { it.toBoolean() }.getOrElse(false)
|
||||
@@ -132,11 +135,11 @@ tasks.named('processGametestResources') {
|
||||
def voidReuse = providers.gradleProperty('sanctuaryVoidReuseTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('voidReuse', voidReuse)
|
||||
filesMatching('fabric.mod.json') {
|
||||
expand(gametest_entrypoint: atlasOnly ? 'fr.koka.sanctuary.gametest.TerrainAtlas305GameTests' : aerial24Flow ? 'fr.koka.sanctuary.gametest.Aerial24FlowAdmissionGameTests' : aerial24 ? 'fr.koka.sanctuary.gametest.Aerial24WorldGameTests' : admission23 ? 'fr.koka.sanctuary.gametest.Expansion23AdmissionGameTests' : ocean23 ? 'fr.koka.sanctuary.gametest.Ocean23WorldGameTests' : secrets23 ? 'fr.koka.sanctuary.gametest.Secrets23WorldGameTests' : native23 ? 'fr.koka.sanctuary.gametest.Native23WorldGameTests' : 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
|
||||
expand(gametest_entrypoint: villageOnly ? 'fr.koka.sanctuary.gametest.Village306GameTests' : atlasOnly ? 'fr.koka.sanctuary.gametest.TerrainAtlas305GameTests' : aerial24Flow ? 'fr.koka.sanctuary.gametest.Aerial24FlowAdmissionGameTests' : aerial24 ? 'fr.koka.sanctuary.gametest.Aerial24WorldGameTests' : admission23 ? 'fr.koka.sanctuary.gametest.Expansion23AdmissionGameTests' : ocean23 ? 'fr.koka.sanctuary.gametest.Ocean23WorldGameTests' : secrets23 ? 'fr.koka.sanctuary.gametest.Secrets23WorldGameTests' : native23 ? 'fr.koka.sanctuary.gametest.Native23WorldGameTests' : 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')
|
||||
if (atlasOnly) filter { line -> line.replaceAll(/("fabric-gametest": \["[^"]+")[^\]]*/, '$1') }
|
||||
if (atlasOnly || villageOnly) filter { line -> line.replaceAll(/("fabric-gametest": \["[^"]+")[^\]]*/, '$1') }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-3
@@ -70,11 +70,14 @@ public final class Alpha28NativeStructuresGameTests {
|
||||
report.put("locate_mineshaft", locateProof);
|
||||
if (located != null) {
|
||||
String id = located.getSecond().unwrapKey().orElseThrow().identifier().toString();
|
||||
for (var candidate : ExpansionStructures23.candidates(root)) {
|
||||
var start = level.getChunk(candidate.chunk().x(), candidate.chunk().z(), ChunkStatus.STRUCTURE_STARTS, true)
|
||||
var searchChunks = new java.util.LinkedHashSet<ChunkPos>();
|
||||
ExpansionStructures23.candidates(root).forEach(c -> searchChunks.add(c.chunk()));
|
||||
host.starterMines306().forEach(m -> searchChunks.add(m.start().getChunkPos()));
|
||||
for (var candidate : searchChunks) {
|
||||
var start = level.getChunk(candidate.x(), candidate.z(), ChunkStatus.STRUCTURE_STARTS, true)
|
||||
.getStartForStructure(located.getSecond().value());
|
||||
if (start != null && start.isValid() && start.getPieces().getFirst().getLocatorPosition().equals(located.getFirst())) {
|
||||
found.put(id, start); locateProof.put("start_chunk", List.of(candidate.chunk().x(), candidate.chunk().z())); break;
|
||||
found.put(id, start); locateProof.put("start_chunk", List.of(candidate.x(), candidate.z())); break;
|
||||
}
|
||||
}
|
||||
helper.assertTrue(found.containsKey(id), "The native /locate result belongs to a real saved start");
|
||||
|
||||
+1
-1
@@ -155,7 +155,7 @@ public final class Swamp27WorldGameTests {
|
||||
if (state.getFluidState().is(FluidTags.WATER)) { water++; chunkWater++; }
|
||||
if (plant(state)) {
|
||||
// A feature must obey native placement; old vanilla vines elsewhere are irrelevant.
|
||||
if (!state.is(Blocks.VINE)) helper.assertTrue(state.canSurvive(level, at), "Native swamp plant retains its support: " + at + " " + state);
|
||||
if (!state.is(Blocks.VINE)) helper.assertTrue(state.canSurvive(level, at), "Native swamp plant retains its support: " + at + " " + state + " below=" + level.getBlockState(at.below()));
|
||||
plants++; chunkPlants++; counts.merge(id(state), 1, Integer::sum);
|
||||
}
|
||||
if ((!state.isAir() && !plant(state)) || !state.getFluidState().isEmpty()) continue;
|
||||
|
||||
+2
-2
@@ -108,7 +108,7 @@ public final class Terraces29WorldGameTests {
|
||||
var columns = new ArrayList<Object>(); int retained=0,land=0,highest=0;
|
||||
for(int z=-r;z<=r;z+=12)for(int x=-r;x<=r;x+=12){
|
||||
int a=0,b=0,c=0;
|
||||
for(int y=380;y>=32;y-=2){
|
||||
for(int y=398;y>=32;y-=2){
|
||||
if(a==0 && old.sampleValue(context,x,y,z)>0)a=y;
|
||||
if(b==0 && now.sampleValue(context,x,y,z)>0)b=y;
|
||||
if(c==0 && global.sampleValue(context,x,y,z)>0)c=y;
|
||||
@@ -117,7 +117,7 @@ public final class Terraces29WorldGameTests {
|
||||
if(a>=200){land++;if(Math.abs(a-b)<=2)retained++;}
|
||||
highest=Math.max(highest,b); columns.add(List.of(x,z,a,b,c));
|
||||
}
|
||||
helper.assertTrue(highest<=360,"Natural summits leave room below the build ceiling");
|
||||
helper.assertTrue(highest<400,"Natural terrain and upper bodies leave at least 240 blocks below the build ceiling");
|
||||
helper.assertTrue(retained>land*.25,"Each size retains substantial original uplands, rather than changing every slope");
|
||||
sizes.add(Map.of("diameter",r*2,"columns",columns,"unchanged_uplands",retained,"uplands",land,"highest_sample",highest));
|
||||
}
|
||||
|
||||
+34
-9
@@ -39,17 +39,17 @@ public final class TerrainAtlas305GameTests {
|
||||
var functions = new LinkedHashMap<String, DensityFunction>();
|
||||
functions.put("plateaus27", field.legacy27());
|
||||
functions.put("global301", field.legacy301());
|
||||
functions.put("stretch304", new Historical304(field));
|
||||
functions.put("current305", field);
|
||||
functions.put("current305", new Historical305(field));
|
||||
functions.put("current306", field);
|
||||
var variants = new LinkedHashMap<String, Object>();
|
||||
for (var entry : functions.entrySet()) {
|
||||
var sampler = random.getSampler(entry.getValue());
|
||||
var context = SamplerContext.builder().enableCaches().build();
|
||||
var columns = new ArrayList<Object>();
|
||||
var columns = new ArrayList<List<Integer>>();
|
||||
for (int z = -bound; z <= bound; z += 12) for (int x = -bound; x <= bound; x += 12) {
|
||||
int top = 0, rock = 0, skin = 0;
|
||||
boolean roof = false;
|
||||
for (int y = 382; y >= 2; y -= 2) {
|
||||
for (int y = 418; y >= 2; y -= 2) {
|
||||
boolean solid = sampler.sampleValue(context, x, y, z) > 0;
|
||||
if (solid) {
|
||||
rock += 2;
|
||||
@@ -62,7 +62,7 @@ public final class TerrainAtlas305GameTests {
|
||||
var sections = new ArrayList<Object>();
|
||||
for (int z : new int[]{0, 96, -(radius / 4)}) {
|
||||
var rows = new ArrayList<Object>();
|
||||
for (int y = 382; y >= 0; y -= 2) {
|
||||
for (int y = 418; y >= 0; y -= 2) {
|
||||
var row = new ArrayList<Integer>();
|
||||
for (int x = -bound; x <= bound; x += 2) {
|
||||
float density = sampler.sampleValue(context, x, y, z);
|
||||
@@ -75,22 +75,37 @@ public final class TerrainAtlas305GameTests {
|
||||
}
|
||||
rows.add(row);
|
||||
}
|
||||
sections.add(Map.of("z", z, "min_x", -bound, "max_y", 382, "step", 2, "density", rows));
|
||||
sections.add(Map.of("z", z, "min_x", -bound, "max_y", 418, "step", 2, "density", rows));
|
||||
}
|
||||
variants.put(entry.getKey(), Map.of("columns", columns, "sections", sections));
|
||||
var upperCuts = new ArrayList<Object>();
|
||||
if(entry.getKey().equals("current306")) {
|
||||
var peaks=new ArrayList<List<Integer>>();
|
||||
for(var peak:columns.stream().filter(c->c.get(2)>=300).sorted(Comparator.comparingInt((List<Integer> c)->c.get(2)).reversed()).toList()) {
|
||||
if(peaks.stream().anyMatch(p->Math.hypot(p.get(0)-peak.get(0),p.get(1)-peak.get(1))<100))continue;
|
||||
peaks.add(peak); if(peaks.size()==3)break;
|
||||
}
|
||||
helper.assertTrue(peaks.size()>=2,"At least two substantial upper bodies sampled");
|
||||
for(var peak:peaks) {
|
||||
var rows=new ArrayList<Object>();
|
||||
for(int y=418;y>=260;y-=2) {var row=new ArrayList<Integer>();for(int x=peak.get(0)-72;x<=peak.get(0)+72;x+=2)
|
||||
row.add(sampler.sampleValue(context,x,y,peak.get(1))>0?1:0);rows.add(row);}
|
||||
upperCuts.add(Map.of("z",peak.get(1),"min_x",peak.get(0)-72,"max_y",418,"step",2,"solid",rows));
|
||||
}
|
||||
}
|
||||
variants.put(entry.getKey(), Map.of("columns", columns, "sections", sections,"upper_sections",upperCuts));
|
||||
}
|
||||
var report = new LinkedHashMap<String, Object>();
|
||||
report.put("schema", 1); report.put("version", version);
|
||||
report.put("revision", System.getProperty("sanctuary.test.atlasRevision", "working-tree"));
|
||||
report.put("minecraft", "26.3-pre-2"); report.put("seed", Long.toString(seed));
|
||||
report.put("diameter", radius * 2); report.put("process_id", ProcessHandle.current().pid());
|
||||
report.put("domain_y", List.of(0, 383)); report.put("build_ceiling_exclusive", 640);
|
||||
report.put("domain_y", List.of(0, 419)); report.put("build_ceiling_exclusive", 640);
|
||||
report.put("map_step", 12); report.put("vertical_step", 2); report.put("density_scale", 10000);
|
||||
report.put("columns", List.of("x", "z", "top_y", "sampled_solid_thickness", "uninterrupted_roof_thickness"));
|
||||
report.put("scope", "Native density, before fluids, surface materials, vegetation and structures; no chunks generated for this export");
|
||||
report.put("variants", variants);
|
||||
report.put("export_seconds", (System.nanoTime() - started) / 1e9);
|
||||
Path path = Path.of("diagnostics/atlas305", "terrain-" + radius * 2 + "-" + seed + ".json");
|
||||
Path path = Path.of("diagnostics/atlas306", "terrain-" + radius * 2 + "-" + seed + ".json");
|
||||
Files.createDirectories(path.getParent()); Files.writeString(path, new Gson().toJson(report));
|
||||
SanctuaryMod.LOGGER.info("Terrain atlas exported {} in {} s", path, report.get("export_seconds"));
|
||||
}
|
||||
@@ -108,4 +123,14 @@ public final class TerrainAtlas305GameTests {
|
||||
// Never serialized: this wrapper only selects the exact diagnostic sampler.
|
||||
@Override public MapCodec<? extends DensityFunction> codec() { return base.codec(); }
|
||||
}
|
||||
private record Historical305(PopulationIslandDensity base) implements DensityFunction {
|
||||
@Override public DensitySampler compileSampler(CompileContext context) { return base.compileHistorical305(context); }
|
||||
@Override public DensityFunction rewriteChildren(DfRewriteRule rule) {
|
||||
return new Historical305((PopulationIslandDensity)rule.rewrite(base));
|
||||
}
|
||||
@Override public Interval range() { return base.range(); }
|
||||
@Override public int domainAxes() { return ALL_AXES; }
|
||||
// Never serialized: this wrapper only selects the exact diagnostic sampler.
|
||||
@Override public MapCodec<? extends DensityFunction> codec() { return base.codec(); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.underground306.Village306;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
public final class Village306GameTests {
|
||||
@GameTest(maxTicks=12000)
|
||||
public void villageInCustomCavern(GameTestHelper helper) throws Exception {
|
||||
var level=helper.getLevel();var host=(SanctuaryChunkGenerator)level.getChunkSource().getGenerator();var p=host.village306();
|
||||
if(host.rootGeneration()!=24){helper.succeed();return;}
|
||||
var report=new LinkedHashMap<String,Object>();report.put("seed",Long.toString(level.getSeed()));report.put("diameter",host.initialDiameter());
|
||||
report.put("process_id",ProcessHandle.current().pid());report.put("diagnostics",p.diagnostics());
|
||||
Path file=Path.of("diagnostics/village306",host.initialDiameter()+"-"+level.getSeed()+".json");Files.createDirectories(file.getParent());
|
||||
Files.writeString(file,new GsonBuilder().setPrettyPrinting().create().toJson(report));
|
||||
if(!host.experimentalStructures() || !level.structureManager().shouldGenerateStructures()) {helper.assertTrue(p.start()==null,"Options disable village");helper.succeed();return;}
|
||||
helper.assertTrue(p.start()!=null,"Underground vanilla village admitted: "+p.diagnostics());
|
||||
helper.assertTrue(host.starterMines306().size()>=(host.initialDiameter()==1024?2:1),"One native mine, two for Large");
|
||||
report.put("mines",host.starterMines306().stream().map(m->m.start().getBoundingBox().toString()).toList());
|
||||
report.put("rail_links",host.heritagePlan19().links().stream().map(l->Map.of("id",l.id(),"kind",l.kind().toString(),"points",l.path())).toList());
|
||||
report.put("rail_cells",host.heritagePlan19().segments().stream().flatMap(seg->seg.cells().stream()).toList());
|
||||
report.put("box",p.start().getBoundingBox().toString());report.put("floor",p.cavern().floorY());report.put("entrance",p.cavern().getLocatorPosition().toShortString());
|
||||
var cp=p.start().getChunkPos();var actual=level.getChunk(cp.x(),cp.z(),ChunkStatus.STRUCTURE_STARTS,true).getStartForStructure(p.start().getStructure());
|
||||
helper.assertTrue(actual!=null&&actual.isValid(),"Actual registered start");
|
||||
var ctx=StructurePieceSerializationContext.fromLevel(level);var tag=actual.createTag(ctx,cp);var restored=StructureStart.loadStaticStart(ctx,tag,level.getSeed());
|
||||
helper.assertTrue(restored!=null&&restored.getPieces().size()==actual.getPieces().size(),"Full graph reloads without losing pieces");
|
||||
var restoredTag=restored.createTag(ctx,cp);
|
||||
report.put("exact_nbt_roundtrip",tag.equals(restoredTag));
|
||||
int normalized=0;
|
||||
for(int i=0;i<actual.getPieces().size();i++) {
|
||||
var before=actual.getPieces().get(i); var after=restored.getPieces().get(i);
|
||||
var a=before.createTag(ctx); var b=after.createTag(ctx);
|
||||
if(before instanceof net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece) {
|
||||
// Native jigsaw collision padding is discarded on template reload. Placement,
|
||||
// projection, junctions, rotation and the template itself must remain identical.
|
||||
var x=before.getBoundingBox();var y=after.getBoundingBox();
|
||||
helper.assertTrue(x.minX()==y.minX()&&x.maxX()==y.maxX()&&x.minZ()==y.minZ()&&x.maxZ()==y.maxZ()
|
||||
&&x.minY()==y.minY()&&y.maxY()<=x.maxY(),"Native reload only removes vertical collision padding");
|
||||
if(!x.equals(y))normalized++;
|
||||
a.remove("BB");b.remove("BB");
|
||||
}
|
||||
helper.assertTrue(a.equals(b),"Every piece preserves its placement and contents through NBT");
|
||||
}
|
||||
report.put("native_padding_normalized",normalized);
|
||||
if(Boolean.getBoolean("sanctuary.test.village306.planOnly")) {
|
||||
report.put("passed",true);Files.writeString(file,new GsonBuilder().setPrettyPrinting().create().toJson(report));helper.succeed();return;
|
||||
}
|
||||
var chunks=p.start().getBoundingBox().intersectingChunks().toList();var iterator=chunks.iterator();Runnable[] next=new Runnable[1];
|
||||
boolean[] tickets={false}; int[] waited={0};
|
||||
next[0]=()->{try{
|
||||
if(iterator.hasNext()){var at=iterator.next();level.getChunk(at.x(),at.z(),ChunkStatus.FULL,true);helper.runAtTickTime(helper.getTick()+1,()->next[0].run());return;}
|
||||
if(!tickets[0]) {chunks.forEach(at->level.getChunkSource().addTicketWithRadius(net.minecraft.server.level.TicketType.DRAGON,at,2));tickets[0]=true;}
|
||||
if((waited[0] < 20 || level.getEntitiesOfClass(net.minecraft.world.entity.monster.zombie.ZombieVillager.class,
|
||||
net.minecraft.world.phys.AABB.of(p.cavern().getBoundingBox())).isEmpty()) && waited[0]++<200) {
|
||||
helper.runAtTickTime(helper.getTick()+1,()->next[0].run());return;
|
||||
}
|
||||
int beds=0,chests=0,roads=0; var box=p.cavern().getBoundingBox();
|
||||
for(int z=box.minZ();z<=box.maxZ();z++)for(int x=box.minX();x<=box.maxX();x++)for(int y=p.cavern().floorY()-2;y<=p.cavern().floorY()+13;y++){
|
||||
var state=level.getBlockState(new BlockPos(x,y,z));if(state.getBlock() instanceof net.minecraft.world.level.block.BedBlock)beds++;
|
||||
if(state.is(net.minecraft.world.level.block.Blocks.CHEST))chests++;if(state.is(net.minecraft.world.level.block.Blocks.DIRT_PATH))roads++;
|
||||
}
|
||||
var zombies=level.getEntitiesOfClass(net.minecraft.world.entity.monster.zombie.ZombieVillager.class,net.minecraft.world.phys.AABB.of(box));
|
||||
report.put("beds_blocks",beds);report.put("chests",chests);report.put("roads",roads);report.put("zombie_villagers",zombies.size());report.put("entity_wait",waited[0]);
|
||||
report.put("monster_rooms",fr.koka.sanctuary.knowledge.structures.StructureCensusService.snapshot(level.getServer()).evidence().sites().values().stream()
|
||||
.filter(site->site.key().id().equals("minecraft:monster_room")).map(site->site.key()).toList());report.put("difficulty",level.getDifficulty().toString());
|
||||
Files.writeString(file,new GsonBuilder().setPrettyPrinting().create().toJson(report));
|
||||
helper.assertTrue(zombies.stream().allMatch(z->z.isPersistenceRequired()),"Native zombie villagers remain persistent and curable");
|
||||
helper.assertTrue(beds>=6,"Vanilla houses have beds");helper.assertTrue(chests>0,"Native chest loot present");helper.assertTrue(roads>20,"Native roads placed underground");helper.assertTrue(!zombies.isEmpty(),"Abandoned village has zombie villagers");
|
||||
report.put("beds_blocks",beds);report.put("chests",chests);report.put("roads",roads);report.put("zombie_villagers",zombies.size());report.put("full_chunks",chunks.size());report.put("passed",true);
|
||||
var cuts=new ArrayList<Object>();
|
||||
for(int z:new int[]{box.minZ()+box.getZSpan()/3,box.minZ()+box.getZSpan()*2/3}) {
|
||||
var rows=new ArrayList<List<Integer>>();
|
||||
for(int y=300;y>=0;y-=2){var row=new ArrayList<Integer>();for(int x=box.minX();x<=box.maxX();x+=2){
|
||||
var state=level.getBlockState(new BlockPos(x,y,z));row.add(state.getFluidState().is(net.minecraft.tags.FluidTags.WATER)?2:
|
||||
state.getFluidState().is(net.minecraft.tags.FluidTags.LAVA)?3:state.isAir()?0:1);
|
||||
}rows.add(row);}cuts.add(Map.of("z",z,"min_x",box.minX(),"max_y",300,"step",2,"blocks",rows));
|
||||
}
|
||||
report.put("native_cuts",cuts);
|
||||
var floorRows=new ArrayList<List<String>>();
|
||||
for(int z=box.minZ();z<=box.maxZ();z++){var row=new ArrayList<String>();for(int x=box.minX();x<=box.maxX();x++)
|
||||
row.add(net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(level.getBlockState(new BlockPos(x,p.cavern().floorY()+2,z)).getBlock()).toString());floorRows.add(row);}
|
||||
report.put("floor_cut",Map.of("min_x",box.minX(),"min_z",box.minZ(),"y",p.cavern().floorY()+2,"blocks",floorRows));
|
||||
|
||||
Files.writeString(file,new GsonBuilder().setPrettyPrinting().create().toJson(report));
|
||||
chunks.forEach(at->level.getChunkSource().removeTicketWithRadius(net.minecraft.server.level.TicketType.DRAGON,at,2));helper.succeed();
|
||||
}catch(Exception error){throw new IllegalStateException(error);}};
|
||||
helper.runAtTickTime(helper.getTick()+1,()->next[0].run());
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
"environment": "*",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"entrypoints": {
|
||||
"fabric-gametest": ["${gametest_entrypoint}", "fr.koka.sanctuary.gametest.Swamp27WorldGameTests", "fr.koka.sanctuary.gametest.Caves28WorldGameTests", "fr.koka.sanctuary.gametest.Terraces29WorldGameTests", "fr.koka.sanctuary.gametest.River30WorldGameTests", "fr.koka.sanctuary.gametest.Transit29WorldGameTests", "fr.koka.sanctuary.gametest.Alpha28NativeStructuresGameTests", "fr.koka.sanctuary.gametest.BlockKnowledgeGameTests", "fr.koka.sanctuary.gametest.CensusGameTestsTerrain", "fr.koka.sanctuary.gametest.CensusStockGameTests", "fr.koka.sanctuary.gametest.MaterialActivityGameTests"],
|
||||
"fabric-gametest": ["${gametest_entrypoint}", "fr.koka.sanctuary.gametest.Swamp27WorldGameTests", "fr.koka.sanctuary.gametest.Caves28WorldGameTests", "fr.koka.sanctuary.gametest.Terraces29WorldGameTests", "fr.koka.sanctuary.gametest.River30WorldGameTests", "fr.koka.sanctuary.gametest.Transit29WorldGameTests", "fr.koka.sanctuary.gametest.Alpha28NativeStructuresGameTests", "fr.koka.sanctuary.gametest.Village306GameTests", "fr.koka.sanctuary.gametest.BlockKnowledgeGameTests", "fr.koka.sanctuary.gametest.CensusGameTestsTerrain", "fr.koka.sanctuary.gametest.CensusStockGameTests", "fr.koka.sanctuary.gametest.MaterialActivityGameTests"],
|
||||
"fabric-client-gametest": ["fr.koka.sanctuary.gametest.SanctuaryClientRenderTests"]
|
||||
},
|
||||
"mixins": ["sanctuary-gametest.mixins.json"],
|
||||
|
||||
@@ -79,6 +79,7 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v24"), SanctuaryChunkGenerator.AERIAL24_CODEC);
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v24"), SanctuaryBiomeSource.AERIAL24_CODEC);
|
||||
fr.koka.sanctuary.worldgen.aerial.AerialStructures24.register();
|
||||
fr.koka.sanctuary.worldgen.underground306.Village306.register();
|
||||
fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.register();
|
||||
fr.koka.sanctuary.worldgen.transit.TransitStructures21.register();
|
||||
fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.register();
|
||||
|
||||
@@ -130,6 +130,14 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
|
||||
fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||
fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||
fr.koka.sanctuary.worldgen.aerial.AerialStructures24.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||
if (this instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator host && structures.shouldGenerateStructures()
|
||||
&& Level.OVERWORLD.equals(dimension) && host.village306().start() != null) {
|
||||
var planned = host.village306().start();
|
||||
if (planned.getChunkPos().equals(chunk.getPos())) {
|
||||
var existing = structures.getStartForStructure(planned.getStructure(), chunk);
|
||||
if (existing == null || !existing.isValid()) structures.setStartForStructure(planned.getStructure(), planned, chunk);
|
||||
}
|
||||
}
|
||||
ExpansionStructures.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||
}
|
||||
@Override public void spawnOriginalMobs(WorldGenRegion region) {
|
||||
|
||||
@@ -92,6 +92,13 @@ public final class ExpansionStructures23 {
|
||||
if (!Level.OVERWORLD.equals(dimension) || !manager.shouldGenerateStructures()) return;
|
||||
ExpansionIsland island = generator.islandAt(chunk.getPos().getMiddleBlockX(), chunk.getPos().getMiddleBlockZ());
|
||||
if (island == null || !nativeGeneration(island.generation())) return;
|
||||
if (island.origin() && generator instanceof SanctuaryChunkGenerator host) {
|
||||
for (var planned : host.starterMines306()) if (planned.start().getChunkPos().equals(chunk.getPos())) {
|
||||
var existing = manager.getStartForStructure(planned.structure().value(),chunk);
|
||||
if(existing == null || !existing.isValid()) manager.setStartForStructure(planned.structure().value(),planned.start(),chunk);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Candidate candidate = candidates(island).stream().filter(c -> c.chunk().equals(chunk.getPos())).findFirst().orElse(null);
|
||||
if (candidate == null) return;
|
||||
for (var old : chunk.getAllStarts().values()) {
|
||||
@@ -159,6 +166,37 @@ public final class ExpansionStructures23 {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Reserve complete native mines for the starter island, before mutable chunk visitation. */
|
||||
public static List<Accepted> starterMines306(ServerLevel level, SanctuaryChunkGenerator host, ExpansionIsland root) {
|
||||
var view = new NativeView(host, root, host.source(), level.registryAccess());
|
||||
var factory = PalettedContainerFactory.create(level.registryAccess());
|
||||
var choices = new ArrayList<>(candidates(root));
|
||||
choices.sort(Comparator.comparingDouble(c -> Math.hypot(c.chunk().getMiddleBlockX(), c.chunk().getMiddleBlockZ())));
|
||||
// Additional initial-world candidates vary the complete native graph, rather than forcing
|
||||
// one unsuitable graph through the island. They become real locator candidates when reserved.
|
||||
var extra = new ArrayList<Candidate>();
|
||||
int extent = root.terrainRadius() * 3 / 4;
|
||||
for(int z=-extent;z<=extent;z+=48)for(int x=-extent;x<=extent;x+=48) {
|
||||
var at=new ChunkPos(x>>4,z>>4);
|
||||
if(Math.hypot(x,z)>extent || choices.stream().anyMatch(c->c.chunk().equals(at)))continue;
|
||||
extra.add(new Candidate(at,mix(root.seed()^at.pack()^0x306A11EL)));
|
||||
}
|
||||
extra.sort(Comparator.comparingLong(Candidate::roll));choices.addAll(extra);
|
||||
var result = new ArrayList<Accepted>();
|
||||
for (var candidate : choices.stream().limit(192).toList()) {
|
||||
var chunk = new ProtoChunk(candidate.chunk(), UpgradeData.EMPTY, level, factory, null);
|
||||
try {
|
||||
var accepted = planFiltered(host, view, root, candidate, level.registryAccess(), level.getStructureTemplateManager(),
|
||||
level.dimension(), chunk, Long.MAX_VALUE, entry -> entry.id().equals("minecraft:mineshaft"));
|
||||
if (accepted == null || result.stream().anyMatch(old -> old.start().getBoundingBox().intersects(accepted.start().getBoundingBox()))) continue;
|
||||
result.add(accepted);
|
||||
if (result.size() >= (host.initialDiameter() == 1024 ? 2 : 1)) break;
|
||||
} catch (SampleBudgetExceeded refused) { /* Continue the finite search, retaining native support rules. */ }
|
||||
}
|
||||
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("Starter mines306 seed {} size {}: {} native mines reserved", root.seed(),host.initialDiameter(),result.size());
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private static Accepted planFiltered(ExpansionChunkGenerator host, ExpansionChunkGenerator view, ExpansionIsland island,
|
||||
Candidate candidate, RegistryAccess registries, StructureTemplateManager templates, ResourceKey<Level> dimension,
|
||||
ChunkAccess chunk, long deadlineNanos, Predicate<Entry> included) {
|
||||
@@ -469,6 +507,11 @@ public final class ExpansionStructures23 {
|
||||
&& Math.abs((long) candidate.chunk().z() - (origin.getZ() >> 4)) <= (long) radius * CELL_CHUNKS) candidates.add(candidate);
|
||||
}
|
||||
}
|
||||
if (nativeRequested && generator instanceof SanctuaryChunkGenerator sanctuary) {
|
||||
for(var mine:sanctuary.starterMines306()) {
|
||||
var at=mine.start().getChunkPos();addPosition(candidates,at.x(),at.z());
|
||||
}
|
||||
}
|
||||
if (sanctuaryRequested && generator instanceof SanctuaryChunkGenerator sanctuary) addSanctuaryCandidates(sanctuary, candidates);
|
||||
candidates.removeIf(c -> Math.abs((long) c.chunk().x() - (origin.getX() >> 4)) > (long) radius * CELL_CHUNKS
|
||||
|| Math.abs((long) c.chunk().z() - (origin.getZ() >> 4)) > (long) radius * CELL_CHUNKS);
|
||||
@@ -508,6 +551,9 @@ public final class ExpansionStructures23 {
|
||||
generator.ruinsPlan17().networks().forEach(s -> addPosition(positions, s.startChunkX(), s.startChunkZ()));
|
||||
generator.transitPlan21().modules().forEach(s -> addPosition(positions, s.startChunkX(), s.startChunkZ()));
|
||||
generator.undergroundHall22().networks().forEach(s -> addPosition(positions, s.startChunkX(), s.startChunkZ()));
|
||||
if (generator.village306().start() != null) {
|
||||
var village = generator.village306().start().getChunkPos(); addPosition(positions, village.x(), village.z());
|
||||
}
|
||||
var mineral = generator.mineralSanctuary23();
|
||||
if (!mineral.isEmpty()) addPosition(positions, mineral.chamber().startChunkX(), mineral.chamber().startChunkZ());
|
||||
if (generator.rootGeneration() == 24) generator.aerialPlan24().sites()
|
||||
|
||||
+10
-2
@@ -214,11 +214,19 @@ public final class StructureCensusService {
|
||||
var session = current(server); if (session.failed) return;
|
||||
try { observation.accept(session); } catch (RuntimeException error) { fail(session, error); }
|
||||
};
|
||||
if (server.isSameThread()) task.run(); else server.execute(task);
|
||||
if (server.isSameThread()) task.run(); else if (server.isRunning()) server.execute(() -> {
|
||||
// Minecraft may execute inline on a generation worker during shutdown. Such late
|
||||
// chunks stay unknown in the census and are observed normally on their next load.
|
||||
if (server.isSameThread()) task.run();
|
||||
});
|
||||
}
|
||||
private static void reportFailure(MinecraftServer server, Exception error) {
|
||||
Runnable task = () -> fail(current(server), error);
|
||||
if (server.isSameThread()) task.run(); else server.execute(task);
|
||||
if (server.isSameThread()) task.run(); else if (server.isRunning()) server.execute(() -> {
|
||||
// Minecraft may execute inline on a generation worker during shutdown. Such late
|
||||
// chunks stay unknown in the census and are observed normally on their next load.
|
||||
if (server.isSameThread()) task.run();
|
||||
});
|
||||
}
|
||||
public static void flush(MinecraftServer server) {
|
||||
assertServerThread(server); var session = SERVERS.get(server);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package fr.koka.sanctuary.mixin;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.underground306.Village306;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
/** Vanilla terrain_matching roads query this at placement time, independently of the planning generator. */
|
||||
@Mixin(WorldGenRegion.class)
|
||||
public abstract class UndergroundHeight306Mixin {
|
||||
@Inject(method = "getHeight", at = @At("HEAD"), cancellable = true)
|
||||
private void sanctuary$undergroundRoad306(Heightmap.Types type, int x, int z, CallbackInfoReturnable<Integer> cir) {
|
||||
Integer floor = Village306.projectedHeight(x, z);
|
||||
if (floor != null && (type == Heightmap.Types.WORLD_SURFACE_WG || type == Heightmap.Types.OCEAN_FLOOR_WG
|
||||
|| type == Heightmap.Types.WORLD_SURFACE || type == Heightmap.Types.OCEAN_FLOOR)) cir.setReturnValue(floor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package fr.koka.sanctuary.mixin;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import fr.koka.sanctuary.worldgen.underground306.Village306;
|
||||
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.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
|
||||
@Mixin(StructureStart.class)
|
||||
public abstract class UndergroundStart306Mixin {
|
||||
@WrapMethod(method = "placeInChunk")
|
||||
private void sanctuary$projectVillage306(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
|
||||
RandomSource random, BoundingBox clip, ChunkPos chunk, Operation<Void> original) {
|
||||
Village306.placing( (StructureStart) (Object) this,
|
||||
() -> original.call(level, manager, generator, random, clip, chunk));
|
||||
}
|
||||
}
|
||||
+8
@@ -21,6 +21,10 @@ public abstract class UnifiedDecorationProtectionMixin {
|
||||
if (!(region.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| !fr.koka.sanctuary.expansion.ExpansionIsland.spatialGeneration(generator.rootGeneration())) return;
|
||||
// Aerial pieces write in genuine void chunks, within their own immutable site and current chunk only.
|
||||
if (fr.koka.sanctuary.worldgen.underground306.Village306.isPlacing()) {
|
||||
if (!fr.koka.sanctuary.worldgen.underground306.Village306.mayWrite(pos)) callback.setReturnValue(false);
|
||||
return;
|
||||
}
|
||||
if (generator.rootGeneration() == 24 && fr.koka.sanctuary.worldgen.aerial.AerialStructures24.isPlacing()) {
|
||||
if (!fr.koka.sanctuary.worldgen.aerial.AerialStructures24.mayWrite(pos)) callback.setReturnValue(false);
|
||||
return; // Vanilla still applies its write-zone and build-height checks.
|
||||
@@ -44,6 +48,10 @@ public abstract class UnifiedDecorationProtectionMixin {
|
||||
if (!(region.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)) return;
|
||||
// Vanilla rejects writes outside its region before reading the destination chunk.
|
||||
if (!region.isWithinWriteZone(pos)) return;
|
||||
if (fr.koka.sanctuary.worldgen.underground306.Village306.isPlacing()) {
|
||||
if (!fr.koka.sanctuary.worldgen.underground306.Village306.mayWrite(pos)) callback.setReturnValue(false);
|
||||
return;
|
||||
}
|
||||
if (generator.rootGeneration() == 24 && fr.koka.sanctuary.worldgen.aerial.AerialStructures24.isPlacing()) {
|
||||
if (!fr.koka.sanctuary.worldgen.aerial.AerialStructures24.mayWrite(pos)) callback.setReturnValue(false);
|
||||
return;
|
||||
|
||||
@@ -15,7 +15,7 @@ import net.minecraft.world.level.levelgen.feature.MonsterRoomFeature;
|
||||
/** A few attempts at real cave walls; the vanilla feature owns its shell, loot and spawner. */
|
||||
public final class CavityMonsterRoom28 {
|
||||
private static final MonsterRoomFeature NATIVE = new MonsterRoomFeature();
|
||||
private static final int MAX_ATTEMPTS = 8;
|
||||
private static final int MAX_ATTEMPTS = 32;
|
||||
private CavityMonsterRoom28() {}
|
||||
|
||||
public static void decorate(WorldGenLevel level, ChunkGenerator generator, ChunkAccess chunk) {
|
||||
@@ -24,10 +24,9 @@ public final class CavityMonsterRoom28 {
|
||||
var pos = chunk.getPos(); var island = host.islandForChunk(pos.x(), pos.z());
|
||||
if (island == null || island.generation() != 24) return;
|
||||
long seed = WoodlandGroveGeometry.seed(island.seed(), pos.x(), 28, pos.z(), 0xD086E028L);
|
||||
if (Math.floorMod(seed, 3) != 0) return;
|
||||
var candidates = new ArrayList<BlockPos>();
|
||||
int bottom = Math.max(level.getMinY() + 2, 32), top = Math.min(level.getMaxY() - 6, 280);
|
||||
for (int dx : new int[]{4, 11}) for (int dz : new int[]{4, 11}) {
|
||||
for (int dx : new int[]{4, 8, 11}) for (int dz : new int[]{4, 8, 11}) {
|
||||
int x = pos.getMinBlockX() + dx, z = pos.getMinBlockZ() + dz;
|
||||
for (int y = bottom; y <= top; y++) {
|
||||
BlockPos at = new BlockPos(x, y, z);
|
||||
@@ -46,7 +45,17 @@ public final class CavityMonsterRoom28 {
|
||||
if (attempts++ >= MAX_ATTEMPTS) break;
|
||||
if (!safe(level, host, at, island.id())) continue;
|
||||
// No virtual solid floor/roof and no prebuilt room. Native rejection remains authoritative.
|
||||
if (NATIVE.place(level, generator, RandomSource.create(WoodlandGroveGeometry.seed(seed, at.getX(), at.getY(), at.getZ(), 0xC0128)), at)) return;
|
||||
if (NATIVE.place(level, generator, RandomSource.create(WoodlandGroveGeometry.seed(seed, at.getX(), at.getY(), at.getZ(), 0xC0128)), at)) {
|
||||
// This late native room can carve a floor below vegetation placed earlier.
|
||||
// Native generation uses quiet writes, so settle those plants locally, without drops.
|
||||
for(int dy=-1;dy<=5;dy++)for(int dx=-4;dx<=4;dx++)for(int dz=-4;dz<=4;dz++) {
|
||||
var plant=at.offset(dx,dy,dz);var state=level.getBlockState(plant);
|
||||
if((state.getBlock() instanceof net.minecraft.world.level.block.VegetationBlock
|
||||
|| state.getBlock() instanceof net.minecraft.world.level.block.SugarCaneBlock)
|
||||
&& !state.canSurvive(level,plant)) level.setBlock(plant,Blocks.AIR.defaultBlockState(),2);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,15 +14,20 @@ public final class DetachedRock302 implements DensitySampler {
|
||||
private final double radius;
|
||||
private final long seed;
|
||||
private final boolean broad;
|
||||
private final int ceiling;
|
||||
public DensitySampler input() { return input; }
|
||||
|
||||
public DetachedRock302(DensitySampler input, double radius, long seed) {
|
||||
this(input, radius, seed, false);
|
||||
}
|
||||
public DetachedRock302(DensitySampler input, double radius, long seed, boolean broad) {
|
||||
this.input = input; this.radius = radius; this.seed = seed; this.broad = broad;
|
||||
this(input,radius,seed,broad,376);
|
||||
}
|
||||
public DetachedRock302(DensitySampler input, double radius, long seed, boolean broad, int ceiling) {
|
||||
this.input = input; this.radius = radius; this.seed = seed; this.broad = broad; this.ceiling=ceiling;
|
||||
}
|
||||
private boolean affected(int x, int y, int z) {
|
||||
if (broad) return y > 170 && y < 376 && Math.hypot(x, z) > 48 && Math.hypot(x,z) < radius + 32;
|
||||
if (broad) return y > 170 && y < ceiling && Math.hypot(x, z) > 48 && Math.hypot(x,z) < radius + 32;
|
||||
return SurfaceRelief302.weight(x,y,z,radius) * Relief30.influence(seed,x,z,radius) > 0;
|
||||
}
|
||||
@Override public float sampleValue(SamplerContext context,int x,int y,int z) {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.*;
|
||||
|
||||
/** A few solid, eroded rock bodies in the upper air, not a field of floating debris. */
|
||||
public final class FloatingTerrain306 implements DensitySampler {
|
||||
public record Mass(int x, int y, int z, int rx, int rz, int depth, int rise) {}
|
||||
private final DensitySampler ground, coarse;
|
||||
private final double radius;
|
||||
private final long seed;
|
||||
private volatile List<Mass> masses;
|
||||
public FloatingTerrain306(DensitySampler ground, DensitySampler coarse, double radius, long seed) {
|
||||
this.ground=ground; this.coarse=coarse; this.radius=radius; this.seed=seed;
|
||||
}
|
||||
public List<Mass> masses(SamplerContext sample) {
|
||||
if(masses!=null) return masses;
|
||||
synchronized(this) {
|
||||
if(masses!=null) return masses;
|
||||
var random=new Random(seed ^ 0x306A1B0D1EL); var chosen=new ArrayList<Mass>();
|
||||
int count=radius>=500?3:2;
|
||||
double phase=random.nextDouble()*Math.PI*2;
|
||||
for(int i=0;i<count;i++) for(int attempt=0;attempt<24;attempt++) {
|
||||
double angle=phase+i*Math.PI*2/count+(random.nextDouble()-.5)*.6;
|
||||
double distance=radius*(.40+random.nextDouble()*.22);
|
||||
var m=new Mass((int)(Math.cos(angle)*distance),352+random.nextInt(20),
|
||||
(int)(Math.sin(angle)*distance),25+random.nextInt(12),22+random.nextInt(12),25+random.nextInt(10),9+random.nextInt(9));
|
||||
boolean clear=true;
|
||||
for(var other:chosen) if(Math.hypot(m.x-other.x,m.z-other.z)<m.rx+other.rx+36) {clear=false;break;}
|
||||
// Probe the existing field only. No terrain edits or chunk loads to make a site fit.
|
||||
for(int z=m.z-m.rz*2;clear&&z<=m.z+m.rz*2;z+=6)
|
||||
for(int x=m.x-m.rx*2;clear&&x<=m.x+m.rx*2;x+=6)
|
||||
for(int y=m.y-m.depth*2-18;y<=m.y+m.rise*2;y+=4)
|
||||
if(ground.sampleValue(sample,x,y,z)>0) {clear=false;break;}
|
||||
if(clear) {chosen.add(m);break;}
|
||||
}
|
||||
masses=List.copyOf(chosen);
|
||||
SanctuaryMod.LOGGER.info("Floating terrain306 radius {}: {} / {} coherent bodies",radius,masses.size(),count);
|
||||
return masses;
|
||||
}
|
||||
}
|
||||
@Override public float sampleValue(SamplerContext sample,int x,int y,int z) {
|
||||
float result=ground.sampleValue(sample,x,y,z);
|
||||
if(y<300||y>=400||Math.hypot(x,z)>radius*.85) return result;
|
||||
for(var m:masses(sample)) {
|
||||
double dx=(x-m.x)/(double)m.rx, dz=(z-m.z)/(double)m.rz;
|
||||
if(Math.abs(dx)>1.8||Math.abs(dz)>1.8||y<m.y-m.depth*1.5||y>m.y+m.rise*1.5) continue;
|
||||
// Low-frequency 3D erosion changes the silhouette; a solid centre cannot become a shell.
|
||||
double noise=Math.clamp(2*coarse.sampleValue(sample,x+457,y-231,z-619),-1,1);
|
||||
dx += .24*Math.clamp(2*coarse.sampleValue(sample,x-331,y+179,z+223),-1,1);
|
||||
dz += .24*Math.clamp(2*coarse.sampleValue(sample,x+613,y-71,z-337),-1,1);
|
||||
double edge=1-dx*dx-dz*dz+.72*noise;
|
||||
double vertical=y<m.y ? (m.y-y)/(double)m.depth : (y-m.y)/(double)m.rise;
|
||||
double body=edge-vertical*vertical;
|
||||
result=Math.max(result,(float)Math.clamp(body*.30,-1,1));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@Override public void sampleVolume(SamplerContext sample,DensityBuffer buffer,DensityVolume volume) {
|
||||
DensitySampler.sampleVolumeNaive(sample,buffer,volume,this);
|
||||
}
|
||||
}
|
||||
+8
-5
@@ -49,13 +49,14 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
|
||||
}
|
||||
|
||||
@Override public DensitySampler compileSampler(CompileContext context) {
|
||||
return compileSampler(context, false);
|
||||
return compileSampler(context, false, .95);
|
||||
}
|
||||
/** Exact rejected30.4 field retained for the reproducible scientific comparison. */
|
||||
public DensitySampler compileHistorical304(CompileContext context) {
|
||||
return compileSampler(context, true);
|
||||
return compileSampler(context, true, .8);
|
||||
}
|
||||
private DensitySampler compileSampler(CompileContext context, boolean historical304) {
|
||||
public DensitySampler compileHistorical305(CompileContext context) { return compileSampler(context, false, .8); }
|
||||
private DensitySampler compileSampler(CompileContext context, boolean historical304, double plateauStrength) {
|
||||
var raw = ExactInterpolationSampler.compileTerrain(terrain, context); var edge = distortion.compileSampler(context);
|
||||
var coarse = sculpt.compileSampler(context); var fine = detail.compileSampler(context);
|
||||
var bottom = underside.compileSampler(context); var warp = riftWarp.compileSampler(context);
|
||||
@@ -93,7 +94,7 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
|
||||
double province = balanced != null ? balanced.province(x, z) : 0;
|
||||
double mappedY = balanced != null && province > 0 ? HighPlateaus304.sourceY(y, province) : y;
|
||||
double weight = geology28 ? CavesRelief28.deformationWeight(x, y, z, capacity.radius()) : 0;
|
||||
if (plateaus != null) weight *= .8 * plateaus.province(x, z);
|
||||
if (plateaus != null) weight *= plateauStrength * plateaus.province(x, z);
|
||||
else if (balanced != null) weight *= province;
|
||||
else if (relief30) weight *= Relief30.influence(seed, x, z, capacity.radius());
|
||||
double surfaceWeight = surface302 ? SurfaceRelief302.weight(x, y, z, capacity.radius())
|
||||
@@ -140,7 +141,9 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
|
||||
DensitySampler.sampleVolumeNaive(sample, buffer, volume, this);
|
||||
}
|
||||
};
|
||||
return surface302 || balanced303 ? new DetachedRock302(field,capacity.radius(),seed, balanced303) : field;
|
||||
if (balanced303 && !historical304 && plateauStrength > .9)
|
||||
field = new FloatingTerrain306(field, coarse, capacity.radius(), seed);
|
||||
return surface302 || balanced303 ? new DetachedRock302(field,capacity.radius(),seed, balanced303, field instanceof FloatingTerrain306 ? 412 : 376) : field;
|
||||
}
|
||||
/** Native diagnostics can compile the exact accepted27 field without new adaptations. */
|
||||
public PopulationIslandDensity legacy27() {
|
||||
|
||||
+21
-1
@@ -137,6 +137,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
lostCityPlan(island);
|
||||
return cityPlacements21.get(cityIdentity(island));
|
||||
}
|
||||
private volatile fr.koka.sanctuary.worldgen.underground306.Village306.Plan village306 = fr.koka.sanctuary.worldgen.underground306.Village306.Plan.EMPTY;
|
||||
private boolean villagePrepared306;
|
||||
private boolean minesPrepared306;
|
||||
private volatile java.util.List<fr.koka.sanctuary.expansion.ExpansionStructures23.Accepted> starterMines306 = java.util.List.of();
|
||||
public java.util.List<fr.koka.sanctuary.expansion.ExpansionStructures23.Accepted> starterMines306() { return starterMines306; }
|
||||
public fr.koka.sanctuary.worldgen.underground306.Village306.Plan village306() { return village306; }
|
||||
private volatile Context originContext;
|
||||
private final int generation;
|
||||
private net.minecraft.world.level.LevelHeightAccessor heightAccessor;
|
||||
@@ -189,6 +195,9 @@ 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) {
|
||||
if (village306.intersects(minX,minY,minZ,maxX,maxY,maxZ)) return true;
|
||||
for (var mine : starterMines306) for(var piece : mine.start().getPieces())
|
||||
if(piece.getBoundingBox().intersects(new net.minecraft.world.level.levelgen.structure.BoundingBox(minX,minY,minZ,maxX,maxY,maxZ))) return true;
|
||||
if (generation == 24) for (var site : aerialPlan24.sites()) {
|
||||
if (minX <= site.maxX() + 1 && maxX >= site.minX() - 1 && minZ <= site.maxZ() + 1 && maxZ >= site.minZ() - 1
|
||||
&& minY <= site.maxY() + 1 && maxY >= site.minY() - 1) return true;
|
||||
@@ -246,6 +255,14 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
prepareRuins(islands);
|
||||
prepareHeritage(islands);
|
||||
prepareTransit(islands);
|
||||
if (!villagePrepared306 && generation == 24 && cityStructuresEnabled && experimentalStructures) {
|
||||
villagePrepared306 = true;
|
||||
village306 = fr.koka.sanctuary.worldgen.underground306.VillagePlanner306.plan(level, this);
|
||||
}
|
||||
if (!minesPrepared306 && generation == 24 && cityStructuresEnabled) {
|
||||
minesPrepared306 = true;
|
||||
starterMines306 = fr.koka.sanctuary.expansion.ExpansionStructures23.starterMines306(level,this,islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow());
|
||||
}
|
||||
}
|
||||
@Override public void setIslands(List<ExpansionIsland> islands) {
|
||||
// Complete and publish city geometry before workers can observe the new owner list.
|
||||
@@ -320,7 +337,9 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()));
|
||||
long start = System.nanoTime();
|
||||
sampled.discoverLandmarks(lostCityPlan(root), ruinsPlan17);
|
||||
var plan = (generation == 23 || generation == 24)
|
||||
var plan = generation == 24
|
||||
? fr.koka.sanctuary.worldgen.heritage.HeritagePlanner23.plan306(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
|
||||
: generation == 23
|
||||
? fr.koka.sanctuary.worldgen.heritage.HeritagePlanner23.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
|
||||
: generation == 22
|
||||
? fr.koka.sanctuary.worldgen.heritage.HeritagePlanner22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
|
||||
@@ -550,6 +569,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
});
|
||||
}
|
||||
public boolean protectsCityDecoration(net.minecraft.core.BlockPos pos) {
|
||||
if (village306.cavern() != null && village306.cavern().getBoundingBox().isInside(pos)) return true;
|
||||
if (generation == 24 && aerialPlan24.protects(pos)) return true;
|
||||
if ((generation == 23 || generation == 24) && mineralSanctuary23.protects(pos.getX(), pos.getY(), pos.getZ())) return true;
|
||||
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|
||||
|
||||
+75
-5
@@ -26,8 +26,14 @@ public final class HeritagePlanner23 {
|
||||
private HeritagePlanner23() {}
|
||||
|
||||
public static HeritagePlan19 plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
|
||||
return plan(seed, cx, cz, diameter, terrain, false);
|
||||
}
|
||||
public static HeritagePlan19 plan306(long seed, int cx, int cz, int diameter, Terrain terrain) {
|
||||
return plan(seed, cx, cz, diameter, terrain, true);
|
||||
}
|
||||
private static HeritagePlan19 plan(long seed, int cx, int cz, int diameter, Terrain terrain, boolean branching) {
|
||||
if (!List.of(512, 724, 1024).contains(diameter)) throw new IllegalArgumentException("Heritage23 requires a starter size");
|
||||
var probe = new Probe(terrain, cx, cz, diameter);
|
||||
var probe = new Probe(terrain, cx, cz, diameter); probe.branching = branching;
|
||||
probe.columnLimit = SITE_COLUMNS; probe.sampleLimit = SITE_SAMPLES;
|
||||
List<Landmark> landmarks = new ArrayList<>(List.copyOf(terrain.landmarks()).stream().limit(12).toList());
|
||||
naturalRailEndpoints(seed, cx, cz, diameter, landmarks, probe);
|
||||
@@ -54,7 +60,8 @@ public final class HeritagePlanner23 {
|
||||
}
|
||||
probe.columnLimit = MAX_COLUMNS; probe.sampleLimit = MAX_SAMPLES;
|
||||
var links = new ArrayList<Link>();
|
||||
naturalRailFragments(seed, landmarks, sites, links, cells, probe);
|
||||
if (branching) branchingRails306(seed, diameter, landmarks, sites, links, cells, probe);
|
||||
else naturalRailFragments(seed, landmarks, sites, links, cells, probe);
|
||||
probe.columnLimit = MAX_COLUMNS; probe.sampleLimit = MAX_SAMPLES;
|
||||
var routeResults = new LinkedHashMap<String, String>();
|
||||
var routeReasons = new java.util.TreeMap<String, Integer>(); probe.reasons = routeReasons;
|
||||
@@ -152,8 +159,8 @@ public final class HeritagePlanner23 {
|
||||
List<Landmark> landmarks, Probe p) {
|
||||
try {
|
||||
double axis = unit(mix(seed ^ 0x23A115L)) * Math.PI * 2;
|
||||
for (int end = 0; end < 2; end++) for (int attempt = 0; attempt < 16; attempt++) {
|
||||
double angle = axis + end * Math.PI + (attempt % 2 == 0 ? 1 : -1) * (attempt / 2) * .07;
|
||||
for (int end = 0; end < (p.branching ? 4 : 2); end++) for (int attempt = 0; attempt < 16; attempt++) {
|
||||
double angle = axis + end * (p.branching ? Math.PI / 2 : Math.PI) + (attempt % 2 == 0 ? 1 : -1) * (attempt / 2) * .07;
|
||||
double radius = diameter * (attempt < 8 ? .25 : .18);
|
||||
int x = cx + (int) Math.round(Math.cos(angle) * radius);
|
||||
int z = cz + (int) Math.round(Math.sin(angle) * radius);
|
||||
@@ -214,6 +221,69 @@ public final class HeritagePlanner23 {
|
||||
} catch (BudgetExceeded exhausted) { p.reject("fragment-budget"); }
|
||||
}
|
||||
|
||||
/** Grow along locally feasible grades, then fork from an actual saved endpoint. */
|
||||
private static void branchingRails306(long seed, int diameter, List<Landmark> landmarks, List<Site> sites,
|
||||
List<Link> links, Map<Long, OwnedCell> cells, Probe p) {
|
||||
p.columnLimit = MAX_COLUMNS - 1_500; p.sampleLimit = MAX_SAMPLES - 80_000;
|
||||
record Arm(Landmark from, int facing, int depth) {}
|
||||
var queue = new java.util.ArrayDeque<Arm>();
|
||||
for (var l : landmarks) if (l.id().startsWith("natural_rail_")) queue.add(new Arm(l,l.entry().facing(),0));
|
||||
int serial = 0;
|
||||
try {
|
||||
while (!queue.isEmpty() && landmarks.size() < 16 && links.size() < 20) {
|
||||
var arm=queue.removeFirst(); var from=arm.from();
|
||||
var path=new ArrayList<XZ>(); path.add(new XZ(from.entry().x(),from.entry().z()));
|
||||
RouteDraft best=null; int face=arm.facing();
|
||||
String id="rail_branch_"+serial;
|
||||
var request=new Request("branch_"+serial,from.id(),id,RouteKind.FREIGHT);
|
||||
int target=diameter/4;
|
||||
for(int step=0;step<target/8;step++) {
|
||||
RouteDraft chosen=null; List<XZ> chosenPath=null; int chosenFace=face;
|
||||
var last=path.getLast();
|
||||
for(int turn:new int[]{0,1,-1}) {
|
||||
int direction=(face+turn+4)&3;
|
||||
// Give long straight runs priority, with curves only where the grade needs them.
|
||||
var end=last.move(direction,8);int top=p.surface(end.x(),end.z());
|
||||
if(top<14||top>350)continue;
|
||||
if(path.stream().anyMatch(at->Math.abs(at.x()-end.x())+Math.abs(at.z()-end.z())<5))continue;
|
||||
if(cells.containsKey(key(end.x(),end.z())))continue;
|
||||
var proposed=new ArrayList<>(path);line(proposed,end);
|
||||
var endEntry=new Entry(end.x(),top+1,end.z(),(direction+2)&3);
|
||||
var historical=landmarks.stream().filter(l->!l.id().startsWith("natural_rail_")&&!l.id().startsWith("rail_branch_")).toList();
|
||||
var draft=profile(seed,request,new Endpoint(from.id(),from.entry(),false),new Endpoint(id,endEntry,false),proposed,sites,historical,cells,p);
|
||||
if(draft!=null){chosen=draft;chosenPath=proposed;chosenFace=direction;break;}
|
||||
}
|
||||
if(chosen==null)break;
|
||||
best=chosen;path=new ArrayList<>(chosenPath);face=chosenFace;
|
||||
}
|
||||
if(best==null||best.path().size()<17)continue;
|
||||
var end=best.path().getLast();
|
||||
var terminal=new Landmark(id,LandmarkKind.MINE,new Entry(end.x(),end.floorY()+1,end.z(),face));
|
||||
landmarks.add(terminal);
|
||||
if(best.path().size() >= 49 && landmarks.size() < 15 && links.size() < 19) {
|
||||
int middle=best.path().size()/2;var at=best.path().get(middle);var after=best.path().get(middle+1);
|
||||
int tangent=direction(at.x(),at.z(),after.x(),after.z());
|
||||
var fork=new Landmark(id+"_fork",LandmarkKind.MINE,new Entry(at.x(),at.floorY()+1,at.z(),tangent));
|
||||
landmarks.add(fork);
|
||||
links.add(new Link(request.id()+"_a",request.from(),fork.id(),request.kind(),best.path().subList(0,middle+1)));
|
||||
links.add(new Link(request.id()+"_b",fork.id(),request.to(),request.kind(),best.path().subList(middle,best.path().size())));
|
||||
queue.addFirst(new Arm(fork,(tangent+1)&3,arm.depth()+1));
|
||||
queue.addLast(new Arm(fork,(tangent+3)&3,arm.depth()+1));
|
||||
} else links.add(new Link(request.id(),request.from(),request.to(),request.kind(),best.path()));
|
||||
for(var cell:best.cells().values()) {
|
||||
long key=key(cell.x(),cell.z());var previous=cells.get(key);
|
||||
if(previous==null)cells.put(key,new OwnedCell(request,cell));
|
||||
else if(cell.rail()!=Rail.NONE && previous.cell().rail()!=cell.rail()) {
|
||||
var c=previous.cell();cells.put(key,new OwnedCell(previous.request(),new Cell(c.x(),c.floorY(),c.z(),c.foundationDepth(),cell.rail(),false)));
|
||||
}
|
||||
}
|
||||
serial++;
|
||||
// Forward continuation and a lateral branch share their ballast; the switch is repairable.
|
||||
if(arm.depth()<2){queue.addFirst(new Arm(terminal,(face+(serial%2==0?1:3))&3,arm.depth()+1));queue.addLast(new Arm(terminal,face,arm.depth()+1));}
|
||||
}
|
||||
} catch(BudgetExceeded exhausted){p.reject("branched-rail-budget");}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -665,7 +735,7 @@ public final class HeritagePlanner23 {
|
||||
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;
|
||||
boolean branching; 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(x - cx, z - cz) < 48) return INVALID;
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package fr.koka.sanctuary.worldgen.underground306;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.minecraft.core.Registry;
|
||||
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.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureType;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceType;
|
||||
|
||||
public final class Village306 {
|
||||
public static final ResourceKey<Structure> KEY = ResourceKey.create(Registries.STRUCTURE, SanctuaryMod.id("underground_abandoned_village"));
|
||||
public static final StructureType<VillageStructure306> TYPE = Registry.register(BuiltInRegistries.STRUCTURE_TYPE,
|
||||
SanctuaryMod.id("underground_site_v306"), () -> VillageStructure306.CODEC);
|
||||
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
|
||||
SanctuaryMod.id("village_cavern_v306"), VillageCavern306::new);
|
||||
public record Plan(StructureStart start, VillageCavern306 cavern, Map<String,Integer> diagnostics) {
|
||||
public static final Plan EMPTY = new Plan(null, null, Map.of());
|
||||
public boolean intersects(int x0,int y0,int z0,int x1,int y1,int z1) {
|
||||
return cavern != null && cavern.getBoundingBox().intersects(new net.minecraft.world.level.levelgen.structure.BoundingBox(x0,y0,z0,x1,y1,z1));
|
||||
}
|
||||
}
|
||||
private static final ThreadLocal<VillageCavern306> PLACING = new ThreadLocal<>();
|
||||
public static void register() {}
|
||||
public static void placing(StructureStart start, Runnable action) {
|
||||
VillageCavern306 cavity = start.getPieces().stream().filter(VillageCavern306.class::isInstance)
|
||||
.map(VillageCavern306.class::cast).findFirst().orElse(null);
|
||||
var previous = PLACING.get();
|
||||
if (cavity != null) PLACING.set(cavity);
|
||||
try { action.run(); } finally { if (previous == null) PLACING.remove(); else PLACING.set(previous); }
|
||||
}
|
||||
public static boolean isPlacing() { return PLACING.get() != null; }
|
||||
public static boolean mayWrite(net.minecraft.core.BlockPos pos) { var c = PLACING.get(); return c != null && c.getBoundingBox().isInside(pos); }
|
||||
public static Integer projectedHeight(int x,int z) { var c = PLACING.get(); return c != null && c.projects(x,z) ? c.floorY() : null; }
|
||||
private Village306() {}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
package fr.koka.sanctuary.worldgen.underground306;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
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.Blocks;
|
||||
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;
|
||||
|
||||
/** Saved local cavern. All houses, roads, chests and inhabitants are native village pieces. */
|
||||
public final class VillageCavern306 extends StructurePiece {
|
||||
private final int floorY;
|
||||
private final long seed;
|
||||
private final BlockPos entrance;
|
||||
private final List<BoundingBox> volumes;
|
||||
|
||||
public VillageCavern306(int floorY, long seed, BlockPos entrance, List<BoundingBox> volumes) {
|
||||
super(Village306.PIECE, 0, bounds(volumes));
|
||||
this.floorY = floorY; this.seed = seed; this.entrance = entrance.immutable();
|
||||
this.volumes = List.copyOf(volumes); setOrientation(null); validate();
|
||||
}
|
||||
public VillageCavern306(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||
super(Village306.PIECE, tag);
|
||||
if (tag.getIntOr("Schema", 0) != 1) throw new IllegalArgumentException("Unknown underground306 piece schema");
|
||||
floorY = tag.getInt("Floor").orElseThrow();
|
||||
seed = tag.getLong("Seed").orElseThrow();
|
||||
entrance = new BlockPos(tag.getInt("EntryX").orElseThrow(), tag.getInt("EntryY").orElseThrow(), tag.getInt("EntryZ").orElseThrow());
|
||||
List<BoundingBox> decoded = new ArrayList<>();
|
||||
ListTag list = tag.getList("Volumes").orElseThrow();
|
||||
if (list.isEmpty() || list.size() > 512) throw new IllegalArgumentException("Invalid underground306 volume count");
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
var t = list.getCompound(i).orElseThrow();
|
||||
decoded.add(new BoundingBox(t.getInt("x0").orElseThrow(), t.getInt("y0").orElseThrow(), t.getInt("z0").orElseThrow(),
|
||||
t.getInt("x1").orElseThrow(), t.getInt("y1").orElseThrow(), t.getInt("z1").orElseThrow()));
|
||||
}
|
||||
volumes = List.copyOf(decoded);
|
||||
setOrientation(null); validate();
|
||||
if (!getBoundingBox().equals(bounds(volumes))) throw new IllegalArgumentException("Underground306 piece bounds mismatch");
|
||||
}
|
||||
private void validate() {
|
||||
if (floorY < 3 || floorY > 345 || volumes.isEmpty() || volumes.size() > 512
|
||||
|| getBoundingBox().getXSpan() > 256 || getBoundingBox().getZSpan() > 256
|
||||
|| getBoundingBox().minY() < 1 || getBoundingBox().maxY() > 358)
|
||||
throw new IllegalArgumentException("Invalid underground306 geometry");
|
||||
}
|
||||
public static BoundingBox bounds(List<BoundingBox> volumes) {
|
||||
return BoundingBox.encapsulatingBoxes(volumes).orElseThrow();
|
||||
}
|
||||
public int floorY() { return floorY; }
|
||||
public long seed() { return seed; }
|
||||
public List<BoundingBox> volumes() { return volumes; }
|
||||
@Override public BlockPos getLocatorPosition() { return entrance; }
|
||||
public boolean projects(int x, int z) {
|
||||
return roofAt(x, z) >= floorY + 2;
|
||||
}
|
||||
/** Broad rounded chambers overlap around the complete native graph; no global cave field. */
|
||||
public int roofAt(int x, int z) {
|
||||
int roof = -1;
|
||||
for (var b : volumes) {
|
||||
double dx = Math.max(0, Math.max(b.minX() + 8 - x, x - (b.maxX() - 8))) / 8.0;
|
||||
double dz = Math.max(0, Math.max(b.minZ() + 8 - z, z - (b.maxZ() - 8))) / 8.0;
|
||||
double edge = dx * dx + dz * dz;
|
||||
if (edge > 1) continue;
|
||||
double ripple = Math.sin((x + (seed & 255)) * .071) * Math.cos(z * .063) * 2;
|
||||
roof = Math.max(roof, b.maxY() - 3 + (int) Math.round(ripple - edge * 5));
|
||||
}
|
||||
return roof;
|
||||
}
|
||||
public BlockState terrainOverride(int x, int y, int z) {
|
||||
if (!getBoundingBox().isInside(x, y, z) || roofAt(x,z) < floorY + 2 || y > roofAt(x,z)) return null;
|
||||
if (y < floorY - 1) return Blocks.STONE.defaultBlockState();
|
||||
if (y == floorY - 1) return Blocks.DIRT.defaultBlockState();
|
||||
return Blocks.CAVE_AIR.defaultBlockState();
|
||||
}
|
||||
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||
tag.putInt("Schema", 1); tag.putInt("Floor", floorY); tag.putLong("Seed", seed);
|
||||
tag.putInt("EntryX", entrance.getX()); tag.putInt("EntryY", entrance.getY()); tag.putInt("EntryZ", entrance.getZ());
|
||||
ListTag list = new ListTag();
|
||||
for (var b : volumes) {
|
||||
var t = new CompoundTag(); t.putInt("x0", b.minX()); t.putInt("y0", b.minY()); t.putInt("z0", b.minZ());
|
||||
t.putInt("x1", b.maxX()); t.putInt("y1", b.maxY()); t.putInt("z1", b.maxZ()); list.add(t);
|
||||
}
|
||||
tag.put("Volumes", list);
|
||||
}
|
||||
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
|
||||
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
|
||||
var b = getBoundingBox();
|
||||
for (int z = Math.max(b.minZ(), clip.minZ()); z <= Math.min(b.maxZ(), clip.maxZ()); z++)
|
||||
for (int x = Math.max(b.minX(), clip.minX()); x <= Math.min(b.maxX(), clip.maxX()); x++)
|
||||
for (int y = b.minY(); y <= b.maxY(); y++) {
|
||||
var block = terrainOverride(x, y, z);
|
||||
// Keep existing geology under the walking floor; only bridge local cracks.
|
||||
if (block != null && !(y < floorY - 1 && level.getBlockState(new BlockPos(x,y,z)).isSolid()))
|
||||
placeBlock(level, block, x, y, z, clip);
|
||||
}
|
||||
}
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
package fr.koka.sanctuary.worldgen.underground306;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
|
||||
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.util.*;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.biome.Biomes;
|
||||
import net.minecraft.world.level.chunk.PalettedContainerFactory;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.UpgradeData;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
import net.minecraft.world.level.levelgen.structure.*;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||
|
||||
/** Count-bounded search, independent of loaded chunks and machine speed. */
|
||||
public final class VillagePlanner306 {
|
||||
private final SanctuaryChunkGenerator host;
|
||||
private final ServerLevel level;
|
||||
private final ExpansionIsland root;
|
||||
private final Map<Long, NoiseColumn> columns = new HashMap<>();
|
||||
private final Map<String,Integer> diagnostics = new LinkedHashMap<>();
|
||||
private static final int MAX_COLUMNS = 32000;
|
||||
private VillagePlanner306(ServerLevel level, SanctuaryChunkGenerator host) {
|
||||
this.level=level; this.host=host; this.root=host.source().islands().stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
||||
}
|
||||
public static Village306.Plan plan(ServerLevel level, SanctuaryChunkGenerator host) {
|
||||
var p=new VillagePlanner306(level,host); long began=System.nanoTime();
|
||||
Village306.Plan result;
|
||||
try { result=p.search(); } catch (BudgetExceeded exhausted) { p.reject("column-budget"); result=new Village306.Plan(null,null,Map.copyOf(p.diagnostics)); }
|
||||
SanctuaryMod.LOGGER.info("Village306 seed {} size {}: {}, {} columns, {} ms; {}", level.getSeed(),host.initialDiameter(),
|
||||
result.start()==null ? "REFUSED" : result.start().getBoundingBox(),p.columns.size(),(System.nanoTime()-began)/1000000,result.diagnostics());
|
||||
return result;
|
||||
}
|
||||
private void reject(String why) { diagnostics.merge(why,1,Integer::sum); }
|
||||
private static final class BudgetExceeded extends RuntimeException {}
|
||||
private NoiseColumn column(int x,int z) {
|
||||
long key=BlockPos.asLong(x,0,z); var found=columns.get(key);
|
||||
if(found==null) { if(columns.size()>=MAX_COLUMNS) throw new BudgetExceeded(); found=host.getBaseColumn(x,z,level,host.randomState(root)); columns.put(key,found); }
|
||||
return found;
|
||||
}
|
||||
private boolean solid(int x,int y,int z) { var b=column(x,z).getBlock(y); return b.isSolid() && b.getFluidState().isEmpty(); }
|
||||
private Village306.Plan search() {
|
||||
// Prefer the island's interior rock, not its arrival or thin outer rim.
|
||||
var candidates=new ArrayList<ChunkPos>();
|
||||
double phase=(mix(root.seed()) & 65535)*Math.PI*2/65536;
|
||||
for(int ring=0;ring<3;ring++) for(int angle=0;angle<8;angle++) {
|
||||
double a=phase+angle*Math.PI/4+ring*.19, radius=host.initialDiameter()*(.12+ring*.055);
|
||||
candidates.add(new ChunkPos((int)Math.round(Math.cos(a)*radius)>>4,(int)Math.round(Math.sin(a)*radius)>>4));
|
||||
}
|
||||
for(int ring=0;ring<3;ring++) for(int angle=0;angle<12;angle++) {
|
||||
double a=phase+angle*Math.PI/6+ring*.17, radius=host.initialDiameter()*(.27+ring*.055);
|
||||
var at=new ChunkPos((int)Math.round(Math.cos(a)*radius)>>4,(int)Math.round(Math.sin(a)*radius)>>4);
|
||||
if(!candidates.contains(at))candidates.add(at);
|
||||
}
|
||||
for(var chunk:candidates) {
|
||||
// Full graph once per position, never reduced to fit a cavity.
|
||||
for(int floor:new int[]{112,144,80,176,208,192}) {
|
||||
reject("attempts");
|
||||
var start=nativeStart(chunk,floor);
|
||||
if(!start.isValid()) {reject("native-refused");continue;}
|
||||
long houses=start.getPieces().stream().filter(p->p instanceof PoolElementStructurePiece pool && pool.getElement().toString().contains("/houses/")).count();
|
||||
if(houses<6) {reject("too-few-houses");break;}
|
||||
var volumes=new ArrayList<BoundingBox>();
|
||||
for(var piece:start.getPieces()) {
|
||||
var b=piece.getBoundingBox();
|
||||
volumes.add(new BoundingBox(b.minX()-10,Math.min(floor-10,b.minY()-2),b.minZ()-10,b.maxX()+10,Math.max(floor+19,b.maxY()+12),b.maxZ()+10));
|
||||
}
|
||||
var bounds=VillageCavern306.bounds(volumes);
|
||||
if(bounds.getXSpan()>240 || bounds.getZSpan()>240 || bounds.minX()<chunk.getMinBlockX()-128 || bounds.maxX()>chunk.getMaxBlockX()+128
|
||||
|| bounds.minZ()<chunk.getMinBlockZ()-128 || bounds.maxZ()>chunk.getMaxBlockZ()+128) {reject("reference-range");continue;}
|
||||
if(host.intersectsExperimentalStructures(bounds.minX(),bounds.minY(),bounds.minZ(),bounds.maxX(),bounds.maxY(),bounds.maxZ())) {reject("reserved");continue;}
|
||||
boolean outside=false;
|
||||
for(var b:volumes) if(!root.contains(b.minX(),b.minZ()) || !root.contains(b.maxX(),b.maxZ())) {outside=true;break;}
|
||||
if(outside) {reject("outside-island");continue;}
|
||||
var cavity=new VillageCavern306(floor,mix(root.seed()^chunk.pack()),new BlockPos(chunk.getMiddleBlockX(),floor,chunk.getMiddleBlockZ()),volumes);
|
||||
String refusal=certify(cavity);
|
||||
if(refusal!=null) {reject(refusal);continue;}
|
||||
var entrance=access(start,cavity,volumes,chunk);
|
||||
if(entrance==null) {reject("no-access");continue;}
|
||||
cavity=new VillageCavern306(floor,mix(root.seed()^chunk.pack()),entrance,volumes);
|
||||
var completed=cavity.getBoundingBox();
|
||||
if(host.intersectsExperimentalStructures(completed.minX(),completed.minY(),completed.minZ(),completed.maxX(),completed.maxY(),completed.maxZ())
|
||||
|| !root.contains(completed.minX(),completed.minZ()) || !root.contains(completed.maxX(),completed.maxZ())
|
||||
|| completed.minX()<chunk.getMinBlockX()-128 || completed.maxX()>chunk.getMaxBlockX()+128
|
||||
|| completed.minZ()<chunk.getMinBlockZ()-128 || completed.maxZ()>chunk.getMaxBlockZ()+128) {reject("access-reservation");continue;}
|
||||
if(certify(cavity)!=null) {reject("access-terrain");continue;}
|
||||
List<StructurePiece> pieces=new ArrayList<>(); pieces.add(cavity);pieces.addAll(start.getPieces());
|
||||
var complete=new StructureStart(level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(Village306.KEY).value(),chunk,0,new PiecesContainer(pieces));
|
||||
diagnostics.put("houses",(int)houses);diagnostics.put("native-pieces",start.getPieces().size());diagnostics.put("columns",columns.size());
|
||||
return new Village306.Plan(complete,cavity,Map.copyOf(diagnostics));
|
||||
}
|
||||
}
|
||||
reject("no-fitting-site");return new Village306.Plan(null,null,Map.copyOf(diagnostics));
|
||||
}
|
||||
private String certify(VillageCavern306 cavity) {
|
||||
var b=cavity.getBoundingBox(); int count=0,floors=0,roofs=0;
|
||||
for(int z=b.minZ()+2;z<=b.maxZ();z+=6) for(int x=b.minX()+2;x<=b.maxX();x+=6) {
|
||||
int roof=cavity.roofAt(x,z), floor=cavity.floorY(); if(roof<floor+2)continue; count++;
|
||||
if(solid(x,floor-10,z))floors++;
|
||||
boolean covered=false; for(int y=roof+1;y<=roof+14;y++) if(solid(x,y,z)&&solid(x,y+1,z)) {covered=true;break;}
|
||||
if(covered)roofs++;
|
||||
for(int y=floor-10;y<=roof+2;y+=2) if(!column(x,z).getBlock(y).getFluidState().isEmpty())return "fluid";
|
||||
}
|
||||
// The local floor can bridge small cracks; broad void-facing platforms are refused.
|
||||
diagnostics.merge("best-floor-percent",count==0?0:floors*100/count,Math::max);
|
||||
if(count==0 || floors<count*.8)return "thin-foundation";
|
||||
if(roofs<count*.7)return "insufficient-cover";
|
||||
return null;
|
||||
}
|
||||
private BlockPos access(StructureStart start,VillageCavern306 cave,List<BoundingBox> volumes,ChunkPos origin) {
|
||||
int floor=cave.floorY(); var centre=start.getBoundingBox().getCenter();
|
||||
var streets=start.getPieces().stream().filter(p->p instanceof PoolElementStructurePiece pool && pool.getElement().toString().contains("/streets/"))
|
||||
.sorted(Comparator.comparingDouble((StructurePiece p)->p.getBoundingBox().getCenter().distSqr(centre)).reversed()).toList();
|
||||
for(var street:streets) {
|
||||
var at=street.getBoundingBox().getCenter();
|
||||
for(var dir:net.minecraft.core.Direction.Plane.HORIZONTAL) {
|
||||
List<BoundingBox> path=new ArrayList<>();
|
||||
for(int step=0;step<=48;step+=3) {
|
||||
int x=at.getX()+dir.getStepX()*step,z=at.getZ()+dir.getStepZ()*step;
|
||||
if(Math.abs(x-origin.getMiddleBlockX())>116 || Math.abs(z-origin.getMiddleBlockZ())>116 || !root.contains(x,z))break;
|
||||
var box=new BoundingBox(x-8,floor-5,z-8,x+8,floor+12,z+8);
|
||||
if(host.intersectsExperimentalStructures(box.minX(),box.minY(),box.minZ(),box.maxX(),box.maxY(),box.maxZ()))break;
|
||||
if(!solid(x,floor-5,z))break;
|
||||
boolean fluid=false;for(int y=floor-5;y<=floor+6;y++)if(!column(x,z).getBlock(y).getFluidState().isEmpty())fluid=true;
|
||||
if(fluid)break;
|
||||
// Narrow connector volumes must retain the same eight-block shoulder convention.
|
||||
path.add(new BoundingBox(x-8,floor-5,z-8,x+8,floor+12,z+8));
|
||||
if(cave.roofAt(x,z)<0 && column(x,z).getBlock(floor).isAir() && column(x,z).getBlock(floor+2).isAir()) {
|
||||
volumes.addAll(path);return new BlockPos(x,floor,z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Natural openings already intersecting a chamber are useful entrances too.
|
||||
var b=cave.getBoundingBox();
|
||||
for(int z=b.minZ();z<=b.maxZ();z+=4)for(int x=b.minX();x<=b.maxX();x+=4)
|
||||
if(cave.projects(x,z)&&column(x,z).getBlock(floor).isAir()&&column(x,z).getBlock(floor+2).isAir()&&solid(x,floor-5,z))return new BlockPos(x,floor,z);
|
||||
return null;
|
||||
}
|
||||
private StructureStart nativeStart(ChunkPos chunk,int floor) {
|
||||
var registry=level.registryAccess().lookupOrThrow(Registries.STRUCTURE); var ops=RegistryOps.create(JsonOps.INSTANCE,level.registryAccess());
|
||||
var original=registry.getValue(Identifier.withDefaultNamespace("village_plains"));
|
||||
JsonObject json=Structure.DIRECT_CODEC.encodeStart(ops,original).getOrThrow().getAsJsonObject().deepCopy();
|
||||
json.addProperty("start_pool","sanctuary:underground306/zombie_plains_starts");json.addProperty("size",6);json.addProperty("max_distance_from_center",80);
|
||||
json.addProperty("terrain_adaptation","none");json.remove("project_start_to_heightmap");
|
||||
var height=new JsonObject();height.addProperty("absolute",floor);json.add("start_height",height);
|
||||
var structure=Structure.DIRECT_CODEC.parse(ops,json).getOrThrow();var random=host.randomState(root);
|
||||
var proto=new ProtoChunk(chunk,UpgradeData.EMPTY,level,PalettedContainerFactory.create(level.registryAccess()),null);
|
||||
return structure.generate(registry.getOrThrow(Village306.KEY),level.dimension(),level.registryAccess(),new FloorView(host,level,floor),host.getBiomeSource(),
|
||||
random.createClimateSampler(SamplerContext.builder().enableCaches().build()),random,level.getStructureTemplateManager(),root.seed(),chunk,0,proto,b->!b.is(Biomes.DEEP_DARK));
|
||||
}
|
||||
private static final class FloorView extends ExpansionChunkGenerator {
|
||||
final SanctuaryChunkGenerator host;final int floor;
|
||||
FloorView(SanctuaryChunkGenerator host,ServerLevel level,int floor){super(host.source(),host.initialDiameter(),true);this.host=host;this.floor=floor;registries=level.registryAccess();}
|
||||
@Override public int getBaseHeight(int x,int z,Heightmap.Types type,LevelHeightAccessor height,RandomState state){return floor;}
|
||||
@Override public NoiseColumn getBaseColumn(int x,int z,LevelHeightAccessor height,RandomState state){return host.getBaseColumn(x,z,height,state);}
|
||||
@Override public RandomState randomState(ExpansionIsland island){return host.randomState(island);}
|
||||
}
|
||||
private static long mix(long v){v=(v^(v>>>30))*0xbf58476d1ce4e5b9L;v=(v^(v>>>27))*0x94d049bb133111ebL;return v^(v>>>31);}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package fr.koka.sanctuary.worldgen.underground306;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import java.util.Optional;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureType;
|
||||
public final class VillageStructure306 extends Structure {
|
||||
public static final MapCodec<VillageStructure306> CODEC = simpleCodec(VillageStructure306::new);
|
||||
public VillageStructure306(StructureSettings settings) { super(settings); }
|
||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) { return Optional.empty(); }
|
||||
@Override public StructureType<?> type() { return Village306.TYPE; }
|
||||
}
|
||||
@@ -210,6 +210,7 @@
|
||||
"sanctuary.world_options.secrets23_hint": "Natural island, mineral secrets and abandoned railways. Vanilla structures remain possible. Expansions may contain oceans.",
|
||||
"sanctuary.expansion.relief.ocean": "Oceanic continent",
|
||||
"sanctuary.expansion.error.native_structure_unavailable": "No suitable structure found: expansion was not reserved.",
|
||||
"structure.sanctuary.underground_abandoned_village": "Underground abandoned village",
|
||||
"structure.sanctuary.mineral_sanctuary": "Mineral sanctuary",
|
||||
"sanctuary.structures.census.unavailable": "Structure registry unavailable: coverage is unknown.",
|
||||
"sanctuary.structures.census.summary": "Structures: %s starts, %s monster rooms, %s observed chunks.",
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
"sanctuary.world_options.secrets23_hint": "Île naturelle, secrets minéraux et voies abandonnées. Les structures vanilla restent possibles. Les expansions peuvent accueillir des océans.",
|
||||
"sanctuary.expansion.relief.ocean": "Continent océanique",
|
||||
"sanctuary.expansion.error.native_structure_unavailable": "Aucune structure adaptée trouvée : expansion non réservée.",
|
||||
"structure.sanctuary.underground_abandoned_village": "Village abandonné souterrain",
|
||||
"structure.sanctuary.mineral_sanctuary": "Sanctuaire minéral",
|
||||
"sanctuary.structures.census.unavailable": "Registre des structures indisponible : couverture inconnue.",
|
||||
"sanctuary.structures.census.summary": "Structures : %s départs, %s donjons, %s chunks observés.",
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"type": "sanctuary:underground_site_v306",
|
||||
"biomes": "#minecraft:is_overworld",
|
||||
"step": "top_layer_modification",
|
||||
"terrain_adaptation": "none",
|
||||
"spawn_overrides": {}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"elements": [
|
||||
{
|
||||
"element": {
|
||||
"element_type": "minecraft:legacy_single_pool_element",
|
||||
"location": "minecraft:village/plains/zombie/town_centers/plains_fountain_01",
|
||||
"processors": "minecraft:zombie_plains",
|
||||
"projection": "rigid"
|
||||
},
|
||||
"weight": 1
|
||||
},
|
||||
{
|
||||
"element": {
|
||||
"element_type": "minecraft:legacy_single_pool_element",
|
||||
"location": "minecraft:village/plains/zombie/town_centers/plains_meeting_point_1",
|
||||
"processors": "minecraft:zombie_plains",
|
||||
"projection": "rigid"
|
||||
},
|
||||
"weight": 1
|
||||
},
|
||||
{
|
||||
"element": {
|
||||
"element_type": "minecraft:legacy_single_pool_element",
|
||||
"location": "minecraft:village/plains/zombie/town_centers/plains_meeting_point_2",
|
||||
"processors": "minecraft:zombie_plains",
|
||||
"projection": "rigid"
|
||||
},
|
||||
"weight": 1
|
||||
},
|
||||
{
|
||||
"element": {
|
||||
"element_type": "minecraft:legacy_single_pool_element",
|
||||
"location": "minecraft:village/plains/zombie/town_centers/plains_meeting_point_3",
|
||||
"processors": "minecraft:zombie_plains",
|
||||
"projection": "rigid"
|
||||
},
|
||||
"weight": 1
|
||||
}
|
||||
],
|
||||
"fallback": "minecraft:empty"
|
||||
}
|
||||
@@ -33,7 +33,9 @@
|
||||
"StructureCensusMonsterRoomMixin",
|
||||
"RuinedPortal23Accessor",
|
||||
"IglooHeight23Mixin",
|
||||
"ScatteredHeight23Accessor"
|
||||
"ScatteredHeight23Accessor",
|
||||
"UndergroundHeight306Mixin",
|
||||
"UndergroundStart306Mixin"
|
||||
],
|
||||
"client": [
|
||||
"client.SanctuarySkyMixin",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "0.1.0-alpha.30.5"
|
||||
version = "0.1.0-alpha.30.6"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
@@ -20,6 +20,7 @@ def main():
|
||||
parser.add_argument("--rendered", type=Path, required=True)
|
||||
parser.add_argument("--jar", type=Path, required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--extra", type=Path, action="append", default=[])
|
||||
args = parser.parse_args()
|
||||
if args.output.exists():
|
||||
raise SystemExit("Refusing to overwrite an existing immutable atlas")
|
||||
@@ -48,14 +49,20 @@ def main():
|
||||
files["raw/" + source.name] = raw
|
||||
for name in ("maps.png", "maps.pdf", "sections.png", "sections.pdf", "summary.json", "provenance.json"):
|
||||
files[f"figures/{source.stem}/{name}"] = (rendered / name).read_bytes()
|
||||
for name in ("upper-terrain.png", "upper-terrain.pdf"):
|
||||
if (rendered / name).is_file(): files[f"figures/{source.stem}/{name}"] = (rendered / name).read_bytes()
|
||||
cases.append({"seed": data["seed"], "diameter": data["diameter"], "directory": source.stem})
|
||||
identities = {(c["seed"], c["diameter"]) for c in cases}
|
||||
if not cases or len(identities) != len(cases):
|
||||
raise SystemExit("Empty atlas or duplicate seed/size")
|
||||
for name in ("scripts/terrain_atlas.py", "scripts/terrain-atlas-requirements.txt",
|
||||
"scripts/archive_terrain_atlas.py", "docs/terrain-atlas.md",
|
||||
"docs/generation-alpha30.5.md", "docs/testing-alpha30.5.md"):
|
||||
"scripts/structure_atlas.py", f"docs/generation-alpha{version.split('alpha.')[-1]}.md", f"docs/testing-alpha{version.split('alpha.')[-1]}.md"):
|
||||
files[name] = (ROOT / name).read_bytes()
|
||||
for path in args.extra:
|
||||
key = "structures/" + path.name
|
||||
if key in files: raise SystemExit("Duplicate extra atlas file")
|
||||
files[key] = path.read_bytes()
|
||||
rows = ["# Sanctuary — atlas " + version, "", f"Source : `{commit}`.", "",
|
||||
"Mesures natives avant décoration ; voir docs/terrain-atlas.md pour les échelles et limites.", "",
|
||||
"| Diamètre | Graine | Cartes | Coupes |", "|---|---|---|---|"]
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Plot observed village blocks and planned surface rails against the native terrain atlas."""
|
||||
import argparse,json,hashlib
|
||||
from pathlib import Path
|
||||
import matplotlib
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.colors import ListedColormap
|
||||
import numpy as np
|
||||
|
||||
def render(source, terrain, output):
|
||||
data=json.loads(source.read_text()); native=json.loads(terrain.read_text())
|
||||
if not data.get('passed') or data['seed']!=str(native['seed']) or data['diameter']!=native['diameter']:
|
||||
raise ValueError('Mismatched or failed native witnesses')
|
||||
output.mkdir(parents=True,exist_ok=True)
|
||||
name=f"structures-{data['diameter']}-{data['seed']}"
|
||||
columns=np.asarray(native['variants']['current306']['columns'])
|
||||
xs,zs=np.unique(columns[:,0]),np.unique(columns[:,1]);grid=columns.reshape(len(zs),len(xs),5)
|
||||
extent=(xs[0]-6,xs[-1]+6,zs[-1]+6,zs[0]-6)
|
||||
fig,ax=plt.subplots(figsize=(10,10),layout='constrained')
|
||||
img=ax.imshow(np.ma.masked_where(grid[:,:,2]==0,grid[:,:,2]),extent=extent,cmap='terrain',vmin=0,vmax=400,interpolation='nearest')
|
||||
freight=[l for l in data['rail_links'] if l['kind']=='FREIGHT']
|
||||
rail_area=len({(c['x'],c['z']) for c in data['rail_cells'] if c['rail']!='NONE'})
|
||||
ballast=len({(c['x'],c['z']) for c in data['rail_cells']})
|
||||
land=int(np.count_nonzero(grid[:,:,2]))*native['map_step']**2
|
||||
upper=int(np.count_nonzero(grid[:,:,2]>=180))*native['map_step']**2
|
||||
for link in data['rail_links']:
|
||||
points=link['points'];ax.plot([p['x'] for p in points],[p['z'] for p in points],color='#601d15' if link['kind']=='FREIGHT' else '#404040',lw=1.5 if link['kind']=='FREIGHT' else .9)
|
||||
ax.scatter([0],[0],marker='+',s=70,c='black',label='Origine naturelle protégée')
|
||||
ax.plot([],[],color='#601d15',label='Voies ferrées prévues (lacunes réparables)')
|
||||
ax.plot([],[],color='#404040',label='Chemins prévus')
|
||||
ax.set(xlabel='X (blocs)',ylabel='Z (blocs) · nord en haut',aspect='equal')
|
||||
ax.set_title(f"Sanctuary {data['diameter']} · graine {data['seed']}\nRéseau de surface sur le relief natif 30.6")
|
||||
ax.legend(fontsize=9,loc='upper left');fig.colorbar(img,ax=ax,shrink=.72,label='Altitude du terrain (Y)')
|
||||
fig.supxlabel(f"{rail_area} colonnes de voie · {ballast} colonnes de sol aménagé (chemins inclus)\n"
|
||||
f"{100*ballast/land:.2f} % de l’emprise terrestre projetée estimée ; {100*ballast/upper:.2f} % des surfaces ≥ Y180.\n"
|
||||
"Relief échantillonné tous les 12 blocs ; tracés issus du plan, avant placement complet des rails.",fontsize=9)
|
||||
for ext in ['png','pdf']:fig.savefig(output/f'{name}-reseau.{ext}',dpi=160)
|
||||
plt.close(fig)
|
||||
if 'floor_cut' in data:
|
||||
floor=data['floor_cut']; ids=np.array(floor['blocks']); y=floor['y']
|
||||
def category(s):
|
||||
if s in ('minecraft:air','minecraft:cave_air','minecraft:void_air'):return 0
|
||||
if s in ('minecraft:stone','minecraft:dirt','minecraft:granite','minecraft:andesite','minecraft:diorite','minecraft:deepslate','minecraft:tuff'):return 1
|
||||
if 'water' in s:return 4
|
||||
if 'cobweb' in s:return 3
|
||||
return 2
|
||||
pixels=np.vectorize(category)(ids)
|
||||
fig,axes=plt.subplots(1,3,figsize=(18,7),layout='constrained')
|
||||
palette=ListedColormap(['#eef1f0','#666d70','#b98651','#976da6','#489dc8'])
|
||||
axes[0].imshow(pixels,extent=(floor['min_x']-.5,floor['min_x']+ids.shape[1]-.5,floor['min_z']+ids.shape[0]-.5,floor['min_z']-.5),cmap=palette,vmin=0,vmax=4,interpolation='nearest',aspect='equal')
|
||||
axes[0].set_title(f'Coupe horizontale Y{y}\nBrun : constructions / décor ; violet : toiles')
|
||||
axes[0].set(xlabel='X (blocs)',ylabel='Z (blocs)')
|
||||
for ax,cut in zip(axes[1:],data['native_cuts']):
|
||||
blocks=np.asarray(cut['blocks']);step=cut['step']
|
||||
ax.imshow(blocks,extent=(cut['min_x']-1,cut['min_x']+blocks.shape[1]*step-1,-1,301),cmap=ListedColormap(['#eef1f0','#666d70','#489dc8','#ea8542']),vmin=0,vmax=3,interpolation='nearest',aspect='equal')
|
||||
ax.axhline(data['floor'],color='#b98651',lw=.8,ls='--');ax.set(xlabel='X (blocs)',ylabel='Y (blocs)');ax.set_title(f"Coupe verticale Z{cut['z']}\nTrait : sol du village")
|
||||
fig.suptitle(f"Village abandonné · diamètre {data['diameter']} · graine {data['seed']}\nBlocs réellement générés, après structures et décoration",fontsize=15)
|
||||
fig.supxlabel(f"{data['diagnostics']['houses']} maisons natives · {data['chests']} coffres · {data['zombie_villagers']} zombie-villageois\n"
|
||||
"Échantillonnage horizontal : 1 bloc ; vertical : 2 blocs. Aucune interpolation d’affichage.",fontsize=10)
|
||||
for ext in ['png','pdf']:fig.savefig(output/f'{name}-village.{ext}',dpi=160)
|
||||
plt.close(fig)
|
||||
result={'seed':data['seed'],'diameter':data['diameter'],'rail_columns':rail_area,'prepared_columns_including_paths':ballast,'estimated_land_area':land,
|
||||
'ratio_percent':100*ballast/land,'ratio_upper_percent':100*ballast/upper,'structure_source_sha256':hashlib.sha256(source.read_bytes()).hexdigest(),
|
||||
'terrain_source_sha256':hashlib.sha256(terrain.read_bytes()).hexdigest()}
|
||||
(output/f'{name}-summary.json').write_text(json.dumps(result,indent=2)+'\n')
|
||||
print(json.dumps(result))
|
||||
|
||||
if __name__=='__main__':
|
||||
p=argparse.ArgumentParser(description=__doc__);p.add_argument('source',type=Path);p.add_argument('terrain',type=Path);p.add_argument('--output',type=Path,required=True);a=p.parse_args();render(a.source,a.terrain,a.output)
|
||||
@@ -20,6 +20,7 @@ LABELS = {
|
||||
"global301": "Bruit 3D global · 30.1",
|
||||
"stretch304": "Étirement vertical · 30.4",
|
||||
"current305": "Plateaux + déformation 3D · 30.5",
|
||||
"current306": "Relief légèrement renforcé · 30.6",
|
||||
}
|
||||
|
||||
|
||||
@@ -32,8 +33,9 @@ def save(fig, directory, name):
|
||||
def render(source, output):
|
||||
raw = source.read_bytes()
|
||||
data = json.loads(raw)
|
||||
if data["schema"] != 1 or list(data["variants"]) != list(LABELS):
|
||||
if data["schema"] != 1 or len(data["variants"]) != 4 or any(k not in LABELS for k in data["variants"]):
|
||||
raise ValueError(f"Unsupported atlas schema/variants: {source}")
|
||||
labels = {key: LABELS[key] for key in data["variants"]}
|
||||
target = output / source.stem
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
title = f"Sanctuary · diamètre {data['diameter']} · graine {data['seed']}"
|
||||
@@ -51,11 +53,11 @@ def render(source, output):
|
||||
extent = (xs[0] - step / 2, xs[-1] + step / 2, zs[-1] + step / 2, zs[0] - step / 2)
|
||||
fig, axes = plt.subplots(3, 4, figsize=(18, 14), layout="constrained", sharex=True, sharey=True)
|
||||
fig.suptitle(title + "\nRelief et épaisseur de roche", fontsize=18)
|
||||
metrics = [(2, "Altitude du sommet (Y)", "terrain", 0, 360),
|
||||
metrics = [(2, "Altitude du sommet (Y)", "terrain", 0, data["domain_y"][1]),
|
||||
(3, "Épaisseur solide cumulée (blocs)", "viridis", 0, 300),
|
||||
(4, "Roche continue sous le sommet (blocs)", "magma", 0, 100)]
|
||||
for row, (index, label, cmap, vmin, vmax) in enumerate(metrics):
|
||||
for col, (key, grid) in enumerate(zip(LABELS, maps)):
|
||||
for col, (key, grid) in enumerate(zip(labels, maps)):
|
||||
ax = axes[row, col]
|
||||
values = np.ma.masked_where(grid[:, :, 2] == 0, grid[:, :, index])
|
||||
image = ax.imshow(values, extent=extent, origin="upper", interpolation="nearest",
|
||||
@@ -65,7 +67,7 @@ def render(source, output):
|
||||
if col == 0:
|
||||
ax.set_ylabel("Z (blocs) · nord en haut")
|
||||
if row == 0:
|
||||
ax.set_title(LABELS[key], fontsize=11)
|
||||
ax.set_title(labels[key], fontsize=11)
|
||||
ax.plot(0, 0, "+", color="black", ms=8, label="Origine")
|
||||
fig.colorbar(image, ax=axes[row, :], shrink=.85, label=label, extend="max")
|
||||
fig.supxlabel(f"Pas horizontal : {step} blocs · vertical : {data['vertical_step']} blocs · gris = aucun solide échantillonné.\n" + provenance, fontsize=9)
|
||||
@@ -74,7 +76,7 @@ def render(source, output):
|
||||
section_count = len(variants[0]["sections"])
|
||||
fig, axes = plt.subplots(4, section_count, figsize=(20, 12), layout="constrained", sharex=True, sharey=True)
|
||||
fig.suptitle(title + "\nCoupes de densité · proportions spatiales conservées", fontsize=18)
|
||||
for row, (key, variant) in enumerate(zip(LABELS, variants)):
|
||||
for row, (key, variant) in enumerate(zip(labels, variants)):
|
||||
for col, section in enumerate(variant["sections"]):
|
||||
ax = axes[row, col]
|
||||
density = np.asarray(section["density"], dtype=float) / data["density_scale"]
|
||||
@@ -87,17 +89,30 @@ def render(source, output):
|
||||
if row == 0:
|
||||
ax.set_title(f"Plan vertical Z = {section['z']}")
|
||||
if col == 0:
|
||||
ax.set_ylabel(LABELS[key] + "\nY (blocs)", fontsize=10)
|
||||
ax.set_ylabel(labels[key] + "\nY (blocs)", fontsize=10)
|
||||
ax.set_xlabel("X (blocs)")
|
||||
ax.set_ylim(0, 384)
|
||||
ax.set_ylim(0, data["domain_y"][1] + 1)
|
||||
fig.colorbar(image, ax=axes, shrink=.6, label="Densité sans unité · bleu : vide (≤ 0) · rouge : roche (> 0)", extend="both")
|
||||
fig.supxlabel("Coupes échantillonnées tous les 2 blocs, sans interpolation d’affichage ; isosurface zéro en noir.\n" + provenance, fontsize=9)
|
||||
save(fig, target, "sections")
|
||||
|
||||
upper = data["variants"].get("current306", {}).get("upper_sections", [])
|
||||
if upper:
|
||||
fig, axes = plt.subplots(1, len(upper), figsize=(5*len(upper), 6), layout="constrained", squeeze=False)
|
||||
fig.suptitle(title + "\nMorceaux de terrain supérieur · densité native", fontsize=15)
|
||||
for ax, cut in zip(axes[0], upper):
|
||||
values = np.array(cut["solid"])
|
||||
dy=cut["step"]
|
||||
ax.imshow(values, cmap="Greys", vmin=0, vmax=1, interpolation="nearest", aspect="equal",
|
||||
extent=(cut["min_x"]-1,cut["min_x"]+values.shape[1]*dy-1,cut["max_y"]-values.shape[0]*dy+1,cut["max_y"]+1))
|
||||
ax.set_title(f"Z = {cut['z']}"); ax.set_xlabel("X");ax.set_ylabel("Y")
|
||||
fig.supxlabel("Noir : roche · blanc : air. Aucune interpolation d’affichage. Avant végétation et structures.",fontsize=9)
|
||||
save(fig,target,"upper-terrain")
|
||||
|
||||
summary = {"seed": data["seed"], "diameter": data["diameter"], "variants": {}}
|
||||
base = maps[0][:, :, 2]
|
||||
common = np.logical_and.reduce([m[:, :, 2] >= 200 for m in maps])
|
||||
for key, grid in zip(LABELS, maps):
|
||||
for key, grid in zip(labels, maps):
|
||||
land = grid[:, :, 2] > 0
|
||||
uplands = base >= 200
|
||||
summary["variants"][key] = {
|
||||
|
||||
Reference in New Issue
Block a user