Deliver alpha.30 plateaus, open river banks and underground routes
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-12 10:36:55 +02:00
parent 8c90c61c53
commit bf327b75d4
24 changed files with 645 additions and 44 deletions
+9
View File
@@ -1,5 +1,14 @@
# Changelog
## 0.1.0-alpha.30 — 2026-09-12
Trois zones de déformation 3D, plateaux naturels entre les massifs, grande rivière
à niveaux locaux et brèches de berge de cinq blocs maximum laissant couler leau.
Recherche des traversées mieux répartie, sanctuaire minéral avec seconde recherche
sur les flancs, mines vanilla mieux admises. Les navires conservent leur modèle
et leur placement proche du rivage. Nouveau monde de test nécessaire.
Voir [la génération](docs/generation-alpha30.md) et [les essais](docs/testing-alpha30.md).
## 0.1.0-alpha.29 — 2026-09-12
Petites corniches variables sur le relief 3D de la28 ; correction des traversées
+11 -5
View File
@@ -21,12 +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.29corniches et traversées
## Alpha.30plateaux, rivières ouvertes et traversées
Peaufinage de la 28 validée sur Windows : petits paliers dans le relief 3D,
correction des traversées manquantes en Grand et des refus excessifs de mines.
Créer un **nouveau monde**. Voir [la génération](docs/generation-alpha29.md),
[les essais](docs/testing-alpha29.md) et [packwiz](docs/packwiz.md).
La déformation 3D se concentre dans trois grandes zones ; les plateaux naturels
reprennent leur place entre elles. Une rivière cherche un parcours à l’échelle
de l’île. Des brèches courtes dans ses berges et celles des bassins laissent
leau tomber sur les flancs ou dans le vide. Les traversées souterraines et
le placement du sanctuaire minéral sont renforcés ; les navires restaurés
restent proches des rivages.
Créer un **nouveau monde**. Voir [la génération](docs/generation-alpha30.md),
[les essais et leurs limites](docs/testing-alpha30.md) et [packwiz](docs/packwiz.md).
Lalpha.29 reste un point de reprise local non publié, intégré à cette livraison.
## Alpha.28 — grottes et lieux proches
+12
View File
@@ -988,3 +988,15 @@ graine `-7228211907433324401`, corriger ladmission et la pose des modules.
Ajouter un léger terracement variable au relief3D, préserver les profondeurs,
et vérifier les véritables mines vanilla sur la graine signalée.
Voir [génération](generation-alpha29.md) et [vérifications](testing-alpha29.md).
### WG-32 — Dernière passe de terrain alpha — alpha.30
Branche `codex/terrain-finale-alpha30`, depuis le point de reprise local alpha.29
`8c90c61`. Concentrer la déformation signée dans trois zones, retrouver les
plateaux naturels entre elles et des rivières longues à niveaux locaux.
Ouvrir des brèches de berge courtes pour laisser leau couler librement.
Assurer une recherche mieux répartie des traversées, notamment en Grand ;
chercher aussi le sanctuaire minéral sur les flancs en cas de refus intérieur.
Conserver les bateaux proches, les cavités et les générateurs vanilla de la 28/29.
Voir [le périmètre](generation-alpha30.md) et [les preuves](testing-alpha30.md).
+80
View File
@@ -0,0 +1,80 @@
# Alpha.30 — plateaux, rivières et traversées
Dernière passe de génération alpha demandée par le créateur. Branche
`codex/terrain-finale-alpha30`, depuis le point de reprise local alpha.29
`8c90c61` ; cette dernière na pas été publiée. Minecraft **26.3-pre-2**,
Fabric Loader **0.19.5**, Fabric API **0.160.0+26.3** conservés.
## Relief
La déformation signée par les bruits Minecraft de la 28 agit dans trois zones
étendues, orientées par la graine. Entre elles, son poids devient nul et le
champ de plateaux naturel précédent reprend sa place. Il sagit de déplacer
et daffaisser la roche existante, pas dajouter trois cônes. Le petit
terracement systématique essayé en 29 est désactivé.
Les cavités et failles bornées de la 28 restent actives. Aucun nouveau
creusement ellipsoïdal, plafond du monde ou changement des nuages. La variation
de relief est nulle sous Y180. Les tailles 512 / 724 / 1 024 et le bouton natif
Personnaliser sont conservés, avec Moyen par défaut.
Ce changement vise des étendues plus calmes séparées par du relief marqué.
Il ne garantit pas que toute surface ait exactement deux niveaux, ni que toute
petite pointe rocheuse ait disparu. Le rendu est à juger dans un nouveau monde.
## Hydrologie
Une recherche sur la principale masse connectée cherche une rivière longue,
évitant les fortes pentes quand un passage plus bas existe. Elle suit le terrain
réel avec un lit de cinq blocs environ, des niveaux locaux, du gravier et de
largile. Les petits bassins et réseaux précédents restent présents.
Quand une berge est séparée dun espace ouvert par **cinq blocs au maximum**,
une brèche de trois blocs de largeur ouvre le passage. Le plan cherche jusqu’à
six sorties espacées pour la grande rivière et une sortie supplémentaire par
réseau régional. Il nimpose pas deux cascades terminales, ni de bassin de
réception : leau peut descendre sur l’île ou dans le vide. La brèche ne pose
ni fondation ni colonne deau artificielle ; les ticks vanilla propagent leau
depuis les sources voisines. Les emprises de chute restent réservées aux
expansions. Larrivée naturelle reste à l’écart du tracé.
La recherche travaille sur une grille puis vérifie les blocs du lit. Les
passages trop creux peuvent interrompre ce lit et laisser fuir leau. Ce nest
pas une simulation de bassin versant avec débit conservé ou pente monotone.
Ces ajouts concernent l’île initiale ; les profils des continents dexpansion
ne sont pas remplacés par cette rivière.
## Traversée et sanctuaire minéral
La recherche des deux entrées répartit plus tôt ses essais entre plusieurs
alignements au lieu d’épuiser une seule bande. Les appuis et la protection
des secrets gardent leur autorité. La seconde passe peut omettre un pied
optionnel protégé seulement si les autres appuis restent valides. Les modules
industriels, escaliers, trottoirs et parois existants sont conservés.
Le sanctuaire garde ses 79 × 73 × 27 blocs et sa palette minérale. Après les
48 candidats intérieurs, une seconde recherche de 48 candidats sur les flancs
accepte davantage dexposition, avec des attaches rocheuses et le plancher
praticable. Un échec complet reste diagnostiqué ; la structure ne peut pas être
posée dans un volume intégralement vide ou en écrasant un autre secret.
Aucun boss, machine, portail actif ou dimension ajouté.
Les corrections de mines de la 29 sont intégrées : hauteur choisie dans la
roche disponible, admission des pièces et appuis réels, croisements en bois
acceptés lorsquils sont portés. Les pièces, spawners, rails et butins restent
vanilla. Leur présence reste conditionnelle aux candidats et au terrain.
## Satellites et sauvegardes
Deux navires restaurés près des rivages et deux îlots au-dessus de Sanctuary,
dont un à cascade : modèles et règles de proximité conservés. Les points de
vue doivent désormais éviter les sommets que lhydrologie va creuser en bassin
ou en brèche ; aucun bloc artificiel nest ajouté pour soutenir un témoin.
Le prototype conserve `sanctuary:island_v24` et ses profils courants, conformément
au choix de travailler sur des mondes de test jetables. **Créer un nouveau monde.**
Il ny a ni migration ni régénération dune sauvegarde personnelle. Les réglages
Générer les structures et Structures expérimentales gardent leur rôle ; la
rivière et le relief sont des phénomènes naturels indépendants de ces options.
Les résultats effectivement mesurés sont dans [les vérifications](testing-alpha30.md).
+145
View File
@@ -0,0 +1,145 @@
# Alpha.30 — vérifications
Essais du 12 septembre 2026 sur **Mac**, dans des serveurs de développement
sans rendu client. Le retour Windows du créateur concerne la 28 ; aucun essai
Windows natif de la 30 nest revendiqué. Nouveau monde nécessaire.
## Témoin Grand signalé par le créateur
**1 024 blocs, graine `-7228211907433324401`**, processus 62002 :
**24/24 GameTests passent**. Reçus dans `build/alpha30-evidence/grand-user/`.
- Traversée réellement générée : 23 modules, 52 chunks FULL, 374 blocs entre
les accès ; parcours contrôlé sur 736 pas et 71 points, avec les collisions
natives, escaliers et raccords. Entrées : **192 238 -195** et **192 256 179**.
- Rivière : tracé entre des points distants denviron **865 blocs**, 6 924 cellules
de lit et de brèche, six ouvertures. Huit chunks FULL échantillonnés ; après
120 ticks, six échantillons contiennent de leau sous les deux sorties suivies.
Ce test ne prétend pas simuler toutes les chutes jusqu’à Y0.
- Sanctuaire minéral accepté dès la recherche intérieure ; emprise depuis
**-122 131 265**, 79 × 73 × 27. Le recours exposé nest pas exercé sur ce témoin.
- Quatre satellites, deux villageois, quatre coffres et une cascade d’îlot :
44 chunks FULL contrôlés. Les témoins de pillage, déplacement et démolition
sont préparés pour le contrôle de persistance.
- Le petit échantillon de structures vanilla ne contient pas de mine. Ce nest
pas un inventaire exhaustif de l’île et aucune présence de mine ny est annoncée.
## Petit 512, graine 2026
Processus **62463**, **24/24 GameTests passent**. Reçus :
`build/alpha30-evidence/small-2026-final2/`.
La traversée produit 15 modules et leurs départs natifs, avec 204 blocs entre
les entrées. Ce contrôle est limité aux plans et départs ; aucun parcours FULL
complet de cette traversée nest revendiqué sur Petit. La rivière couvre une
distance denviron 362 blocs, avec 2 551 cellules et quatre brèches. Sept chunks
FULL sont échantillonnés ; leau coule sous les sorties contrôlées.
Le sanctuaire possède son véritable départ de structure et son sol central
est contrôlé après génération au point **-54 162 69**, avec lespace libre
au-dessus. Les quatre satellites passent sur 41 chunks FULL.
Un premier essai a révélé que le témoin de vue de l’îlot à cascade se trouvait
sur une ancienne hauteur creusée en bassin. La correction choisit une berge
sèche proche, sans ajouter de support artificiel. Une première correction trop
large épuisait le budget de colonnes ; elle a été remplacée avant validation.
La commande finale dure 2 min 8 s.
## Moyen 724, graine 0
Processus **62604**, reçus `build/alpha30-evidence/medium-0/`.
La traversée réelle passe : **19 modules, 42 chunks FULL, 272 blocs entre les
accès, 456 pas et 62 points accessibles**. Entrées : **-136 248 -53** et
**136 260 -53**. La rivière s’étend sur environ 554 blocs : 3 942 cellules,
six brèches, neuf chunks FULL contrôlés et écoulement sous les sorties suivies.
Le sol central du sanctuaire est présent en **106 189 -63**, avec lespace
libre au-dessus ; les satellites passent également.
Le test exigeant une mine sur cette graine échoue : aucun départ de mine nest
retenu parmi ses 19 candidats, y compris via la recherche native. Les **23 autres
tests passent**. Le témoin de mine de la 29 nest donc pas réutilisé comme preuve
de présence en 30. Un témoin séparé est recherché, sans forcer une implantation
incompatible. Les refus conditionnels restent un comportement voulu du jeu.
## Grand 1 024, graine 42 — témoin de mine
Processus **62979**, **24/24 GameTests passent**, y compris lexigence de trouver
une mine réelle. Reçus dans `build/alpha30-evidence/grand-42/`.
Deux départs de mines vanilla sont observés dans les candidats examinés. La
mine visitée possède **83 pièces** ; l’échantillon de 16 chunks FULL du test
contient notamment **138 planches de chêne, 120 barrières, 43 rails et 23 toiles**.
Point de visite : **6 189 6**. Le test contrôle les pièces sauvegardées et leur
aller-retour NBT ; il ne visite pas lintégralité de la mine et ne garantit pas
un spawner dans chaque corridor. Une lecture séparée de la sauvegarde confirme
un véritable spawner `cave_spider` en **46 188 25** dans un corridor :
`build/alpha30-evidence/grand-42/saved-spawners.json`. Son activation par un joueur
na pas été testée.
Deux huttes de sorcière sont également observées. La hutte visitée est en
**83 148 -396**, avec ses pièces, appuis et son mobilier vanilla.
La traversée possède **25 modules répartis sur 24 départs natifs**, avec 374 blocs entre
les entrées ; cette graine utilise le contrôle des plans, sans nouveau parcours
FULL de toute la traversée. Le sanctuaire possède son vrai départ et son sol
central. La rivière s’étend sur environ **882 blocs**, avec 5 822 cellules et
six ouvertures ; les écoulements suivis passent sous ticks réels.
`./gradlew check build` passe sur ce témoin : **88 tâches**, dont les tests purs,
les contrôles de sérialisation et les **24 GameTests natifs**, en **6 min 28 s**.
Le coût inclut toute la suite et nest pas un temps de chargement de partie.
Commande :
```sh
./gradlew check build -PsanctuaryTestDiameter=1024 -PsanctuaryTestSeed=42 \
-PsanctuaryDensityMaps=false -PsanctuaryAerial24Tests=true \
-PsanctuaryTransit29Tests=true -PsanctuaryTransit29PlanOnly=true \
-PsanctuaryRequireMineshaft29=true
```
## Relief
Sur ce Grand, 8 112 densités sous Y180 sont identiques bit à bit à la 28.
Dans les coupes hautes, 4 544 / 94 640 échantillons changent de signe, soit
**4,80 %**. Deux coupes au pas dun bloc, Z0 et Z96, ont été rendues et examinées :
les plateaux redeviennent larges entre les zones où subsistent les surplombs.
Aperçu local `build/alpha30-preview/relief-1024--7228211907433324401.png`.
Les mêmes coupes ont aussi été examinées sur Petit2026 : 602 / 40 460 signes
modifiés (1,49 %), avec des sommets plus plats entre les zones déformées.
Ce contrôle de densité ne remplace pas une exploration visuelle dans le client
et ne démontre pas labsence de toute petite pointe ou masse déconnectée.
## Tests ciblés et limites
Le test pur `river30Smoke` couvre trois rayons sur une masse rocheuse synthétique :
longueur du tracé, lit dans la roche, arrivée protégée, brèches de cinq blocs
maximum, déterminisme et espaces non déformés entre les trois secteurs.
Ce nest pas une matrice de neuf mondes Minecraft natifs.
La recherche reste bornée par la taille de l’île et le nombre de candidats.
Sur le Grand, la commande native complète dure **4 min 44 s**, incluant le
lancement, la planification, les structures et les nombreux chunks demandés
par les tests. Ce temps nest ni un temps de rendu client, ni un benchmark
comparatif avec la 28. Aucun objectif chiffré de performance nest revendiqué.
Les tests vérifient des occurrences et parcours précis, pas une garantie
universelle sur toutes les graines. Les mines restent conditionnelles, et le
sanctuaire peut encore être refusé si aucune implantation na dattache valable.
Les réglages, le Blocodex et la suite générale restent soumis à `check build`.
## Redémarrage et artefacts
Grand1 024, graine42, processus **63276** : **24/24 tests passent après un
redémarrage réel** (2 min 14 s). Les 47 chunks FULL du contrôle aérien conservent
coffres pillés, villageois déplacés et destructions préparées à la création,
sans duplication ni reconstruction des témoins. Les départs de mine et leurs
pièces, le sol du sanctuaire et les écoulements échantillonnés passent à nouveau.
Le journal dexpansion reste inchangé. Preuves :
`build/alpha30-evidence/grand-42-reload/` et `build/alpha30-reload.log`.
`./gradlew assemblePack` passe après `check build` (le second lancement exclut
la tâche `:sanctuary:check` déjà réussie). Le JAR, le MRpack autonome et le ZIP
damorçage Prism passent la vérification locale : versions, dépendances exactes,
index et empreintes concordent. Reçus : `build/alpha30-artifacts.json`,
`build/alpha30-delivery-local.json`. Les fichiers du client Windows nont pas
été modifiés. La publication et la synchronisation de linstance Mac sont
suivies dans [la distribution](packwiz.md).
+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.29
pack_version=0.1.0-alpha.29
mod_version=0.1.0-alpha.30
pack_version=0.1.0-alpha.30
maven_group=fr.koka.sanctuary
+8
View File
@@ -657,3 +657,11 @@ tasks.named('check') { dependsOn('terraces29Smoke') }
}
tasks.named('check') { dependsOn(taskName) }
}
tasks.register('river30Smoke', JavaExec) {
group = 'verification'
dependsOn tasks.named('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.River30Smoke'
}
tasks.named('check') { dependsOn('river30Smoke') }
@@ -0,0 +1,62 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import fr.koka.sanctuary.worldgen.*;
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.server.level.TicketType;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.status.ChunkStatus;
public final class River30WorldGameTests {
@GameTest(maxTicks=12000)
public void riverAndOpenBanksUseRealTerrain(GameTestHelper helper) throws Exception {
var level=helper.getLevel(); var generator=(SanctuaryChunkGenerator)level.getChunkSource().getGenerator();
var binding=generator.starterContext();var plan=PopulationHydrologyRuntime.river30(binding.generator(),binding.random());
var report=new LinkedHashMap<String,Object>();report.put("seed",level.getSeed());report.put("diameter",generator.initialDiameter());
report.put("process_id",ProcessHandle.current().pid());report.put("river_cells",plan.cells().size());report.put("features",plan.features());report.put("spills",plan.spills());
report.put("mineral",generator.mineralSanctuary23());
helper.assertTrue(plan.features().stream().anyMatch(f->f.kind()==PopulationHydrology.Kind.RIVER),"An island-wide river must be present in the release witness");
if(level.structureManager().shouldGenerateStructures()&&generator.experimentalStructures())
helper.assertFalse(generator.mineralSanctuary23().isEmpty(),"The mineral sanctuary must be present in the release witness");
var selected=plan.cells().stream().filter(PopulationHydrology.Cell::hasWater).toList();
var witnesses=new ArrayList<PopulationHydrology.Cell>();
for(int i=0;i<6;i++)witnesses.add(selected.get(i*(selected.size()-1)/5));
var falls=plan.spills().stream().limit(2).toList();var chunks=new LinkedHashSet<ChunkPos>();
witnesses.forEach(c->chunks.add(new ChunkPos(c.x()>>4,c.z()>>4)));
for(var fall:falls){chunks.add(new ChunkPos(fall.source().x()>>4,fall.source().z()>>4));chunks.add(new ChunkPos(fall.outlet().x()>>4,fall.outlet().z()>>4));}
var mineral=generator.mineralSanctuary23().chamber();
if(mineral!=null){
var key=fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.KEY;
var structure=level.registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.STRUCTURE).getOrThrow(key).value();
var cp=new ChunkPos(mineral.startChunkX(),mineral.startChunkZ());
var start=level.getChunk(cp.x(),cp.z(),ChunkStatus.STRUCTURE_STARTS,true).getStartForStructure(structure);
helper.assertTrue(start!=null&&start.isValid()&&start.getPieces().size()==1,"The mineral plan produces its actual native structure start");
chunks.add(cp);
}
var iterator=chunks.iterator();Runnable[] step=new Runnable[1];
step[0]=()->{
if(iterator.hasNext()) {var c=iterator.next();level.getChunk(c.x(),c.z(),ChunkStatus.FULL,true);helper.runAtTickTime(helper.getTick()+1,()->step[0].run());return;}
chunks.forEach(c->level.getChunkSource().addTicketWithRadius(TicketType.DRAGON,c,2));
helper.runAtTickTime(helper.getTick()+120,()->{
try {
if(mineral!=null){
var floor=new BlockPos(mineral.centerX(),mineral.minY()+2,mineral.centerZ());
helper.assertTrue(level.getBlockState(floor).isSolid()&&level.getBlockState(floor.above()).isAir(),"The central mineral floor and guardian space are really generated");
report.put("mineral_native_floor",List.of(floor.getX(),floor.getY(),floor.getZ()));
}
for(var c:witnesses)helper.assertTrue(!level.getFluidState(new BlockPos(c.x(),c.waterY(),c.z())).isEmpty(),"Generated river water remains at its supported local bed");
int flowing=0;
for(var fall:falls)for(int dx=-1;dx<=1;dx++)for(int dz=-1;dz<=1;dz++)
if(!level.getFluidState(new BlockPos(fall.outlet().x()+dx,fall.outlet().y()-3,fall.outlet().z()+dz)).isEmpty()){flowing++;}
if(!falls.isEmpty())helper.assertTrue(flowing>0,"Vanilla fluid ticks carry water below at least one opened bank");
report.put("full_chunks",chunks.size());report.put("flowing_below_openings",flowing);report.put("passed",true);
Path out=Path.of("diagnostics/alpha30/river.json");Files.createDirectories(out.getParent());Files.writeString(out,new GsonBuilder().setPrettyPrinting().create().toJson(report));helper.succeed();
}catch(Exception error){throw new RuntimeException(error);}finally{chunks.forEach(c->level.getChunkSource().removeTicketWithRadius(TicketType.DRAGON,c,2));}
});
};step[0].run();
}
}
@@ -22,7 +22,7 @@ public final class Terraces29WorldGameTests {
var field = (PopulationIslandDensity) level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION)
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION,
SanctuaryMod.id("population_" + generator.capacity().players()))).value();
helper.assertTrue(field.terraces29(), "Current resources enable the fine ledges");
helper.assertTrue(field.relief30() && !field.terraces29(), "Alpha30 localizes deformation and disables periodic micro-ledges");
var random = generator.starterContext().random();
var current = random.getSampler(field); var previous = random.getSampler(field.legacy28());
var context = SamplerContext.builder().enableCaches().build();
@@ -31,19 +31,19 @@ public final class Terraces29WorldGameTests {
report.put("process_id", ProcessHandle.current().pid());
int radius = Math.min(400, generator.initialDiameter() / 2), lowerSamples = 0, unchanged = 0, changed = 0;
for (int z = -radius; z <= radius; z += 32) for (int x = -radius; x <= radius; x += 32) {
for (int y = 32; y <= 214; y += 13) {
for (int y = 32; y <= 180; y += 13) {
helper.assertTrue(Float.floatToIntBits(current.sampleValue(context, x, y, z))
== Float.floatToIntBits(previous.sampleValue(context, x, y, z)), "Lower density is exactly alpha28");
lowerSamples++;
}
for (int y = 215; y <= 320; y++) {
for (int y = 181; y <= 320; y++) {
boolean old = previous.sampleValue(context, x, y, z) > 0;
boolean now = current.sampleValue(context, x, y, z) > 0;
if (old == now) unchanged++; else changed++;
if (Math.hypot(x, z) <= 48) helper.assertTrue(old == now, "Arrival silhouette is unchanged");
}
}
helper.assertTrue(changed > 0 && unchanged > changed * 20L, "The edit produces small ledges, not a replacement of the upper mass");
helper.assertTrue(changed > 0 && unchanged > changed * 3L, "Localized relief retains most of the accepted upper mass");
report.put("unchanged_lower_density_samples", lowerSamples);
report.put("unchanged_upper_samples", unchanged); report.put("changed_upper_samples", changed);
var sections = new LinkedHashMap<String, Object>();
@@ -60,7 +60,7 @@ public final class Terraces29WorldGameTests {
sections.put("z" + z, Map.of("before", before, "after", after, "min_x", -radius, "max_y", 305, "step", 1));
}
report.put("sections", sections); report.put("passed", true);
Path path = Path.of("diagnostics/alpha29", "terraces-" + generator.initialDiameter() + "-" + level.getSeed() + ".json");
Path path = Path.of("diagnostics/alpha30", "relief-" + generator.initialDiameter() + "-" + level.getSeed() + ".json");
Files.createDirectories(path.getParent());
Files.writeString(path, new GsonBuilder().setPrettyPrinting().create().toJson(report));
helper.succeed();
@@ -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.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.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"],
@@ -31,7 +31,7 @@ public final class PopulationHydrology {
@FunctionalInterface
public interface Sampler { float sample(int x, int y, int z); }
public enum Kind { POND, LAKE, RIVER, TERRACE }
public enum Kind { POND, LAKE, RIVER, TERRACE, OUTLET }
public enum ShoreMaterial { SAND, GRAVEL, CLAY, STONE, GRASS }
public record Point(int x, int z) {}
public record Bounds(int minX, int minZ, int maxX, int maxZ) {}
@@ -133,6 +133,23 @@ public final class PopulationHydrology {
public List<Feature> features() { return features; }
public boolean isRiverCell(Cell cell) { return features.stream().anyMatch(f -> f.kind() == Kind.RIVER && f.id() == cell.featureId()); }
public boolean isTerraceCell(Cell cell) { return features.stream().anyMatch(f -> f.kind() == Kind.TERRACE && f.id() == cell.featureId()); }
public boolean isOutletCell(Cell cell) { return features.stream().anyMatch(f -> f.kind() == Kind.OUTLET && f.id() == cell.featureId()); }
/** Add an island-wide channel or short bank cuts, retaining the deeper water terraces. */
public Plan overlay30(Plan other) {
var surface = new LinkedHashMap<Long, Cell>();
var lowerCells = new ArrayList<Cell>();
for (var cell : allCells) {
if (isTerraceCell(cell)) lowerCells.add(cell);
else surface.put(key(cell.x(), cell.z()), cell);
}
for (var cell : other.allCells) surface.put(key(cell.x(), cell.z()), cell);
var mergedFeatures = new LinkedHashMap<Long, Feature>();
features.forEach(f -> mergedFeatures.put(f.id(), f));
other.features.forEach(f -> mergedFeatures.put(f.id(), f));
var mergedSpills = new ArrayList<>(spills); mergedSpills.addAll(other.spills);
var mergedCells = new ArrayList<>(surface.values()); mergedCells.addAll(lowerCells);
return rebuild(mergedCells, List.copyOf(mergedFeatures.values()), springs, springFlow, terraces, mergedSpills);
}
public List<Terrace> terraces() { return terraces; }
public List<Spill> spills() { return spills; }
public boolean isSpillOpening(int x, int y, int z) { return spillOpenings.contains(new Position(x, y, z)); }
@@ -254,6 +271,10 @@ public final class PopulationHydrology {
public static Plan create(long seed, Sampler sampler) {
return new Planner(seed, sampler).build();
}
static Plan additional30(Map<Long, Cell> cells, List<Feature> features, List<Spill> spills, int columns, int samples) {
return new Plan(cells, features, List.of(), Set.of(), columns, samples,
new PopulationTerraces.Result(List.of(), List.of(), List.of(), spills, Set.of()));
}
private record Column(int top, int solidBottom) {}
private record Candidate(int x, int z, double score) {}
@@ -17,6 +17,25 @@ public final class PopulationHydrologyRuntime {
public record RegionPlan(IslandCapacity.Region region, PopulationHydrology.Plan water, PopulationLavaDeposit.Plan lava) {}
private static final Map<RandomState, PopulationPlanCache<IslandCapacity.Region, RegionPlan>> WORLDS =
Collections.synchronizedMap(new WeakHashMap<>());
private static final Map<RandomState, PopulationHydrology.Plan> RIVERS30 = new WeakHashMap<>();
public static boolean current30(NoiseBasedChunkGenerator generator) {
var settings = generator.generatorSettings();
return settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_5"))
|| settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_10"))
|| settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_20"));
}
public static PopulationHydrology.Plan river30(NoiseBasedChunkGenerator generator, RandomState state) {
synchronized (RIVERS30) {
return RIVERS30.computeIfAbsent(state, ignored -> {
var sampler = state.getSampler(generator.generatorSettings().value().noiseRouter().finalDensity());
var plan = River30.create(state.seed(), (int) capacity(generator).radius(),
new SignMemo((x,y,z) -> sampler.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)));
SanctuaryMod.LOGGER.info("River30 seed {}: {} cells, {} outlets, {} path points", state.seed(),
plan.cells().size(), plan.spills().size(), plan.features().stream().mapToInt(f -> f.path().size()).sum());
return plan;
});
}
}
private PopulationHydrologyRuntime() {}
public static boolean enabled(NoiseBasedChunkGenerator generator) { return IslandCapacity.forGenerator(generator) != null; }
public static IslandCapacity capacity(NoiseBasedChunkGenerator generator) {
@@ -47,6 +66,10 @@ public final class PopulationHydrologyRuntime {
});
long seed = PopulationIslandDensity.regionSeed(state.seed(), region);
var water = PopulationHydrology.create(seed, memo).admitted();
if (current30(generator)) {
water = River30.openBanks(seed, water, memo, 1)
.overlay30(river30(generator, state).translated(-region.originX(), -region.originZ()));
}
var lava = PopulationLavaDeposit.create(seed, memo, water).admitted();
var result = new RegionPlan(region, water.translated(region.originX(), region.originZ()),
lava.translated(region.originX(), region.originZ()));
@@ -133,6 +156,7 @@ public final class PopulationHydrologyRuntime {
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
// Validate all replacements and two intact support layers before modifying this chunk.
for (var cell : cells) {
if (plan.isOutletCell(cell)) continue;
for (int y = cell.bedY() - cell.sedimentDepth() - 1; y <= cell.bedY(); y++) {
requireSolid(chunk, pos.set(cell.x(), y, cell.z()), randomState.seed());
}
@@ -13,7 +13,7 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
DensityFunction sculpt, DensityFunction detail, DensityFunction underside,
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity,
DensityFunction caves28, DensityFunction ridges28, DensityFunction erosion28, boolean geology28,
boolean terraces29) implements DensityFunction {
boolean terraces29, boolean relief30) implements DensityFunction {
public static final MapCodec<PopulationIslandDensity> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
DensityFunction.CODEC.fieldOf("terrain").forGetter(PopulationIslandDensity::terrain),
DensityFunction.CODEC.fieldOf("distortion").forGetter(PopulationIslandDensity::distortion),
@@ -27,14 +27,15 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
DensityFunction.CODEC.optionalFieldOf("ridges_28", DensityFunctions.zero()).forGetter(PopulationIslandDensity::ridges28),
DensityFunction.CODEC.optionalFieldOf("erosion_28", DensityFunctions.zero()).forGetter(PopulationIslandDensity::erosion28),
Codec.BOOL.optionalFieldOf("geology_28", false).forGetter(PopulationIslandDensity::geology28),
Codec.BOOL.optionalFieldOf("terraces_29", false).forGetter(PopulationIslandDensity::terraces29)
Codec.BOOL.optionalFieldOf("terraces_29", false).forGetter(PopulationIslandDensity::terraces29),
Codec.BOOL.optionalFieldOf("relief_30", false).forGetter(PopulationIslandDensity::relief30)
).apply(instance, PopulationIslandDensity::new));
public PopulationIslandDensity(DensityFunction terrain, DensityFunction distortion,
DensityFunction sculpt, DensityFunction detail, DensityFunction underside,
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity) {
this(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail, capacity,
DensityFunctions.zero(), DensityFunctions.zero(), DensityFunctions.zero(), false, false);
DensityFunctions.zero(), DensityFunctions.zero(), DensityFunctions.zero(), false, false, false);
}
public PopulationIslandDensity(DensityFunction terrain, DensityFunction distortion,
@@ -42,7 +43,7 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity,
DensityFunction caves28, DensityFunction ridges28, DensityFunction erosion28, boolean geology28) {
this(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail, capacity,
caves28, ridges28, erosion28, geology28, false);
caves28, ridges28, erosion28, geology28, false, false);
}
@Override public DensitySampler compileSampler(CompileContext context) {
@@ -77,6 +78,7 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
float fineValue = fine.sampleValue(sample, x, localY, z);
float terrainValue;
double weight = geology28 ? CavesRelief28.deformationWeight(x, y, z, capacity.radius()) : 0;
if (relief30) weight *= Relief30.influence(seed, x, z, capacity.radius());
if (weight > 0) {
var shift = CavesRelief28.warp(weight,
erosion.sampleValue(sample, x + 173, y - 59, z - 89),
@@ -118,7 +120,7 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
/** Exact accepted28 deformation and caves, without the fine local ledges. */
public PopulationIslandDensity legacy28() {
return new PopulationIslandDensity(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail,
capacity, caves28, ridges28, erosion28, geology28, false);
capacity, caves28, ridges28, erosion28, geology28, false, false);
}
public static long regionSeed(long seed, IslandCapacity.Region region) {
long value = seed ^ ((long) region.x() << 32) ^ (region.z() & 0xffffffffL) ^ 0xA10CA7E1L;
@@ -129,7 +131,7 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
@Override public DensityFunction rewriteChildren(DfRewriteRule rule) {
return new PopulationIslandDensity(rule.rewrite(terrain), rule.rewrite(distortion), rule.rewrite(sculpt),
rule.rewrite(detail), rule.rewrite(underside), rule.rewrite(riftWarp), rule.rewrite(riftDetail), capacity,
rule.rewrite(caves28), rule.rewrite(ridges28), rule.rewrite(erosion28), geology28, terraces29);
rule.rewrite(caves28), rule.rewrite(ridges28), rule.rewrite(erosion28), geology28, terraces29, relief30);
}
@Override public Interval range() { return Interval.of(-1, 1); }
@Override public int domainAxes() { return ALL_AXES; }
@@ -0,0 +1,23 @@
package fr.koka.sanctuary.worldgen;
/** Three broad areas of signed deformation; the intervening rock keeps the original plateau field. */
public final class Relief30 {
private Relief30() {}
public static double influence(long seed, int x, int z, double radius) {
long mixed = seed ^ 0x30BADC0FFEEL;
mixed = (mixed ^ (mixed >>> 30)) * 0xbf58476d1ce4e5b9L;
double rotation = (mixed >>> 11) * 0x1.0p-53 * Math.PI * 2;
double strongest = 0;
for (int i = 0; i < 3; i++) {
double angle = rotation + i * Math.PI * 2 / 3;
double cx = Math.cos(angle) * radius * .52, cz = Math.sin(angle) * radius * .52;
double dx = x - cx, dz = z - cz;
double along = (dx * Math.cos(angle) + dz * Math.sin(angle)) / (radius * .47);
double across = (-dx * Math.sin(angle) + dz * Math.cos(angle)) / (radius * .34);
double distance = Math.hypot(along, across);
double t = Math.clamp((1 - distance) / .70, 0, 1);
strongest = Math.max(strongest, t * t * (3 - 2 * t));
}
return strongest;
}
}
@@ -0,0 +1,146 @@
package fr.koka.sanctuary.worldgen;
import fr.koka.sanctuary.worldgen.PopulationHydrology.*;
import java.util.*;
/** One island-scale route follows actual rock, with local water levels and short open banks. */
public final class River30 {
private static final int STEP = 8;
private static final int[][] DIR = {{1,0},{-1,0},{0,1},{0,-1}};
private River30() {}
private static long key(int x, int z) { return ((long)x << 32) ^ (z & 0xffffffffL); }
private static int x(long k) { return (int)(k >> 32); }
private static int z(long k) { return (int)k; }
private record Node(long key, double cost) {}
private static final class Rock {
final Sampler sampler; final Map<Long,Integer> tops = new HashMap<>(); int samples;
Rock(Sampler sampler) { this.sampler = sampler; }
boolean solid(int x,int y,int z) { samples++; return y > 1 && y < 384 && sampler.sample(x,y,z) > 0; }
int top(int x,int z) {
return tops.computeIfAbsent(key(x,z), unused -> {
for (int y=358;y>=140;y--) if (solid(x,y,z)) {
for(int d=1;d<=6;d++) if(!solid(x,y-d,z)) return -1;
return y;
}
return -1;
});
}
}
public static Plan create(long seed, int radius, Sampler sampler) {
var rock = new Rock(sampler); var nodes = new TreeMap<Long,Integer>();
int limit = radius / STEP * STEP;
for(int x=-limit;x<=limit;x+=STEP) for(int z=-limit;z<=limit;z+=STEP) {
if(Math.hypot(x,z)>radius*.95 || Math.hypot(x,z)<52) continue;
int y=rock.top(x,z); if(y>=140) nodes.put(key(x,z),y);
}
// Ignore isolated peripheral rock when selecting opposite banks: route in the largest connected mass.
var adjacency=new HashMap<Long,List<Long>>();
for(long k:nodes.keySet()) {
var neighbours=new ArrayList<Long>();
for(var d:DIR) {
long next=key(x(k)+d[0]*STEP,z(k)+d[1]*STEP);
if(!nodes.containsKey(next)||Math.abs(nodes.get(next)-nodes.get(k))>48)continue;
int gaps=0;for(int n=1;n<STEP;n++)if(rock.top(x(k)+d[0]*n,z(k)+d[1]*n)<140)gaps++;
if(gaps<=2)neighbours.add(next);
}
adjacency.put(k,neighbours);
}
var remaining=new TreeSet<>(nodes.keySet());Set<Long> largest=Set.of();
while(!remaining.isEmpty()) {
var component=new HashSet<Long>();var queue=new ArrayDeque<Long>();queue.add(remaining.first());
while(!queue.isEmpty()){long k=queue.remove();if(!component.add(k))continue;queue.addAll(adjacency.get(k));}
remaining.removeAll(component);if(component.size()>largest.size())largest=component;
}
nodes.keySet().retainAll(largest);
List<Long> best = List.of(); double longest = 0;
// Four deterministic orientations avoid choosing an axis whose shore is entirely broken.
for(int axis=0;axis<4;axis++) {
double angle=(seed & 1023)*Math.PI/512 + axis*Math.PI/4, dx=Math.cos(angle), dz=Math.sin(angle);
var ordered=nodes.keySet().stream().sorted(Comparator.<Long>comparingDouble(k->x(k)*dx+z(k)*dz).thenComparingLong(k->k)).toList();
if(ordered.isEmpty()) break;
long start=ordered.getFirst(); var costs=new HashMap<Long,Double>(); var parent=new HashMap<Long,Long>();
var queue=new PriorityQueue<Node>(Comparator.comparingDouble(Node::cost).thenComparingLong(Node::key));
costs.put(start,0.0);parent.put(start,start);queue.add(new Node(start,0));
while(!queue.isEmpty()) {
var at=queue.remove(); if(at.cost()!=costs.get(at.key())) continue;
for(long next:adjacency.get(at.key())) {
if(!nodes.containsKey(next)) continue;
int difference=Math.abs(nodes.get(next)-nodes.get(at.key()));
double cost=at.cost()+STEP+difference*difference*.18+Math.max(0,nodes.get(next)-215)*.13;
if(cost>=costs.getOrDefault(next,Double.POSITIVE_INFINITY)) continue;
costs.put(next,cost);parent.put(next,at.key());queue.add(new Node(next,cost));
}
}
for(int i=ordered.size()-1;i>=0;i--) {
long end=ordered.get(i); if(!parent.containsKey(end)) continue;
double span=Math.hypot(x(end)-x(start),z(end)-z(start));
if(span>longest) { var path=new ArrayList<Long>();for(long k=end;;k=parent.get(k)){path.add(k);if(k==start)break;}Collections.reverse(path);best=path;longest=span; }
break;
}
}
var cells=new LinkedHashMap<Long,Cell>();var path=new ArrayList<Point>(); long id=0x300000000L;
if(longest>=radius*.7) for(int i=1;i<best.size();i++) {
long a=best.get(i-1),b=best.get(i);int dx=Integer.signum(x(b)-x(a)),dz=Integer.signum(z(b)-z(a));
for(int n=0;n<=STEP;n++) {
int cx=x(a)+dx*n,cz=z(a)+dz*n;path.add(new Point(cx,cz));
int center=rock.top(cx,cz);
for(int side=-2;side<=2;side++) {
int px=cx+dz*side,pz=cz-dx*side,top=rock.top(px,pz);
if(top<140 || Math.abs(top-center)>5) continue;
// The bed stays in existing rock. Higher neighbouring faces become natural banks.
int level=Math.min(top-1,center-1),bed=level-2;boolean support=true;
for(int y=bed-3;y<=bed;y++) if(!rock.solid(px,y,pz)) {support=false;break;}
if(support) cells.put(key(px,pz),new Cell(px,pz,level,bed,top,side==0?ShoreMaterial.GRAVEL:ShoreMaterial.CLAY,id,2));
}
}
}
System.getLogger(River30.class.getName()).log(System.Logger.Level.INFO,"River30 connectedNodes="+nodes.size()+" span="+longest+" columns="+rock.tops.size());
var features=new ArrayList<Feature>();
if(!cells.isEmpty()) features.add(feature(id,Kind.RIVER,cells.values(),path));
var plan=PopulationHydrology.additional30(cells,features,List.of(),rock.tops.size(),rock.samples);
return openBanks(seed,plan,sampler,6);
}
/** Cut only a short bank. Nothing is built beneath a fall and its landing is unrestricted. */
public static Plan openBanks(long seed,Plan water,Sampler sampler,int maximum) {
var cuts=new LinkedHashMap<Long,Cell>();var features=new ArrayList<Feature>();var spills=new ArrayList<Spill>();
var mouths=new ArrayList<Point>();
var candidates=water.cells().stream().filter(Cell::hasWater).sorted(Comparator
.<Cell>comparingLong(c->mix(seed^key(c.x(),c.z()))).thenComparingInt(Cell::x).thenComparingInt(Cell::z)).toList();
for(var wet:candidates) {
if(mouths.size()>=maximum) break;
if(mouths.stream().anyMatch(p->Math.hypot(p.x()-wet.x(),p.z()-wet.z())<48)) continue;
for(var d:DIR) {
if(water.cellAt(wet.x()+d[0],wet.z()+d[1])!=null) continue;
int reach=0;
for(int n=1;n<=5;n++) {
int px=wet.x()+d[0]*n,pz=wet.z()+d[1]*n;
if(sampler.sample(px,wet.waterY(),pz)<=0 && sampler.sample(px,wet.waterY()-3,pz)<=0
&& sampler.sample(px,wet.waterY()-6,pz)<=0) {reach=n;break;}
}
if(reach==0) continue;
long id=0x310000000L+(mix(seed)&0xfffffffL)*16+mouths.size();var local=new LinkedHashMap<Long,Cell>();
boolean conflict=false;
for(int n=1;n<=reach;n++) for(int side=-1;side<=1;side++) {
int px=wet.x()+d[0]*n+d[1]*side,pz=wet.z()+d[1]*n-d[0]*side;
var old=water.cellAt(px,pz);
if(old!=null && old.hasWater()) {conflict=true;continue;}
local.put(key(px,pz),new Cell(px,pz,-1,wet.waterY()-1,wet.waterY()+2,ShoreMaterial.STONE,id,0));
}
if(conflict) continue;
cuts.putAll(local);features.add(feature(id,Kind.OUTLET,local.values(),List.of()));mouths.add(new Point(wet.x(),wet.z()));
var source=new Position(wet.x(),wet.waterY(),wet.z());
var outlet=new Position(wet.x()+d[0]*reach,wet.waterY(),wet.z()+d[1]*reach);
var bounds=new FlowBounds(Math.min(source.x(),outlet.x())-3,0,Math.min(source.z(),outlet.z())-3,
Math.max(source.x(),outlet.x())+3,wet.waterY()+3,Math.max(source.z(),outlet.z())+3);
spills.add(new Spill(id,wet.featureId(),-1,source,outlet,List.of(source,outlet),bounds));break;
}
}
return water.overlay30(PopulationHydrology.additional30(cuts,features,spills,0,0));
}
private static Feature feature(long id,Kind kind,Collection<Cell> cells,List<Point> path) {
int x0=Integer.MAX_VALUE,z0=x0,x1=Integer.MIN_VALUE,z1=x1,water=-1,count=0;
for(var c:cells){x0=Math.min(x0,c.x());z0=Math.min(z0,c.z());x1=Math.max(x1,c.x());z1=Math.max(z1,c.z());if(c.hasWater()){water=c.waterY();count++;}}
return new Feature(id,kind,water,path,count,new Bounds(x0,z0,x1,z1));
}
private static long mix(long n){n=(n^(n>>>30))*0xbf58476d1ce4e5b9L;n=(n^(n>>>27))*0x94d049bb133111ebL;return n^(n>>>31);}
}
@@ -109,6 +109,13 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
if (current == null || heightAccessor == null) throw new IllegalStateException("Aerial sampling before world attachment");
return current.generator().getBaseColumn(x, z, heightAccessor, current.random());
}
public boolean aerialDryWitness30(int x, int groundY, int z) {
var current = originContext;
if (current == null) throw new IllegalStateException("Aerial sampling before world attachment");
for (var cell : PopulationHydrologyRuntime.cellsAt(current.generator(), current.random(), x, z))
if (groundY >= cell.bedY() && groundY <= Math.max(cell.carveTop(), cell.waterY()) + 1) return false;
return true;
}
private volatile fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlan23 mineralSanctuary23 = fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlan23.EMPTY;
private boolean mineralPrepared23;
@@ -399,7 +406,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain21(root, terrain.generator(), terrain.random(), heightAccessor,
pos -> protectsDecoration(pos) || protectsCore22(pos));
long started = System.nanoTime();
mineralSanctuary23 = fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlanner23.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(),
mineralSanctuary23 = fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlanner23.plan30(root.seed(), root.centerX(), root.centerZ(), root.diameter(),
new fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlanner23.Terrain() {
@Override public boolean owns(int x, int z) { return sampled.owns(x, z); }
@Override public int surfaceY(int x, int z) { return sampled.ground(x, z).top(); }
@@ -22,6 +22,8 @@ public final class AerialPlanner24 {
NoiseColumn column(int x, int z);
/** Planned structures may add rock above the original noise column or cut its support. */
default boolean reserved(int x, int z, int minY, int maxY) { return false; }
/** The raw noise surface may subsequently be cut into a pond or an open river bank. */
default boolean dryWitness(int x, int groundY, int z) { return true; }
}
public static AerialPlan24 forWorld(ServerLevel level, SanctuaryChunkGenerator generator) {
@@ -30,6 +32,9 @@ public final class AerialPlanner24 {
generator.prepareAerialSampling24(level);
return plan(level.getSeed(), generator.initialDiameter(), new Terrain() {
@Override public NoiseColumn column(int x, int z) { return generator.aerialBaseColumn24(x, z); }
@Override public boolean dryWitness(int x, int groundY, int z) {
return generator.aerialDryWitness30(x, groundY, z);
}
@Override public boolean reserved(int x, int z, int minY, int maxY) {
return generator.intersectsExperimentalStructures(x, minY, z, x, maxY, z);
}
@@ -81,7 +86,8 @@ public final class AerialPlanner24 {
cz = (int) Math.round(Math.sin(angle) * distance);
int top = probe.surface(cx, cz);
if (top < 120 || probe.terrain.reserved(cx, cz, top, top + 3)) continue;
shore = new Shore(cx, top + 1, cz);
shore = dryViewpoint(probe, cx, cz);
if (shore == null) continue;
int highest = top;
for (int ox = -worldWidth / 2; ox <= worldWidth / 2; ox += 4)
for (int oz = -worldDepth / 2; oz <= worldDepth / 2; oz += 4)
@@ -196,6 +202,19 @@ public final class AerialPlanner24 {
return true;
}
private record Shore(int x, int y, int z) {}
private static Shore dryViewpoint(Probe probe, int x, int z) {
// Keep the site above the same terrain. Move only its saved walking viewpoint
// to a nearby dry bank when the centre is subsequently carved by hydrology.
for (int r = 0; r <= 12; r += 2) for (int dx = -r; dx <= r; dx += 2)
for (int dz = -r; dz <= r; dz += 2) {
if (Math.max(Math.abs(dx), Math.abs(dz)) != r) continue;
int top = probe.surface(x + dx, z + dz);
if (top >= 120 && probe.terrain.dryWitness(x + dx, top, z + dz)
&& !probe.terrain.reserved(x + dx, z + dz, top, top + 3))
return new Shore(x + dx, top + 1, z + dz);
}
return null;
}
private static final class Probe {
final Terrain terrain; final int radius;
final Map<Long, NoiseColumn> columns = new HashMap<>();
@@ -5,7 +5,7 @@ import java.util.Map;
/** Finite terrain search: at most48 horizontal candidates, four orientations, three depths. */
public final class MineralSanctuaryPlanner23 {
public static final int MAX_COLUMNS = 10_000, MAX_SAMPLES = 1_200_000;
public static final int MAX_COLUMNS = 24_000, MAX_SAMPLES = 2_400_000;
private MineralSanctuaryPlanner23() {}
public interface Terrain {
boolean owns(int x, int z);
@@ -14,12 +14,19 @@ public final class MineralSanctuaryPlanner23 {
boolean protectedAt(int x, int y, int z);
}
public static MineralSanctuaryPlan23 plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
return search(seed, cx, cz, diameter, terrain, false);
}
public static MineralSanctuaryPlan23 plan30(long seed, int cx, int cz, int diameter, Terrain terrain) {
return search(seed, cx, cz, diameter, terrain, true);
}
private static MineralSanctuaryPlan23 search(long seed, int cx, int cz, int diameter, Terrain terrain, boolean exposedFallback) {
if (diameter != 512 && diameter != 724 && diameter != 1024) throw new IllegalArgumentException("Unsupported mineral island size");
var p = new Probe(terrain);
try {
for (int attempt = 0; attempt < 48; attempt++) {
for (int attempt = 0; attempt < (exposedFallback ? 96 : 48); attempt++) {
double angle = unit(mix(seed ^ 0x23CA7EL)) * Math.PI * 2 + attempt * 2.399963229728653;
double radius = diameter * (.17 + .045 * (attempt % 4));
p.exposed = attempt >= 48;
double radius = diameter * (p.exposed ? .27 + .04 * (attempt % 5) : .17 + .045 * (attempt % 4));
int x = cx + (int) Math.round(Math.cos(angle) * radius), z = cz + (int) Math.round(Math.sin(angle) * radius);
int surface = p.surface(x, z);
if (surface < 80) { p.reject("candidate-surface"); continue; }
@@ -39,7 +46,7 @@ public final class MineralSanctuaryPlanner23 {
var result = new MineralSanctuaryPlan23(c, p.samples, p.columns.size(), p.rejections);
System.getLogger(MineralSanctuaryPlanner23.class.getName()).log(System.Logger.Level.INFO,
"Mineral23 accepted=" + (c != null) + " columns=" + p.columns.size() + " samples=" + p.samples
+ " chamber=" + c + " rejected=" + p.rejections);
+ " exposed=" + p.exposed + " chamber=" + c + " rejected=" + p.rejections);
return result;
}
/** The fissure ends in existing traversable air (a cave or the exposed island side), not sealed rock. */
@@ -81,9 +88,9 @@ public final class MineralSanctuaryPlanner23 {
tested++;
}
}
if (columns == 0 || covered * 5 < columns * 3 || coveredQuadrants != 15)
if (columns == 0 || covered * 5 < columns * (p.exposed ? 1 : 3) || (p.exposed ? Integer.bitCount(coveredQuadrants) < 2 : coveredQuadrants != 15))
return p.reject("coarse-cover-distribution");
return tested > 0 && rock * 5 >= tested * 3 || p.reject("coarse-rock");
return tested > 0 && rock * 5 >= tested * (p.exposed ? 1 : 3) || p.reject("coarse-rock");
}
/** Reject unsupported candidates using a small shared column lattice before reading the full floor. */
private static boolean coarseBearings(MineralSanctuaryPlan23.Chamber c, Probe p) {
@@ -98,12 +105,12 @@ public final class MineralSanctuaryPlanner23 {
else gaps.add(point);
}
// Sampling is deliberately permissive about the proportion; dense certification still requires60%.
if (anchors.size() < gaps.size()) return p.reject("coarse-anchor-majority");
if (anchors.size() * (p.exposed ? 2 : 1) < gaps.size()) return p.reject("coarse-anchor-majority");
for (var gap : gaps) {
boolean supported = false;
for (var anchor : anchors) {
int dx = gap[0] - anchor[0], dz = gap[1] - anchor[1];
if (dx * dx + dz * dz <= 100) { supported = true; break; }
if (dx * dx + dz * dz <= (p.exposed ? 324 : 100)) { supported = true; break; }
}
if (!supported) return p.reject("coarse-anchor-distance");
}
@@ -137,14 +144,14 @@ public final class MineralSanctuaryPlanner23 {
anchored[x][z] = true; direct++;
}
}
if (columns == 0 || direct * 5 < columns * 3) return p.reject("floor-anchor-majority");
if (covered * 5 < columns * 3) return p.reject("full-cover-distribution");
if (columns == 0 || direct * 5 < columns * (p.exposed ? 2 : 3)) return p.reject("floor-anchor-majority");
if (covered * 5 < columns * (p.exposed ? 1 : 3)) return p.reject("full-cover-distribution");
for (int z = 0; z < c.depth(); z++) for (int x = 0; x < c.width(); x++) {
if (!footprint[x][z] || anchored[x][z]) continue;
boolean nearby = false;
for (int dz = -10; dz <= 10 && !nearby; dz++) for (int dx = -10; dx <= 10; dx++) {
for (int dz = p.exposed ? -18 : -10; dz <= (p.exposed ? 18 : 10) && !nearby; dz++) for (int dx = p.exposed ? -18 : -10; dx <= (p.exposed ? 18 : 10); dx++) {
int ax = x + dx, az = z + dz;
if (dx * dx + dz * dz <= 100 && ax >= 0 && ax < c.width() && az >= 0 && az < c.depth()
if (dx * dx + dz * dz <= (p.exposed ? 324 : 100) && ax >= 0 && ax < c.width() && az >= 0 && az < c.depth()
&& anchored[ax][az]) { nearby = true; break; }
}
if (!nearby) return p.reject("floor-anchor-distance");
@@ -158,7 +165,7 @@ public final class MineralSanctuaryPlanner23 {
}
private static final class Probe {
final Terrain terrain; final Map<Long, Integer> columns = new HashMap<>();
final Map<String, Integer> rejections = new java.util.TreeMap<>(); int samples;
final Map<String, Integer> rejections = new java.util.TreeMap<>(); int samples; boolean exposed;
Probe(Terrain terrain) { this.terrain = java.util.Objects.requireNonNull(terrain); }
int surface(int x, int z) {
if (!terrain.owns(x, z)) return -1;
@@ -22,17 +22,17 @@ public final class TransitPlanner29 {
if (radius * 2 >= diameter / 4) radii.add(radius);
try {
// Cheap, cached entrance scans precede any expensive underground volume scan.
search: for (int shift : new int[]{-diameter / 8, diameter / 8, -diameter / 16, diameter / 16, 0,
-3 * diameter / 16, 3 * diameter / 16})
for (int advance : new int[]{0, -17, 17, -34, 34, -51, 51})
for (int radius : radii) for (boolean alongX : new boolean[]{true, false}) {
search: for (int advance : new int[]{0, -17, 17, -34, 34, -51, 51})
for (int radius : radii) for (boolean alongX : new boolean[]{true, false})
for (int shift : new int[]{-diameter / 8, diameter / 8, -diameter / 16, diameter / 16, 0,
-3 * diameter / 16, 3 * diameter / 16}) {
if (++layouts > 320 || p.columns.size() >= 3_000) break search;
int ax = cx + (alongX ? advance - radius : shift), az = cz + (alongX ? shift : advance - radius);
int bx = cx + (alongX ? advance + radius : shift), bz = cz + (alongX ? shift : advance + radius);
int a = entrance(ax, az, p), b = entrance(bx, bz, p);
if (a < 0 || b < 0 || Math.abs(a - b) > 95) continue;
entries.add(new Alignment(ax, az, bx, bz, a, b, alongX));
if (entries.size() >= 24) break search;
if (entries.size() >= 48) break search;
}
// Preserve every complete route already accepted by the primary search. Only after
// its failure may an incompatible optional footing be omitted, using other bearings.
@@ -64,5 +64,6 @@
"xz_scale": 2.0,
"y_scale": 2.0
},
"terraces_29": true
"terraces_29": false,
"relief_30": true
}
@@ -64,5 +64,6 @@
"xz_scale": 2.0,
"y_scale": 2.0
},
"terraces_29": true
"terraces_29": false,
"relief_30": true
}
@@ -64,5 +64,6 @@
"xz_scale": 2.0,
"y_scale": 2.0
},
"terraces_29": true
"terraces_29": false,
"relief_30": true
}
@@ -0,0 +1,27 @@
package fr.koka.sanctuary.worldgen;
public final class River30Smoke {
public static void main(String[] args) {
for (int radius : new int[]{256,362,512}) {
PopulationHydrology.Sampler rock=(x,y,z)->Math.hypot(x,z)<radius*.82 && y>=60 && y<=230 ? 1 : -1;
var plan=River30.create(42,radius,rock);
var river=plan.features().stream().filter(f->f.kind()==PopulationHydrology.Kind.RIVER).findFirst().orElseThrow();
require(river.bounds().maxX()-river.bounds().minX()>radius || river.bounds().maxZ()-river.bounds().minZ()>radius,"Island-wide route");
require(plan.spills().size()>0,"A nearby edge can discharge into native void");
for(var c:plan.cells()) {
if(plan.isOutletCell(c)) {require(c.sedimentDepth()==0&&!c.hasWater(),"Outlet only removes its bank");continue;}
require(Math.hypot(c.x(),c.z())>=49,"Arrival remains natural");
for(int y=c.bedY()-c.sedimentDepth()-1;y<=c.bedY();y++) require(rock.sample(c.x(),y,c.z())>0,"Bed consists of native rock");
}
for(var spill:plan.spills()) require(Math.abs(spill.outlet().x()-spill.source().x())+Math.abs(spill.outlet().z()-spill.source().z())<=5,"Bank cut at most five blocks");
require(plan.cells().equals(River30.create(42,radius,rock).cells()),"Immutable deterministic plan");
}
for(long seed:new long[]{0,42,2026,-7228211907433324401L}) {
int untouched=0;
for(int x=-350;x<=350;x+=10)for(int z=-350;z<=350;z+=10)if(Math.hypot(x,z)<350 && Relief30.influence(seed,x,z,362)==0)untouched++;
require(untouched>1000,"Wide undeformed regions remain between the three deformation areas");
}
System.out.println("River30: island-scale supported channels, five-block open banks, natural arrival, deterministic plans and separated relief regions passed.");
}
static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "0.1.0-alpha.29"
version = "0.1.0-alpha.30"
pack-format = "packwiz:1.1.0"
[index]