fix(worldgen): restore curved rivers and global relief in alpha30.1
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.1.0-alpha.30.1 — 2026-09-12
|
||||||
|
|
||||||
|
Retrait des rivières sur grille de la 30 ; brèches de cascade sur les anciennes
|
||||||
|
rivières courbes et leurs bassins. Retour du relief global de la 28, sauf une
|
||||||
|
bande étroite de plateaux naturels pour les accès aux traversées.
|
||||||
|
Voir [le correctif](docs/generation-alpha30.1.md). Nouveau monde nécessaire.
|
||||||
|
|
||||||
## 0.1.0-alpha.30 — 2026-09-12
|
## 0.1.0-alpha.30 — 2026-09-12
|
||||||
|
|
||||||
Trois zones de déformation 3D, plateaux naturels entre les massifs, grande rivière
|
Trois zones de déformation 3D, plateaux naturels entre les massifs, grande rivière
|
||||||
|
|||||||
@@ -21,6 +21,16 @@ 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
|
marais souterrains et la restauration de vrais modèles d’épaves vanilla. Voir le
|
||||||
[chantier et ses limites](docs/generation-alpha27.md).
|
[chantier et ses limites](docs/generation-alpha27.md).
|
||||||
|
|
||||||
|
## Alpha.30.1 — rivières courbes et relief global
|
||||||
|
|
||||||
|
Les rivières angulaires de la 30 sont retirées. Les anciens cours d’eau et
|
||||||
|
bassins peuvent déborder par une courte brèche dans leur berge. Le relief 3D
|
||||||
|
redevient global, avec une seule bande étroite de plateaux naturels pour
|
||||||
|
faciliter les accès aux traversées.
|
||||||
|
|
||||||
|
Créer un **nouveau monde**. Voir [le correctif](docs/generation-alpha30.1.md)
|
||||||
|
et [ses vérifications](docs/testing-alpha30.1.md).
|
||||||
|
|
||||||
## Alpha.30 — plateaux, rivières ouvertes et traversées
|
## Alpha.30 — plateaux, rivières ouvertes et traversées
|
||||||
|
|
||||||
La déformation 3D se concentre dans trois grandes zones ; les plateaux naturels
|
La déformation 3D se concentre dans trois grandes zones ; les plateaux naturels
|
||||||
|
|||||||
@@ -1000,3 +1000,12 @@ 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.
|
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.
|
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).
|
Voir [le périmètre](generation-alpha30.md) et [les preuves](testing-alpha30.md).
|
||||||
|
|
||||||
|
|
||||||
|
### WG-33 — Rivières courbes et relief global — alpha.30.1
|
||||||
|
|
||||||
|
Branche `codex/curved-rivers-alpha30-1`. Retirer la rivière angulaire de la 30,
|
||||||
|
retrouver la déformation globale de la 28 avec une zone restreinte de plateau,
|
||||||
|
et ouvrir les berges des anciens cours d’eau vers les espaces vides proches.
|
||||||
|
Conserver les recherches de traversées et de sanctuaire améliorées en 30.
|
||||||
|
Voir [le correctif](generation-alpha30.1.md) et [les essais](testing-alpha30.1.md).
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Alpha.30.1 — retour aux rivières courbes et au relief global
|
||||||
|
|
||||||
|
Correctif demandé après l’essai de la 30 : les nouvelles rivières angulaires
|
||||||
|
évoquaient des canaux construits, et la limitation du relief à trois secteurs
|
||||||
|
rendait trop de surface plate.
|
||||||
|
|
||||||
|
## Changements
|
||||||
|
|
||||||
|
- Suppression du générateur de rivière sur grille ajouté en 30 et de son plan
|
||||||
|
global. L’hydrologie retrouve exclusivement les rivières courbes, bassins,
|
||||||
|
terrasses et sources déjà présents dans `PopulationHydrology`.
|
||||||
|
- Retour de la déformation 3D signée de la 28 sur l’ensemble du terrain, sauf
|
||||||
|
une bande étroite de plateaux naturels vers Z = −rayon/4. Elle suit un axe
|
||||||
|
d’implantation possible des accès de traversée. La transition est progressive ;
|
||||||
|
le champ antérieur y est retrouvé sans dalle ajoutée ni remplissage des grottes.
|
||||||
|
Les tests synthétiques gardent le poids global exact sur plus de 85 % de
|
||||||
|
l’emprise circulaire, et un cœur de plateau sur moins de 8 %.
|
||||||
|
- Brèches recherchées dans les **berges des anciens cours d’eau**, avec priorité
|
||||||
|
aux rivières, puis aux lacs et petits bassins. Les berges sèches déjà garnies
|
||||||
|
de sédiments ne bloquent plus cette recherche, contrairement à la 30.
|
||||||
|
- Chaque brèche est longue de cinq blocs maximum et large de trois blocs.
|
||||||
|
Son fond descend légèrement pour laisser l’écoulement vanilla atteindre la
|
||||||
|
lèvre. Elle ne crée ni nouvelle source, ni rivière supplémentaire, ni
|
||||||
|
fondation sous la chute. Aucune vérification d’un bassin de réception :
|
||||||
|
l’eau peut descendre sur l’île ou dans le vide.
|
||||||
|
- Jusqu’à six brèches espacées par région, uniquement lorsqu’une rive est
|
||||||
|
assez proche d’un espace ouvert. Un bassin situé loin du bord reste fermé.
|
||||||
|
|
||||||
|
Les tracés courbes ne sont pas redessinés par la passe de brèche. Le retour du
|
||||||
|
relief global peut toutefois changer leur implantation d’une version à l’autre,
|
||||||
|
puisqu’ils recherchent leur place dans la roche disponible.
|
||||||
|
|
||||||
|
Les corrections de traversées, le sanctuaire minéral, les mines vanilla,
|
||||||
|
les navires restaurés et les deux îlots restent présents. La 30.1 conserve
|
||||||
|
Minecraft **26.3-pre-2**, les tailles **512 / 724 / 1 024**, Moyen par défaut,
|
||||||
|
et le bouton natif Personnaliser. Aucune nouvelle interface à traduire.
|
||||||
|
|
||||||
|
## Essai et distribution
|
||||||
|
|
||||||
|
Version mod et pack : **0.1.0-alpha.30.1**, branche
|
||||||
|
`codex/curved-rivers-alpha30-1`. **Créer un nouveau monde** : le prototype
|
||||||
|
conserve `sanctuary:island_v24` ; aucune migration ou régénération de sauvegarde
|
||||||
|
personnelle n’est effectuée. Les structures conservent leurs options existantes.
|
||||||
|
|
||||||
|
Résultats effectivement vérifiés et coordonnées dans
|
||||||
|
[les essais](testing-alpha30.1.md). La distribution suit [packwiz](packwiz.md).
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Alpha.30.1 — vérifications
|
||||||
|
|
||||||
|
Essais natifs sur **Mac**, le 12 septembre 2026, en serveur de développement
|
||||||
|
sans rendu client. Aucun essai Windows natif de ce correctif n’est revendiqué.
|
||||||
|
|
||||||
|
## Moyen 724, graine 0
|
||||||
|
|
||||||
|
Processus **64315** : **24/24 GameTests passent**, en 2 min 30 s pour la commande.
|
||||||
|
Reçus dans `build/alpha301-evidence/medium-0/`.
|
||||||
|
|
||||||
|
Deux brèches sont rattachées à la véritable **ancienne rivière courbe**, et non
|
||||||
|
à un nouveau tracé. Sources : **13 247 79** et **-105 247 -69** ; sorties :
|
||||||
|
**13 246 82** et **-105 246 -65**. Elles traversent respectivement trois et
|
||||||
|
quatre blocs de berge. Le test sous ticks réels observe l’écoulement en dessous.
|
||||||
|
Une lecture séparée de la sauvegarde confirme des colonnes continues d’eau aux
|
||||||
|
sorties, de **Y245 à Y225** et de **Y246 à Y226** à ce stade de simulation.
|
||||||
|
Aucune source n’a été ajoutée par la brèche. Cela ne prétend pas avoir attendu
|
||||||
|
leur propagation jusqu’au fond du monde. Preuve : `saved-water-columns.json`.
|
||||||
|
|
||||||
|
La traversée est réellement générée : **19 modules, 28 chunks FULL, 272 blocs
|
||||||
|
entre les accès, 478 pas contrôlés et 62 points accessibles**. Entrées :
|
||||||
|
**90 248 -144** et **90 225 128**. Le sanctuaire possède son départ natif et
|
||||||
|
un sol central libre au-dessus. Les quatre satellites sont contrôlés.
|
||||||
|
|
||||||
|
Une mine vanilla de **143 pièces** est retenue ; le test inspecte un échantillon
|
||||||
|
de 16 chunks FULL. Il trouve notamment 244 planches de chêne, 200 barrières,
|
||||||
|
102 rails, 50 toiles et un spawner dans les pièces chargées. Point de visite :
|
||||||
|
**152 175 -120**. Ce n’est pas une visite de la totalité de la mine.
|
||||||
|
|
||||||
|
Comparaison avec le relief de la 28 : **6 348 densités inférieures inchangées**,
|
||||||
|
et seulement **466 / 74 060** signes modifiés dans les échantillons hauts
|
||||||
|
(**0,63 %**). Ce changement est localisé dans la bande de plateau ; le reste
|
||||||
|
utilise exactement le poids global de déformation précédent.
|
||||||
|
|
||||||
|
## Grand 1 024, graine -7228211907433324401
|
||||||
|
|
||||||
|
Le premier passage natif (processus **64637**) vérifie la traversée complète :
|
||||||
|
**23 modules, 52 chunks FULL, 374 blocs entre les accès, 788 pas contrôlés et
|
||||||
|
71 points accessibles**. Entrées : **192 238 -195** et **192 277 179**.
|
||||||
|
Le sanctuaire minéral et les quatre satellites sont également retenus.
|
||||||
|
|
||||||
|
La comparaison au relief 28 conserve **8 112 densités inférieures** ;
|
||||||
|
**940 / 94 640** signes hauts diffèrent (**0,99 %**). La recherche native
|
||||||
|
échantillonnée observe des portails en ruine et un camp, mais pas de mine dans
|
||||||
|
ce témoin limité. La preuve de mine de cette livraison est celle du Moyen 0.
|
||||||
|
|
||||||
|
La région centrale de cette graine contient des lacs et bassins, sans ancienne
|
||||||
|
rivière admise. Le premier passage a donc révélé une assertion de test trop
|
||||||
|
stricte : elle exigeait une rivière sur chaque témoin, contrairement au caractère
|
||||||
|
conditionnel de l’hydrologie d’origine. Le test est corrigé sans forcer une
|
||||||
|
rivière dans le générateur. Le témoin spécifique de berge de rivière peut être
|
||||||
|
exigé avec `-PsanctuaryRequireCurvedRiverBank=true` sur une graine adaptée.
|
||||||
|
|
||||||
|
Le redémarrage (processus **65123**) passe les **24/24 GameTests** :
|
||||||
|
`check build` réussit en **4 min 19 s**. Le contrôle d’eau inspecte 8 chunks
|
||||||
|
FULL et trouve 7 positions d’eau sous les deux brèches testées. Sorties :
|
||||||
|
**-14 236 41** et **-50 237 -80**. La traversée et le sol du sanctuaire sont
|
||||||
|
revérifiés. Les satellites sont rechargés après pillage, extraction d’un
|
||||||
|
villageois et démolition partielle : les témoins ne sont pas reconstruits et
|
||||||
|
le villageois n’est pas dupliqué. Reçus :
|
||||||
|
`build/alpha301-evidence/large-user/reload/`.
|
||||||
|
|
||||||
|
Commande de vérification :
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./gradlew check build -PsanctuaryTestDiameter=1024 \
|
||||||
|
-PsanctuaryTestSeed=-7228211907433324401 -PsanctuaryDensityMaps=false \
|
||||||
|
-PsanctuaryAerial24Tests=true -PsanctuaryAerial24Phase=reload1 \
|
||||||
|
-PsanctuaryExpansionReload=true -PsanctuaryTransit29Tests=true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tests ciblés
|
||||||
|
|
||||||
|
`river30Smoke` passe : ouverture à travers une berge sèche existante, conservation
|
||||||
|
exacte des points du tracé et des cellules d’eau, absence de nouvelle source,
|
||||||
|
déterminisme et conservation d’une berge intérieure éloignée du vide.
|
||||||
|
Une brèche de cinq blocs possède une légère descente pour l’écoulement vanilla.
|
||||||
|
Les trois tailles conservent le poids global exact sur plus de 85 % de leur
|
||||||
|
emprise circulaire ; le cœur de plateau couvre moins de 8 %.
|
||||||
|
|
||||||
|
Les brèches restent conditionnelles à la proximité d’un espace ouvert. Ces tests
|
||||||
|
ne justifient pas d’annoncer une cascade pour tous les bassins ou toutes les graines.
|
||||||
|
La taille Petit est couverte par les tests de champ et les tests synthétiques ;
|
||||||
|
elle n’a pas fait l’objet d’un nouveau passage natif complet pour ce correctif.
|
||||||
|
`assemblePack` passe après le `check build` réussi ; la tâche `:sanctuary:check`
|
||||||
|
est exclue de cette seconde commande pour ne pas répéter la suite inchangée.
|
||||||
|
Le MRpack autonome, le JAR intégré et le ZIP d’amorçage Prism sont vérifiés
|
||||||
|
localement : versions, index, empreintes et contenu concordent. Reçus :
|
||||||
|
`build/alpha301-artifacts.json` et `build/alpha301-delivery-local.json`.
|
||||||
|
La publication et la synchronisation sont consignées dans [Distribution](packwiz.md). Créer un nouveau monde pour l’essai ; aucune sauvegarde personnelle
|
||||||
|
n’est régénérée.
|
||||||
+2
-2
@@ -8,6 +8,6 @@ loader_version=0.19.5
|
|||||||
loom_version=1.17.20
|
loom_version=1.17.20
|
||||||
fabric_api_version=0.160.0+26.3
|
fabric_api_version=0.160.0+26.3
|
||||||
|
|
||||||
mod_version=0.1.0-alpha.30
|
mod_version=0.1.0-alpha.30.1
|
||||||
pack_version=0.1.0-alpha.30
|
pack_version=0.1.0-alpha.30.1
|
||||||
maven_group=fr.koka.sanctuary
|
maven_group=fr.koka.sanctuary
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ tasks.named('runGameTest') {
|
|||||||
systemProperty('sanctuary.test.transit29', providers.gradleProperty('sanctuaryTransit29Tests').getOrElse('false'))
|
systemProperty('sanctuary.test.transit29', providers.gradleProperty('sanctuaryTransit29Tests').getOrElse('false'))
|
||||||
systemProperty('sanctuary.test.transit29.planOnly', providers.gradleProperty('sanctuaryTransit29PlanOnly').getOrElse('false'))
|
systemProperty('sanctuary.test.transit29.planOnly', providers.gradleProperty('sanctuaryTransit29PlanOnly').getOrElse('false'))
|
||||||
systemProperty('sanctuary.test.requireMineshaft29', providers.gradleProperty('sanctuaryRequireMineshaft29').getOrElse('false'))
|
systemProperty('sanctuary.test.requireMineshaft29', providers.gradleProperty('sanctuaryRequireMineshaft29').getOrElse('false'))
|
||||||
|
systemProperty('sanctuary.test.requireCurvedRiverBank', providers.gradleProperty('sanctuaryRequireCurvedRiverBank').getOrElse('false'))
|
||||||
systemProperty('sanctuary.expansion.structureDiagnostics', providers.gradleProperty('sanctuaryStructureDiagnostics').getOrElse('false'))
|
systemProperty('sanctuary.expansion.structureDiagnostics', providers.gradleProperty('sanctuaryStructureDiagnostics').getOrElse('false'))
|
||||||
systemProperty('sanctuary.test.heritage21', providers.gradleProperty('sanctuaryHeritage21Tests').getOrElse('false'))
|
systemProperty('sanctuary.test.heritage21', providers.gradleProperty('sanctuaryHeritage21Tests').getOrElse('false'))
|
||||||
systemProperty('sanctuary.test.lostCity21', providers.gradleProperty('sanctuaryLostCity21Tests').getOrElse('false'))
|
systemProperty('sanctuary.test.lostCity21', providers.gradleProperty('sanctuaryLostCity21Tests').getOrElse('false'))
|
||||||
|
|||||||
+13
-7
@@ -15,17 +15,23 @@ public final class River30WorldGameTests {
|
|||||||
@GameTest(maxTicks=12000)
|
@GameTest(maxTicks=12000)
|
||||||
public void riverAndOpenBanksUseRealTerrain(GameTestHelper helper) throws Exception {
|
public void riverAndOpenBanksUseRealTerrain(GameTestHelper helper) throws Exception {
|
||||||
var level=helper.getLevel(); var generator=(SanctuaryChunkGenerator)level.getChunkSource().getGenerator();
|
var level=helper.getLevel(); var generator=(SanctuaryChunkGenerator)level.getChunkSource().getGenerator();
|
||||||
var binding=generator.starterContext();var plan=PopulationHydrologyRuntime.river30(binding.generator(),binding.random());
|
var binding=generator.starterContext();var plan=PopulationHydrologyRuntime.regionPlan(binding.generator(),binding.random(),new IslandCapacity.Region(0,0)).water();
|
||||||
var report=new LinkedHashMap<String,Object>();report.put("seed",level.getSeed());report.put("diameter",generator.initialDiameter());
|
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("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());
|
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");
|
Path initial=Path.of("diagnostics/alpha301/river-planned.json"); Files.createDirectories(initial.getParent());
|
||||||
|
Files.writeString(initial,new GsonBuilder().setPrettyPrinting().create().toJson(report));
|
||||||
|
report.put("curved_river_count",plan.features().stream().filter(f->f.kind()==PopulationHydrology.Kind.RIVER).count());
|
||||||
if(level.structureManager().shouldGenerateStructures()&&generator.experimentalStructures())
|
if(level.structureManager().shouldGenerateStructures()&&generator.experimentalStructures())
|
||||||
helper.assertFalse(generator.mineralSanctuary23().isEmpty(),"The mineral sanctuary must be present in the release witness");
|
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 selected=plan.cells().stream().filter(PopulationHydrology.Cell::hasWater).toList();
|
||||||
var witnesses=new ArrayList<PopulationHydrology.Cell>();
|
var witnesses=new ArrayList<PopulationHydrology.Cell>();
|
||||||
for(int i=0;i<6;i++)witnesses.add(selected.get(i*(selected.size()-1)/5));
|
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>();
|
var falls=plan.spills().stream().filter(f -> plan.features().stream().anyMatch(feature -> feature.kind()==PopulationHydrology.Kind.OUTLET && feature.id()==f.id())).limit(2).toList();
|
||||||
|
if(Boolean.getBoolean("sanctuary.test.requireCurvedRiverBank"))
|
||||||
|
helper.assertTrue(falls.stream().anyMatch(f -> plan.features().stream().anyMatch(feature -> feature.id()==f.fromFeatureId() && feature.kind()==PopulationHydrology.Kind.RIVER)),
|
||||||
|
"The required witness opens a bank of an original curved river");
|
||||||
|
helper.assertTrue(plan.features().stream().noneMatch(f -> f.id()==0x300000000L),"The rejected grid river is absent");var chunks=new LinkedHashSet<ChunkPos>();
|
||||||
witnesses.forEach(c->chunks.add(new ChunkPos(c.x()>>4,c.z()>>4)));
|
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));}
|
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();
|
var mineral=generator.mineralSanctuary23().chamber();
|
||||||
@@ -48,13 +54,13 @@ public final class River30WorldGameTests {
|
|||||||
helper.assertTrue(level.getBlockState(floor).isSolid()&&level.getBlockState(floor.above()).isAir(),"The central mineral floor and guardian space are really generated");
|
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()));
|
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");
|
for(var c:witnesses)helper.assertTrue(level.getFluidState(new BlockPos(c.x(),c.waterY(),c.z())).is(net.minecraft.tags.FluidTags.WATER),"Generated river water remains at its supported local bed");
|
||||||
int flowing=0;
|
int flowing=0;
|
||||||
for(var fall:falls)for(int dx=-1;dx<=1;dx++)for(int dz=-1;dz<=1;dz++)
|
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(level.getFluidState(new BlockPos(fall.outlet().x()+dx,fall.outlet().y()-3,fall.outlet().z()+dz)).is(net.minecraft.tags.FluidTags.WATER)){flowing++;}
|
||||||
if(!falls.isEmpty())helper.assertTrue(flowing>0,"Vanilla fluid ticks carry water below at least one opened bank");
|
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);
|
report.put("tested_outlets",falls); 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();
|
Path out=Path.of("diagnostics/alpha301/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));}
|
}catch(Exception error){throw new RuntimeException(error);}finally{chunks.forEach(c->level.getChunkSource().removeTicketWithRadius(TicketType.DRAGON,c,2));}
|
||||||
});
|
});
|
||||||
};step[0].run();
|
};step[0].run();
|
||||||
|
|||||||
+3
-3
@@ -22,7 +22,7 @@ public final class Terraces29WorldGameTests {
|
|||||||
var field = (PopulationIslandDensity) level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION)
|
var field = (PopulationIslandDensity) level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION)
|
||||||
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION,
|
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION,
|
||||||
SanctuaryMod.id("population_" + generator.capacity().players()))).value();
|
SanctuaryMod.id("population_" + generator.capacity().players()))).value();
|
||||||
helper.assertTrue(field.relief30() && !field.terraces29(), "Alpha30 localizes deformation and disables periodic micro-ledges");
|
helper.assertTrue(field.relief30() && !field.terraces29(), "Alpha30.1 restores global deformation except a narrow plateau");
|
||||||
var random = generator.starterContext().random();
|
var random = generator.starterContext().random();
|
||||||
var current = random.getSampler(field); var previous = random.getSampler(field.legacy28());
|
var current = random.getSampler(field); var previous = random.getSampler(field.legacy28());
|
||||||
var context = SamplerContext.builder().enableCaches().build();
|
var context = SamplerContext.builder().enableCaches().build();
|
||||||
@@ -43,7 +43,7 @@ public final class Terraces29WorldGameTests {
|
|||||||
if (Math.hypot(x, z) <= 48) helper.assertTrue(old == now, "Arrival silhouette is unchanged");
|
if (Math.hypot(x, z) <= 48) helper.assertTrue(old == now, "Arrival silhouette is unchanged");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
helper.assertTrue(changed > 0 && unchanged > changed * 3L, "Localized relief retains most of the accepted upper mass");
|
helper.assertTrue(changed > 0 && unchanged > changed * 3L, "A narrow plateau retains most of the accepted global relief");
|
||||||
report.put("unchanged_lower_density_samples", lowerSamples);
|
report.put("unchanged_lower_density_samples", lowerSamples);
|
||||||
report.put("unchanged_upper_samples", unchanged); report.put("changed_upper_samples", changed);
|
report.put("unchanged_upper_samples", unchanged); report.put("changed_upper_samples", changed);
|
||||||
var sections = new LinkedHashMap<String, Object>();
|
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));
|
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);
|
report.put("sections", sections); report.put("passed", true);
|
||||||
Path path = Path.of("diagnostics/alpha30", "relief-" + generator.initialDiameter() + "-" + level.getSeed() + ".json");
|
Path path = Path.of("diagnostics/alpha301", "relief-" + generator.initialDiameter() + "-" + level.getSeed() + ".json");
|
||||||
Files.createDirectories(path.getParent());
|
Files.createDirectories(path.getParent());
|
||||||
Files.writeString(path, new GsonBuilder().setPrettyPrinting().create().toJson(report));
|
Files.writeString(path, new GsonBuilder().setPrettyPrinting().create().toJson(report));
|
||||||
helper.succeed();
|
helper.succeed();
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.worldgen.PopulationHydrology.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/** Opens existing curved rivers and basins; never draws an additional river. */
|
||||||
|
public final class BankOutlets30 {
|
||||||
|
private static final int[][] DIR = {{1,0},{-1,0},{0,1},{0,-1}};
|
||||||
|
private BankOutlets30() {}
|
||||||
|
private static long key(int x, int z) { return ((long)x << 32) ^ (z & 0xffffffffL); }
|
||||||
|
/** 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 kinds = new HashMap<Long,Kind>();
|
||||||
|
water.features().forEach(f -> kinds.put(f.id(), f.kind()));
|
||||||
|
var candidates=water.cells().stream().filter(Cell::hasWater)
|
||||||
|
.filter(c -> Set.of(Kind.RIVER, Kind.LAKE, Kind.POND).contains(kinds.get(c.featureId())))
|
||||||
|
.sorted(Comparator.<Cell>comparingInt(c -> kinds.get(c.featureId()) == Kind.RIVER ? 0 : 1)
|
||||||
|
.thenComparingLong(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) {
|
||||||
|
var adjacent=water.cellAt(wet.x()+d[0],wet.z()+d[1]);
|
||||||
|
if(adjacent!=null && adjacent.hasWater()) 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-n/3,wet.waterY()+2,ShoreMaterial.STONE,id,0));
|
||||||
|
}
|
||||||
|
if(conflict) continue;
|
||||||
|
// Keep writes local to the same regional owner as the existing water.
|
||||||
|
if(local.values().stream().anyMatch(c -> c.x() < -255 || c.x() > 254 || c.z() < -255 || c.z() > 254)) 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()-reach/3,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);}
|
||||||
|
}
|
||||||
+1
-15
@@ -17,25 +17,12 @@ public final class PopulationHydrologyRuntime {
|
|||||||
public record RegionPlan(IslandCapacity.Region region, PopulationHydrology.Plan water, PopulationLavaDeposit.Plan lava) {}
|
public record RegionPlan(IslandCapacity.Region region, PopulationHydrology.Plan water, PopulationLavaDeposit.Plan lava) {}
|
||||||
private static final Map<RandomState, PopulationPlanCache<IslandCapacity.Region, RegionPlan>> WORLDS =
|
private static final Map<RandomState, PopulationPlanCache<IslandCapacity.Region, RegionPlan>> WORLDS =
|
||||||
Collections.synchronizedMap(new WeakHashMap<>());
|
Collections.synchronizedMap(new WeakHashMap<>());
|
||||||
private static final Map<RandomState, PopulationHydrology.Plan> RIVERS30 = new WeakHashMap<>();
|
|
||||||
public static boolean current30(NoiseBasedChunkGenerator generator) {
|
public static boolean current30(NoiseBasedChunkGenerator generator) {
|
||||||
var settings = generator.generatorSettings();
|
var settings = generator.generatorSettings();
|
||||||
return settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_5"))
|
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_10"))
|
||||||
|| settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_20"));
|
|| 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() {}
|
private PopulationHydrologyRuntime() {}
|
||||||
public static boolean enabled(NoiseBasedChunkGenerator generator) { return IslandCapacity.forGenerator(generator) != null; }
|
public static boolean enabled(NoiseBasedChunkGenerator generator) { return IslandCapacity.forGenerator(generator) != null; }
|
||||||
public static IslandCapacity capacity(NoiseBasedChunkGenerator generator) {
|
public static IslandCapacity capacity(NoiseBasedChunkGenerator generator) {
|
||||||
@@ -67,8 +54,7 @@ public final class PopulationHydrologyRuntime {
|
|||||||
long seed = PopulationIslandDensity.regionSeed(state.seed(), region);
|
long seed = PopulationIslandDensity.regionSeed(state.seed(), region);
|
||||||
var water = PopulationHydrology.create(seed, memo).admitted();
|
var water = PopulationHydrology.create(seed, memo).admitted();
|
||||||
if (current30(generator)) {
|
if (current30(generator)) {
|
||||||
water = River30.openBanks(seed, water, memo, 1)
|
water = BankOutlets30.openBanks(seed, water, memo, 6);
|
||||||
.overlay30(river30(generator, state).translated(-region.originX(), -region.originZ()));
|
|
||||||
}
|
}
|
||||||
var lava = PopulationLavaDeposit.create(seed, memo, water).admitted();
|
var lava = PopulationLavaDeposit.create(seed, memo, water).admitted();
|
||||||
var result = new RegionPlan(region, water.translated(region.originX(), region.originZ()),
|
var result = new RegionPlan(region, water.translated(region.originX(), region.originZ()),
|
||||||
|
|||||||
@@ -1,23 +1,15 @@
|
|||||||
package fr.koka.sanctuary.worldgen;
|
package fr.koka.sanctuary.worldgen;
|
||||||
|
|
||||||
/** Three broad areas of signed deformation; the intervening rock keeps the original plateau field. */
|
/** Alpha30.1 restores the global signed warp, except for one narrow natural plateau area. */
|
||||||
public final class Relief30 {
|
public final class Relief30 {
|
||||||
private Relief30() {}
|
private Relief30() {}
|
||||||
public static double influence(long seed, int x, int z, double radius) {
|
public static double influence(long seed, int x, int z, double radius) {
|
||||||
long mixed = seed ^ 0x30BADC0FFEEL;
|
// The earlier plateau field remains along one candidate transit alignment.
|
||||||
mixed = (mixed ^ (mixed >>> 30)) * 0xbf58476d1ce4e5b9L;
|
// Nothing is filled or levelled: the raw field and all existing caves remain active.
|
||||||
double rotation = (mixed >>> 11) * 0x1.0p-53 * Math.PI * 2;
|
double along = x / (radius * .62);
|
||||||
double strongest = 0;
|
double across = (z + radius * .25) / Math.max(20, radius * .08);
|
||||||
for (int i = 0; i < 3; i++) {
|
double distance = Math.hypot(along, across);
|
||||||
double angle = rotation + i * Math.PI * 2 / 3;
|
double t = Math.clamp((distance - 1) / .45, 0, 1);
|
||||||
double cx = Math.cos(angle) * radius * .52, cz = Math.sin(angle) * radius * .52;
|
return t * t * (3 - 2 * t);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,146 +0,0 @@
|
|||||||
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);}
|
|
||||||
}
|
|
||||||
@@ -1,27 +1,39 @@
|
|||||||
package fr.koka.sanctuary.worldgen;
|
package fr.koka.sanctuary.worldgen;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import fr.koka.sanctuary.worldgen.PopulationHydrology.*;
|
||||||
|
|
||||||
public final class River30Smoke {
|
public final class River30Smoke {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
for (int radius : new int[]{256,362,512}) {
|
var cells = new LinkedHashMap<Long,Cell>();
|
||||||
PopulationHydrology.Sampler rock=(x,y,z)->Math.hypot(x,z)<radius*.82 && y>=60 && y<=230 ? 1 : -1;
|
var path = List.of(new Point(0,0),new Point(-1,2),new Point(-2,3),new Point(-4,4));
|
||||||
var plan=River30.create(42,radius,rock);
|
cells.put(key(0,0),new Cell(0,0,230,227,233,ShoreMaterial.GRAVEL,77,2));
|
||||||
var river=plan.features().stream().filter(f->f.kind()==PopulationHydrology.Kind.RIVER).findFirst().orElseThrow();
|
// Existing dry sediment is precisely what alpha30 incorrectly treated as an obstacle.
|
||||||
require(river.bounds().maxX()-river.bounds().minX()>radius || river.bounds().maxZ()-river.bounds().minZ()>radius,"Island-wide route");
|
for(int x=1;x<=4;x++)for(int z=-2;z<=2;z++)
|
||||||
require(plan.spills().size()>0,"A nearby edge can discharge into native void");
|
cells.put(key(x,z),new Cell(x,z,-1,230,234,ShoreMaterial.SAND,77,2));
|
||||||
for(var c:plan.cells()) {
|
var original=PopulationHydrology.additional30(cells,
|
||||||
if(plan.isOutletCell(c)) {require(c.sedimentDepth()==0&&!c.hasWater(),"Outlet only removes its bank");continue;}
|
List.of(new Feature(77,Kind.RIVER,230,path,1,new Bounds(0,-2,4,2))),List.of(),0,0);
|
||||||
require(Math.hypot(c.x(),c.z())>=49,"Arrival remains natural");
|
Sampler coast=(x,y,z)->x<=4 && y<=234 ? 1 : -1;
|
||||||
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");
|
var opened=BankOutlets30.openBanks(42,original,coast,2);
|
||||||
|
require(opened.spills().size()==1,"One short cut through an existing dry bank reaches the void");
|
||||||
|
require(opened.features().stream().filter(f->f.kind()==Kind.RIVER).toList().equals(original.features()),"Original curve and river identity unchanged");
|
||||||
|
require(opened.cells().stream().filter(Cell::hasWater).toList().equals(original.cells().stream().filter(Cell::hasWater).toList()),"No water source or new channel is added");
|
||||||
|
var fall=opened.spills().getFirst();
|
||||||
|
require(fall.outlet().x()==5 && fall.outlet().y()<fall.source().y(),"Five-block breach steps down for vanilla flow to reach its lip");
|
||||||
|
for(var cell:opened.cells())if(opened.isOutletCell(cell))
|
||||||
|
require(cell.sedimentDepth()==0&&!cell.hasWater(),"The breach only removes rock");
|
||||||
|
require(opened.cells().equals(BankOutlets30.openBanks(42,original,coast,2).cells()),"Deterministic openings");
|
||||||
|
require(BankOutlets30.openBanks(42,original,(x,y,z)->1,2).cells().equals(original.cells()),"Inland bank with no void remains intact");
|
||||||
|
for(int radius:new int[]{256,362,512}) {
|
||||||
|
int global=0,total=0,plateau=0;
|
||||||
|
for(int x=-radius;x<=radius;x+=8)for(int z=-radius;z<=radius;z+=8)if(Math.hypot(x,z)<radius) {
|
||||||
|
double w=Relief30.influence(42,x,z,radius);total++;
|
||||||
|
require(w>=0&&w<=1,"Weight is bounded");if(w==1)global++;if(w==0)plateau++;
|
||||||
}
|
}
|
||||||
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(global>total*.85&&plateau>0&&plateau<total*.08,"Global noise covers most of the island, with one small plateau area");
|
||||||
require(plan.cells().equals(River30.create(42,radius,rock).cells()),"Immutable deterministic plan");
|
|
||||||
}
|
}
|
||||||
for(long seed:new long[]{0,42,2026,-7228211907433324401L}) {
|
System.out.println("River30.1: original curves and water retained, dry-bank breach, real downward lip, determinism, inland retention and predominantly global relief passed.");
|
||||||
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 long key(int x,int z){return ((long)x<<32)^(z&0xffffffffL);}
|
||||||
static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
|
static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
name = "Sanctuary"
|
name = "Sanctuary"
|
||||||
author = "KOKA99CAB"
|
author = "KOKA99CAB"
|
||||||
version = "0.1.0-alpha.30"
|
version = "0.1.0-alpha.30.1"
|
||||||
pack-format = "packwiz:1.1.0"
|
pack-format = "packwiz:1.1.0"
|
||||||
|
|
||||||
[index]
|
[index]
|
||||||
|
|||||||
Reference in New Issue
Block a user