Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1490fb2a80 | ||
|
|
9a65e96061 | ||
|
|
d5cd895403 |
@@ -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.7 — trésors aériens et vestiges asséchés
|
||||
|
||||
Le relief de 30.6 est conservé. Les nouveaux morceaux de terrain aérien reçoivent
|
||||
les minerais et trésors des anciens îlots artificiels, retirés de la planification.
|
||||
Les rails privilégient les lignes directes. Une seconde traversée souterraine et
|
||||
quelques petites ruines océaniques asséchées sont cherchées lorsque le terrain
|
||||
les accueille.
|
||||
|
||||
Voir [la génération](docs/generation-alpha30.7.md) et [les essais](docs/testing-alpha30.7.md).
|
||||
**Nouveau monde requis.** [MRpack et release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.30.7) ·
|
||||
[Atlas scientifique](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.7/Sanctuary-Atlas-0.1.0-alpha.30.7.zip).
|
||||
|
||||
## 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
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Alpha.30.7 — dernières installations de la génération
|
||||
|
||||
Cette passe conserve le relief et les morceaux de terrain naturel de l’alpha.30.6.
|
||||
Les fonctions de densité, cavités, rivières, biomes et tailles ne changent pas.
|
||||
Minecraft reste en 26.3-pre-2 ; plafond à Y640, Moyen 724 par défaut.
|
||||
Créer un nouveau monde pour voir la nouvelle répartition des installations.
|
||||
|
||||
## Trésors dans le terrain aérien
|
||||
|
||||
Les deux anciens îlots minéraux construits comme des structures ne sont plus
|
||||
planifiés dans les nouveaux mondes. Les deux morceaux naturels de Petit et Moyen,
|
||||
ou les trois de Grand, portent désormais chacun un coffre enterré et des filons.
|
||||
Le coffre utilise la table de richesses aériennes existante, uniquement vanilla.
|
||||
Aucun objet de quête n’est ajouté.
|
||||
|
||||
L’enrichissement remplace seulement la roche existante entre Y300 et Y399 :
|
||||
charbon, cuivre et fer dans les couches supérieures ; or, lapis, redstone, diamant
|
||||
et émeraude plus à l’intérieur. La profondeur est mesurée dans chaque colonne de
|
||||
l’îlot. Les réserves sont finies, sans renouvellement après minage ou pillage.
|
||||
Le profil est volontairement très riche ; les comptages par minerai figurent
|
||||
dans les rapports aériens de l’atlas. La forme, le sol et la végétation de ces morceaux naturels restent ceux de 30.6.
|
||||
L’ancienne cascade artificielle disparaît avec son ancien îlot.
|
||||
|
||||
Les deux navires restaurés restent près de leurs côtes. Les nouveaux trésors
|
||||
possèdent des départs natifs, des pièces sauvegardées et des réservations limitées
|
||||
à leurs emprises. Ils utilisent l’identifiant de localisation
|
||||
`sanctuary:aerial_mineral_islet`. Le terrain aérien naturel existe également quand
|
||||
les structures sont désactivées ; ses coffres et son enrichissement expérimental
|
||||
suivent les options de structures.
|
||||
|
||||
## Rails et traversées
|
||||
|
||||
Chaque prolongement ferroviaire garde son axe. Un terrain incompatible termine
|
||||
le parcours au lieu de provoquer une succession de virages. Les embranchements
|
||||
partent des longues lignes principales ; les extensions successives ne produisent
|
||||
plus de branches tournantes récursives. Une séparation évite les lignes presque
|
||||
parallèles autour d’une même voie. Cela peut réduire la longueur totale du réseau :
|
||||
les 6–8 % anciennement évoqués ne constituent pas un quota.
|
||||
|
||||
Après la première traversée souterraine, une seconde recherche est effectuée dans
|
||||
l’espace restant. Elle respecte les mêmes règles de sol, couverture et accès,
|
||||
avec une séparation de 24 blocs autour des modules de la première. Si elle ne
|
||||
tient pas dans le terrain et le budget de recherche, la première reste conservée.
|
||||
Chaque traversée possède ses deux accès ; aucun croisement aveugle n’est creusé
|
||||
pour forcer la seconde.
|
||||
|
||||
## Ruines d’un ancien fond marin
|
||||
|
||||
`sanctuary:drained_ocean_ruin` place quelques petits templates vanilla de ruines
|
||||
océaniques chaudes dans les cavités sèches existantes entre Y60 et Y174. Trois
|
||||
emplacements sont recherchés, quatre en Grand, sans garantie de quantité. Les
|
||||
refus de sol, dégagement ou réservation sont journalisés. Aucun nouveau grand
|
||||
volume souterrain n’est creusé pour les accueillir.
|
||||
|
||||
Les modèles vanilla habités (`warm_2`, `warm_3`, `warm_4`, `warm_5`, `warm_8`),
|
||||
leur dégradation et leurs tables de butin sont conservés.
|
||||
Les marqueurs d’habitants placent des zombies vanilla persistants ; des coraux
|
||||
morts complètent les vestiges et sont posés avec `waterlogged=false`. Les blocs
|
||||
gorgés d’eau sont asséchés. La protection contre la décoration ultérieure reste
|
||||
limitée à la petite emprise de chaque ruine. Les pièces
|
||||
sauvegardées conservent leur altitude souterraine au lieu de recalculer un fond
|
||||
océanique lors du rechargement. Ces ruines Sanctuary ont leur propre identifiant ;
|
||||
elles ne remplacent pas les ruines aquatiques des expansions océaniques.
|
||||
|
||||
Voir [les vérifications](testing-alpha30.7.md). La structure envisagée à Y600 reste
|
||||
une idée ; cette livraison n’ajoute rien à cette hauteur.
|
||||
@@ -13,6 +13,46 @@ JAR Sanctuary sont des pièces jointes de releases Gitea. Aucun JAR de mod ou
|
||||
dépendance téléchargée n'entre dans l'historique Git. Le canal Beta est distinct
|
||||
de l'ancien pack 26.2.
|
||||
|
||||
## Alpha.30.7 publiée — 12 septembre 2026
|
||||
|
||||
La [release alpha.30.7](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.30.7)
|
||||
est publiée depuis `9a65e960614d22f751018aec3698ec7d4abefb34`, branche
|
||||
`codex/finishing-worldgen-alpha30-7`. Les anciens îlots artificiels sont remplacés
|
||||
par les trésors et filons des corps naturels de 30.6. Rails directs, seconde
|
||||
traversée conditionnelle et petites ruines océaniques asséchées dans les cavités.
|
||||
**Nouveau monde requis.** Aucun lieu ajouté à Y600.
|
||||
|
||||
Les neuf relevés natifs de densité sont exactement identiques à ceux de 30.6.
|
||||
`check build` et `assemblePack` réussissent. Les tests finaux ciblés vérifient
|
||||
Moyen / 0 dans les vrais chunks : deux traversées, deux îlots enrichis, deux
|
||||
navires avec habitants, le village, la mine et trois ruines sèches. Le Grand /
|
||||
-7228211907433324401 possède trois îlots enrichis et une traversée conservée,
|
||||
la seconde étant refusée faute d’accès ; ses quatre ruines sèches passent la
|
||||
validation dédiée finale. Voir [la portée exacte des essais](testing-alpha30.7.md).
|
||||
|
||||
Artefacts publics vérifiés par empreintes :
|
||||
|
||||
- [MRpack alpha.30.7](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.7/Sanctuary-0.1.0-alpha.30.7.mrpack).
|
||||
- [Atlas complet : cartes, coupes et données natives](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.7/Sanctuary-Atlas-0.1.0-alpha.30.7.zip).
|
||||
- [Installations du Moyen](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.7/Sanctuary-0.1.0-alpha.30.7-Moyen-installations.png) et [du Grand](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.7/Sanctuary-0.1.0-alpha.30.7-Grand-installations.png).
|
||||
- [Coupes du Grand](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.7/Sanctuary-0.1.0-alpha.30.7-Grand-coupes.png) et [cartes du Grand](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.7/Sanctuary-0.1.0-alpha.30.7-Grand-cartes.png).
|
||||
|
||||
Canal packwiz vérifié : `82a61692862fbd304891daeeee605240c5324525`.
|
||||
Deux synchronisations isolées puis deux dans **la même instance Sanctuary Beta
|
||||
sur Mac**, Minecraft fermé, réussissent. Les **925 fichiers personnels et
|
||||
réglages** suivis conservent leurs empreintes ; aucun monde personnel n’a été
|
||||
ouvert, converti ou régénéré. Le dossier demeure `Sanctuary-0.1.0-alpha.1`.
|
||||
Sauvegarde ciblée : `sanctuary-backups/before-0.1.0-alpha.30.7/`.
|
||||
Minecraft 26.3-pre-2, Loader 0.19.5, Fabric API 0.160.0+26.3 sont inchangés.
|
||||
|
||||
SHA-256 JAR : `9b8be381c2bbf894be813ca7c6473418e5110063d1e3ec8b1ba3bda568220e53`.
|
||||
SHA-256 MRpack : `5912c33557c24bc67d85c7f5f0df291bbd3aa87a971db7b33017d340adc18f63`.
|
||||
SHA-256 atlas : `997779ae13d4784551d3be4defe988c3f8991c9e0f6569926e16b77b4420f38d`.
|
||||
Reçus ignorés : `build/alpha307-delivery-public.json`,
|
||||
`build/alpha307-isolated-validation.json`, `build/alpha307-prism-validation.json`,
|
||||
`build/alpha307-atlas-archive.json` et `build/alpha307-atlas-public.json`.
|
||||
Aucun essai ni déploiement Windows natif n’est revendiqué.
|
||||
|
||||
## Alpha.30.6 publiée — 12 septembre 2026
|
||||
|
||||
La [release alpha.30.6](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.30.6)
|
||||
|
||||
@@ -100,3 +100,29 @@ build/atlas-venv/bin/python scripts/structure_atlas.py \
|
||||
mods/sanctuary/build/run/gameTest/diagnostics/atlas306/terrain-724-0.json \
|
||||
--output build/atlas306/structures
|
||||
```
|
||||
|
||||
## Finition 30.7
|
||||
|
||||
Le terrain de 30.6 est conservé. L’export natif est répété sur les neuf couples
|
||||
512 / 724 / 1 024 × 0 / 42 / -7228211907433324401 pour comparer exactement les
|
||||
colonnes et les coupes avant décoration. L’enrichissement des minerais et les
|
||||
petites structures n’appartiennent pas à ce champ de densité.
|
||||
|
||||
`scripts/finishing_atlas.py` superpose les rails planifiés, les axes des traversées
|
||||
parcourues dans les blocs, les ruines asséchées et les réservations aériennes.
|
||||
Les rectangles représentent les emprises ; ils ne représentent pas des îlots
|
||||
rectangulaires. Le script refuse des rapports de graines ou tailles différentes
|
||||
et des témoins natifs non réussis.
|
||||
|
||||
```sh
|
||||
build/atlas-venv/bin/python scripts/finishing_atlas.py \
|
||||
--terrain build/atlas307/raw/terrain-724-0.json \
|
||||
--village CHEMIN_DU_RAPPORT_VILLAGE \
|
||||
--transit CHEMIN_DU_RAPPORT_TRANSIT \
|
||||
--aerial CHEMIN_DU_RAPPORT_AERIEN \
|
||||
--ruins CHEMIN_DU_RAPPORT_RUINES --output build/atlas307/installations
|
||||
```
|
||||
|
||||
Les preuves, cartes PNG/PDF et leurs empreintes sont ajoutées à l’archive de
|
||||
livraison. Ces cartes sont des vues de diagnostic opérateur, pas des cartes
|
||||
automatiquement révélées aux joueurs.
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# Vérifications alpha.30.7
|
||||
|
||||
Essais automatisés sur macOS, Java 25, Minecraft 26.3-pre-2. Aucun essai Windows
|
||||
ni visite visuelle interactive de l’alpha.30.7 n’a été réalisé. Les mondes des
|
||||
fixtures restent dans le dossier de développement ignoré.
|
||||
|
||||
## Témoins dans de vrais chunks
|
||||
|
||||
| Témoin | Terrain aérien enrichi | Navires / villageois | Traversées praticables | Village abandonné | Mines |
|
||||
|---|---:|---:|---|---|---:|
|
||||
| Moyen 724, graine 0 | 2 corps, 2 coffres | 2 / 2 | 2, portées 272 et 204 blocs | 15 maisons, 99 pièces, 10 zombie-villageois | 1 |
|
||||
| Grand 1 024, graine -7228211907433324401 | 3 corps, 3 coffres | 2 / 2 | 1, portée 374 blocs | 12 maisons, 70 pièces, 9 zombie-villageois | 2 |
|
||||
|
||||
La seconde traversée du Grand est refusée : aucun accès ne satisfait le contrôle
|
||||
civil. La première reste générée et parcourable. « Deux si la place le permet »
|
||||
ne signifie donc pas deux dans chaque monde.
|
||||
|
||||
Les fixtures aériennes contrôlent les départs natifs, les coffres enterrés,
|
||||
les huit minerais dans chaque corps, la présence des deux villageois, puis
|
||||
pillent les coffres, minent un témoin et déplacent les habitants dans leur monde
|
||||
jetable. Les fixtures de traversée vérifient les blocs de circulation de chaque
|
||||
réseau séparément, ses accès et ses parois. Le village conserve ses pièces
|
||||
Jigsaw, ses coffres et ses habitants. Les rails sont mesurés comme plans et
|
||||
ballast : aucune circulation de wagonnet n’est certifiée sur les voies abandonnées.
|
||||
|
||||
## Ruines asséchées
|
||||
|
||||
Sur Moyen / 0 : trois ruines réellement posées, trois coffres, trois zombies
|
||||
persistants et neuf coraux morts. Le contrôle après ticks vérifie l’absence
|
||||
d’eau dans les emprises, les blocs vanilla et le roundtrip NBT des départs.
|
||||
|
||||
Sur Grand / -7228211907433324401 : quatre ruines réellement posées, quatre
|
||||
coffres, cinq zombies persistants et douze coraux morts. La passe dédiée réussit
|
||||
en 3 min 21 s, dont 15 s d’exécution des deux tests après préparation du monde.
|
||||
|
||||
Le test a révélé que les coraux morts ont un état `waterlogged=true` par défaut.
|
||||
Ils sont désormais explicitement posés secs. La correction est locale à la ruine ;
|
||||
aucun filtre étendu ne supprime les eaux ou la décoration des grottes voisines.
|
||||
|
||||
Les sauvegardes de pièces et les marqueurs d’habitants sont contrôlés par
|
||||
sérialisation native. Une reprise complète dans un nouveau processus JVM après
|
||||
pillage et démolition n’a pas été rejouée pour cette livraison ; elle ne doit pas
|
||||
être confondue avec ce roundtrip NBT.
|
||||
|
||||
## Commandes et portée
|
||||
|
||||
Une première exécution complète de `check build` a réussi avec 26 GameTests.
|
||||
Après la correction finale des coraux, les contrôles purs et les cinq GameTests
|
||||
ciblant les comportements modifiés ont tous réussi (5 min 47 s) :
|
||||
|
||||
```sh
|
||||
./gradlew check build \
|
||||
-PsanctuaryFocusedTests=aerial,transit,village,drained \
|
||||
-PsanctuaryAerial24Tests=true -PsanctuaryTransit29Tests=true
|
||||
```
|
||||
|
||||
Le sélecteur `sanctuaryFocusedTests` limite seulement les fixtures natives ;
|
||||
les contrôles de `check` restent exécutés. Sans ce paramètre, la suite native
|
||||
complète conserve son fonctionnement habituel.
|
||||
|
||||
Les témoins Grand pour les îlots, navires, traversée et village proviennent de
|
||||
la passe précédente de cette même livraison. Seuls la pose sèche des coraux et
|
||||
les contrôles locaux des ruines ont ensuite changé ; la validation dédiée des
|
||||
ruines Grand a réussi séparément avec la correction finale.
|
||||
|
||||
Les durées ci-dessus sont celles des fixtures (préparation et lecture de milliers
|
||||
de blocs incluses), pas une mesure de FPS ni une promesse de durée de chargement.
|
||||
Les neuf graines/tailles de l’atlas ne constituent pas neuf visites complètes de
|
||||
structures. Les plafonds, reliefs et corps flottants restent ceux de 30.6.
|
||||
|
||||
## Atlas et intégrité du terrain
|
||||
|
||||
L’export natif final (3 min 42 s) a réussi pour les neuf couples
|
||||
512 / 724 / 1 024 × graines 0 / 42 / -7228211907433324401.
|
||||
Toutes les colonnes et coupes des quatre variantes sont **exactement égales** à
|
||||
l’export 30.6, avec les mêmes coordonnées et résolutions. La comparaison porte
|
||||
sur les tableaux de densité avant décoration, pas sur les matériaux enrichis ni
|
||||
sur les installations. Les empreintes des deux jeux de données sont archivées
|
||||
avec le rapport `density-306-307-comparison.json`.
|
||||
|
||||
`./gradlew assemblePack -x :sanctuary:check` a réussi après le `check build` final.
|
||||
Le MRpack a été vérifié : version, dépendance exacte, tailles, empreintes, CRC,
|
||||
absence de sauvegardes personnelles et absence de second mécanisme de mise à jour.
|
||||
|
||||
## Livraison
|
||||
|
||||
Créer un **nouveau monde Sanctuary**. Le codec de prototype `sanctuary:island_v24`
|
||||
est conservé ; aucune migration ni régénération des sauvegardes n’est exécutée.
|
||||
Les réservations aériennes déjà enregistrées dans les anciens mondes sont gardées.
|
||||
Le pack reste sur Minecraft 26.3-pre-2, Fabric Loader 0.19.5 et Fabric API
|
||||
0.160.0+26.3. La réception publique et Prism est consignée dans `docs/packwiz.md`.
|
||||
+2
-2
@@ -8,6 +8,6 @@ loader_version=0.19.5
|
||||
loom_version=1.17.20
|
||||
fabric_api_version=0.160.0+26.3
|
||||
|
||||
mod_version=0.1.0-alpha.30.6
|
||||
pack_version=0.1.0-alpha.30.6
|
||||
mod_version=0.1.0-alpha.30.7
|
||||
pack_version=0.1.0-alpha.30.7
|
||||
maven_group=fr.koka.sanctuary
|
||||
|
||||
@@ -89,6 +89,11 @@ tasks.named('runGameTest') {
|
||||
|
||||
tasks.named('processGametestResources') {
|
||||
def villageOnly = providers.gradleProperty('sanctuaryVillageOnly').map { it.toBoolean() }.getOrElse(false)
|
||||
def focused = providers.gradleProperty('sanctuaryFocusedTests').getOrElse('').split(',').findAll { !it.isEmpty() }
|
||||
def focusClasses = [aerial: 'Aerial24WorldGameTests', transit: 'Transit29WorldGameTests', village: 'Village306GameTests', drained: 'Drained307GameTests']
|
||||
focused.each { if (!focusClasses.containsKey(it)) throw new GradleException('Unknown focused native test: ' + it) }
|
||||
inputs.property('focusedNative', focused)
|
||||
|
||||
inputs.property('villageOnly', villageOnly)
|
||||
def atlasOnly = providers.gradleProperty('sanctuaryAtlasOnly').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('atlasOnly', atlasOnly)
|
||||
@@ -140,6 +145,10 @@ tasks.named('processGametestResources') {
|
||||
: unified ? 'fr.koka.sanctuary.gametest.UnifiedWorldGameTests'
|
||||
: 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests')
|
||||
if (atlasOnly || villageOnly) filter { line -> line.replaceAll(/("fabric-gametest": \["[^"]+")[^\]]*/, '$1') }
|
||||
if (!focused.isEmpty()) filter { line ->
|
||||
line.contains('"fabric-gametest":') ? ' "fabric-gametest": [' + focused.collect { '"fr.koka.sanctuary.gametest.' + focusClasses[it] + '"' }.join(', ') + '],' : line
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+21
-9
@@ -69,9 +69,9 @@ public final class Aerial24WorldGameTests {
|
||||
helper.assertTrue(StructureCensusService.aerialSites(level).isEmpty(), "Disabled aerial sites are not presented as planned");
|
||||
report.put("passed", true); write(Path.of("diagnostics/aerial24/off.json"), report); helper.succeed(); return;
|
||||
}
|
||||
helper.assertTrue(plan.sites().size() == 4, "All four initial aerial sites have a finite saved plan");
|
||||
helper.assertTrue(plan.sites().size() == (generator.initialDiameter()==1024?5:4), "Both ships and all natural floating bodies have a saved plan");
|
||||
helper.assertTrue(plan.sites().stream().filter(AerialPlan24.Site::merchant).count() == 2, "Two merchant ships");
|
||||
helper.assertTrue(plan.sites().stream().filter(AerialPlan24.Site::waterfall).count() == 1, "One waterfall site");
|
||||
helper.assertTrue(plan.sites().stream().filter(AerialPlan24.Site::waterfall).count() == 0, "No artificial mineral-islet waterfall remains");
|
||||
if (generator.initialDiameter() == 724 && (level.getSeed() == 0 || level.getSeed() == 42))
|
||||
AerialRenderer24.writeDiagnostic(Path.of("diagnostics/aerial24/blocks.json"), plan.sites());
|
||||
report.put("geometry", JSON.toJsonTree(plan.sites())); report.put("claim_count", plan.claimsChunks().size());
|
||||
@@ -121,6 +121,9 @@ public final class Aerial24WorldGameTests {
|
||||
if (!tickets[0]) {
|
||||
entityChunks.forEach(pos -> level.getChunkSource().addTicketWithRadius(TicketType.DRAGON, pos, 2)); tickets[0] = true;
|
||||
}
|
||||
// Other simultaneous fixtures may release a shared vanilla DRAGON ticket.
|
||||
// Refresh our own still-required chunks until the lifecycle inspection completes.
|
||||
entityChunks.forEach(pos -> level.getChunkSource().addTicketWithRadius(TicketType.DRAGON, pos, 2));
|
||||
var occupants = occupants(level, plan);
|
||||
var readiness = new TreeMap<String, Object>(); boolean entitiesReady = true;
|
||||
for (var pos : entityChunks) {
|
||||
@@ -171,7 +174,7 @@ public final class Aerial24WorldGameTests {
|
||||
report.put("native_tags", tags); report.put("full_chunks", chunks.size());
|
||||
report.put("blocks_after", countSites(level, plan));
|
||||
var census = StructureCensusService.aerialSites(level);
|
||||
helper.assertTrue(census.size() == 4 && census.stream().allMatch(site -> site.status() == StructureCensusService.AerialStatus.GENERATED),
|
||||
helper.assertTrue(census.size() == plan.sites().size() && census.stream().allMatch(site -> site.status() == StructureCensusService.AerialStatus.GENERATED),
|
||||
"Four historical generated sites remain indexed after loot, mining and demolition");
|
||||
report.put("census_after", census);
|
||||
helper.assertTrue(equalFiles(journalBefore, files(world.resolve("data").resolve(generator.journalDirectory()))), "Aerial tests never reserve or activate an expansion");
|
||||
@@ -197,7 +200,7 @@ public final class Aerial24WorldGameTests {
|
||||
double gap = AerialPlanner24.shoreDistance(site);
|
||||
helper.assertTrue(gap >= 16 && gap <= 48, "Native ship is just off its verified shore");
|
||||
helper.assertTrue(Math.abs(AerialGeometry24.entry(site).getY() - site.shoreY()) <= 4, "Boarding deck follows the nearby shore height");
|
||||
} else {
|
||||
} else if(site.kind()==AerialPlan24.Kind.MINERAL_ISLET) {
|
||||
helper.assertTrue(root.ownsChunk(site.startChunkX(), site.startChunkZ()), "Mineral islet floats above Sanctuary terrain");
|
||||
var column = generator.aerialBaseColumn24(site.shoreX(), site.shoreZ());
|
||||
helper.assertTrue(column.getBlock(site.shoreY() - 1).isSolid(), "Overhead islet retains a real ground witness below it");
|
||||
@@ -275,12 +278,12 @@ public final class Aerial24WorldGameTests {
|
||||
helper.assertTrue(AerialRenderer24.loot(site).equals(chest.getLootTable()), "Aerial chest retains its unopened site-specific loot table");
|
||||
for (var entry : model.entrySet()) if (!entry.getValue().isAir() && !entry.getValue().is(Blocks.WATER))
|
||||
helper.assertTrue(level.getBlockState(entry.getKey()).is(entry.getValue().getBlock()), "Native FULL keeps authored material at " + site.id() + " " + entry.getKey());
|
||||
for (var pos : AerialGeometry24.interiorChecks(site)) {
|
||||
for (var pos : site.kind()==AerialPlan24.Kind.NATURAL_ISLET ? List.<BlockPos>of() : AerialGeometry24.interiorChecks(site)) {
|
||||
helper.assertTrue(level.getBlockState(pos).getCollisionShape(level, pos).isEmpty()
|
||||
&& level.getBlockState(pos.above()).getCollisionShape(level, pos.above()).isEmpty(), "Usable two-block headroom at " + site.id());
|
||||
helper.assertTrue(!level.getBlockState(pos.below()).getCollisionShape(level, pos.below()).isEmpty(), "Interior stands on a real floor");
|
||||
}
|
||||
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET) for (int dy = 1; dy <= 3; dy++)
|
||||
if (!site.merchant()) for (int dy = 1; dy <= 3; dy++)
|
||||
helper.assertTrue(level.getBlockState(chestAt.above(dy)).isSolid(), "Mineral chest needs excavation through three layers");
|
||||
} else helper.assertTrue(chest.getLootTable() == null && chest.isEmpty(), "Looted chest never refills on reload");
|
||||
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET)
|
||||
@@ -299,9 +302,14 @@ public final class Aerial24WorldGameTests {
|
||||
exterior.add(Map.of("position", coordinates(at), "observed_block", level.getBlockState(at).toString()));
|
||||
report.put(site.id() + "_exterior_observations", exterior);
|
||||
}
|
||||
helper.assertTrue(chestCount == 4 && waterSources == 1, "Four physical chests and exactly one planned source");
|
||||
helper.assertTrue(chestCount == plan.sites().size() && waterSources == 0, "One physical chest per site and no artificial waterfall");
|
||||
report.put("chests", chestCount); report.put("waterfall_sources", waterSources); report.put("blocks_before", countSites(level, plan));
|
||||
report.put("vegetation", vegetation);
|
||||
if(fresh) for(var site:plan.sites()) if(site.kind()==AerialPlan24.Kind.NATURAL_ISLET) {
|
||||
var blocks=countSites(level,plan).get(site.id());
|
||||
for(String id:List.of("coal","iron","copper","gold","lapis","redstone","diamond","emerald"))
|
||||
helper.assertTrue(blocks.getOrDefault("minecraft:"+id+"_ore",0)>0,"Finite "+id+" veins in "+site.id());
|
||||
}
|
||||
return expected;
|
||||
}
|
||||
|
||||
@@ -359,8 +367,12 @@ public final class Aerial24WorldGameTests {
|
||||
for (int slot = 0; slot < chest.getContainerSize(); slot++) quantity += chest.getItem(slot).getCount();
|
||||
helper.assertTrue(quantity > 0, "Site-specific chest produces usable vanilla loot"); loot.put(site.id(), quantity);
|
||||
chest.clearContent(); chest.setChanged(); emptied.add(coordinates(at));
|
||||
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET) {
|
||||
var ores = models.get(site.id()).entrySet().stream().filter(entry -> ore(entry.getValue())).sorted(Map.Entry.comparingByKey(
|
||||
if (!site.merchant()) {
|
||||
var body = new HashMap<>(models.get(site.id()));
|
||||
if(site.kind()==AerialPlan24.Kind.NATURAL_ISLET) for(var pos:BlockPos.betweenClosed(site.minX(),site.minY(),site.minZ(),site.maxX(),site.maxY(),site.maxZ())) {
|
||||
var state=level.getBlockState(pos); if(ore(state)) body.put(pos.immutable(),state);
|
||||
}
|
||||
var ores = body.entrySet().stream().filter(entry -> ore(entry.getValue())).sorted(Map.Entry.comparingByKey(
|
||||
Comparator.<BlockPos>comparingInt(BlockPos::getX).thenComparingInt(BlockPos::getY).thenComparingInt(BlockPos::getZ))).limit(16).toList();
|
||||
helper.assertTrue(!ores.isEmpty(), "Each mineral island contains actual finite veins");
|
||||
for (var entry : ores) { level.setBlock(entry.getKey(), Blocks.AIR.defaultBlockState(), 2); removed.add(coordinates(entry.getKey())); }
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
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 Drained307GameTests {
|
||||
@GameTest(maxTicks=12000)
|
||||
public void nativeRuinsRemainDryAndKeepMarkers(GameTestHelper helper) throws Exception {
|
||||
var level=helper.getLevel();var host=(SanctuaryChunkGenerator)level.getChunkSource().getGenerator();
|
||||
if(host.rootGeneration()!=24){helper.succeed();return;}
|
||||
var plans=host.dryRuins307();
|
||||
if(!level.structureManager().shouldGenerateStructures()||!host.experimentalStructures()) {
|
||||
helper.assertTrue(plans.isEmpty(),"Disabled structures reserve no dry ruins");helper.succeed();return;
|
||||
}
|
||||
var report=new LinkedHashMap<String,Object>();report.put("seed",Long.toString(level.getSeed()));report.put("diameter",host.initialDiameter());
|
||||
report.put("planned",plans.size());report.put("bounds",plans.stream().map(s->s.getBoundingBox().toString()).toList());
|
||||
Path out=Path.of("diagnostics/drained307",host.initialDiameter()+"-"+level.getSeed()+".json");Files.createDirectories(out.getParent());
|
||||
Files.writeString(out,new GsonBuilder().setPrettyPrinting().create().toJson(report));
|
||||
if(Boolean.getBoolean("sanctuary.test.village306.planOnly")) {helper.succeed();return;}
|
||||
var chunks=new LinkedHashSet<net.minecraft.world.level.ChunkPos>();plans.forEach(s->s.getBoundingBox().intersectingChunks().forEach(chunks::add));
|
||||
var iterator=chunks.iterator();Runnable[] step=new Runnable[1];boolean[] tickets={false};int[] wait={0};
|
||||
step[0]=()->{try {
|
||||
if(iterator.hasNext()) {var cp=iterator.next();level.getChunk(cp.x(),cp.z(),ChunkStatus.FULL,true);helper.runAtTickTime(helper.getTick()+1,()->step[0].run());return;}
|
||||
if(!tickets[0]){chunks.forEach(cp->level.getChunkSource().addTicketWithRadius(net.minecraft.server.level.TicketType.DRAGON,cp,2));tickets[0]=true;}
|
||||
if(wait[0]++<40){helper.runAtTickTime(helper.getTick()+1,()->step[0].run());return;}
|
||||
var counts=new TreeMap<String,Integer>();var saved=new ArrayList<String>();int zombies=0,chests=0,corals=0;
|
||||
var ctx=StructurePieceSerializationContext.fromLevel(level);
|
||||
for(var planned:plans) {
|
||||
var cp=planned.getChunkPos();var actual=level.getChunk(cp.x(),cp.z()).getStartForStructure(planned.getStructure());
|
||||
helper.assertTrue(actual!=null&&actual.isValid(),"Dry ruin has a native start");
|
||||
var tag=actual.createTag(ctx,cp);var restored=StructureStart.loadStaticStart(ctx,tag,level.getSeed());
|
||||
helper.assertTrue(restored!=null&&tag.equals(restored.createTag(ctx,cp)),"Ruin floor, orientation, seed and occupant flags survive native serialization");
|
||||
saved.add(tag.toString());var box=planned.getBoundingBox();
|
||||
for(var at:BlockPos.betweenClosed(box.minX(),box.minY(),box.minZ(),box.maxX(),box.maxY(),box.maxZ())) {
|
||||
var state=level.getBlockState(at);var id=BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString();
|
||||
if(!state.isAir())counts.merge(id,1,Integer::sum);
|
||||
helper.assertTrue(state.getFluidState().isEmpty(),"Drained ruin has no water or waterlogged block at "+at);
|
||||
if(id.contains("dead_")&&id.contains("coral"))corals++;
|
||||
if(level.getBlockEntity(at) instanceof net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity chest) {
|
||||
chests++;helper.assertTrue(net.minecraft.world.level.storage.loot.BuiltInLootTables.UNDERWATER_RUIN_SMALL.equals(chest.getLootTable()),"Native ocean loot retained");
|
||||
}
|
||||
}
|
||||
var inhabitants=level.getEntitiesOfClass(net.minecraft.world.entity.monster.zombie.Zombie.class,net.minecraft.world.phys.AABB.of(box).inflate(8));
|
||||
zombies+=inhabitants.stream().filter(z->z.isPersistenceRequired()).count();
|
||||
}
|
||||
report.put("blocks",counts);report.put("chests",chests);report.put("dead_corals",corals);report.put("persistent_zombies",zombies);report.put("saved_starts",saved);
|
||||
if(!plans.isEmpty()) {helper.assertTrue(chests>0,"Native treasure marker placed");helper.assertTrue(corals>0,"Dead coral accents present");helper.assertTrue(zombies>0,"Persistent vanilla zombie occupants present");}
|
||||
report.put("passed",true);Files.writeString(out,new GsonBuilder().setPrettyPrinting().create().toJson(report));
|
||||
chunks.forEach(cp->level.getChunkSource().removeTicketWithRadius(net.minecraft.server.level.TicketType.DRAGON,cp,2));helper.succeed();
|
||||
}catch(Exception e){throw new IllegalStateException(e);}};
|
||||
helper.runAtTickTime(helper.getTick()+1,()->step[0].run());
|
||||
}
|
||||
}
|
||||
+16
-5
@@ -39,9 +39,9 @@ public final class Transit29WorldGameTests {
|
||||
}
|
||||
helper.assertFalse(plan.isEmpty(), "A structures-enabled Sanctuary world must retain its main traversal: " + plan.diagnostic());
|
||||
var accesses = plan.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.ACCESS).toList();
|
||||
helper.assertTrue(accesses.size() == 2, "The complete traversal has two independent surface accesses");
|
||||
int span = Math.max(Math.abs(accesses.getFirst().centerX() - accesses.getLast().centerX()),
|
||||
Math.abs(accesses.getFirst().centerZ() - accesses.getLast().centerZ()));
|
||||
helper.assertTrue(List.of(2,4).contains(accesses.size()), "The complete traversal has two independent surface accesses");
|
||||
int span = Math.max(Math.abs(accesses.getFirst().centerX() - accesses.get(1).centerX()),
|
||||
Math.abs(accesses.getFirst().centerZ() - accesses.get(1).centerZ()));
|
||||
helper.assertTrue(span >= generator.initialDiameter() / 4, "The main route spans at least a quarter of the island diameter");
|
||||
report.put("span", span);
|
||||
var holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(TransitStructures21.KEY);
|
||||
@@ -72,7 +72,7 @@ public final class Transit29WorldGameTests {
|
||||
for (int z = module.minZ() - 3; z < module.minZ(); z++) chunks.add(new ChunkPos(x >> 4, z >> 4));
|
||||
}
|
||||
}
|
||||
helper.assertTrue(chunks.size() <= 160, "The native traversal witness stays inside its bounded footprint");
|
||||
helper.assertTrue(chunks.size() <= 320, "The native traversal witness stays inside its bounded footprint");
|
||||
report.put("local_starts", starts.size()); report.put("planned_footprint_chunks", chunks.size());
|
||||
boolean planOnly = Boolean.getBoolean("sanctuary.test.transit29.planOnly");
|
||||
report.put("plan_only", planOnly);
|
||||
@@ -107,7 +107,18 @@ public final class Transit29WorldGameTests {
|
||||
}
|
||||
report.put("closed_wall_checks", closedWallChecks);
|
||||
report.put("module_biomes", biomes);
|
||||
report.put("walk", Transit21WorldGameTests.verifyAccess(helper, plan));
|
||||
var walks=new ArrayList<Object>();
|
||||
var remaining=new ArrayList<>(plan.modules());
|
||||
while(!remaining.isEmpty()) {
|
||||
var group=new ArrayList<TransitPlan21.Module>();group.add(remaining.removeFirst());
|
||||
boolean grew;
|
||||
do {grew=false;for(var candidate:List.copyOf(remaining)) if(group.stream().anyMatch(m->
|
||||
m.minX()<=candidate.maxX()+1&&m.maxX()+1>=candidate.minX()&&m.minZ()<=candidate.maxZ()+1&&m.maxZ()+1>=candidate.minZ())) {
|
||||
group.add(candidate);remaining.remove(candidate);grew=true;
|
||||
}}while(grew);
|
||||
walks.add(Transit21WorldGameTests.verifyAccess(helper,new TransitPlan21(group,0,"accepted29 native component")));
|
||||
}
|
||||
report.put("walk",walks);
|
||||
var nearest = TransitStructures21.findNearest(level, HolderSet.direct(holder), net.minecraft.core.BlockPos.ZERO, 128, false);
|
||||
helper.assertTrue(nearest != null && accesses.stream().anyMatch(m -> TransitPiece21.locator(m).equals(nearest.getFirst())),
|
||||
"The real structure locator returns one of the walkable surface gates");
|
||||
|
||||
@@ -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.Village306GameTests", "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.Drained307GameTests", "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"],
|
||||
|
||||
@@ -80,6 +80,7 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
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.drained307.DryOcean307.register();
|
||||
fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.register();
|
||||
fr.koka.sanctuary.worldgen.transit.TransitStructures21.register();
|
||||
fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.register();
|
||||
|
||||
@@ -138,6 +138,12 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
|
||||
if (existing == null || !existing.isValid()) structures.setStartForStructure(planned.getStructure(), planned, chunk);
|
||||
}
|
||||
}
|
||||
if (this instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator host && structures.shouldGenerateStructures() && Level.OVERWORLD.equals(dimension)) {
|
||||
for (var planned : host.dryRuins307()) 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) {
|
||||
|
||||
@@ -554,6 +554,7 @@ public final class ExpansionStructures23 {
|
||||
if (generator.village306().start() != null) {
|
||||
var village = generator.village306().start().getChunkPos(); addPosition(positions, village.x(), village.z());
|
||||
}
|
||||
generator.dryRuins307().forEach(s -> addPosition(positions, s.getChunkPos().x(), s.getChunkPos().z()));
|
||||
var mineral = generator.mineralSanctuary23();
|
||||
if (!mineral.isEmpty()) addPosition(positions, mineral.chamber().startChunkX(), mineral.chamber().startChunkZ());
|
||||
if (generator.rootGeneration() == 24) generator.aerialPlan24().sites()
|
||||
|
||||
+13
@@ -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.drained307.DryOcean307.isPlacing()) {
|
||||
if (!fr.koka.sanctuary.worldgen.drained307.DryOcean307.mayWrite(pos)) callback.setReturnValue(false);
|
||||
return;
|
||||
}
|
||||
if (fr.koka.sanctuary.worldgen.underground306.Village306.isPlacing()) {
|
||||
if (!fr.koka.sanctuary.worldgen.underground306.Village306.mayWrite(pos)) callback.setReturnValue(false);
|
||||
return;
|
||||
@@ -48,6 +52,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.drained307.DryOcean307.isPlacing()) {
|
||||
if (!fr.koka.sanctuary.worldgen.drained307.DryOcean307.mayWrite(pos)) callback.setReturnValue(false);
|
||||
return;
|
||||
}
|
||||
if (fr.koka.sanctuary.worldgen.underground306.Village306.isPlacing()) {
|
||||
if (!fr.koka.sanctuary.worldgen.underground306.Village306.mayWrite(pos)) callback.setReturnValue(false);
|
||||
return;
|
||||
@@ -56,6 +64,11 @@ public abstract class UnifiedDecorationProtectionMixin {
|
||||
if (!fr.koka.sanctuary.worldgen.aerial.AerialStructures24.mayWrite(pos)) callback.setReturnValue(false);
|
||||
return;
|
||||
}
|
||||
// Native vegetation patches may call setBlock without ensureCanWrite, including
|
||||
// from a neighbouring chunk after this ruin was placed. Preserve its dry footprint.
|
||||
if (generator.protectsDryRuin307(pos)) {
|
||||
callback.setReturnValue(false); return;
|
||||
}
|
||||
if ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) {
|
||||
if (fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.isPlacing()) return;
|
||||
var island = generator.islandAt(pos.getX(), pos.getZ());
|
||||
|
||||
+23
-1
@@ -104,6 +104,16 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
prepareHeritage(root);
|
||||
prepareTransit(root);
|
||||
}
|
||||
public java.util.List<FloatingTerrain306.Mass> naturalMasses306(ServerLevel level) {
|
||||
int players=initialDiameter()==512?5:initialDiameter()==724?10:20;
|
||||
var field=level.registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.DENSITY_FUNCTION)
|
||||
.getOrThrow(net.minecraft.resources.ResourceKey.create(net.minecraft.core.registries.Registries.DENSITY_FUNCTION,
|
||||
SanctuaryMod.id("population_"+players))).value();
|
||||
var sampler=originContext.random().getSampler(field);
|
||||
if(sampler instanceof DetachedRock302 detached) sampler=detached.input();
|
||||
if(!(sampler instanceof FloatingTerrain306 floating)) throw new IllegalStateException("Missing natural upper terrain sampler");
|
||||
return floating.masses(net.minecraft.world.level.levelgen.densityfunction.SamplerContext.builder().enableCaches().build());
|
||||
}
|
||||
public net.minecraft.world.level.NoiseColumn aerialBaseColumn24(int x, int z) {
|
||||
var current = originContext;
|
||||
if (current == null || heightAccessor == null) throw new IllegalStateException("Aerial sampling before world attachment");
|
||||
@@ -140,6 +150,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
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 boolean dryRuinsPrepared307;
|
||||
private volatile java.util.List<net.minecraft.world.level.levelgen.structure.StructureStart> dryRuins307 = java.util.List.of();
|
||||
public java.util.List<net.minecraft.world.level.levelgen.structure.StructureStart> dryRuins307() { return dryRuins307; }
|
||||
public boolean protectsDryRuin307(net.minecraft.core.BlockPos pos) {
|
||||
return dryRuins307.stream().anyMatch(s -> s.getBoundingBox().isInside(pos));
|
||||
}
|
||||
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; }
|
||||
@@ -196,6 +212,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
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 ruin : dryRuins307) if (ruin.getBoundingBox().intersects(new net.minecraft.world.level.levelgen.structure.BoundingBox(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()) {
|
||||
@@ -263,6 +280,10 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
minesPrepared306 = true;
|
||||
starterMines306 = fr.koka.sanctuary.expansion.ExpansionStructures23.starterMines306(level,this,islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow());
|
||||
}
|
||||
if (!dryRuinsPrepared307 && generation == 24 && cityStructuresEnabled && experimentalStructures) {
|
||||
dryRuinsPrepared307 = true;
|
||||
dryRuins307 = fr.koka.sanctuary.worldgen.drained307.DryOcean307.plan(level, this);
|
||||
}
|
||||
}
|
||||
@Override public void setIslands(List<ExpansionIsland> islands) {
|
||||
// Complete and publish city geometry before workers can observe the new owner list.
|
||||
@@ -372,7 +393,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()) || heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ()));
|
||||
long start = System.nanoTime();
|
||||
transitPlan21 = generation == 24
|
||||
? fr.koka.sanctuary.worldgen.transit.TransitPlanner29.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
|
||||
? fr.koka.sanctuary.worldgen.transit.TransitPlanner29.plan307(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
|
||||
: generation >= 22
|
||||
? fr.koka.sanctuary.worldgen.transit.TransitPlanner22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
|
||||
: fr.koka.sanctuary.worldgen.transit.TransitPlanner21.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
|
||||
@@ -570,6 +591,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;
|
||||
for (var ruin : dryRuins307) if (ruin.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())
|
||||
|
||||
@@ -15,6 +15,7 @@ public final class AerialGeometry24 {
|
||||
public static BlockPos chest(AerialPlan24.Site s) {
|
||||
return switch (s.kind()) {
|
||||
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> world(s, 7, 4, 24);
|
||||
case NATURAL_ISLET -> world(s, s.width()/2, s.height()/2, s.depth()/2);
|
||||
case MINERAL_ISLET -> {
|
||||
int x = s.width() / 2 + 4, z = s.depth() / 2 + 2;
|
||||
yield world(s, x, terrainTop(s, x, z) - 3, z);
|
||||
@@ -37,6 +38,7 @@ public final class AerialGeometry24 {
|
||||
public static BlockPos entry(AerialPlan24.Site s) {
|
||||
return switch (s.kind()) {
|
||||
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> world(s, 3, 5, 13);
|
||||
case NATURAL_ISLET -> chest(s);
|
||||
case MINERAL_ISLET -> world(s, s.width() / 2, terrainTop(s, s.width() / 2, s.depth() / 2) + 1, s.depth() / 2);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ public final class AerialPiece24 extends StructurePiece {
|
||||
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
|
||||
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
|
||||
AerialStructures24.placing(site, () -> {
|
||||
if(site.kind()==AerialPlan24.Kind.NATURAL_ISLET) NaturalIslet307.enrich(level,site,clip);
|
||||
AerialRenderer24.render(site, new LostCityRenderer.BlockWriter() {
|
||||
private BlockPos position(int x, int y, int z) { return new BlockPos(site.minX() + x, site.minY() + y, site.minZ() + z); }
|
||||
@Override public void set(int x, int y, int z, BlockState state) {
|
||||
|
||||
@@ -19,16 +19,19 @@ public final class AerialPlan24 {
|
||||
private final List<Site> sites;
|
||||
private final Set<Long> claims;
|
||||
|
||||
public enum Kind { MERCHANT_PROVISIONS, MERCHANT_TOOLS, MINERAL_ISLET }
|
||||
public enum Kind { MERCHANT_PROVISIONS, MERCHANT_TOOLS, MINERAL_ISLET, NATURAL_ISLET }
|
||||
|
||||
public AerialPlan24(List<Site> sites) {
|
||||
this.sites = List.copyOf(sites);
|
||||
if (!sites.isEmpty() && (sites.size() != 4
|
||||
|| sites.stream().filter(s -> s.kind() == Kind.MERCHANT_PROVISIONS).count() != 1
|
||||
|| sites.stream().filter(s -> s.kind() == Kind.MERCHANT_TOOLS).count() != 1
|
||||
|| sites.stream().filter(s -> s.kind() == Kind.MINERAL_ISLET).count() != 2
|
||||
|| sites.stream().filter(Site::waterfall).count() != 1))
|
||||
throw new IllegalArgumentException("An aerial plan contains all four sites and exactly one waterfall, or is disabled");
|
||||
boolean legacy = sites.size()==4 && sites.stream().filter(s -> s.kind()==Kind.MINERAL_ISLET).count()==2
|
||||
&& sites.stream().filter(Site::waterfall).count()==1;
|
||||
boolean natural = (sites.size()==4 || sites.size()==5)
|
||||
&& sites.stream().filter(s -> s.kind()==Kind.NATURAL_ISLET).count()==sites.size()-2
|
||||
&& sites.stream().noneMatch(Site::waterfall);
|
||||
if (!sites.isEmpty() && (!(legacy || natural)
|
||||
|| sites.stream().filter(s -> s.kind()==Kind.MERCHANT_PROVISIONS).count()!=1
|
||||
|| sites.stream().filter(s -> s.kind()==Kind.MERCHANT_TOOLS).count()!=1))
|
||||
throw new IllegalArgumentException("Incomplete aerial sites");
|
||||
var ids = new HashSet<String>();
|
||||
var columns = new HashSet<Long>();
|
||||
for (Site site : sites) {
|
||||
@@ -55,7 +58,8 @@ public final class AerialPlan24 {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public boolean protects(BlockPos pos) { return sites.stream().anyMatch(s -> s.mayWrite(pos)); }
|
||||
public boolean protects(BlockPos pos) { return sites.stream().anyMatch(s -> s.kind()==Kind.NATURAL_ISLET
|
||||
? AerialGeometry24.chest(s).distManhattan(pos)<=2 : s.mayWrite(pos)); }
|
||||
|
||||
/** Width and depth are local dimensions; facing rotates them around the world-space bounding-box minimum. */
|
||||
public record Site(String id, Kind kind, int minX, int minY, int minZ, int width, int height, int depth,
|
||||
@@ -64,8 +68,8 @@ public final class AerialPlan24 {
|
||||
if (id == null || !id.matches("[a-z][a-z0-9_]{0,47}") || kind == null || facing < 0 || facing > 3
|
||||
|| Math.abs((long) minX) > 2048 || Math.abs((long) minZ) > 2048
|
||||
|| Math.abs((long) shoreX) > 1024 || Math.abs((long) shoreZ) > 1024
|
||||
|| shoreY < 1 || shoreY > 383 || minY < 32 || height < 8 || height > 35 + VEGETATION_HEADROOM || minY + height > 350
|
||||
|| width < 7 || width > 64 || depth < 7 || depth > 64
|
||||
|| shoreY < 1 || shoreY > 383 || minY < 32 || height < 8 || height > (kind==Kind.NATURAL_ISLET ? 110 : 35 + VEGETATION_HEADROOM) || minY + height > (kind==Kind.NATURAL_ISLET ? 430 : 350)
|
||||
|| width < 7 || width > (kind==Kind.NATURAL_ISLET ? 136 : 64) || depth < 7 || depth > (kind==Kind.NATURAL_ISLET ? 136 : 64)
|
||||
|| waterfall && kind != Kind.MINERAL_ISLET)
|
||||
throw new IllegalArgumentException("Invalid aerial site geometry");
|
||||
if (kind == Kind.MINERAL_ISLET && (width < 40 || depth < 40 || height < 20 + VEGETATION_HEADROOM)
|
||||
|
||||
+26
-2
@@ -38,10 +38,14 @@ public final class AerialPlanner24 {
|
||||
@Override public boolean reserved(int x, int z, int minY, int maxY) {
|
||||
return generator.intersectsExperimentalStructures(x, minY, z, x, maxY, z);
|
||||
}
|
||||
});
|
||||
}, generator.naturalMasses306(level));
|
||||
}
|
||||
|
||||
public static AerialPlan24 plan(long seed, int diameter, Terrain terrain) {
|
||||
return plan(seed,diameter,terrain,null);
|
||||
}
|
||||
private static AerialPlan24 plan(long seed,int diameter,Terrain terrain,
|
||||
java.util.List<fr.koka.sanctuary.worldgen.FloatingTerrain306.Mass> natural) {
|
||||
if (diameter != 512 && diameter != 724 && diameter != 1024) throw new IllegalArgumentException("Unsupported aerial root size");
|
||||
int radius = diameter == 724 ? 395 : diameter / 2 + 32;
|
||||
var probe = new Probe(terrain, radius);
|
||||
@@ -50,7 +54,7 @@ public final class AerialPlanner24 {
|
||||
Kind[] kinds = {Kind.MERCHANT_PROVISIONS, Kind.MERCHANT_TOOLS, Kind.MINERAL_ISLET, Kind.MINERAL_ISLET};
|
||||
String[] ids = {"merchant_provisions", "merchant_tools", "mineral_islet_water", "mineral_islet_dry"};
|
||||
double initialAngle = unit(mix(seed ^ 0xA24AE21AL)) * Math.PI * 2;
|
||||
for (int ordinal = 0; ordinal < kinds.length; ordinal++) {
|
||||
for (int ordinal = 0; ordinal < (natural==null ? kinds.length : 2); ordinal++) {
|
||||
long siteSeed = ExpansionIsland.deriveSeed(seed ^ 0xA24AE21AL, ids[ordinal]);
|
||||
boolean merchant = ordinal < 2;
|
||||
int width = merchant ? 11 : 40 + 4 * (int) Math.floorMod(siteSeed, 7);
|
||||
@@ -153,6 +157,26 @@ public final class AerialPlanner24 {
|
||||
sites.add(accepted);
|
||||
accepted.claimedChunks().forEach(c -> claimed.add(c.pack()));
|
||||
}
|
||||
if(natural!=null) {
|
||||
if(natural.size()!=(diameter==1024?3:2)) throw new IllegalStateException("Missing natural upper terrain");
|
||||
for(int i=0;i<natural.size();i++) {
|
||||
var m=natural.get(i);
|
||||
var coast=shore(probe,Math.atan2(m.z(),m.x()),radius);
|
||||
if(coast==null) throw new IllegalStateException("No shore witness for natural islet");
|
||||
int rx=(int)Math.ceil(m.rx()*1.8),rz=(int)Math.ceil(m.rz()*1.8),half=(int)Math.ceil(m.depth()*1.5);
|
||||
var site=new Site("natural_islet_"+i,Kind.NATURAL_ISLET,m.x()-rx,m.y()-half,m.z()-rz,
|
||||
rx*2+1,half*2+1,rz*2+1,0,ExpansionIsland.deriveSeed(seed,"natural_islet_"+i),
|
||||
coast.x,coast.y,coast.z,false);
|
||||
// A chest replaces one existing rock block; the surrounding body must already exist.
|
||||
var chest=AerialGeometry24.chest(site);
|
||||
for(int dx=-1;dx<=1;dx++)for(int dz=-1;dz<=1;dz++)for(int dy=-1;dy<=2;dy++)
|
||||
if(!probe.column(chest.getX()+dx,chest.getZ()+dz).getBlock(chest.getY()+dy).isSolid())
|
||||
throw new IllegalStateException("Natural treasure has no stable rock cover: "+site.id());
|
||||
if(site.claimedChunks().stream().anyMatch(c->claimed.contains(c.pack())))
|
||||
throw new IllegalStateException("Natural treasure reservation overlaps another aerial site");
|
||||
sites.add(site);site.claimedChunks().forEach(c->claimed.add(c.pack()));
|
||||
}
|
||||
}
|
||||
var result = new AerialPlan24(sites);
|
||||
System.getLogger(AerialPlanner24.class.getName()).log(System.Logger.Level.INFO,
|
||||
"Aerial24 seed=" + seed + " diameter=" + diameter + " sites=" + result.sites().size()
|
||||
|
||||
+3
-2
@@ -60,7 +60,7 @@ public final class AerialRenderer24 {
|
||||
return Map.copyOf(blocks);
|
||||
}
|
||||
public static ResourceKey<LootTable> loot(AerialPlan24.Site s) {
|
||||
return ResourceKey.create(Registries.LOOT_TABLE, SanctuaryMod.id("chests/aerial24/" + s.kind().name().toLowerCase(java.util.Locale.ROOT)));
|
||||
return ResourceKey.create(Registries.LOOT_TABLE, SanctuaryMod.id("chests/aerial24/" + (s.kind()==AerialPlan24.Kind.NATURAL_ISLET ? "mineral_islet" : s.kind().name().toLowerCase(java.util.Locale.ROOT))));
|
||||
}
|
||||
public static void render(AerialPlan24.Site s, LostCityRenderer.BlockWriter out) { render(s, out, s.bounds()); }
|
||||
public static void render(AerialPlan24.Site s, LostCityRenderer.BlockWriter out, BoundingBox clip) {
|
||||
@@ -74,12 +74,13 @@ public final class AerialRenderer24 {
|
||||
if (clip.isInside(s.minX() + x, s.minY() + y, s.minZ() + z)) out.chest(x, y, z, table, seed);
|
||||
}
|
||||
};
|
||||
var connected = s.kind() == AerialPlan24.Kind.MINERAL_ISLET ? null : ConnectedBlocks20.buffer(clipped);
|
||||
var connected = !s.merchant() ? null : ConnectedBlocks20.buffer(clipped);
|
||||
var destination = connected == null ? clipped : connected;
|
||||
var b = new Builder(s, destination, connected == null ? clip : s.bounds());
|
||||
switch (s.kind()) {
|
||||
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> b.ship();
|
||||
case MINERAL_ISLET -> b.islet();
|
||||
case NATURAL_ISLET -> { /* Existing density owns the whole natural silhouette. */ }
|
||||
}
|
||||
var chest = AerialGeometry24.chest(s);
|
||||
destination.chest(chest.getX() - s.minX(), chest.getY() - s.minY(), chest.getZ() - s.minZ(), loot(s), s.seed() ^ 0x24C4E57L);
|
||||
|
||||
+2
-2
@@ -36,10 +36,10 @@ public final class AerialStructures24 {
|
||||
private static ResourceKey<Structure> key(String id) { return ResourceKey.create(Registries.STRUCTURE, SanctuaryMod.id(id)); }
|
||||
public static void register() {}
|
||||
public static String family(AerialPlan24.Site site) {
|
||||
return switch (site.kind()) { case MINERAL_ISLET -> "mineral_islet"; default -> "merchant_ship"; };
|
||||
return switch (site.kind()) { case MINERAL_ISLET, NATURAL_ISLET -> "mineral_islet"; default -> "merchant_ship"; };
|
||||
}
|
||||
public static ResourceKey<Structure> key(AerialPlan24.Site site) {
|
||||
return switch (site.kind()) { case MINERAL_ISLET -> MINERAL_ISLET; default -> MERCHANT_SHIP; };
|
||||
return switch (site.kind()) { case MINERAL_ISLET, NATURAL_ISLET -> MINERAL_ISLET; default -> MERCHANT_SHIP; };
|
||||
}
|
||||
public static String structureId(AerialPlan24.Site site) { return key(site).identifier().toString(); }
|
||||
public static boolean isPlacing() { return PLACING.get() != null; }
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package fr.koka.sanctuary.worldgen.aerial;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
|
||||
/** Finite seams replace only the existing rock. The accepted30.6 density is untouched. */
|
||||
public final class NaturalIslet307 {
|
||||
private NaturalIslet307() {}
|
||||
public static void enrich(WorldGenLevel level,AerialPlan24.Site site,BoundingBox clip) {
|
||||
for(int x=Math.max(site.minX(),clip.minX());x<=Math.min(site.maxX(),clip.maxX());x++)
|
||||
for(int z=Math.max(site.minZ(),clip.minZ());z<=Math.min(site.maxZ(),clip.maxZ());z++) {
|
||||
int top=-1,bottom=-1;
|
||||
for(int y=Math.min(399,site.maxY());y>=Math.max(300,site.minY());y--) {
|
||||
var state=level.getBlockState(new BlockPos(x,y,z));
|
||||
if(state.is(BlockTags.BASE_STONE_OVERWORLD)) {if(top<0)top=y;bottom=y;}
|
||||
}
|
||||
if(top<0)continue;
|
||||
for(int y=bottom;y<=top;y++) {
|
||||
var at=new BlockPos(x,y,z);var state=level.getBlockState(at);
|
||||
if(!state.is(BlockTags.BASE_STONE_OVERWORLD))continue;
|
||||
var ore=ore(site.seed(),x,y,z,(top-y)/(double)Math.max(1,top-bottom));
|
||||
if(ore!=null)level.setBlock(at,ore.defaultBlockState(),2);
|
||||
}
|
||||
}
|
||||
}
|
||||
static Block ore(long seed,int x,int y,int z,double depth) {
|
||||
long cell=AerialGeometry24.mix(seed ^ ((long)Math.floorDiv(x,4)<<42)
|
||||
^ ((long)Math.floorDiv(z,4)<<21) ^ Math.floorDiv(y,3));
|
||||
int roll=(int)Math.floorMod(cell,100);
|
||||
if(roll>=37)return null;
|
||||
if(depth<.27)return roll<15?Blocks.COAL_ORE:roll<27?Blocks.COPPER_ORE:Blocks.IRON_ORE;
|
||||
if(depth<.60)return roll<13?Blocks.IRON_ORE:roll<21?Blocks.GOLD_ORE:roll<29?Blocks.LAPIS_ORE:Blocks.COAL_ORE;
|
||||
return roll<10?Blocks.IRON_ORE:roll<19?Blocks.REDSTONE_ORE:roll<27?Blocks.GOLD_ORE:roll<34?Blocks.DIAMOND_ORE:Blocks.EMERALD_ORE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package fr.koka.sanctuary.worldgen.drained307;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.util.*;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.registries.*;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.*;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.levelgen.structure.*;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.*;
|
||||
|
||||
/** Sparse dry remnants on existing lower cave floors; no new cavern or ocean density. */
|
||||
public final class DryOcean307 {
|
||||
public static final ResourceKey<Structure> KEY=ResourceKey.create(Registries.STRUCTURE,SanctuaryMod.id("drained_ocean_ruin"));
|
||||
public static final StructureType<DryOceanStructure307> TYPE=Registry.register(BuiltInRegistries.STRUCTURE_TYPE,
|
||||
SanctuaryMod.id("drained_ocean_v307"),()->DryOceanStructure307.CODEC);
|
||||
public static final StructurePieceType PIECE=Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
|
||||
SanctuaryMod.id("drained_ocean_piece_v307"),DryOceanPiece307::new);
|
||||
private static final ThreadLocal<BoundingBox> PLACING=new ThreadLocal<>();
|
||||
private DryOcean307(){}
|
||||
public static void register(){}
|
||||
public static boolean isPlacing(){return PLACING.get()!=null;}
|
||||
public static boolean mayWrite(BlockPos pos){return PLACING.get()!=null&&PLACING.get().isInside(pos);}
|
||||
static void placing(BoundingBox box,Runnable action){var old=PLACING.get();PLACING.set(box);try{action.run();}finally{if(old==null)PLACING.remove();else PLACING.set(old);}}
|
||||
public static List<StructureStart> plan(ServerLevel level,SanctuaryChunkGenerator host) {
|
||||
var result=new ArrayList<StructureStart>();var columns=new HashMap<Long,NoiseColumn>();var reasons=new TreeMap<String,Integer>();
|
||||
var random=new Random(level.getSeed() ^ 0x3070CEA1L);
|
||||
java.util.function.BiFunction<Integer,Integer,NoiseColumn> column=(x,z)->{
|
||||
long key=BlockPos.asLong(x,0,z);var c=columns.get(key);
|
||||
if(c==null){if(columns.size()>=8000)throw new Budget();c=host.aerialBaseColumn24(x,z);columns.put(key,c);}return c;
|
||||
};
|
||||
int target=host.initialDiameter()==1024?4:3;
|
||||
try {
|
||||
outer:for(int attempt=0;attempt<512 && result.size()<target;attempt++) {
|
||||
double a=random.nextDouble()*Math.PI*2,r=host.initialDiameter()*(.10+random.nextDouble()*.27);
|
||||
int x=(int)(Math.cos(a)*r),z=(int)(Math.sin(a)*r);
|
||||
if(result.stream().anyMatch(s->Math.hypot(s.getBoundingBox().getCenter().getX()-x,s.getBoundingBox().getCenter().getZ()-z)<48))continue;
|
||||
var center=column.apply(x,z);
|
||||
for(int floor=60;floor<=174;floor++) {
|
||||
if(!center.getBlock(floor-1).isSolid()||!center.getBlock(floor).isAir()||!center.getBlock(floor+6).isAir())continue;
|
||||
String template="underwater_ruin/warm_"+new int[]{2,3,4,5,8}[random.nextInt(5)];
|
||||
var piece=new DryOceanPiece307(level.getStructureTemplateManager(),template,new BlockPos(x,floor,z),
|
||||
Rotation.values()[random.nextInt(4)],random.nextLong());
|
||||
var box=piece.getBoundingBox();
|
||||
if(host.intersectsExperimentalStructures(box.minX()-1,box.minY()-2,box.minZ()-1,box.maxX()+1,box.maxY()+2,box.maxZ()+1)) {
|
||||
reasons.merge("reserved",1,Integer::sum);continue;
|
||||
}
|
||||
int support=0,total=0;boolean fits=true;
|
||||
for(int px=box.minX();fits&&px<=box.maxX();px++)for(int pz=box.minZ();fits&&pz<=box.maxZ();pz++) {
|
||||
var c=column.apply(px,pz);total++;
|
||||
if(c.getBlock(floor-1).isSolid()&&c.getBlock(floor-2).isSolid())support++;
|
||||
for(int py=floor;py<=box.maxY()+1;py++)
|
||||
if(!c.getBlock(py).isAir()||host.protectsDecoration(new BlockPos(px,py,pz))) {fits=false;break;}
|
||||
}
|
||||
if(!fits||support*100<total*90){reasons.merge("cave-floor-or-clearance",1,Integer::sum);continue;}
|
||||
var cp=new ChunkPos(x>>4,z>>4);
|
||||
if(result.stream().anyMatch(s->s.getChunkPos().equals(cp)))continue;
|
||||
result.add(new StructureStart(level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY).value(),
|
||||
cp,0,new PiecesContainer(List.of(piece))));
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
}catch(Budget exhausted){reasons.merge("column-budget",1,Integer::sum);}
|
||||
SanctuaryMod.LOGGER.info("Drained307 seed {}: {} / {} dry ruins, {} columns; {}",level.getSeed(),result.size(),target,columns.size(),reasons);
|
||||
return List.copyOf(result);
|
||||
}
|
||||
private static final class Budget extends RuntimeException {}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package fr.koka.sanctuary.worldgen.drained307;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.util.valueproviders.ConstantInt;
|
||||
import net.minecraft.world.entity.EntitySpawnReason;
|
||||
import net.minecraft.world.entity.EntityTypes;
|
||||
import net.minecraft.world.level.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.entity.ChestBlockEntity;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.structure.*;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.*;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.rule.blockentity.AppendLoot;
|
||||
import net.minecraft.world.level.storage.loot.BuiltInLootTables;
|
||||
|
||||
/** Native ocean-ruin templates at a certified cave floor, with dry markers and native loot. */
|
||||
public final class DryOceanPiece307 extends TemplateStructurePiece {
|
||||
private final long seed;
|
||||
private final Set<Long> occupants=new HashSet<>();
|
||||
public DryOceanPiece307(StructureTemplateManager manager,String template,BlockPos at,Rotation rotation,long seed) {
|
||||
super(DryOcean307.PIECE,0,manager,Identifier.withDefaultNamespace(template),template,settings(rotation),at);
|
||||
this.seed=seed;
|
||||
}
|
||||
public DryOceanPiece307(StructurePieceSerializationContext context,CompoundTag tag) {
|
||||
super(DryOcean307.PIECE,tag,context.structureTemplateManager(),ignored->settings(tag.read("Rot",Rotation.LEGACY_CODEC).orElseThrow()));
|
||||
seed=tag.getLong("Seed").orElseThrow();
|
||||
for(long pos:tag.getLongArray("Occupants").orElseThrow())occupants.add(pos);
|
||||
if(!templateName.matches("underwater_ruin/warm_[1-8]") || occupants.size()>16)
|
||||
throw new IllegalArgumentException("Invalid drained ruin");
|
||||
}
|
||||
private static StructurePlaceSettings settings(Rotation rotation) {
|
||||
// Same warm-ruin archaeology recipe: up to five native suspicious sand blocks.
|
||||
var archaeology=new CappedProcessor(new RuleProcessor(List.of(new ProcessorRule(
|
||||
new BlockMatchTest(Blocks.SAND),AlwaysTrueTest.INSTANCE,PosAlwaysTrueTest.INSTANCE,
|
||||
Blocks.SUSPICIOUS_SAND.defaultBlockState(),new AppendLoot(BuiltInLootTables.OCEAN_RUIN_WARM_ARCHAEOLOGY)))),ConstantInt.of(5));
|
||||
return new StructurePlaceSettings().setRotation(rotation).addProcessor(new BlockRotProcessor(.8F))
|
||||
.addProcessor(BlockIgnoreProcessor.STRUCTURE_AND_AIR).addProcessor(archaeology);
|
||||
}
|
||||
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context,CompoundTag tag) {
|
||||
super.addAdditionalSaveData(context,tag);tag.store("Rot",Rotation.LEGACY_CODEC,placeSettings.getRotation());
|
||||
tag.putLong("Seed",seed);tag.putLongArray("Occupants",occupants.stream().sorted().mapToLong(Long::longValue).toArray());
|
||||
}
|
||||
@Override protected synchronized void handleDataMarker(String marker,BlockPos pos,ServerLevelAccessor level,RandomSource random,BoundingBox clip) {
|
||||
if(!clip.isInside(pos))return;
|
||||
if(marker.equals("chest")) {
|
||||
if(level.getBlockState(pos).is(Blocks.CHEST))return;
|
||||
level.setBlock(pos,Blocks.CHEST.defaultBlockState(),2);
|
||||
if(level.getBlockEntity(pos) instanceof ChestBlockEntity chest)
|
||||
chest.setLootTable(BuiltInLootTables.UNDERWATER_RUIN_SMALL,seed ^ pos.asLong());
|
||||
} else if(marker.equals("drowned")) {
|
||||
level.setBlock(pos,Blocks.AIR.defaultBlockState(),2);
|
||||
if(!occupants.add(pos.asLong()))return;
|
||||
var zombie=EntityTypes.ZOMBIE.create(level.getLevel(),EntitySpawnReason.STRUCTURE);
|
||||
if(zombie==null)throw new IllegalStateException("Missing ruin zombie");
|
||||
zombie.snapTo(pos,0,0);zombie.finalizeSpawn(level,level.getCurrentDifficultyAt(pos),EntitySpawnReason.STRUCTURE,null);
|
||||
zombie.setPersistenceRequired();zombie.setUUID(UUID.nameUUIDFromBytes(("sanctuary:drained307:"+seed+":"+pos.asLong()).getBytes(StandardCharsets.UTF_8)));
|
||||
level.addFreshEntityWithPassengers(zombie);
|
||||
}
|
||||
}
|
||||
@Override public synchronized void postProcess(WorldGenLevel level,StructureManager manager,ChunkGenerator generator,
|
||||
RandomSource random,BoundingBox clip,ChunkPos chunk,BlockPos pivot) {
|
||||
DryOcean307.placing(getBoundingBox(),()->{
|
||||
super.postProcess(level,manager,generator,RandomSource.create(seed ^ chunk.pack()),clip,chunk,pivot);
|
||||
var box=getBoundingBox();
|
||||
for(int x=Math.max(box.minX(),clip.minX());x<=Math.min(box.maxX(),clip.maxX());x++)
|
||||
for(int z=Math.max(box.minZ(),clip.minZ());z<=Math.min(box.maxZ(),clip.maxZ());z++) {
|
||||
for(int y=box.minY();y<=box.maxY();y++) {
|
||||
var pos=new BlockPos(x,y,z);var state=level.getBlockState(pos);
|
||||
if(state.is(Blocks.WATER))level.setBlock(pos,Blocks.AIR.defaultBlockState(),2);
|
||||
else if(state.hasProperty(BlockStateProperties.WATERLOGGED)&&state.getValue(BlockStateProperties.WATERLOGGED))
|
||||
level.setBlock(pos,state.setValue(BlockStateProperties.WATERLOGGED,false),2);
|
||||
}
|
||||
long roll=mix(seed ^ BlockPos.asLong(x,0,z));
|
||||
if(Math.floorMod(roll,7)!=0)continue;
|
||||
for(int y=box.minY()+1;y<box.maxY();y++) {
|
||||
var pos=new BlockPos(x,y,z);var below=level.getBlockState(pos.below());
|
||||
if(level.getBlockState(pos).isAir() && below.isFaceSturdy(level,pos.below(),net.minecraft.core.Direction.UP)
|
||||
&& !below.is(Blocks.CHEST)) {
|
||||
Block coral=switch((int)Math.floorMod(roll>>>8,4)) {
|
||||
case 0->Blocks.DEAD_BRAIN_CORAL_BLOCK;case 1->Blocks.DEAD_TUBE_CORAL;
|
||||
case 2->Blocks.DEAD_FIRE_CORAL;default->Blocks.DEAD_BUBBLE_CORAL;
|
||||
};
|
||||
var dry=coral.defaultBlockState();
|
||||
// Coral plants default to waterlogged even when already dead.
|
||||
// A broken support would otherwise release a new water source here.
|
||||
if(dry.hasProperty(BlockStateProperties.WATERLOGGED)) dry=dry.setValue(BlockStateProperties.WATERLOGGED,false);
|
||||
if(dry.canSurvive(level,pos))level.setBlock(pos,dry,2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
private static long mix(long n){n=(n^n>>>30)*0xbf58476d1ce4e5b9L;n=(n^n>>>27)*0x94d049bb133111ebL;return n^n>>>31;}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package fr.koka.sanctuary.worldgen.drained307;
|
||||
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 DryOceanStructure307 extends Structure {
|
||||
public static final MapCodec<DryOceanStructure307> CODEC=simpleCodec(DryOceanStructure307::new);
|
||||
public DryOceanStructure307(StructureSettings settings){super(settings);}
|
||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context){return Optional.empty();}
|
||||
@Override public StructureType<?> type(){return DryOcean307.TYPE;}
|
||||
}
|
||||
+13
-5
@@ -240,13 +240,14 @@ public final class HeritagePlanner23 {
|
||||
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}) {
|
||||
for(int turn:new int[]{0}) {
|
||||
int direction=(face+turn+4)&3;
|
||||
// Give long straight runs priority, with curves only where the grade needs them.
|
||||
// A branch stays on its initial axis. An obstruction ends it instead of curling back.
|
||||
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;
|
||||
// Keep a real separation from existing parallel routes, outside the shared junction.
|
||||
if(proposedNearRoute307(path.getFirst(), end, cells)) 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();
|
||||
@@ -260,7 +261,7 @@ public final class HeritagePlanner23 {
|
||||
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) {
|
||||
if(arm.depth()==0 && best.path().size() >= 65 && 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));
|
||||
@@ -279,11 +280,18 @@ public final class HeritagePlanner23 {
|
||||
}
|
||||
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));}
|
||||
if(arm.depth()==0) queue.addLast(new Arm(terminal,face,1));
|
||||
}
|
||||
} catch(BudgetExceeded exhausted){p.reject("branched-rail-budget");}
|
||||
}
|
||||
|
||||
private static boolean proposedNearRoute307(XZ start, XZ end, Map<Long, OwnedCell> cells) {
|
||||
if(Math.abs(start.x()-end.x())+Math.abs(start.z()-end.z()) <= 16) return false;
|
||||
for(int dx=-10;dx<=10;dx++) for(int dz=-10;dz<=10;dz++)
|
||||
if(cells.containsKey(key(end.x()+dx,end.z()+dz))) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -64,8 +64,8 @@ public record TransitPlan21(List<Module> modules, int samples, String diagnostic
|
||||
modules = List.copyOf(modules);
|
||||
if (modules.size() > 64 || samples < 0 || samples > MAX_SAMPLES || diagnostic == null || diagnostic.length() > 1024)
|
||||
throw new IllegalArgumentException("Invalid transit planning result");
|
||||
if (!modules.isEmpty() && modules.stream().filter(m -> m.kind() == Kind.ACCESS).count() != 2)
|
||||
throw new IllegalArgumentException("A through-gallery needs two surface accesses");
|
||||
if (!modules.isEmpty() && !List.of(2L,4L).contains(modules.stream().filter(m -> m.kind() == Kind.ACCESS).count()))
|
||||
throw new IllegalArgumentException("Each through-gallery needs two surface accesses");
|
||||
for (int i = 0; i < modules.size(); i++) for (int j = i + 1; j < modules.size(); j++) {
|
||||
var a = modules.get(i); var b = modules.get(j);
|
||||
if (a.minX() <= b.maxX() && a.maxX() >= b.minX() && a.minZ() <= b.maxZ() && a.maxZ() >= b.minZ())
|
||||
|
||||
@@ -12,6 +12,27 @@ public final class TransitPlanner29 {
|
||||
public static final int MAX_COLUMNS = 12_000;
|
||||
private TransitPlanner29() {}
|
||||
|
||||
public static TransitPlan21 plan307(long seed, int cx, int cz, int diameter, Terrain terrain) {
|
||||
var first=plan(seed,cx,cz,diameter,terrain);
|
||||
if(first.isEmpty()) return first;
|
||||
Terrain remaining=new Terrain() {
|
||||
@Override public boolean owns(int x,int z) {return terrain.owns(x,z);}
|
||||
@Override public boolean solid(int x,int y,int z) {return terrain.solid(x,y,z);}
|
||||
@Override public int surfaceY(int x,int z) {return terrain.surfaceY(x,z);}
|
||||
@Override public boolean protectedAt(int x,int y,int z) {
|
||||
// Separate corridors and their entrances, with untouched rock between them.
|
||||
for(var m:first.modules()) if(x>=m.minX()-24&&x<=m.maxX()+24&&z>=m.minZ()-24&&z<=m.maxZ()+24) return true;
|
||||
return terrain.protectedAt(x,y,z);
|
||||
}
|
||||
};
|
||||
var second=plan(seed ^ 0x3077A115L,cx,cz,diameter,remaining);
|
||||
if(second.isEmpty() || first.modules().size()+second.modules().size()>64
|
||||
|| first.samples()+second.samples()>TransitPlan21.MAX_SAMPLES)
|
||||
return new TransitPlan21(first.modules(),first.samples(),first.diagnostic()+"; second-refused="+second.diagnostic());
|
||||
var modules=new ArrayList<>(first.modules());modules.addAll(second.modules());
|
||||
return new TransitPlan21(modules,first.samples()+second.samples(),"two-traversals; "+first.diagnostic()+"; "+second.diagnostic());
|
||||
}
|
||||
|
||||
public static TransitPlan21 plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
|
||||
if (!List.of(512, 724, 1024).contains(diameter)) throw new IllegalArgumentException("Unsupported transit diameter");
|
||||
var p = new Probe(terrain); int layouts = 0; String reason = "no natural entrance pair";
|
||||
|
||||
@@ -238,5 +238,6 @@
|
||||
"sanctuary.structures.census.aerial.status.unknown": "unknown",
|
||||
"structure.sanctuary.aerial_merchant_ship": "Merchant ship",
|
||||
"structure.sanctuary.aerial_mineral_islet": "Mineral islet",
|
||||
"biome.sanctuary.swamp27_caves": "Underground swamp"
|
||||
"biome.sanctuary.swamp27_caves": "Underground swamp",
|
||||
"structure.sanctuary.drained_ocean_ruin": "Drained Ocean Ruins"
|
||||
}
|
||||
|
||||
@@ -238,5 +238,6 @@
|
||||
"sanctuary.structures.census.aerial.status.unknown": "inconnu",
|
||||
"structure.sanctuary.aerial_merchant_ship": "Bateau marchand",
|
||||
"structure.sanctuary.aerial_mineral_islet": "Îlot minéral",
|
||||
"biome.sanctuary.swamp27_caves": "Marais souterrain"
|
||||
"biome.sanctuary.swamp27_caves": "Marais souterrain",
|
||||
"structure.sanctuary.drained_ocean_ruin": "Ruines océaniques asséchées"
|
||||
}
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"type": "sanctuary:drained_ocean_v307",
|
||||
"biomes": "#minecraft:is_overworld",
|
||||
"step": "top_layer_modification",
|
||||
"terrain_adaptation": "none",
|
||||
"spawn_overrides": {}
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "0.1.0-alpha.30.6"
|
||||
version = "0.1.0-alpha.30.7"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
@@ -57,7 +57,7 @@ def main():
|
||||
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",
|
||||
"scripts/structure_atlas.py", f"docs/generation-alpha{version.split('alpha.')[-1]}.md", f"docs/testing-alpha{version.split('alpha.')[-1]}.md"):
|
||||
"scripts/structure_atlas.py", "scripts/finishing_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
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Archive native 30.7 structure witnesses over the unchanged 30.6 terrain field."""
|
||||
import argparse, hashlib, json, re
|
||||
from pathlib import Path
|
||||
import matplotlib
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.patches import Rectangle
|
||||
import numpy as np
|
||||
|
||||
p=argparse.ArgumentParser(description=__doc__)
|
||||
for name in ('terrain','village','transit','aerial','ruins','output'): p.add_argument('--'+name,type=Path,required=True)
|
||||
a=p.parse_args()
|
||||
data={name:json.loads(getattr(a,name).read_text()) for name in ('terrain','village','transit','aerial','ruins')}
|
||||
t=data['terrain']; seed=str(t['seed']); size=t['diameter']
|
||||
for name in ('village','transit','aerial','ruins'):
|
||||
d=data[name]
|
||||
if str(d['seed'])!=seed or d['diameter']!=size or not d.get('passed'): raise ValueError('Unmatched/failed '+name)
|
||||
cols=np.asarray(t['variants']['current306']['columns']);xs,zs=np.unique(cols[:,0]),np.unique(cols[:,1]);g=cols.reshape(len(zs),len(xs),5)
|
||||
fig,ax=plt.subplots(figsize=(11,10),layout='constrained')
|
||||
step=t['map_step'];extent=(xs[0]-step/2,xs[-1]+step/2,zs[-1]+step/2,zs[0]-step/2)
|
||||
im=ax.imshow(np.ma.masked_where(g[:,:,2]==0,g[:,:,2]),extent=extent,cmap='terrain',vmin=0,vmax=400,interpolation='nearest')
|
||||
for link in data['village']['rail_links']:
|
||||
if link['kind']=='FREIGHT': ax.plot([v['x'] for v in link['points']],[v['z'] for v in link['points']],color='#962427',lw=2)
|
||||
walks=data['transit'].get('walk',[])
|
||||
if isinstance(walks,dict): walks=[walks]
|
||||
for walk in walks:
|
||||
gates=[v['gate'] for v in walk['surface_accesses']]
|
||||
ax.plot([v[0] for v in gates],[v[2] for v in gates],color='#223494',lw=2,ls='--',marker='s',ms=4)
|
||||
for b in data['ruins']['bounds']:
|
||||
v={k:int(n) for k,n in re.findall(r'(minX|minZ|maxX|maxZ)=(-?\d+)',b)}
|
||||
ax.add_patch(Rectangle((v['minX'],v['minZ']),v['maxX']-v['minX']+1,v['maxZ']-v['minZ']+1,fill=False,edgecolor='#02abb2',linewidth=2))
|
||||
for site in data['aerial']['geometry']:
|
||||
width,depth=(site['width'],site['depth']) if site['facing']%2==0 else (site['depth'],site['width'])
|
||||
colour='#8a4196' if site['kind']=='NATURAL_ISLET' else '#41301f'
|
||||
ax.add_patch(Rectangle((site['minX'],site['minZ']),width,depth,fill=False,edgecolor=colour,linewidth=1.3))
|
||||
ax.scatter([site['minX']+width//2],[site['minZ']+depth//2],color=colour,s=20,marker='x')
|
||||
ax.scatter([0],[0],marker='+',s=70,color='black')
|
||||
for label,color,style in [('Rails prévus — lacunes réparables','#962427','-'),('Axes des traversées parcourues','#223494','--'),('Ruines asséchées — emprises','#02abb2','-'),('Trésors du terrain aérien — emprises','#8a4196','-'),('Navires restaurés — emprises','#41301f','-')]:ax.plot([],[],color=color,ls=style,label=label)
|
||||
ax.legend(loc='upper left',fontsize=8);ax.set(xlabel='X (blocs)',ylabel='Z (blocs) · nord en haut',aspect='equal')
|
||||
ax.set_title(f'Sanctuary alpha.30.7 · {size} · graine {seed}\nInstallations sur le relief natif inchangé de 30.6')
|
||||
fig.colorbar(im,ax=ax,shrink=.7,label='Altitude du terrain Y')
|
||||
fig.supxlabel('Relief échantillonné ; rails planifiés. Traversées, coffres et ruines contrôlés dans les chunks FULL.\nLes rectangles indiquent des réservations, pas la forme construite ni les frontières des biomes.',fontsize=9)
|
||||
a.output.mkdir(parents=True,exist_ok=True)
|
||||
for ext in ('png','pdf'):fig.savefig(a.output/f'installations-{size}-{seed}.{ext}',dpi=160,bbox_inches='tight',pad_inches=.12)
|
||||
(a.output/f'installations-{size}-{seed}-provenance.json').write_text(json.dumps({k:{'path':str(getattr(a,k)),'sha256':hashlib.sha256(getattr(a,k).read_bytes()).hexdigest()} for k in data},indent=2)+'\n')
|
||||
Reference in New Issue
Block a user