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
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
[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
|
||||
|
||||
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.
|
||||
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).
|
||||
|
||||
|
||||
### 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
|
||||
fabric_api_version=0.160.0+26.3
|
||||
|
||||
mod_version=0.1.0-alpha.30
|
||||
pack_version=0.1.0-alpha.30
|
||||
mod_version=0.1.0-alpha.30.1
|
||||
pack_version=0.1.0-alpha.30.1
|
||||
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.planOnly', providers.gradleProperty('sanctuaryTransit29PlanOnly').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.test.heritage21', providers.gradleProperty('sanctuaryHeritage21Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.lostCity21', providers.gradleProperty('sanctuaryLostCity21Tests').getOrElse('false'))
|
||||
|
||||
+13
-7
@@ -15,17 +15,23 @@ 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 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());
|
||||
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");
|
||||
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())
|
||||
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>();
|
||||
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)));
|
||||
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();
|
||||
@@ -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");
|
||||
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;
|
||||
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");
|
||||
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();
|
||||
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/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));}
|
||||
});
|
||||
};step[0].run();
|
||||
|
||||
+3
-3
@@ -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.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 current = random.getSampler(field); var previous = random.getSampler(field.legacy28());
|
||||
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");
|
||||
}
|
||||
}
|
||||
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_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/alpha30", "relief-" + generator.initialDiameter() + "-" + level.getSeed() + ".json");
|
||||
Path path = Path.of("diagnostics/alpha301", "relief-" + generator.initialDiameter() + "-" + level.getSeed() + ".json");
|
||||
Files.createDirectories(path.getParent());
|
||||
Files.writeString(path, new GsonBuilder().setPrettyPrinting().create().toJson(report));
|
||||
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) {}
|
||||
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) {
|
||||
@@ -67,8 +54,7 @@ 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()));
|
||||
water = BankOutlets30.openBanks(seed, water, memo, 6);
|
||||
}
|
||||
var lava = PopulationLavaDeposit.create(seed, memo, water).admitted();
|
||||
var result = new RegionPlan(region, water.translated(region.originX(), region.originZ()),
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
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 {
|
||||
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;
|
||||
// The earlier plateau field remains along one candidate transit alignment.
|
||||
// Nothing is filled or levelled: the raw field and all existing caves remain active.
|
||||
double along = x / (radius * .62);
|
||||
double across = (z + radius * .25) / Math.max(20, radius * .08);
|
||||
double distance = Math.hypot(along, across);
|
||||
double t = Math.clamp((distance - 1) / .45, 0, 1);
|
||||
return t * t * (3 - 2 * t);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
import java.util.*;
|
||||
import fr.koka.sanctuary.worldgen.PopulationHydrology.*;
|
||||
|
||||
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");
|
||||
var cells = new LinkedHashMap<Long,Cell>();
|
||||
var path = List.of(new Point(0,0),new Point(-1,2),new Point(-2,3),new Point(-4,4));
|
||||
cells.put(key(0,0),new Cell(0,0,230,227,233,ShoreMaterial.GRAVEL,77,2));
|
||||
// Existing dry sediment is precisely what alpha30 incorrectly treated as an obstacle.
|
||||
for(int x=1;x<=4;x++)for(int z=-2;z<=2;z++)
|
||||
cells.put(key(x,z),new Cell(x,z,-1,230,234,ShoreMaterial.SAND,77,2));
|
||||
var original=PopulationHydrology.additional30(cells,
|
||||
List.of(new Feature(77,Kind.RIVER,230,path,1,new Bounds(0,-2,4,2))),List.of(),0,0);
|
||||
Sampler coast=(x,y,z)->x<=4 && y<=234 ? 1 : -1;
|
||||
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(plan.cells().equals(River30.create(42,radius,rock).cells()),"Immutable deterministic plan");
|
||||
require(global>total*.85&&plateau>0&&plateau<total*.08,"Global noise covers most of the island, with one small plateau area");
|
||||
}
|
||||
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.");
|
||||
System.out.println("River30.1: original curves and water retained, dry-bank breach, real downward lip, determinism, inland retention and predominantly global relief 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);}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "0.1.0-alpha.30"
|
||||
version = "0.1.0-alpha.30.1"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
Reference in New Issue
Block a user