Add underground village, branched rails and upper terrain for alpha.30.6
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-12 19:04:57 +02:00
parent 16e480bfd0
commit fbd870a4e3
38 changed files with 1180 additions and 53 deletions
+12
View File
@@ -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 dune cavité locale adaptée ; les mines sont réservées à
lavance et les donjons natifs disposent de davantage de tentatives. Les voies
abandonnées sallongent et se ramifient entre les plateaux.
Deux nouveaux morceaux de terrain naturel sur Petit/Moyen, trois sur Grand,
occupent le ciel vers Y300400. 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 [latlas 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
+97
View File
@@ -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 laccord 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
Lamplitude 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 ny a ni sommet imposé ni ajout de bruit de détail. Latlas 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 dair dessous. Le filtrage des
petits fragments sapplique également à ces hauteurs. La surface projetée
reste minoritaire ; aucun semis de blocs flottants nest ajouté.
Lidée dun lieu exceptionnel vers **Y600** est conservée pour plus tard.
Aucune structure, plateforme ou arène ny 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 nest remplacé, aucun graphe nest 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 lemplacement.
- Le sous-sol doit offrir une majorité dappuis 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 nest 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 dappuis et daltitude existants. Le plan
accepté est celui effectivement placé et localisé. Un échec reste explicite
au journal ; les graines testées ne prouvent pas lensemble des graines possibles.
Les mines restent indépendantes du commutateur expérimental ; loption 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 dun donjon sont nettoyées dans son volume, sans
modifier les plantes valides ni produire de drops. Cela sapplique aussi
aux expansions du générateur courant ; aucune expansion nest 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 nest nécessaire pour conserver une voie.
Larrivée naturelle reste protégée sur 48 blocs autour de lorigine.
Les **68 % évoqués sont un repère visuel, pas un quota imposé**. Latlas des
structures mesure les colonnes aménagées et les superpose au relief. Les voies
sarrêtent lorsquun 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 darrê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 sarrê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 datlas](terrain-atlas.md).
+25 -3
View File
@@ -20,8 +20,8 @@ expansion nest 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 den faire une archive. Aucune sauvegarde personnelle nest utilisée.
représente pas les blocs ou les biomes ; du blanc nimplique pas de neige.
- Altitude : dernier solide rencontré, recherche verticale par pas de 2 blocs.
Une cellule grise signifie quaucun solide na é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 nest ni une densité physique, ni un volume exact de matériaux minables.
- Roche continue : épaisseur depuis le sommet jusquau 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
Lexport 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 nest
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
```
+108
View File
@@ -0,0 +1,108 @@
# Vérifications alpha.30.6
Essais natifs sur **Mac**, Minecraft 26.3-pre-2, Java 25. Aucun essai Windows
natif nest revendiqué. Un **nouveau monde** est nécessaire.
## Atlas final
Neuf couples : Petit 512 / Moyen 724 / Grand 1024 × graines 0, 42 et
-7228211907433324401. Le serveur dexport plat passe ses deux tests ;
lacquisition 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**.
Laugmentation du domaine daffichage à Y420 ajoute seulement de lair 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 dexpansion ; ses corridors et leurs éléments sont inspectés.
93 salles vanilla `monster_room` ont été observées dans lensemble de cette
passe : ce nest pas le nombre total de donjons de l’île ni une garantie par graine.
Ce témoin complet précède lultime sculpture des morceaux de terrain aérien.
La génération sous Y300 na 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 dun 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
nest pas une mesure du temps douverture dun 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 lemprise terrestre projetée estimée (0,96 % au-dessus de Y180),
et non les 68 % é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
Lassemblage 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.
Lassemblage 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`.
Larchive fige les mesures, les images et lempreinte du JAR de cette livraison.
Les pourcentages de voies sont des mesures du plan ; aucun trajet en minecart
sur lintégralité du réseau nest 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 nest implémentée.
+2 -2
View File
@@ -8,6 +8,6 @@ loader_version=0.19.5
loom_version=1.17.20
fabric_api_version=0.160.0+26.3
mod_version=0.1.0-alpha.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
+5 -2
View File
@@ -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') }
}
}
@@ -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");
@@ -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;
@@ -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));
}
@@ -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()
@@ -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));
}
}
@@ -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);
}
}
@@ -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() {
@@ -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())
@@ -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;
@@ -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() {}
}
@@ -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);
}
}
}
@@ -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);}
}
@@ -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.",
@@ -0,0 +1,7 @@
{
"type": "sanctuary:underground_site_v306",
"biomes": "#minecraft:is_overworld",
"step": "top_layer_modification",
"terrain_adaptation": "none",
"spawn_overrides": {}
}
@@ -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
View File
@@ -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]
+8 -1
View File
@@ -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 |", "|---|---|---|---|"]
+70
View File
@@ -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 lemprise 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 daffichage.",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)
+23 -8
View File
@@ -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 daffichage ; 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 daffichage. 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] = {