mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-11-01 08:50:06 +00:00
Merge branch 'perf-raster' into perf-review
This commit is contained in:
commit
e9c18cdc08
13 files changed
+463
-54
No files matched your search
+40
-2
@@ -57,7 +57,7 @@ import java.util.function.Predicate;
|
|||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
public final class HydrologyRuntime {
|
public final class HydrologyRuntime {
|
||||||
private static final int RASTER_CAPACITY = 1024;
|
private static final int RASTER_CAPACITY = 8192;
|
||||||
private static final int KEEP_SCOPES = 2;
|
private static final int KEEP_SCOPES = 2;
|
||||||
private static final int CHUNKS_PER_WINDOW = WindowKey.SIZE >> 4;
|
private static final int CHUNKS_PER_WINDOW = WindowKey.SIZE >> 4;
|
||||||
public static final int MAX_PREGENERATION_REGIONS = 4096;
|
public static final int MAX_PREGENERATION_REGIONS = 4096;
|
||||||
@@ -73,6 +73,7 @@ public final class HydrologyRuntime {
|
|||||||
private final RasterLru rasters;
|
private final RasterLru rasters;
|
||||||
private final ThreadLocal<LastRaster> last;
|
private final ThreadLocal<LastRaster> last;
|
||||||
private final Set<Long> queued;
|
private final Set<Long> queued;
|
||||||
|
private final ConcurrentHashMap<Long, CompletableFuture<ChunkWaterRaster>> building;
|
||||||
private final RasterFailures failures;
|
private final RasterFailures failures;
|
||||||
private final RasterFailures windowPreviewFailures;
|
private final RasterFailures windowPreviewFailures;
|
||||||
private final RasterFailures regionPreviewFailures;
|
private final RasterFailures regionPreviewFailures;
|
||||||
@@ -96,6 +97,7 @@ public final class HydrologyRuntime {
|
|||||||
this.rasters = null;
|
this.rasters = null;
|
||||||
this.last = null;
|
this.last = null;
|
||||||
this.queued = null;
|
this.queued = null;
|
||||||
|
this.building = null;
|
||||||
this.failures = null;
|
this.failures = null;
|
||||||
this.windowPreviewFailures = null;
|
this.windowPreviewFailures = null;
|
||||||
this.regionPreviewFailures = null;
|
this.regionPreviewFailures = null;
|
||||||
@@ -123,6 +125,7 @@ public final class HydrologyRuntime {
|
|||||||
this.rasters = new RasterLru();
|
this.rasters = new RasterLru();
|
||||||
this.last = ThreadLocal.withInitial(LastRaster::new);
|
this.last = ThreadLocal.withInitial(LastRaster::new);
|
||||||
this.queued = ConcurrentHashMap.newKeySet();
|
this.queued = ConcurrentHashMap.newKeySet();
|
||||||
|
this.building = new ConcurrentHashMap<>();
|
||||||
this.failures = new RasterFailures(ticker);
|
this.failures = new RasterFailures(ticker);
|
||||||
this.windowPreviewFailures = new RasterFailures(ticker);
|
this.windowPreviewFailures = new RasterFailures(ticker);
|
||||||
this.regionPreviewFailures = new RasterFailures(ticker);
|
this.regionPreviewFailures = new RasterFailures(ticker);
|
||||||
@@ -238,7 +241,7 @@ public final class HydrologyRuntime {
|
|||||||
requireOpen();
|
requireOpen();
|
||||||
long key = key(chunkX, chunkZ);
|
long key = key(chunkX, chunkZ);
|
||||||
ChunkWaterRaster known = remembered(key, slot);
|
ChunkWaterRaster known = remembered(key, slot);
|
||||||
return known != null ? known : remember(key, planned(chunkX, chunkZ, key));
|
return known != null ? known : build(chunkX, chunkZ, key, slot);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChunkWaterRaster rasterSnapshot(int chunkX, int chunkZ) {
|
public ChunkWaterRaster rasterSnapshot(int chunkX, int chunkZ) {
|
||||||
@@ -525,6 +528,41 @@ public final class HydrologyRuntime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ChunkWaterRaster build(int chunkX, int chunkZ, long key, LastRaster slot) {
|
||||||
|
CompletableFuture<ChunkWaterRaster> claim = new CompletableFuture<>();
|
||||||
|
CompletableFuture<ChunkWaterRaster> running = building.putIfAbsent(key, claim);
|
||||||
|
if (running != null) {
|
||||||
|
ChunkWaterRaster built = built(running);
|
||||||
|
slot.remember(key, built);
|
||||||
|
return built;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ChunkWaterRaster known = remembered(key, slot);
|
||||||
|
ChunkWaterRaster raster = known != null ? known : remember(key, planned(chunkX, chunkZ, key));
|
||||||
|
claim.complete(raster);
|
||||||
|
return raster;
|
||||||
|
} catch (RuntimeException | Error failure) {
|
||||||
|
claim.completeExceptionally(failure);
|
||||||
|
throw failure;
|
||||||
|
} finally {
|
||||||
|
building.remove(key, claim);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ChunkWaterRaster built(CompletableFuture<ChunkWaterRaster> running) {
|
||||||
|
try {
|
||||||
|
return running.join();
|
||||||
|
} catch (CompletionException failure) {
|
||||||
|
if (failure.getCause() instanceof RuntimeException cause) {
|
||||||
|
throw cause;
|
||||||
|
}
|
||||||
|
if (failure.getCause() instanceof Error cause) {
|
||||||
|
throw cause;
|
||||||
|
}
|
||||||
|
throw failure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private ChunkWaterRaster planned(int chunkX, int chunkZ, long chunk) {
|
private ChunkWaterRaster planned(int chunkX, int chunkZ, long chunk) {
|
||||||
try {
|
try {
|
||||||
List<WindowKey> windowKeys = neighbourhood(chunkX, chunkZ);
|
List<WindowKey> windowKeys = neighbourhood(chunkX, chunkZ);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import art.arcane.iris.generation.hydrology.water.PondMask;
|
|||||||
import art.arcane.iris.generation.hydrology.water.Reach;
|
import art.arcane.iris.generation.hydrology.water.Reach;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -28,17 +29,20 @@ final class ChunkFeatures {
|
|||||||
private final List<Pond> ponds;
|
private final List<Pond> ponds;
|
||||||
private final List<Pond> lakes;
|
private final List<Pond> lakes;
|
||||||
private final List<SeaInlet> inlets;
|
private final List<SeaInlet> inlets;
|
||||||
|
private final int[] inletBoxes;
|
||||||
private final int[] lakeBoxes;
|
private final int[] lakeBoxes;
|
||||||
private final ReachSegments segments = new ReachSegments(SEGMENT_CAPACITY);
|
private final ReachSegments segments = new ReachSegments(SEGMENT_CAPACITY);
|
||||||
private final MaskEdges pondShores = new MaskEdges(MaskEdges.Kind.WATER);
|
private final MaskEdges pondShores = new MaskEdges(MaskEdges.Kind.WATER);
|
||||||
private final MaskEdges pondBanks = new MaskEdges(MaskEdges.Kind.BANK);
|
private final MaskEdges pondBanks = new MaskEdges(MaskEdges.Kind.BANK);
|
||||||
private final MaskEdges lakeReach = new MaskEdges(MaskEdges.Kind.GROWN_WATER);
|
private final MaskEdges lakeReach = new MaskEdges(MaskEdges.Kind.GROWN_WATER);
|
||||||
|
private boolean inert;
|
||||||
|
|
||||||
private ChunkFeatures(List<Reach> reaches, List<Pond> ponds, List<Pond> lakes, List<SeaInlet> inlets) {
|
private ChunkFeatures(List<Reach> reaches, List<Pond> ponds, List<Pond> lakes, List<SeaInlet> inlets, int[] inletBoxes) {
|
||||||
this.reaches = reaches;
|
this.reaches = reaches;
|
||||||
this.ponds = ponds;
|
this.ponds = ponds;
|
||||||
this.lakes = lakes;
|
this.lakes = lakes;
|
||||||
this.inlets = inlets;
|
this.inlets = inlets;
|
||||||
|
this.inletBoxes = inletBoxes;
|
||||||
this.lakeBoxes = new int[4 * lakes.size()];
|
this.lakeBoxes = new int[4 * lakes.size()];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,12 +52,15 @@ final class ChunkFeatures {
|
|||||||
List<Reach> reaches = new ArrayList<>();
|
List<Reach> reaches = new ArrayList<>();
|
||||||
List<Pond> ponds = new ArrayList<>();
|
List<Pond> ponds = new ArrayList<>();
|
||||||
List<SeaInlet> inlets = new ArrayList<>();
|
List<SeaInlet> inlets = new ArrayList<>();
|
||||||
|
int[] inletBoxes = new int[0];
|
||||||
for (IndexedWindow window : windows) {
|
for (IndexedWindow window : windows) {
|
||||||
for (SeaInlet inlet : window.plan().inlets()) {
|
for (SeaInlet inlet : window.plan().inlets()) {
|
||||||
int[] bounds = SeaInletRasterizer.bounds(inlet);
|
int[] bounds = SeaInletRasterizer.bounds(inlet);
|
||||||
int padding = RiffleProfile.RADIUS + 1;
|
int padding = RiffleProfile.RADIUS + 1;
|
||||||
if (bounds[0] <= chunkX * CHUNK + CHUNK - 1 + padding && bounds[2] >= chunkX * CHUNK - padding
|
if (bounds[0] <= chunkX * CHUNK + CHUNK - 1 + padding && bounds[2] >= chunkX * CHUNK - padding
|
||||||
&& bounds[1] <= chunkZ * CHUNK + CHUNK - 1 + padding && bounds[3] >= chunkZ * CHUNK - padding) {
|
&& bounds[1] <= chunkZ * CHUNK + CHUNK - 1 + padding && bounds[3] >= chunkZ * CHUNK - padding) {
|
||||||
|
inletBoxes = Arrays.copyOf(inletBoxes, inletBoxes.length + 4);
|
||||||
|
System.arraycopy(bounds, 0, inletBoxes, 4 * inlets.size(), 4);
|
||||||
inlets.add(inlet);
|
inlets.add(inlet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,17 +71,22 @@ final class ChunkFeatures {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
List<Pond> lakes = lakes(chunkX, chunkZ, halo, lakeRegionPlans, lakeRegions);
|
List<Pond> lakes = lakes(chunkX, chunkZ, halo, lakeRegionPlans, lakeRegions);
|
||||||
ChunkFeatures features = new ChunkFeatures(reaches, ponds, lakes, inlets);
|
ChunkFeatures features = new ChunkFeatures(reaches, ponds, lakes, inlets, inletBoxes);
|
||||||
features.index(settings, chunkX, chunkZ);
|
features.index(settings, chunkX, chunkZ);
|
||||||
return features;
|
return features;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean empty() {
|
boolean inert() {
|
||||||
return reaches.isEmpty() && ponds.isEmpty() && lakes.isEmpty() && inlets.isEmpty();
|
return inert;
|
||||||
}
|
}
|
||||||
|
|
||||||
SeaInlet openInletAt(int x, int z, int[] column) {
|
SeaInlet openInletAt(int x, int z, int[] column) {
|
||||||
for (SeaInlet inlet : inlets) {
|
for (int index = 0; index < inlets.size(); index++) {
|
||||||
|
int box = 4 * index;
|
||||||
|
if (x < inletBoxes[box] || z < inletBoxes[box + 1] || x > inletBoxes[box + 2] || z > inletBoxes[box + 3]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
SeaInlet inlet = inlets.get(index);
|
||||||
if (SeaInletRasterizer.column(inlet, x, z, column) && column[1] == SeaInlet.OPEN) {
|
if (SeaInletRasterizer.column(inlet, x, z, column) && column[1] == SeaInlet.OPEN) {
|
||||||
return inlet;
|
return inlet;
|
||||||
}
|
}
|
||||||
@@ -179,6 +191,8 @@ final class ChunkFeatures {
|
|||||||
for (Reach reach : reaches) {
|
for (Reach reach : reaches) {
|
||||||
segments.addNear(reach, minX - 1f, minZ - 1f, maxX + 1f, maxZ + 1f, outward);
|
segments.addNear(reach, minX - 1f, minZ - 1f, maxX + 1f, maxZ + 1f, outward);
|
||||||
}
|
}
|
||||||
|
int riffle = RiffleProfile.RADIUS + 1;
|
||||||
|
segments.coverChannels(minX - riffle, minZ - riffle, maxX + riffle, maxZ + riffle);
|
||||||
HydrologySettings.PondShore shore = settings.ponds().shore();
|
HydrologySettings.PondShore shore = settings.ponds().shore();
|
||||||
int reach = Math.max(shore.taper(), ChunkWaterRaster.MAX_SHORE_DISTANCE) + 1;
|
int reach = Math.max(shore.taper(), ChunkWaterRaster.MAX_SHORE_DISTANCE) + 1;
|
||||||
int shelf = shore.shelf() + 1;
|
int shelf = shore.shelf() + 1;
|
||||||
@@ -192,6 +206,39 @@ final class ChunkFeatures {
|
|||||||
lakeReach.collect(mask, minX - shoreReach, minZ - shoreReach, maxX + shoreReach, maxZ + shoreReach);
|
lakeReach.collect(mask, minX - shoreReach, minZ - shoreReach, maxX + shoreReach, maxZ + shoreReach);
|
||||||
System.arraycopy(grownBox(mask), 0, lakeBoxes, 4 * index, 4);
|
System.arraycopy(grownBox(mask), 0, lakeBoxes, 4 * index, 4);
|
||||||
}
|
}
|
||||||
|
inert = segments.isEmpty() && !inletsTouch(minX - 1, minZ - 1, maxX + 1, maxZ + 1)
|
||||||
|
&& !pondsTouch(minX - 1 - reach, minZ - 1 - reach, maxX + 1 + reach, maxZ + 1 + reach)
|
||||||
|
&& !lakesTouch(minX - 2 - shoreReach, minZ - 2 - shoreReach, maxX + 2 + shoreReach, maxZ + 2 + shoreReach);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean inletsTouch(int minX, int minZ, int maxX, int maxZ) {
|
||||||
|
for (int box = 0; box < inletBoxes.length; box += 4) {
|
||||||
|
if (inletBoxes[box] <= maxX && inletBoxes[box + 2] >= minX && inletBoxes[box + 1] <= maxZ && inletBoxes[box + 3] >= minZ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean pondsTouch(int minX, int minZ, int maxX, int maxZ) {
|
||||||
|
for (Pond pond : ponds) {
|
||||||
|
PondMask mask = pond.mask();
|
||||||
|
int fromX = mask.originX() - (mask.step() >> 1);
|
||||||
|
int fromZ = mask.originZ() - (mask.step() >> 1);
|
||||||
|
if (fromX <= maxX && fromX + mask.width() * mask.step() > minX && fromZ <= maxZ && fromZ + mask.depth() * mask.step() > minZ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean lakesTouch(int minX, int minZ, int maxX, int maxZ) {
|
||||||
|
for (int box = 0; box < lakeBoxes.length; box += 4) {
|
||||||
|
if (lakeBoxes[box] <= maxX && lakeBoxes[box + 2] > minX && lakeBoxes[box + 1] <= maxZ && lakeBoxes[box + 3] > minZ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Map<WindowKey, IndexedWindow> windowsByKey(int chunkX, int chunkZ, List<IndexedWindow> windows) {
|
private static Map<WindowKey, IndexedWindow> windowsByKey(int chunkX, int chunkZ, List<IndexedWindow> windows) {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ public final class ChunkRasterizer {
|
|||||||
List<RegionPlan> lakeRegionPlans) {
|
List<RegionPlan> lakeRegionPlans) {
|
||||||
Objects.requireNonNull(naturalHeight, "naturalHeight");
|
Objects.requireNonNull(naturalHeight, "naturalHeight");
|
||||||
ChunkFeatures features = ChunkFeatures.gather(settings, chunkX, chunkZ, halo, windows, lakeRegionPlans, lakeRegions(chunkX, chunkZ));
|
ChunkFeatures features = ChunkFeatures.gather(settings, chunkX, chunkZ, halo, windows, lakeRegionPlans, lakeRegions(chunkX, chunkZ));
|
||||||
if (features.empty()) {
|
if (features.inert()) {
|
||||||
return ChunkWaterRaster.EMPTY;
|
return ChunkWaterRaster.EMPTY;
|
||||||
}
|
}
|
||||||
return new RasterComposer(settings, features, chunkX, chunkZ).compose(naturalHeight);
|
return new RasterComposer(settings, features, chunkX, chunkZ).compose(naturalHeight);
|
||||||
|
|||||||
+78
-18
@@ -2,6 +2,8 @@ package art.arcane.iris.generation.hydrology.raster;
|
|||||||
|
|
||||||
import art.arcane.iris.generation.hydrology.water.HydrologyTier;
|
import art.arcane.iris.generation.hydrology.water.HydrologyTier;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
public final class ChunkWaterRaster {
|
public final class ChunkWaterRaster {
|
||||||
public static final int MAX_SHORE_DISTANCE = 32;
|
public static final int MAX_SHORE_DISTANCE = 32;
|
||||||
public static final ChunkWaterRaster PROVISIONAL = new ChunkWaterRaster(true);
|
public static final ChunkWaterRaster PROVISIONAL = new ChunkWaterRaster(true);
|
||||||
@@ -13,10 +15,13 @@ public final class ChunkWaterRaster {
|
|||||||
private final int[] terrain;
|
private final int[] terrain;
|
||||||
private final int[] head;
|
private final int[] head;
|
||||||
private final byte[] role;
|
private final byte[] role;
|
||||||
private final HydrologyTier[] tier;
|
private final byte[] tier;
|
||||||
private final long[] featureId;
|
private final HydrologyTier[] tiers;
|
||||||
private final String[] fluid;
|
private final byte[] feature;
|
||||||
private final int[] caveCeiling;
|
private final long[] features;
|
||||||
|
private final byte[] fluid;
|
||||||
|
private final String[] fluids;
|
||||||
|
private final boolean caveMask;
|
||||||
private final byte[] shoreDistance;
|
private final byte[] shoreDistance;
|
||||||
|
|
||||||
private ChunkWaterRaster(boolean provisional) {
|
private ChunkWaterRaster(boolean provisional) {
|
||||||
@@ -25,26 +30,32 @@ public final class ChunkWaterRaster {
|
|||||||
this.head = null;
|
this.head = null;
|
||||||
this.role = null;
|
this.role = null;
|
||||||
this.tier = null;
|
this.tier = null;
|
||||||
this.featureId = null;
|
this.tiers = null;
|
||||||
|
this.feature = null;
|
||||||
|
this.features = null;
|
||||||
this.fluid = null;
|
this.fluid = null;
|
||||||
this.caveCeiling = null;
|
this.fluids = null;
|
||||||
|
this.caveMask = false;
|
||||||
this.shoreDistance = null;
|
this.shoreDistance = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
ChunkWaterRaster(int[] terrain, int[] head, byte[] role, HydrologyTier[] tier, long[] featureId, String[] fluid, int[] caveCeiling,
|
ChunkWaterRaster(int[] terrain, int[] head, byte[] role, Palette<HydrologyTier> tiers, LongPalette features, Palette<String> fluids,
|
||||||
byte[] shoreDistance) {
|
boolean caveMask, byte[] shoreDistance) {
|
||||||
if (terrain.length != COLUMNS || head.length != COLUMNS || role.length != COLUMNS || tier.length != COLUMNS
|
if (terrain.length != COLUMNS || head.length != COLUMNS || role.length != COLUMNS || tiers.indices.length != COLUMNS
|
||||||
|| featureId.length != COLUMNS || fluid.length != COLUMNS || caveCeiling.length != COLUMNS || shoreDistance.length != COLUMNS) {
|
|| features.indices.length != COLUMNS || fluids.indices.length != COLUMNS || shoreDistance.length != COLUMNS) {
|
||||||
throw new IllegalArgumentException("A chunk water raster needs " + COLUMNS + " entries per array");
|
throw new IllegalArgumentException("A chunk water raster needs " + COLUMNS + " entries per array");
|
||||||
}
|
}
|
||||||
this.provisional = false;
|
this.provisional = false;
|
||||||
this.terrain = terrain;
|
this.terrain = terrain;
|
||||||
this.head = head;
|
this.head = head;
|
||||||
this.role = role;
|
this.role = role;
|
||||||
this.tier = tier;
|
this.tier = tiers.indices;
|
||||||
this.featureId = featureId;
|
this.tiers = tiers.values();
|
||||||
this.fluid = fluid;
|
this.feature = features.indices;
|
||||||
this.caveCeiling = caveCeiling;
|
this.features = features.values();
|
||||||
|
this.fluid = fluids.indices;
|
||||||
|
this.fluids = fluids.values();
|
||||||
|
this.caveMask = caveMask;
|
||||||
this.shoreDistance = shoreDistance;
|
this.shoreDistance = shoreDistance;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,22 +79,22 @@ public final class ChunkWaterRaster {
|
|||||||
|
|
||||||
public HydrologyTier tier(int lx, int lz) {
|
public HydrologyTier tier(int lx, int lz) {
|
||||||
int index = index(lx, lz);
|
int index = index(lx, lz);
|
||||||
return tier == null ? null : tier[index];
|
return tier == null ? null : tiers[tier[index] & 0xFF];
|
||||||
}
|
}
|
||||||
|
|
||||||
public long featureId(int lx, int lz) {
|
public long featureId(int lx, int lz) {
|
||||||
int index = index(lx, lz);
|
int index = index(lx, lz);
|
||||||
return featureId == null ? 0L : featureId[index];
|
return feature == null ? 0L : features[feature[index] & 0xFF];
|
||||||
}
|
}
|
||||||
|
|
||||||
public String fluid(int lx, int lz) {
|
public String fluid(int lx, int lz) {
|
||||||
int index = index(lx, lz);
|
int index = index(lx, lz);
|
||||||
return fluid == null ? null : fluid[index];
|
return fluid == null ? null : fluids[fluid[index] & 0xFF];
|
||||||
}
|
}
|
||||||
|
|
||||||
public int caveCeiling(int lx, int lz) {
|
public int caveCeiling(int lx, int lz) {
|
||||||
int index = index(lx, lz);
|
int index = index(lx, lz);
|
||||||
return caveCeiling == null ? Integer.MAX_VALUE : caveCeiling[index];
|
return caveMask && head[index] != Integer.MIN_VALUE ? terrain[index] - 1 : Integer.MAX_VALUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int shoreDistance(int lx, int lz) {
|
public int shoreDistance(int lx, int lz) {
|
||||||
@@ -105,4 +116,53 @@ public final class ChunkWaterRaster {
|
|||||||
}
|
}
|
||||||
return lz << 4 | lx;
|
return lz << 4 | lx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static final class Palette<T> {
|
||||||
|
private final byte[] indices = new byte[COLUMNS];
|
||||||
|
private final T[] seen;
|
||||||
|
private int size;
|
||||||
|
|
||||||
|
Palette(T[] seen) {
|
||||||
|
if (seen.length != COLUMNS) {
|
||||||
|
throw new IllegalArgumentException("A raster palette needs room for " + COLUMNS + " values");
|
||||||
|
}
|
||||||
|
this.seen = seen;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set(int column, T value) {
|
||||||
|
int found = 0;
|
||||||
|
while (found < size && seen[found] != value) {
|
||||||
|
found++;
|
||||||
|
}
|
||||||
|
if (found == size) {
|
||||||
|
seen[size++] = value;
|
||||||
|
}
|
||||||
|
indices[column] = (byte) found;
|
||||||
|
}
|
||||||
|
|
||||||
|
private T[] values() {
|
||||||
|
return Arrays.copyOf(seen, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class LongPalette {
|
||||||
|
private final byte[] indices = new byte[COLUMNS];
|
||||||
|
private final long[] seen = new long[COLUMNS];
|
||||||
|
private int size;
|
||||||
|
|
||||||
|
void set(int column, long value) {
|
||||||
|
int found = 0;
|
||||||
|
while (found < size && seen[found] != value) {
|
||||||
|
found++;
|
||||||
|
}
|
||||||
|
if (found == size) {
|
||||||
|
seen[size++] = value;
|
||||||
|
}
|
||||||
|
indices[column] = (byte) found;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long[] values() {
|
||||||
|
return Arrays.copyOf(seen, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+18
-15
@@ -385,34 +385,37 @@ final class RasterComposer {
|
|||||||
|
|
||||||
private ChunkWaterRaster pack() {
|
private ChunkWaterRaster pack() {
|
||||||
int columns = ChunkWaterRaster.COLUMNS;
|
int columns = ChunkWaterRaster.COLUMNS;
|
||||||
|
boolean affected = false;
|
||||||
|
for (int lz = 0; lz < CHUNK && !affected; lz++) {
|
||||||
|
for (int lx = 0; lx < CHUNK && !affected; lx++) {
|
||||||
|
int cell = (lz + 1) * GRID + lx + 1;
|
||||||
|
affected = role[cell] != WaterRole.NONE || terrain[cell] != natural[cell] || shore(cell) < ChunkWaterRaster.MAX_SHORE_DISTANCE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!affected) {
|
||||||
|
return ChunkWaterRaster.EMPTY;
|
||||||
|
}
|
||||||
int[] terrainOut = new int[columns];
|
int[] terrainOut = new int[columns];
|
||||||
int[] headOut = new int[columns];
|
int[] headOut = new int[columns];
|
||||||
byte[] roleOut = new byte[columns];
|
byte[] roleOut = new byte[columns];
|
||||||
HydrologyTier[] tierOut = new HydrologyTier[columns];
|
ChunkWaterRaster.Palette<HydrologyTier> tierOut = new ChunkWaterRaster.Palette<>(new HydrologyTier[columns]);
|
||||||
long[] featureOut = new long[columns];
|
ChunkWaterRaster.LongPalette featureOut = new ChunkWaterRaster.LongPalette();
|
||||||
String[] fluidOut = new String[columns];
|
ChunkWaterRaster.Palette<String> fluidOut = new ChunkWaterRaster.Palette<>(new String[columns]);
|
||||||
int[] caveOut = new int[columns];
|
|
||||||
byte[] shoreOut = new byte[columns];
|
byte[] shoreOut = new byte[columns];
|
||||||
boolean caveMask = settings.deformation().caveMask();
|
|
||||||
boolean affected = false;
|
|
||||||
for (int lz = 0; lz < CHUNK; lz++) {
|
for (int lz = 0; lz < CHUNK; lz++) {
|
||||||
for (int lx = 0; lx < CHUNK; lx++) {
|
for (int lx = 0; lx < CHUNK; lx++) {
|
||||||
int cell = (lz + 1) * GRID + lx + 1;
|
int cell = (lz + 1) * GRID + lx + 1;
|
||||||
int index = lz << 4 | lx;
|
int index = lz << 4 | lx;
|
||||||
int shore = shore(cell);
|
|
||||||
terrainOut[index] = terrain[cell];
|
terrainOut[index] = terrain[cell];
|
||||||
headOut[index] = head[cell];
|
headOut[index] = head[cell];
|
||||||
roleOut[index] = (byte) role[cell].ordinal();
|
roleOut[index] = (byte) role[cell].ordinal();
|
||||||
tierOut[index] = tier[cell];
|
tierOut.set(index, tier[cell]);
|
||||||
featureOut[index] = feature[cell];
|
featureOut.set(index, feature[cell]);
|
||||||
fluidOut[index] = fluid[cell];
|
fluidOut.set(index, fluid[cell]);
|
||||||
caveOut[index] = caveMask && head[cell] != Integer.MIN_VALUE ? terrain[cell] - 1 : Integer.MAX_VALUE;
|
shoreOut[index] = (byte) shore(cell);
|
||||||
shoreOut[index] = (byte) shore;
|
|
||||||
affected |= role[cell] != WaterRole.NONE || terrain[cell] != natural[cell] || shore < ChunkWaterRaster.MAX_SHORE_DISTANCE;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return affected ? new ChunkWaterRaster(terrainOut, headOut, roleOut, tierOut, featureOut, fluidOut, caveOut, shoreOut)
|
return new ChunkWaterRaster(terrainOut, headOut, roleOut, tierOut, featureOut, fluidOut, settings.deformation().caveMask(), shoreOut);
|
||||||
: ChunkWaterRaster.EMPTY;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int shore(int cell) {
|
private int shore(int cell) {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import java.util.Arrays;
|
|||||||
|
|
||||||
final class ReachSegments {
|
final class ReachSegments {
|
||||||
private static final int RUN = 16;
|
private static final int RUN = 16;
|
||||||
|
private static final int COVER_SHIFT = 2;
|
||||||
|
private static final float COVER_MARGIN = 1f;
|
||||||
|
|
||||||
private Reach[] reaches;
|
private Reach[] reaches;
|
||||||
private int[] segments;
|
private int[] segments;
|
||||||
@@ -24,6 +26,13 @@ final class ReachSegments {
|
|||||||
private int[] runBands = new int[8];
|
private int[] runBands = new int[8];
|
||||||
private int runs;
|
private int runs;
|
||||||
private boolean sealed;
|
private boolean sealed;
|
||||||
|
private boolean covered;
|
||||||
|
private int coverX;
|
||||||
|
private int coverZ;
|
||||||
|
private int coverWidth;
|
||||||
|
private int coverDepth;
|
||||||
|
private int[] coverStarts;
|
||||||
|
private int[] coverEntries;
|
||||||
|
|
||||||
ReachSegments(int capacity) {
|
ReachSegments(int capacity) {
|
||||||
int initial = Math.max(1, capacity);
|
int initial = Math.max(1, capacity);
|
||||||
@@ -71,6 +80,19 @@ final class ReachSegments {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean isEmpty() {
|
||||||
|
return size == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void coverChannels(int minX, int minZ, int maxX, int maxZ) {
|
||||||
|
covered = true;
|
||||||
|
coverX = minX;
|
||||||
|
coverZ = minZ;
|
||||||
|
coverWidth = ((maxX - minX) >> COVER_SHIFT) + 1;
|
||||||
|
coverDepth = ((maxZ - minZ) >> COVER_SHIFT) + 1;
|
||||||
|
sealed = false;
|
||||||
|
}
|
||||||
|
|
||||||
void nearest(double x, double z, Scope scope, ReachCursor cursor) {
|
void nearest(double x, double z, Scope scope, ReachCursor cursor) {
|
||||||
if (!sealed) {
|
if (!sealed) {
|
||||||
seal();
|
seal();
|
||||||
@@ -89,6 +111,16 @@ final class ReachSegments {
|
|||||||
hint(channelHint, x, z, unbounded, cursor);
|
hint(channelHint, x, z, unbounded, cursor);
|
||||||
hint(bankHint, x, z, unbounded, cursor);
|
hint(bankHint, x, z, unbounded, cursor);
|
||||||
hint(edgeHint, x, z, unbounded, cursor);
|
hint(edgeHint, x, z, unbounded, cursor);
|
||||||
|
int cell = scope == Scope.CHANNEL ? coveredCell(x, z) : -1;
|
||||||
|
if (cell >= 0) {
|
||||||
|
for (int entry = coverStarts[cell]; entry < coverStarts[cell + 1]; entry++) {
|
||||||
|
int index = coverEntries[entry];
|
||||||
|
if (index != channelHint && index != bankHint && index != edgeHint && !beyondSegment(index, x, z, scope, cursor)) {
|
||||||
|
evaluate(index, x, z, false, cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
int first = 0;
|
int first = 0;
|
||||||
for (int run = 0; run < runs; run++) {
|
for (int run = 0; run < runs; run++) {
|
||||||
int end = runEnds[run];
|
int end = runEnds[run];
|
||||||
@@ -99,9 +131,7 @@ final class ReachSegments {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (int index = first; index < end; index++) {
|
for (int index = first; index < end; index++) {
|
||||||
if (!beyond(x, z, Math.min(startX[index], endX[index]), Math.min(startZ[index], endZ[index]), Math.max(startX[index], endX[index]),
|
if (!beyondSegment(index, x, z, scope, cursor) && index != channelHint && index != bankHint && index != edgeHint) {
|
||||||
Math.max(startZ[index], endZ[index]), limit(radius[index], bands[index], scope, cursor))
|
|
||||||
&& index != channelHint && index != bankHint && index != edgeHint) {
|
|
||||||
evaluate(index, x, z, unbounded, cursor);
|
evaluate(index, x, z, unbounded, cursor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,6 +139,23 @@ final class ReachSegments {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int coveredCell(double x, double z) {
|
||||||
|
if (coverStarts == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int cellX = ((int) Math.floor(x) - coverX) >> COVER_SHIFT;
|
||||||
|
int cellZ = ((int) Math.floor(z) - coverZ) >> COVER_SHIFT;
|
||||||
|
if (cellX < 0 || cellZ < 0 || cellX >= coverWidth || cellZ >= coverDepth) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return cellZ * coverWidth + cellX;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean beyondSegment(int index, double x, double z, Scope scope, ReachCursor cursor) {
|
||||||
|
return beyond(x, z, Math.min(startX[index], endX[index]), Math.min(startZ[index], endZ[index]), Math.max(startX[index], endX[index]),
|
||||||
|
Math.max(startZ[index], endZ[index]), limit(radius[index], bands[index], scope, cursor));
|
||||||
|
}
|
||||||
|
|
||||||
private void hint(int index, double x, double z, boolean unbounded, ReachCursor cursor) {
|
private void hint(int index, double x, double z, boolean unbounded, ReachCursor cursor) {
|
||||||
if (index >= 0 && index < size) {
|
if (index >= 0 && index < size) {
|
||||||
evaluate(index, x, z, unbounded, cursor);
|
evaluate(index, x, z, unbounded, cursor);
|
||||||
@@ -171,9 +218,56 @@ final class ReachSegments {
|
|||||||
addRun(start, end);
|
addRun(start, end);
|
||||||
start = end;
|
start = end;
|
||||||
}
|
}
|
||||||
|
if (covered) {
|
||||||
|
cover();
|
||||||
|
}
|
||||||
sealed = true;
|
sealed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void cover() {
|
||||||
|
int cells = coverWidth * coverDepth;
|
||||||
|
int[] starts = new int[cells + 1];
|
||||||
|
int[] range = new int[4];
|
||||||
|
for (int index = 0; index < size; index++) {
|
||||||
|
if (coverRange(index, range)) {
|
||||||
|
for (int cellZ = range[1]; cellZ <= range[3]; cellZ++) {
|
||||||
|
for (int cellX = range[0]; cellX <= range[2]; cellX++) {
|
||||||
|
starts[cellZ * coverWidth + cellX + 1]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int cell = 0; cell < cells; cell++) {
|
||||||
|
starts[cell + 1] += starts[cell];
|
||||||
|
}
|
||||||
|
int[] entries = new int[starts[cells]];
|
||||||
|
int[] fill = Arrays.copyOf(starts, cells);
|
||||||
|
for (int index = 0; index < size; index++) {
|
||||||
|
if (coverRange(index, range)) {
|
||||||
|
for (int cellZ = range[1]; cellZ <= range[3]; cellZ++) {
|
||||||
|
for (int cellX = range[0]; cellX <= range[2]; cellX++) {
|
||||||
|
entries[fill[cellZ * coverWidth + cellX]++] = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
coverStarts = starts;
|
||||||
|
coverEntries = entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean coverRange(int index, int[] range) {
|
||||||
|
float reach = radius[index] + COVER_MARGIN;
|
||||||
|
range[0] = Math.max(0, coverCell(Math.min(startX[index], endX[index]) - reach, coverX));
|
||||||
|
range[1] = Math.max(0, coverCell(Math.min(startZ[index], endZ[index]) - reach, coverZ));
|
||||||
|
range[2] = Math.min(coverWidth - 1, coverCell(Math.max(startX[index], endX[index]) + reach, coverX));
|
||||||
|
range[3] = Math.min(coverDepth - 1, coverCell(Math.max(startZ[index], endZ[index]) + reach, coverZ));
|
||||||
|
return range[0] <= range[2] && range[1] <= range[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int coverCell(float block, int origin) {
|
||||||
|
return ((int) Math.floor(block) - origin) >> COVER_SHIFT;
|
||||||
|
}
|
||||||
|
|
||||||
private void addRun(int start, int end) {
|
private void addRun(int start, int end) {
|
||||||
if (runs == runEnds.length) {
|
if (runs == runEnds.length) {
|
||||||
runEnds = Arrays.copyOf(runEnds, 2 * runs);
|
runEnds = Arrays.copyOf(runEnds, 2 * runs);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ final class RiffleProfile {
|
|||||||
private static final int LIMIT = 10 * RADIUS;
|
private static final int LIMIT = 10 * RADIUS;
|
||||||
private static final int DRY = Integer.MIN_VALUE;
|
private static final int DRY = Integer.MIN_VALUE;
|
||||||
private static final int[] SIDES = {1, -1, GRID, -GRID};
|
private static final int[] SIDES = {1, -1, GRID, -GRID};
|
||||||
|
private static final int[] REMAINING = remaining();
|
||||||
|
|
||||||
private final List<Field> fields = new ArrayList<>();
|
private final List<Field> fields = new ArrayList<>();
|
||||||
|
|
||||||
@@ -85,6 +86,20 @@ final class RiffleProfile {
|
|||||||
return (int) Math.max(original, Math.min(head - 1L, Math.round(raised)));
|
return (int) Math.max(original, Math.min(head - 1L, Math.round(raised)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int[] remaining() {
|
||||||
|
int[] remaining = new int[GRID * GRID];
|
||||||
|
int low = PADDING - 1;
|
||||||
|
int high = PADDING + CHUNK;
|
||||||
|
for (int z = 0; z < GRID; z++) {
|
||||||
|
for (int x = 0; x < GRID; x++) {
|
||||||
|
int outsideX = Math.max(0, Math.max(low - x, x - high));
|
||||||
|
int outsideZ = Math.max(0, Math.max(low - z, z - high));
|
||||||
|
remaining[z * GRID + x] = 10 * Math.max(outsideX, outsideZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return remaining;
|
||||||
|
}
|
||||||
|
|
||||||
private record Field(int crest, int[] distances) {
|
private record Field(int crest, int[] distances) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +122,9 @@ final class RiffleProfile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void offer(int cell, int distance) {
|
private void offer(int cell, int distance) {
|
||||||
|
if (distance + REMAINING[cell] >= LIMIT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (distances[cell] < LIMIT) {
|
if (distances[cell] < LIMIT) {
|
||||||
remove(cell);
|
remove(cell);
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -10,6 +10,7 @@ import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
|||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
public final class CavePoolFiller {
|
public final class CavePoolFiller {
|
||||||
public static final int REACH = HydrologySettings.MAX_CAVE_POOL_RADIUS + 1;
|
public static final int REACH = HydrologySettings.MAX_CAVE_POOL_RADIUS + 1;
|
||||||
@@ -40,15 +41,19 @@ public final class CavePoolFiller {
|
|||||||
this.density = settings.density();
|
this.density = settings.density();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void fill(int chunkX, int chunkZ, CarvedTerrain carved, MantleWriter writer, long seed) {
|
public void fill(int chunkX, int chunkZ, Supplier<? extends CarvedTerrain> terrain, MantleWriter writer, long seed) {
|
||||||
if (!pools.enabled() || density == 0f) {
|
if (!pools.enabled() || density == 0f) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int attempts = attempts(seed, chunkX, chunkZ);
|
int attempts = attempts(seed, chunkX, chunkZ);
|
||||||
|
CarvedTerrain carved = null;
|
||||||
for (int attempt = 0; attempt < attempts; attempt++) {
|
for (int attempt = 0; attempt < attempts; attempt++) {
|
||||||
if (density < 1f && CellHash.unit(CellHash.mix(seed, SALT_DENSITY + attempt, chunkX, chunkZ)) >= density) {
|
if (density < 1f && CellHash.unit(CellHash.mix(seed, SALT_DENSITY + attempt, chunkX, chunkZ)) >= density) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (carved == null) {
|
||||||
|
carved = terrain.get();
|
||||||
|
}
|
||||||
long column = CellHash.mix(seed, SALT_COLUMN + attempt, chunkX, chunkZ);
|
long column = CellHash.mix(seed, SALT_COLUMN + attempt, chunkX, chunkZ);
|
||||||
int x = (chunkX << 4) + (int) (column & 15);
|
int x = (chunkX << 4) + (int) (column & 15);
|
||||||
int z = (chunkZ << 4) + (int) ((column >>> 4) & 15);
|
int z = (chunkZ << 4) + (int) ((column >>> 4) & 15);
|
||||||
|
|||||||
@@ -85,8 +85,9 @@ public final class MantleHydrologyComponent extends IrisMantleComponent {
|
|||||||
generateFalls(writer, complex, runtime, own, chunkX, chunkZ);
|
generateFalls(writer, complex, runtime, own, chunkX, chunkZ);
|
||||||
if (poolsActive()) {
|
if (poolsActive()) {
|
||||||
HydrologySettings settings = runtime.settings();
|
HydrologySettings settings = runtime.settings();
|
||||||
new CavePoolFiller(settings).fill(chunkX, chunkZ,
|
Engine engine = getEngineMantle().getEngine();
|
||||||
new MantleCarvedTerrain(writer, complex, getEngineMantle().getEngine(), chunkX, chunkZ), writer, settings.seed());
|
new CavePoolFiller(settings).fill(chunkX, chunkZ, () -> new MantleCarvedTerrain(writer, complex, engine, chunkX, chunkZ), writer,
|
||||||
|
settings.seed());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+28
@@ -216,6 +216,34 @@ public class HydrologyRuntimeTest {
|
|||||||
assertEquals(planned + 1L, runtime.previewRevision());
|
assertEquals(planned + 1L, runtime.previewRevision());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test(timeout = 180_000L)
|
||||||
|
public void concurrentDemandsShareOneRasterBuild() throws Exception {
|
||||||
|
FakePlanners planners = new FakePlanners(settings(0, false));
|
||||||
|
HydrologyRuntime runtime = runtime(planners, () -> false);
|
||||||
|
runtime.prepareWindowsAround(0, 0, 0, 0, 1).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
|
CountDownLatch gate = new CountDownLatch(1);
|
||||||
|
planners.naturalGate = gate;
|
||||||
|
AtomicReference<ChunkWaterRaster> first = new AtomicReference<>();
|
||||||
|
AtomicReference<ChunkWaterRaster> second = new AtomicReference<>();
|
||||||
|
Thread leader = new Thread(() -> first.set(runtime.raster(0, 0)));
|
||||||
|
Thread follower = new Thread(() -> second.set(runtime.raster(0, 0)));
|
||||||
|
try {
|
||||||
|
leader.start();
|
||||||
|
await(() -> planners.naturalSamples.get() > 0, "the first demand starts sampling");
|
||||||
|
follower.start();
|
||||||
|
await(() -> follower.getState() == Thread.State.WAITING || follower.getState() == Thread.State.TIMED_WAITING,
|
||||||
|
"the second demand waits");
|
||||||
|
assertEquals(0, planners.naturalSamplesOn(follower));
|
||||||
|
} finally {
|
||||||
|
gate.countDown();
|
||||||
|
leader.join(TimeUnit.SECONDS.toMillis(TIMEOUT_SECONDS));
|
||||||
|
follower.join(TimeUnit.SECONDS.toMillis(TIMEOUT_SECONDS));
|
||||||
|
}
|
||||||
|
assertFalse(first.get().provisional());
|
||||||
|
assertSame(first.get(), second.get());
|
||||||
|
assertEquals(0, planners.naturalSamplesOn(follower));
|
||||||
|
}
|
||||||
|
|
||||||
@Test(timeout = 180_000L)
|
@Test(timeout = 180_000L)
|
||||||
public void failedRasterDoesNotAdvancePreviewRevision() throws Exception {
|
public void failedRasterDoesNotAdvancePreviewRevision() throws Exception {
|
||||||
FakePlanners planners = new FakePlanners(settings(0, false));
|
FakePlanners planners = new FakePlanners(settings(0, false));
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package art.arcane.iris.generation.hydrology.raster;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.hydrology.config.IrisBankStyle;
|
||||||
|
import art.arcane.iris.generation.hydrology.config.IrisHydrologyMaterials;
|
||||||
|
import art.arcane.iris.generation.hydrology.water.HydrologyTier;
|
||||||
|
import art.arcane.iris.generation.hydrology.water.Reach;
|
||||||
|
import art.arcane.iris.generation.hydrology.water.ReachStyle;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.SplittableRandom;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class ReachSegmentsTest {
|
||||||
|
private static final HydrologyTier TIER = new HydrologyTier("test", 0f, 1f, 4f, 1f, 0.4f, 1, true, 6, 4, IrisBankStyle.SMOOTH,
|
||||||
|
false, new IrisHydrologyMaterials(), "water", List.of());
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void coveredChannelLookupsMatchTheFullScan() {
|
||||||
|
SplittableRandom random = new SplittableRandom(1337L);
|
||||||
|
int hits = 0;
|
||||||
|
for (int trial = 0; trial < 40; trial++) {
|
||||||
|
int originX = random.nextInt(-2000, 2000) << 4;
|
||||||
|
int originZ = random.nextInt(-2000, 2000) << 4;
|
||||||
|
List<Reach> reaches = new ArrayList<>();
|
||||||
|
int count = 1 + random.nextInt(12);
|
||||||
|
for (int walk = 0; walk < count; walk++) {
|
||||||
|
reaches.add(randomWalk(trial * 100L + walk, originX + 8, originZ + 8, random));
|
||||||
|
}
|
||||||
|
ReachSegments scanned = segments(reaches, originX, originZ);
|
||||||
|
ReachSegments covered = segments(reaches, originX, originZ);
|
||||||
|
covered.coverChannels(originX - 17, originZ - 17, originX + 32, originZ + 32);
|
||||||
|
ReachCursor scan = new ReachCursor();
|
||||||
|
ReachCursor cover = new ReachCursor();
|
||||||
|
for (int z = originZ - 20; z <= originZ + 35; z++) {
|
||||||
|
for (int x = originX - 20; x <= originX + 35; x++) {
|
||||||
|
ReachSegments.Scope scope = random.nextInt(4) == 0 ? ReachSegments.Scope.SHORE : ReachSegments.Scope.CHANNEL;
|
||||||
|
scanned.nearest(x, z, scope, scan);
|
||||||
|
covered.nearest(x, z, scope, cover);
|
||||||
|
String at = scope + " at " + x + "," + z;
|
||||||
|
assertSameChoice(at + " channel", scan.channel, cover.channel);
|
||||||
|
assertSameChoice(at + " bank", scan.bank, cover.bank);
|
||||||
|
assertSameChoice(at + " edge", scan.edge, cover.edge);
|
||||||
|
if (scan.channel.reach != null) {
|
||||||
|
hits++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue("random walks cross the covered area, hits=" + hits, hits > 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertSameChoice(String what, ReachCursor.Choice expected, ReachCursor.Choice actual) {
|
||||||
|
assertSame(what, expected.reach, actual.reach);
|
||||||
|
assertEquals(what, expected.segment, actual.segment);
|
||||||
|
assertEquals(what, expected.index, actual.index);
|
||||||
|
assertEquals(what, Double.doubleToLongBits(expected.t), Double.doubleToLongBits(actual.t));
|
||||||
|
assertEquals(what, Double.doubleToLongBits(expected.distance), Double.doubleToLongBits(actual.distance));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ReachSegments segments(List<Reach> reaches, int originX, int originZ) {
|
||||||
|
ReachSegments segments = new ReachSegments(16);
|
||||||
|
for (Reach reach : reaches) {
|
||||||
|
segments.addNear(reach, originX - 1f, originZ - 1f, originX + 16f, originZ + 16f, ChunkWaterRaster.MAX_SHORE_DISTANCE + 1f);
|
||||||
|
}
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Reach randomWalk(long id, int centerX, int centerZ, SplittableRandom random) {
|
||||||
|
int points = 2 + random.nextInt(30);
|
||||||
|
float[] xs = new float[points];
|
||||||
|
float[] zs = new float[points];
|
||||||
|
float[] halfWidth = new float[points];
|
||||||
|
xs[0] = centerX + (float) random.nextDouble(-60D, 60D);
|
||||||
|
zs[0] = centerZ + (float) random.nextDouble(-60D, 60D);
|
||||||
|
halfWidth[0] = (float) random.nextDouble(0.5D, 9D);
|
||||||
|
for (int point = 1; point < points; point++) {
|
||||||
|
xs[point] = xs[point - 1] + (float) random.nextDouble(-12D, 12D);
|
||||||
|
zs[point] = zs[point - 1] + (float) random.nextDouble(-12D, 12D);
|
||||||
|
halfWidth[point] = random.nextInt(8) == 0 ? halfWidth[point - 1] : (float) random.nextDouble(0.5D, 9D);
|
||||||
|
}
|
||||||
|
int[] grade = new int[points];
|
||||||
|
float[] depth = new float[points];
|
||||||
|
ReachStyle[] style = new ReachStyle[points];
|
||||||
|
Arrays.fill(grade, 64);
|
||||||
|
Arrays.fill(depth, 1f);
|
||||||
|
Arrays.fill(style, ReachStyle.RUN);
|
||||||
|
return new Reach(id, TIER, -1L, xs, zs, grade, halfWidth, depth, style, 100f);
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
-6
@@ -15,6 +15,7 @@ import java.util.HashMap;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.IntFunction;
|
import java.util.function.IntFunction;
|
||||||
|
|
||||||
@@ -50,18 +51,37 @@ public class CavePoolFillerTest {
|
|||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
for (int chunkX = 0; chunkX < 1024; chunkX++) {
|
for (int chunkX = 0; chunkX < 1024; chunkX++) {
|
||||||
filler.fill(chunkX, 0, carved, writer, SEED);
|
filler.fill(chunkX, 0, () -> carved, writer, SEED);
|
||||||
}
|
}
|
||||||
assertTrue("half density samples approximately half the candidate columns", columns.size() > 3600 && columns.size() < 4500);
|
assertTrue("half density samples approximately half the candidate columns", columns.size() > 3600 && columns.size() < 4500);
|
||||||
Set<Long> repeated = new HashSet<>(columns);
|
Set<Long> repeated = new HashSet<>(columns);
|
||||||
columns.clear();
|
columns.clear();
|
||||||
for (int chunkX = 1023; chunkX >= 0; chunkX--) {
|
for (int chunkX = 1023; chunkX >= 0; chunkX--) {
|
||||||
filler.fill(chunkX, 0, carved, writer, SEED);
|
filler.fill(chunkX, 0, () -> carved, writer, SEED);
|
||||||
}
|
}
|
||||||
assertEquals(repeated, columns);
|
assertEquals(repeated, columns);
|
||||||
verifyNoInteractions(writer);
|
verifyNoInteractions(writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void chunksWithoutAPoolSiteNeverOpenTheirCaves() {
|
||||||
|
IrisHydrology config = new IrisHydrology().setDensity(0.5D);
|
||||||
|
config.getUnderground().getCavePools().setDensity(0.05D);
|
||||||
|
CavePoolFiller filler = new CavePoolFiller(HydrologySettings.compile(config, SEED, 63, 0, 384));
|
||||||
|
CarvedTerrain carved = mock(CarvedTerrain.class);
|
||||||
|
MantleWriter writer = mock(MantleWriter.class);
|
||||||
|
AtomicInteger opened = new AtomicInteger();
|
||||||
|
int chunks = 1024;
|
||||||
|
for (int chunkX = 0; chunkX < chunks; chunkX++) {
|
||||||
|
filler.fill(chunkX, 0, () -> {
|
||||||
|
opened.incrementAndGet();
|
||||||
|
return carved;
|
||||||
|
}, writer, SEED);
|
||||||
|
}
|
||||||
|
assertTrue("only chunks drawing a pool site open their caves, opened " + opened.get(), opened.get() > 0 && opened.get() < chunks / 8);
|
||||||
|
verifyNoInteractions(writer);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void zeroGlobalDensityDoesNotReadCaves() {
|
public void zeroGlobalDensityDoesNotReadCaves() {
|
||||||
IrisHydrology config = new IrisHydrology().setDensity(0D);
|
IrisHydrology config = new IrisHydrology().setDensity(0D);
|
||||||
@@ -69,7 +89,7 @@ public class CavePoolFillerTest {
|
|||||||
CavePoolFiller filler = new CavePoolFiller(HydrologySettings.compile(config, SEED, 63, 0, 384));
|
CavePoolFiller filler = new CavePoolFiller(HydrologySettings.compile(config, SEED, 63, 0, 384));
|
||||||
CarvedTerrain carved = mock(CarvedTerrain.class);
|
CarvedTerrain carved = mock(CarvedTerrain.class);
|
||||||
MantleWriter writer = mock(MantleWriter.class);
|
MantleWriter writer = mock(MantleWriter.class);
|
||||||
filler.fill(0, 0, carved, writer, SEED);
|
filler.fill(0, 0, () -> carved, writer, SEED);
|
||||||
verifyNoInteractions(carved, writer);
|
verifyNoInteractions(carved, writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,10 +163,10 @@ public class CavePoolFillerTest {
|
|||||||
MantleWriter forwardWriter = writer(forward);
|
MantleWriter forwardWriter = writer(forward);
|
||||||
MantleWriter reverseWriter = writer(reverse);
|
MantleWriter reverseWriter = writer(reverse);
|
||||||
for (int index = 0; index < 9; index++) {
|
for (int index = 0; index < 9; index++) {
|
||||||
new CavePoolFiller(settings).fill(index % 3 - 1, index / 3 - 1, world, forwardWriter, SEED);
|
new CavePoolFiller(settings).fill(index % 3 - 1, index / 3 - 1, () -> world, forwardWriter, SEED);
|
||||||
}
|
}
|
||||||
for (int index = 8; index >= 0; index--) {
|
for (int index = 8; index >= 0; index--) {
|
||||||
new CavePoolFiller(settings).fill(index % 3 - 1, index / 3 - 1, world, reverseWriter, SEED);
|
new CavePoolFiller(settings).fill(index % 3 - 1, index / 3 - 1, () -> world, reverseWriter, SEED);
|
||||||
}
|
}
|
||||||
|
|
||||||
assertFalse(forward.isEmpty());
|
assertFalse(forward.isEmpty());
|
||||||
@@ -284,7 +304,7 @@ public class CavePoolFillerTest {
|
|||||||
|
|
||||||
private static Map<Long, HydrologyCaveCell> fill(HydrologySettings settings, VoxelWorld world, int chunkX, int chunkZ) {
|
private static Map<Long, HydrologyCaveCell> fill(HydrologySettings settings, VoxelWorld world, int chunkX, int chunkZ) {
|
||||||
Map<Long, HydrologyCaveCell> written = new HashMap<>();
|
Map<Long, HydrologyCaveCell> written = new HashMap<>();
|
||||||
new CavePoolFiller(settings).fill(chunkX, chunkZ, world, writer(written), SEED);
|
new CavePoolFiller(settings).fill(chunkX, chunkZ, () -> world, writer(written), SEED);
|
||||||
return written;
|
return written;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ public class MantleCarvedTerrainTest {
|
|||||||
for (SeaInlet inlet : complex.getHydrologyRuntime().inlets(0, chunkZ)) {
|
for (SeaInlet inlet : complex.getHydrologyRuntime().inlets(0, chunkZ)) {
|
||||||
rasterizer.write(inlet, 0, chunkZ, writer, HydrologyCaveCell.SEA_FLUID, "", SeaInletRasterizer.SubterrainFluid.NOWHERE);
|
rasterizer.write(inlet, 0, chunkZ, writer, HydrologyCaveCell.SEA_FLUID, "", SeaInletRasterizer.SubterrainFluid.NOWHERE);
|
||||||
}
|
}
|
||||||
new CavePoolFiller(settings).fill(0, chunkZ, new MantleCarvedTerrain(writer, complex, engine, 0, chunkZ), writer, SEED);
|
new CavePoolFiller(settings).fill(0, chunkZ, () -> new MantleCarvedTerrain(writer, complex, engine, 0, chunkZ), writer, SEED);
|
||||||
}
|
}
|
||||||
return cells;
|
return cells;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user