mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-12 03:24:14 +00:00
Refine terrain and pregeneration
- Add coherent river width variation and shape source and terminal pools. - Preserve separated hydrology intervals when checking river connectivity. - Reduce footprint sorting, engine context lookup, and terrain blending work. - Reuse modded snapshot validation after content and registry checks. - Explain production pack update staging and restart requirements. - Retain pregeneration map updates before its desktop window opens. - Improve pregeneration layout, coordinate hover, keyboard controls, and error details. - Add the new desktop labels to every supported language catalog. - Add the buildPsychoLT entry point for verified platform outputs. - Cover terrain, validation, and desktop behavior with regression tests.
This commit is contained in:
+9
-7
@@ -6,10 +6,9 @@ import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.pack.BrokenPackException;
|
||||
import art.arcane.iris.pack.PackValidationCache;
|
||||
import art.arcane.iris.pack.PackFingerprints;
|
||||
import art.arcane.iris.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.pack.PackValidationResult;
|
||||
import art.arcane.iris.pack.PackValidator;
|
||||
import art.arcane.iris.world.history.GenerationEpoch;
|
||||
import art.arcane.iris.world.history.GenerationEpochContractFactory;
|
||||
import art.arcane.iris.world.history.GenerationHistory;
|
||||
@@ -174,7 +173,7 @@ final class ModdedGenerationHistoryStorage {
|
||||
static ActivePack resolveActive(GenerationHistory history) throws IOException {
|
||||
GenerationEpoch epoch = history.activeEpoch();
|
||||
Path packRoot = normalize(history.activePackRoot());
|
||||
validatePack(packRoot, epoch.packFingerprint());
|
||||
validatePack(packRoot);
|
||||
validateDimension(packRoot, epoch);
|
||||
return new ActivePack(
|
||||
packRoot,
|
||||
@@ -184,13 +183,16 @@ final class ModdedGenerationHistoryStorage {
|
||||
);
|
||||
}
|
||||
|
||||
private static void validatePack(Path packRoot, String fingerprint) {
|
||||
String contextFingerprint = PackValidationCache.contextFingerprint();
|
||||
PackValidationResult validation = PackValidator.validate(packRoot.toFile());
|
||||
PackValidationRegistry.publish(packRoot, validation, fingerprint, contextFingerprint);
|
||||
static PackValidationResult validatePack(Path packRoot) {
|
||||
PackValidationResult validation = PackValidationRegistry.getMatching(
|
||||
packRoot, PackFingerprints.computePackTreeFingerprint(packRoot.toFile()));
|
||||
if (validation == null) {
|
||||
validation = ModdedStartup.validatePack(packRoot.toFile());
|
||||
}
|
||||
if (!validation.isLoadable()) {
|
||||
throw new BrokenPackException(packRoot.toString(), validation.getBlockingErrors());
|
||||
}
|
||||
return validation;
|
||||
}
|
||||
|
||||
private static void validateDimension(
|
||||
|
||||
+2
-1
@@ -119,7 +119,8 @@ public final class ModdedPackUpdateService implements ModdedTickableService {
|
||||
+ ", latest built-in release v" + update.version() + ".");
|
||||
updates.add("Back up and review custom edits in " + new File(packsFolder, pack.name())
|
||||
+ "; close Studio, then update with /iris download pack=" + pack.name() + " overwrite=true.");
|
||||
updates.add("Existing worlds retain their recorded production pack; this updates the authoring pack for new worlds.");
|
||||
updates.add("Existing worlds retain their recorded production pack. Back up the complete world, then stage an update with /iris world update <dimension> "
|
||||
+ pack.name() + " and restart the server to activate it.");
|
||||
}
|
||||
}
|
||||
notices = List.copyOf(updates);
|
||||
|
||||
+52
@@ -3,6 +3,7 @@ package art.arcane.iris.modded;
|
||||
import art.arcane.iris.modded.command.ModdedPackCommands;
|
||||
import art.arcane.iris.pack.BrokenPackException;
|
||||
import art.arcane.iris.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.pack.PackValidationCache;
|
||||
import art.arcane.iris.pack.PackValidationResult;
|
||||
import art.arcane.iris.pack.PackValidator;
|
||||
import org.junit.After;
|
||||
@@ -15,6 +16,7 @@ import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
@@ -22,6 +24,7 @@ import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.times;
|
||||
|
||||
public class ModdedPackValidationPublicationTest {
|
||||
@Rule
|
||||
@@ -32,6 +35,55 @@ public class ModdedPackValidationPublicationTest {
|
||||
PackValidationRegistry.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void repeatedSnapshotValidationParsesUnchangedPackOnce() throws Exception {
|
||||
File root = temporaryFolder.newFolder("snapshot");
|
||||
PackValidationResult result = loadable(root);
|
||||
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class)) {
|
||||
validator.when(() -> PackValidator.validate(root)).thenReturn(result);
|
||||
assertSame(result, ModdedGenerationHistoryStorage.validatePack(root.toPath()));
|
||||
assertSame(result, ModdedGenerationHistoryStorage.validatePack(root.toPath()));
|
||||
validator.verify(() -> PackValidator.validate(root), times(1));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void snapshotValidationRejectsChangedBytesWithPreservedTimestamp() throws Exception {
|
||||
File root = temporaryFolder.newFolder("snapshot");
|
||||
File content = new File(root, "content.json");
|
||||
Files.writeString(content.toPath(), "valid");
|
||||
FileTime timestamp = Files.getLastModifiedTime(content.toPath());
|
||||
PackValidationResult broken = new PackValidationResult(root.getName(), List.of("invalid"), List.of(), 1L);
|
||||
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class)) {
|
||||
validator.when(() -> PackValidator.validate(root)).thenReturn(loadable(root), broken);
|
||||
ModdedGenerationHistoryStorage.validatePack(root.toPath());
|
||||
Files.writeString(content.toPath(), "wrong");
|
||||
Files.setLastModifiedTime(content.toPath(), timestamp);
|
||||
assertThrows(BrokenPackException.class,
|
||||
() -> ModdedGenerationHistoryStorage.validatePack(root.toPath()));
|
||||
assertThrows(BrokenPackException.class,
|
||||
() -> ModdedGenerationHistoryStorage.validatePack(root.toPath()));
|
||||
validator.verify(() -> PackValidator.validate(root), times(2));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void snapshotValidationReparsesChangedRegistryContext() throws Exception {
|
||||
File root = temporaryFolder.newFolder("snapshot");
|
||||
AtomicReference<String> context = new AtomicReference<>("first");
|
||||
PackValidationResult first = loadable(root);
|
||||
PackValidationResult second = loadable(root);
|
||||
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class);
|
||||
MockedStatic<PackValidationCache> cache = mockStatic(PackValidationCache.class)) {
|
||||
cache.when(PackValidationCache::contextFingerprint).thenAnswer(ignored -> context.get());
|
||||
validator.when(() -> PackValidator.validate(root)).thenReturn(first, second);
|
||||
assertSame(first, ModdedGenerationHistoryStorage.validatePack(root.toPath()));
|
||||
context.set("second");
|
||||
assertSame(second, ModdedGenerationHistoryStorage.validatePack(root.toPath()));
|
||||
validator.verify(() -> PackValidator.validate(root), times(2));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validationPublishesTheCanonicalRootAndPackName() throws Exception {
|
||||
File root = temporaryFolder.newFolder("terrain");
|
||||
|
||||
+3
@@ -65,6 +65,9 @@ public class ModdedPackUpdateServiceTest {
|
||||
verify(online, times(1)).sendMessage(contains("installed v1, latest built-in release v2"));
|
||||
verify(online, times(1)).sendMessage(contains(new File(root, "overworld").toString()));
|
||||
verify(online, times(1)).sendMessage(contains("/iris download pack=overworld overwrite=true"));
|
||||
verify(online, times(1)).sendMessage(contains("Back up the complete world"));
|
||||
verify(online, times(1)).sendMessage(contains("/iris world update <dimension> overworld"));
|
||||
verify(online, times(1)).sendMessage(contains("restart the server to activate it"));
|
||||
verify(online, never()).sendMessage(contains("plugins/"));
|
||||
NativeProtocolPlayer later = player(true, false);
|
||||
service.notifyPlayer(later);
|
||||
|
||||
@@ -775,6 +775,12 @@ tasks.register('buildAll') {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('buildPsychoLT') {
|
||||
group = 'development'
|
||||
description = 'Builds all verified Iris platform jars and stages them in the local test-server dropins and PluginOuts.'
|
||||
dependsOn('buildAll')
|
||||
}
|
||||
|
||||
tasks.register('irisDev', Copy) {
|
||||
group = 'iris'
|
||||
from(project(':core').layout.buildDirectory.files('libs/core-javadoc.jar', 'libs/core-sources.jar'))
|
||||
|
||||
+1
-1
@@ -589,7 +589,7 @@ public record HydrologyPlannerSettings(
|
||||
}
|
||||
}
|
||||
|
||||
/** A pond at one end of a surface course: a round bowl holding the course's head at that end. */
|
||||
/** A pond at one end of a surface course: a shaped bowl holding the course's head at that end. */
|
||||
public record Pond(boolean enabled, int minimumRadius, int maximumRadius, int depth) {
|
||||
public Pond {
|
||||
if (minimumRadius < 1 || maximumRadius < minimumRadius || depth < 1) {
|
||||
|
||||
+81
-2
@@ -2,6 +2,8 @@ package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceCenterline;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceLayerColumn;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue;
|
||||
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayList;
|
||||
@@ -71,10 +73,16 @@ final class HydrologyRegionalConnectivity {
|
||||
}
|
||||
|
||||
boolean connected(RiverCourse course, HydrologyTerrainSampler sampler, HydrologyPlannerSettings settings) {
|
||||
if (water.isEmpty() || !disjoint.isEmpty()) {
|
||||
if (water.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
LongOpenHashSet visited = new LongOpenHashSet(water.size());
|
||||
if (!disjoint.isEmpty()) {
|
||||
if (!connectedIntervals(visited)) {
|
||||
return false;
|
||||
}
|
||||
return reachesTerminal(course, sampler, settings, visited);
|
||||
}
|
||||
LongArrayFIFOQueue queue = new LongArrayFIFOQueue();
|
||||
long start = water.keySet().iterator().nextLong();
|
||||
queue.enqueue(start);
|
||||
@@ -102,6 +110,65 @@ final class HydrologyRegionalConnectivity {
|
||||
if (visited.size() != water.size()) {
|
||||
return false;
|
||||
}
|
||||
return reachesTerminal(course, sampler, settings, visited);
|
||||
}
|
||||
|
||||
private boolean connectedIntervals(LongOpenHashSet visited) {
|
||||
int count = water.size();
|
||||
for (LongArrayList intervals : disjoint.values()) {
|
||||
count += intervals.size() - 1;
|
||||
}
|
||||
long[] columns = new long[count];
|
||||
long[] intervals = new long[count];
|
||||
Long2IntOpenHashMap starts = new Long2IntOpenHashMap(water.size());
|
||||
starts.defaultReturnValue(-1);
|
||||
int index = 0;
|
||||
for (long column : water.keySet()) {
|
||||
starts.put(column, index);
|
||||
LongArrayList layers = disjoint.get(column);
|
||||
int size = layers == null ? 1 : layers.size();
|
||||
for (int layer = 0; layer < size; layer++) {
|
||||
columns[index] = column;
|
||||
intervals[index++] = layers == null ? water.get(column) : layers.getLong(layer);
|
||||
}
|
||||
}
|
||||
boolean[] reached = new boolean[count];
|
||||
IntArrayFIFOQueue queue = new IntArrayFIFOQueue();
|
||||
queue.enqueue(0);
|
||||
reached[0] = true;
|
||||
visited.add(columns[0]);
|
||||
int reachedCount = 1;
|
||||
while (!queue.isEmpty()) {
|
||||
int current = queue.dequeueInt();
|
||||
int currentBed = (int) (intervals[current] >> 32);
|
||||
int currentHead = (int) intervals[current];
|
||||
int x = RiverFootprint.unpackX(columns[current]);
|
||||
int z = RiverFootprint.unpackZ(columns[current]);
|
||||
for (int[] offset : NEIGHBORS) {
|
||||
long nextColumn = RiverFootprint.pack(x + offset[0], z + offset[1]);
|
||||
int start = starts.get(nextColumn);
|
||||
if (start < 0) {
|
||||
continue;
|
||||
}
|
||||
LongArrayList layers = disjoint.get(nextColumn);
|
||||
int end = start + (layers == null ? 1 : layers.size());
|
||||
for (int next = start; next < end; next++) {
|
||||
if (reached[next] || Math.max(currentBed, (int) (intervals[next] >> 32))
|
||||
>= Math.min(currentHead, (int) intervals[next])) {
|
||||
continue;
|
||||
}
|
||||
reached[next] = true;
|
||||
reachedCount++;
|
||||
visited.add(nextColumn);
|
||||
queue.enqueue(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
return reachedCount == count;
|
||||
}
|
||||
|
||||
private boolean reachesTerminal(RiverCourse course, HydrologyTerrainSampler sampler,
|
||||
HydrologyPlannerSettings settings, LongOpenHashSet visited) {
|
||||
HydraulicSegment first = course.segments().getFirst();
|
||||
HydraulicSegment last = course.segments().getLast();
|
||||
return reaches(last, false, sampler, settings, visited)
|
||||
@@ -154,9 +221,21 @@ final class HydrologyRegionalConnectivity {
|
||||
private boolean touchesOwnedWater(int x, int z, int seaLevel, LongOpenHashSet visited) {
|
||||
for (int[] offset : NEIGHBORS) {
|
||||
long key = RiverFootprint.pack(x + offset[0], z + offset[1]);
|
||||
if (visited.contains(key) && (int) water.get(key) == seaLevel) {
|
||||
if (!visited.contains(key)) {
|
||||
continue;
|
||||
}
|
||||
LongArrayList layers = disjoint.get(key);
|
||||
if (layers == null) {
|
||||
if ((int) water.get(key) == seaLevel) {
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (int index = 0; index < layers.size(); index++) {
|
||||
if ((int) layers.getLong(index) == seaLevel) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
+12
-5
@@ -31,6 +31,7 @@ final class HydrologyRegionalPlanner {
|
||||
private static final long BASIN_SALT = 0x524547424153494eL;
|
||||
private static final long COURSE_SALT = 0x524547434f555253L;
|
||||
private static final long NODE_SALT = 0x5245474e4f444553L;
|
||||
private static final double AUTHORED_WIDTH_WEIGHT = 0.65D;
|
||||
private static final int MAXIMUM_SOURCE_TRIALS = 24;
|
||||
private static final int SOURCE_TRIAL_WINDOW = 16;
|
||||
private static final int MAXIMUM_DIAGNOSTICS = 64;
|
||||
@@ -579,11 +580,17 @@ final class HydrologyRegionalPlanner {
|
||||
if (request.field() != HydrologyGeometrySampler.Field.SURFACE_WIDTH) {
|
||||
return planner.geometrySampler.sample(request);
|
||||
}
|
||||
int authored = planner.geometrySampler.sample(request);
|
||||
double fraction = 0.25D + 0.75D * context.flow().fraction(request.x(), request.z());
|
||||
double sampled = request.minimum() + (request.maximum() - request.minimum()) * fraction;
|
||||
double width = sampled * 0.75D + authored * 0.25D;
|
||||
double freedom = context.morphology().freedomAt(request.x(), request.z());
|
||||
return regionalWidth(request, planner.geometrySampler.sample(request),
|
||||
context.flow().fraction(request.x(), request.z()),
|
||||
context.morphology().freedomAt(request.x(), request.z()));
|
||||
}
|
||||
|
||||
static int regionalWidth(HydrologyGeometrySampler.Request request, int authored,
|
||||
double flow, double freedom) {
|
||||
double fraction = 0.25D + 0.75D * flow;
|
||||
double range = (double) request.maximum() - request.minimum();
|
||||
double sampled = request.minimum() + range * fraction;
|
||||
double width = sampled * (1D - AUTHORED_WIDTH_WEIGHT) + authored * AUTHORED_WIDTH_WEIGHT;
|
||||
double constrained = request.minimum() + (width - request.minimum()) * freedom;
|
||||
return Math.max(request.minimum(), Math.min(request.maximum(), (int) StrictMath.round(constrained)));
|
||||
}
|
||||
|
||||
+4
-4
@@ -16,7 +16,7 @@ import lombok.experimental.Accessors;
|
||||
@Description("Controls the wet channel of a surface river.")
|
||||
@Data
|
||||
public class IrisSurfaceRiverChannelConfig {
|
||||
@Description("Wet channel width in blocks.")
|
||||
@Description("Wet channel width range in blocks. Authored noise and coherent reach variation shape the width; equal endpoints disable the additional variation.")
|
||||
private IrisStyledRange width = range(4D, 8D, 1024D);
|
||||
|
||||
@Description("Wet bed depth in blocks at the channel center.")
|
||||
@@ -44,17 +44,17 @@ public class IrisSurfaceRiverChannelConfig {
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(4)
|
||||
@Description("Width of the spring pool at the headwater relative to the channel width; 1 starts the river at its normal width.")
|
||||
@Description("Peak width of the spring pool relative to the channel width, reached near the first quarter of springLength; 1 keeps the source at normal channel width.")
|
||||
private double springWidthRatio = 2.5D;
|
||||
|
||||
@MinNumber(4)
|
||||
@MaxNumber(96)
|
||||
@Description("Blocks over which the spring pool narrows back to the channel width.")
|
||||
@Description("Blocks over which the source widens into its spring pool and narrows back to the channel width.")
|
||||
private int springLength = 24;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(64)
|
||||
@Description("Stations along the course the sampled width and depth are averaged over, so the channel changes size gradually; 0 follows the sampled values exactly at every station.")
|
||||
@Description("Stations along the course the varied width and sampled depth are averaged over; 0 retains their unsmoothed values at each station.")
|
||||
private int smoothingRadius = 16;
|
||||
|
||||
@MinNumber(0.2)
|
||||
|
||||
+35
-3
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.generation.hydrology.surface;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.HydrologyGeometrySampler;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyHash;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTerrainSample;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTerrainSampler;
|
||||
@@ -8,6 +9,13 @@ import art.arcane.iris.generation.hydrology.HydrologyTerrainSampler;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class ChannelProfileBuilder {
|
||||
private static final long WIDTH_REACH_SALT = 0x5749445448524541L;
|
||||
private static final double WIDTH_REACH_STRENGTH = 0.45D;
|
||||
private static final double SPRING_OPENING_FRACTION = 0.6D;
|
||||
private static final double SPRING_PEAK_PROGRESS = 0.25D;
|
||||
private static final int MINIMUM_WIDTH_WAVELENGTH = 32;
|
||||
private static final int MAXIMUM_WIDTH_WAVELENGTH = 128;
|
||||
|
||||
private final HydrologyPlannerSettings.Surface surface;
|
||||
private final HydrologyTerrainSampler sampler;
|
||||
private final HydrologyGeometrySampler geometry;
|
||||
@@ -25,13 +33,18 @@ public final class ChannelProfileBuilder {
|
||||
public ChannelProfile build(
|
||||
SurfaceCenterline centerline,
|
||||
String profileKey,
|
||||
boolean directOcean
|
||||
boolean directOcean,
|
||||
long worldSeed
|
||||
) {
|
||||
profileKey = profileKey == null ? "" : profileKey;
|
||||
int count = centerline.size();
|
||||
double[] width = new double[count];
|
||||
double[] depth = new double[count];
|
||||
double[] bank = new double[count];
|
||||
int coast = count;
|
||||
long widthSeed = HydrologyHash.mix(worldSeed, WIDTH_REACH_SALT, profileKey.hashCode());
|
||||
int widthWavelength = Math.max(MINIMUM_WIDTH_WAVELENGTH,
|
||||
Math.min(MAXIMUM_WIDTH_WAVELENGTH, surface.maximumWidth() * 12));
|
||||
for (int station = 0; station < count; station++) {
|
||||
int stationX = centerline.x()[station];
|
||||
int stationZ = centerline.z()[station];
|
||||
@@ -60,7 +73,12 @@ public final class ChannelProfileBuilder {
|
||||
double widthMultiplier = terrain == null ? 1D : terrain.widthMultiplier();
|
||||
double depthMultiplier = terrain == null ? 1D : terrain.depthMultiplier();
|
||||
bank[station] = terrain == null ? 1D : terrain.bankMultiplier();
|
||||
width[station] = clamp(sampledWidth * widthMultiplier, surface.minimumWidth(), surface.maximumWidth() * 2D);
|
||||
double variedWidth = sampledWidth;
|
||||
if (surface.maximumWidth() > surface.minimumWidth() && sampledWidth > surface.minimumWidth()) {
|
||||
variedWidth = reachWidth(sampledWidth, surface.minimumWidth(), surface.maximumWidth(),
|
||||
SurfaceNoise.signed(widthSeed, stationX, stationZ, widthWavelength));
|
||||
}
|
||||
width[station] = clamp(variedWidth * widthMultiplier, surface.minimumWidth(), surface.maximumWidth() * 2D);
|
||||
depth[station] = clamp(sampledDepth * depthMultiplier, 1D, surface.maximumDepth() * 2D);
|
||||
}
|
||||
HydrologyPlannerSettings.Channel channel = surface.banks().channel();
|
||||
@@ -74,7 +92,12 @@ public final class ChannelProfileBuilder {
|
||||
for (int station = 0; station < spring; station++) {
|
||||
double remaining = 1D - SurfaceNoise.smoothStep(station / (double) spring);
|
||||
double localRatio = 1D + (springRatio - 1D) * springRoom(centerline, station, smoothWidth[station] * springRatio / 2D);
|
||||
smoothWidth[station] *= 1D + (localRatio - 1D) * remaining;
|
||||
double progress = station / (double) spring;
|
||||
double opening = progress <= SPRING_PEAK_PROGRESS
|
||||
? SPRING_OPENING_FRACTION + (1D - SPRING_OPENING_FRACTION)
|
||||
* SurfaceNoise.smoothStep(progress / SPRING_PEAK_PROGRESS)
|
||||
: 1D - SurfaceNoise.smoothStep((progress - SPRING_PEAK_PROGRESS) / (1D - SPRING_PEAK_PROGRESS));
|
||||
smoothWidth[station] *= 1D + (localRatio - 1D) * opening;
|
||||
smoothDepth[station] += remaining * channel.springExtraDepth();
|
||||
}
|
||||
// The inlet: over its length before the coast the channel widens toward the mouth flare and
|
||||
@@ -146,6 +169,15 @@ public final class ChannelProfileBuilder {
|
||||
return smoothed;
|
||||
}
|
||||
|
||||
static double reachWidth(double authored, int minimum, int maximum, double reach) {
|
||||
double range = (double) maximum - minimum;
|
||||
if (range <= 0D || authored <= minimum) {
|
||||
return authored;
|
||||
}
|
||||
double freedom = Math.min(1D, (authored - minimum) / (range * 0.25D));
|
||||
return clamp(authored + range * WIDTH_REACH_STRENGTH * freedom * reach, minimum, maximum);
|
||||
}
|
||||
|
||||
private static double clamp(double value, double minimum, double maximum) {
|
||||
return Math.max(minimum, Math.min(maximum, value));
|
||||
}
|
||||
|
||||
+22
-13
@@ -28,6 +28,11 @@ public final class ErosionFieldCompiler {
|
||||
private static final long SOURCE_POND_SALT = 0x504f4e4453524345L;
|
||||
private static final long TERMINAL_POND_SALT = 0x504f4e4454524dL;
|
||||
private static final int POND_RIM_SAMPLES_PER_BLOCK = 8;
|
||||
private static final long POND_ASPECT_SALT = 0x504f4e4441535045L;
|
||||
private static final double POND_LONG_AXIS_RATIO = 0.85D;
|
||||
private static final double POND_OFFSET_RATIO = 0.15D;
|
||||
private static final double POND_MINIMUM_SHORT_AXIS_RATIO = 0.6D;
|
||||
private static final double POND_SHORT_AXIS_RANGE = 0.2D;
|
||||
private static final double EPSILON = 1.0E-9D;
|
||||
|
||||
private final HydrologyPlannerSettings.Surface surface;
|
||||
@@ -174,26 +179,24 @@ public final class ErosionFieldCompiler {
|
||||
continue;
|
||||
}
|
||||
long key = RiverFootprint.pack(cellX, cellZ);
|
||||
int index = indices.get(key);
|
||||
int existing = index < 0 ? -1 : stations.getInt(index);
|
||||
double existingDistance = index < 0 ? 0D : distance.getDouble(index);
|
||||
// A cell on a station's cross-section row ties between the two segments meeting
|
||||
// there; the later station owns it so the row uses the head its own ring bounded.
|
||||
if (existing < 0
|
||||
|| cellDistance < existingDistance - EPSILON
|
||||
|| Math.abs(cellDistance - existingDistance) <= EPSILON && station > existing) {
|
||||
int index = indices.putIfAbsent(key, stations.size());
|
||||
if (index < 0) {
|
||||
indices.put(key, stations.size());
|
||||
stations.add(station);
|
||||
distance.add(cellDistance);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
int existing = stations.getInt(index);
|
||||
double existingDistance = distance.getDouble(index);
|
||||
// A cell on a station's cross-section row ties between the two segments meeting
|
||||
// there; the later station owns it so the row uses the head its own ring bounded.
|
||||
if (cellDistance < existingDistance - EPSILON
|
||||
|| Math.abs(cellDistance - existingDistance) <= EPSILON && station > existing) {
|
||||
stations.set(index, station);
|
||||
distance.set(index, cellDistance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
geometry.prepared = true;
|
||||
}
|
||||
int oceanStart = oceanStart(centerline, valley, terminal);
|
||||
@@ -352,7 +355,7 @@ public final class ErosionFieldCompiler {
|
||||
}
|
||||
|
||||
/**
|
||||
* A round bowl around an end station holding that station's head: wet inside its outline, a shore
|
||||
* A shaped bowl around an end station holding that station's head: wet inside its outline, a shore
|
||||
* ring and an eroded rim outside it like the channel's own. Channel water inside the bowl joins
|
||||
* the pond at the pond's level; dry channel columns give way to the bowl. The radius is chosen per
|
||||
* course within the configured range and shrinks where the ground around the rim falls below the
|
||||
@@ -399,9 +402,15 @@ public final class ErosionFieldCompiler {
|
||||
double maximumBlendWidth = Math.min(banks.maximumBlendWidth(),
|
||||
Math.max(0.25D, erosion.excavation().maximumWidth() - shore));
|
||||
int reach = (int) StrictMath.ceil(radius * (1D + roughness) + shore + banks.maximumBlendWidth()) + 1;
|
||||
double tangentX = centerline.tangentX()[station];
|
||||
double tangentZ = centerline.tangentZ()[station];
|
||||
double offset = radius * POND_OFFSET_RATIO;
|
||||
double minor = POND_MINIMUM_SHORT_AXIS_RATIO + POND_SHORT_AXIS_RANGE * HydrologyHash.unit(HydrologyHash.mix(seed, POND_ASPECT_SALT));
|
||||
for (int deltaZ = -reach; deltaZ <= reach; deltaZ++) {
|
||||
for (int deltaX = -reach; deltaX <= reach; deltaX++) {
|
||||
double distance = Math.sqrt((double) deltaX * deltaX + (double) deltaZ * deltaZ);
|
||||
double along = (deltaX * tangentX + deltaZ * tangentZ - offset) / POND_LONG_AXIS_RATIO;
|
||||
double across = (-deltaX * tangentZ + deltaZ * tangentX) / minor;
|
||||
double distance = Math.sqrt(along * along + across * across);
|
||||
if (distance > reach) {
|
||||
continue;
|
||||
}
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ public final class SurfaceCourseBuilder {
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(path);
|
||||
boolean directOcean = terminal == SurfaceTerminal.OCEAN_MOUTH;
|
||||
ChannelProfile channel = new ChannelProfileBuilder(surface, sampler, geometry)
|
||||
.build(centerline, profileKey, directOcean);
|
||||
.build(centerline, profileKey, directOcean, worldSeed);
|
||||
ValleyProfile valley = new RiverProfileSolver(new RiverProfileSolver.Options(surface, seaLevel), sampler)
|
||||
.solve(path, centerline, channel, terminal, terminalHead, minimumCourseLength);
|
||||
if (!valley.accepted()) {
|
||||
|
||||
+28
-4
@@ -21,12 +21,15 @@ import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class SurfaceFootprintCompiler {
|
||||
private static final long COURSE_SEED_SALT = 0x535552464143455fL;
|
||||
private static final Comparator<SurfaceColumn> COLUMN_POSITION_ORDER = Comparator.comparingLong(
|
||||
column -> RiverFootprint.pack(column.x(), column.z()));
|
||||
|
||||
private final HydrologyPlannerSettings settings;
|
||||
private final HydrologyTerrainSampler sampler;
|
||||
@@ -175,11 +178,9 @@ public final class SurfaceFootprintCompiler {
|
||||
seed, centerline, run.channel(), run.valley(), run.terminal(),
|
||||
settings.outlets().maximumOceanApron(), run.ponds(), context,
|
||||
run.field());
|
||||
ArrayList<SurfaceColumn> ordered = new ArrayList<>(field.columns().values());
|
||||
ordered.sort(Comparator.comparingInt(SurfaceColumn::station)
|
||||
.thenComparingLong((SurfaceColumn column) -> RiverFootprint.pack(column.x(), column.z())));
|
||||
SurfaceColumn[] ordered = orderedColumns(field, centerline.size());
|
||||
SurfaceFeatureRefs features = new SurfaceFeatureRefs(course.id());
|
||||
ArrayList<SurfaceLayerColumn> columns = new ArrayList<>(ordered.size());
|
||||
ArrayList<SurfaceLayerColumn> columns = new ArrayList<>(ordered.length);
|
||||
for (SurfaceColumn column : ordered) {
|
||||
if (bounds != null && !bounds.contains(column.x(), column.z())) {
|
||||
continue;
|
||||
@@ -199,6 +200,29 @@ public final class SurfaceFootprintCompiler {
|
||||
return new SurfaceFootprint(columns, field.uncontainedWetCells(), field.rejection(), field.rejectionDetail(), field.bankExcavation());
|
||||
}
|
||||
|
||||
static SurfaceColumn[] orderedColumns(ErosionField field, int stationCount) {
|
||||
int[] ends = new int[stationCount + 1];
|
||||
for (SurfaceColumn column : field.columns().values()) {
|
||||
ends[column.station() + 1]++;
|
||||
}
|
||||
for (int station = 1; station < ends.length; station++) {
|
||||
ends[station] += ends[station - 1];
|
||||
}
|
||||
SurfaceColumn[] ordered = new SurfaceColumn[field.size()];
|
||||
for (SurfaceColumn column : field.columns().values()) {
|
||||
ordered[ends[column.station()]++] = column;
|
||||
}
|
||||
int start = 0;
|
||||
for (int station = 0; station < stationCount; station++) {
|
||||
int end = ends[station];
|
||||
if (end - start > 1) {
|
||||
Arrays.sort(ordered, start, end, COLUMN_POSITION_ORDER);
|
||||
}
|
||||
start = end;
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
|
||||
private SurfaceRunBoundary runBoundary(RiverCourse course, int first, int after,
|
||||
SurfaceCenterline centerline, ChannelProfile channel) {
|
||||
SurfaceRunBoundary.Plane start = null;
|
||||
|
||||
@@ -200,38 +200,37 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
if (!Double.isFinite(surfaceDetail) || surfaceDetail < 0D || surfaceDetail > 1D) {
|
||||
throw new IllegalArgumentException("Generator surfaceDetail must be finite and between 0 and 1");
|
||||
}
|
||||
if (surfaceDetail == 1D || composite.isEmpty()) {
|
||||
return sampleHeight(rx, rz, superSeed);
|
||||
if (composite.isEmpty()) {
|
||||
return 0D;
|
||||
}
|
||||
IrisData data = getLoader();
|
||||
Engine engine = data == null ? null : data.getEngine();
|
||||
if (surfaceDetail == 1D) {
|
||||
return sampleHeight(rx, rz, superSeed, data, engine);
|
||||
}
|
||||
SurfaceCacheStripe[] stripes = surfaceCache(data, engine).stripes();
|
||||
double lowerX = Math.floor(rx / SURFACE_GRID) * SURFACE_GRID;
|
||||
double lowerZ = Math.floor(rz / SURFACE_GRID) * SURFACE_GRID;
|
||||
double dx = (rx - lowerX) / SURFACE_GRID;
|
||||
double dz = (rz - lowerZ) / SURFACE_GRID;
|
||||
double northWest = cachedSurfaceHeight(lowerX, lowerZ, superSeed, stripes);
|
||||
double northWest = cachedSurfaceHeight(lowerX, lowerZ, superSeed, stripes, data, engine);
|
||||
if (dx == 0D && dz == 0D) {
|
||||
return northWest;
|
||||
}
|
||||
double north = dx == 0D ? northWest : IrisInterpolation.lerp(northWest,
|
||||
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ, superSeed, stripes), dx);
|
||||
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ, superSeed, stripes, data, engine), dx);
|
||||
double smooth = north;
|
||||
if (dz != 0D) {
|
||||
double southWest = cachedSurfaceHeight(lowerX, lowerZ + SURFACE_GRID, superSeed, stripes);
|
||||
double southWest = cachedSurfaceHeight(lowerX, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine);
|
||||
double south = dx == 0D ? southWest : IrisInterpolation.lerp(southWest,
|
||||
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID, superSeed, stripes), dx);
|
||||
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine), dx);
|
||||
smooth = IrisInterpolation.lerp(north, south, dz);
|
||||
}
|
||||
return surfaceDetail == 0D ? smooth
|
||||
: smooth + (sampleHeight(rx, rz, superSeed) - smooth) * surfaceDetail;
|
||||
}
|
||||
|
||||
private double sampleHeight(double rx, double rz, long superSeed) {
|
||||
if (composite.isEmpty()) {
|
||||
return 0;
|
||||
: smooth + (sampleHeight(rx, rz, superSeed, data, engine) - smooth) * surfaceDetail;
|
||||
}
|
||||
|
||||
private double sampleHeight(double rx, double rz, long superSeed, IrisData data, Engine engine) {
|
||||
int hc = (int) ((cliffHeightMin * 10) + 10 + cliffHeightMax * getSeed() + offsetX + offsetZ);
|
||||
double h = multiplicitive ? 1 : 0;
|
||||
double tp = 0;
|
||||
@@ -240,18 +239,18 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
|
||||
if (composite.size() == 1) {
|
||||
if (multiplicitive) {
|
||||
h *= composite.get(0).getNoise(getSeed() + superSeed + hc, sampleX, sampleZ, getLoader());
|
||||
h *= composite.get(0).getNoise(getSeed() + superSeed + hc, sampleX, sampleZ, data, engine);
|
||||
} else {
|
||||
tp += composite.get(0).getOpacity();
|
||||
h += composite.get(0).getNoise(getSeed() + superSeed + hc, sampleX, sampleZ, getLoader());
|
||||
h += composite.get(0).getNoise(getSeed() + superSeed + hc, sampleX, sampleZ, data, engine);
|
||||
}
|
||||
} else {
|
||||
for (IrisNoiseGenerator i : composite) {
|
||||
if (multiplicitive) {
|
||||
h *= i.getNoise(getSeed() + superSeed + hc, sampleX, sampleZ, getLoader());
|
||||
h *= i.getNoise(getSeed() + superSeed + hc, sampleX, sampleZ, data, engine);
|
||||
} else {
|
||||
tp += i.getOpacity();
|
||||
h += i.getNoise(getSeed() + superSeed + hc, sampleX, sampleZ, getLoader());
|
||||
h += i.getNoise(getSeed() + superSeed + hc, sampleX, sampleZ, data, engine);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -262,15 +261,15 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
v = 0;
|
||||
}
|
||||
|
||||
v = hasCliffs() ? cliff(rx, rz, v, superSeed + 294596 + hc) : v;
|
||||
v = hasCliffs() ? cliff(rx, rz, v, superSeed + 294596 + hc, data, engine) : v;
|
||||
v = hasCellCracks() ? cell(rx, rz, v, superSeed + 48622 + hc) : v;
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
private double cachedSurfaceHeight(double x, double z, long superSeed, SurfaceCacheStripe[] stripes) {
|
||||
private double cachedSurfaceHeight(double x, double z, long superSeed, SurfaceCacheStripe[] stripes, IrisData data, Engine engine) {
|
||||
if (x != (int) x || z != (int) z) {
|
||||
return sampleHeight(x, z, superSeed);
|
||||
return sampleHeight(x, z, superSeed, data, engine);
|
||||
}
|
||||
long key = ((long) (int) x << 32) | ((int) z & 0xffffffffL);
|
||||
long mixed = key ^ (key >>> 33);
|
||||
@@ -282,7 +281,7 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
if (cached != null) {
|
||||
return cached.height();
|
||||
}
|
||||
double height = sampleHeight(x, z, superSeed);
|
||||
double height = sampleHeight(x, z, superSeed, data, engine);
|
||||
stripe.put(slot, new SurfaceSample(key, superSeed, height));
|
||||
return height;
|
||||
}
|
||||
@@ -321,15 +320,25 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
}
|
||||
|
||||
public double getCliffHeight(double rx, double rz, double superSeed) {
|
||||
IrisData data = getLoader();
|
||||
return getCliffHeight(rx, rz, superSeed, data, data == null ? null : data.getEngine());
|
||||
}
|
||||
|
||||
private double getCliffHeight(double rx, double rz, double superSeed, IrisData data, Engine engine) {
|
||||
int hc = (int) ((cliffHeightMin * 10) + 10 + cliffHeightMax * getSeed() + offsetX + offsetZ);
|
||||
double sampleX = (rx + offsetX) / zoom;
|
||||
double sampleZ = (rz + offsetZ) / zoom;
|
||||
double h = cliffHeightGenerator.getNoise((long) (getSeed() + superSeed + hc), sampleX, sampleZ, getLoader());
|
||||
double h = cliffHeightGenerator.getNoise((long) (getSeed() + superSeed + hc), sampleX, sampleZ, data, engine);
|
||||
return IrisInterpolation.lerp(cliffHeightMin, cliffHeightMax, h);
|
||||
}
|
||||
|
||||
public double cliff(double rx, double rz, double v, double superSeed) {
|
||||
double cliffHeight = getCliffHeight(rx, rz, superSeed - 34857);
|
||||
IrisData data = getLoader();
|
||||
return cliff(rx, rz, v, superSeed, data, data == null ? null : data.getEngine());
|
||||
}
|
||||
|
||||
private double cliff(double rx, double rz, double v, double superSeed, IrisData data, Engine engine) {
|
||||
double cliffHeight = getCliffHeight(rx, rz, superSeed - 34857, data, engine);
|
||||
return (Math.round((v * 255D) / cliffHeight) * cliffHeight) / 255D;
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,11 @@ public class IrisGeneratorStyle {
|
||||
}
|
||||
|
||||
public CNG createForLayer(RNG rng, IrisData data, int octaveMultiplier) {
|
||||
return createNoCache(rng, data, Math.clamp(octaveMultiplier, 1, 16), 0, false, resolveEngine(data));
|
||||
return createForLayer(rng, data, octaveMultiplier, resolveEngine(data));
|
||||
}
|
||||
|
||||
CNG createForLayer(RNG rng, IrisData data, int octaveMultiplier, Engine engine) {
|
||||
return createNoCache(rng, data, Math.clamp(octaveMultiplier, 1, 16), 0, false, engine);
|
||||
}
|
||||
|
||||
public CNG createForPrebake(RNG rng, IrisData data, int fallbackCacheSize) {
|
||||
|
||||
@@ -102,8 +102,7 @@ public class IrisNoiseGenerator {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
protected CNG getGenerator(long superSeed, IrisData data) {
|
||||
Engine engine = data == null ? null : data.getEngine();
|
||||
CNG getGenerator(long superSeed, IrisData data, Engine engine) {
|
||||
long generatorSeed = superSeed + GENERATOR_SEED_SALT - seed;
|
||||
CachedGenerator recent = recentGenerator.get();
|
||||
if (recent != null && recent.key.matches(data, engine, generatorSeed)) {
|
||||
@@ -112,7 +111,7 @@ public class IrisNoiseGenerator {
|
||||
|
||||
GeneratorKey key = new GeneratorKey(data, engine, generatorSeed);
|
||||
CNG generator = generators.computeIfAbsent(key,
|
||||
ignored -> style.createForLayer(new RNG(generatorSeed), data, octaves));
|
||||
ignored -> style.createForLayer(new RNG(generatorSeed), data, octaves, engine));
|
||||
if (generator != null) {
|
||||
recentGenerator.set(new CachedGenerator(key, generator));
|
||||
}
|
||||
@@ -128,6 +127,14 @@ public class IrisNoiseGenerator {
|
||||
return offsetY;
|
||||
}
|
||||
|
||||
return getNoise(superSeed, xv, zv, data, data == null ? null : data.getEngine());
|
||||
}
|
||||
|
||||
double getNoise(long superSeed, double xv, double zv, IrisData data, Engine engine) {
|
||||
if (!enabled) {
|
||||
return offsetY;
|
||||
}
|
||||
|
||||
double x = xv;
|
||||
double z = zv;
|
||||
int g = 33;
|
||||
@@ -135,13 +142,13 @@ public class IrisNoiseGenerator {
|
||||
for (IrisNoiseGenerator i : fracture) {
|
||||
if (i.isEnabled()) {
|
||||
double fractureOffset = i.getOpacity() / 2D;
|
||||
x += i.getNoise(superSeed + seed + g, xv, zv, data) - fractureOffset;
|
||||
z += i.getNoise(superSeed + seed + g, zv, xv, data) - fractureOffset;
|
||||
x += i.getNoise(superSeed + seed + g, xv, zv, data, engine) - fractureOffset;
|
||||
z += i.getNoise(superSeed + seed + g, zv, xv, data, engine) - fractureOffset;
|
||||
}
|
||||
g += 819;
|
||||
}
|
||||
|
||||
CNG cng = getGenerator(superSeed, data);
|
||||
CNG cng = getGenerator(superSeed, data, engine);
|
||||
double sampleX = (x / zoom) + offsetX;
|
||||
double sampleZ = (z / zoom) + offsetZ;
|
||||
double n = cng.noiseFast2D(sampleX, sampleZ) * opacity;
|
||||
|
||||
@@ -111,6 +111,8 @@ public final class DesktopUiMessages {
|
||||
public static final TextKey PREGEN_CACHED = TextKey.of("iris.desktop.pregen.cached", "{method} · cached");
|
||||
public static final TextKey PREGEN_FAILED = TextKey.of("iris.desktop.pregen.failed", "{count} failed · see server log");
|
||||
public static final TextKey PREGEN_MAP = TextKey.of("iris.desktop.pregen.map", "Generation map");
|
||||
public static final TextKey PREGEN_CHUNK_COORDINATES = TextKey.of("iris.desktop.pregen.chunk_coordinates", "Chunk X {x} · Z {z}");
|
||||
public static final TextKey PREGEN_WINDOW_HINT = TextKey.of("iris.desktop.pregen.window_hint", "Closing this window keeps generation running.");
|
||||
public static final TextKey PREGEN_BOUNDS = TextKey.of("iris.desktop.pregen.bounds", "Chunks X {minX} to {maxX} · Z {minZ} to {maxZ}");
|
||||
public static final TextKey PREGEN_WAITING = TextKey.of("iris.desktop.pregen.waiting", "Waiting");
|
||||
public static final TextKey PREGEN_READY = TextKey.of("iris.desktop.pregen.ready", "Ready");
|
||||
@@ -212,6 +214,7 @@ public final class DesktopUiMessages {
|
||||
PREGEN_SIXTY, PREGEN_RATE, PREGEN_ETA, PREGEN_ELAPSED,
|
||||
PREGEN_MEMORY_LABEL, PREGEN_MEMORY_USAGE, PREGEN_PRESSURE, PREGEN_PRESSURE_VALUE,
|
||||
PREGEN_METHOD, PREGEN_CACHED, PREGEN_FAILED, PREGEN_MAP,
|
||||
PREGEN_CHUNK_COORDINATES, PREGEN_WINDOW_HINT,
|
||||
PREGEN_BOUNDS, PREGEN_WAITING, PREGEN_READY, PREGEN_EXISTING,
|
||||
PREGEN_NETWORK, PREGEN_TERRAIN_HINT,
|
||||
IMAGEMAP_TITLE, IMAGEMAP_PRESET, IMAGEMAP_LOAD, IMAGEMAP_IMPORT_PNG,
|
||||
|
||||
@@ -1,90 +1,65 @@
|
||||
package art.arcane.iris.studio.view;
|
||||
|
||||
import art.arcane.iris.localization.DesktopUiMessages;
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.ToolTipManager;
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferInt;
|
||||
import java.util.Arrays;
|
||||
import java.util.BitSet;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
final class PregenMapPanel extends JPanel {
|
||||
private static final int MAX_RASTER_SIZE = 1024;
|
||||
static final Color WAITING = new Color(29, 35, 46);
|
||||
|
||||
private final PregenRenderSnapshot.Bounds bounds;
|
||||
private final PregenMapState state;
|
||||
private final PregenMapState.View view;
|
||||
private final BufferedImage image;
|
||||
private final int[] imagePixels;
|
||||
private final int[] pendingPixels;
|
||||
private final BitSet dirtyPixels;
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
private volatile boolean pendingChanges;
|
||||
private volatile boolean disposed;
|
||||
|
||||
PregenMapPanel(PregenRenderSnapshot.Bounds bounds) {
|
||||
this.bounds = bounds;
|
||||
int width = (int) Math.min(MAX_RASTER_SIZE, bounds.width());
|
||||
int height = (int) Math.min(MAX_RASTER_SIZE, bounds.height());
|
||||
image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||
imagePixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
|
||||
pendingPixels = new int[imagePixels.length];
|
||||
dirtyPixels = new BitSet(imagePixels.length);
|
||||
Arrays.fill(imagePixels, WAITING.getRGB());
|
||||
Arrays.fill(pendingPixels, WAITING.getRGB());
|
||||
PregenMapPanel(PregenMapState state) {
|
||||
this.state = state;
|
||||
bounds = state.bounds();
|
||||
image = new BufferedImage(state.width(), state.height(), BufferedImage.TYPE_INT_RGB);
|
||||
view = state.attach(((DataBufferInt) image.getRaster().getDataBuffer()).getData());
|
||||
setBackground(PregenRenderer.BACKGROUND);
|
||||
ToolTipManager.sharedInstance().registerComponent(this);
|
||||
}
|
||||
|
||||
void submit(int chunkX, int chunkZ, Color color) {
|
||||
if (disposed || !bounds.contains(chunkX, chunkZ)) {
|
||||
return;
|
||||
}
|
||||
int x = (int) (((long) chunkX - bounds.minX()) * image.getWidth() / bounds.width());
|
||||
int z = (int) (((long) chunkZ - bounds.minZ()) * image.getHeight() / bounds.height());
|
||||
int index = z * image.getWidth() + x;
|
||||
int rgb = color.getRGB();
|
||||
lock.lock();
|
||||
try {
|
||||
if (disposed || pendingPixels[index] == rgb) {
|
||||
return;
|
||||
}
|
||||
pendingPixels[index] = rgb;
|
||||
dirtyPixels.set(index);
|
||||
pendingChanges = true;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
if (!disposed) {
|
||||
state.submit(chunkX, chunkZ, color);
|
||||
}
|
||||
}
|
||||
|
||||
boolean flush() {
|
||||
if (!pendingChanges || disposed) {
|
||||
return false;
|
||||
}
|
||||
lock.lock();
|
||||
try {
|
||||
for (int index = dirtyPixels.nextSetBit(0); index >= 0; index = dirtyPixels.nextSetBit(index + 1)) {
|
||||
imagePixels[index] = pendingPixels[index];
|
||||
}
|
||||
dirtyPixels.clear();
|
||||
pendingChanges = false;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
return true;
|
||||
return !disposed && view.flush();
|
||||
}
|
||||
|
||||
void disposeMap() {
|
||||
lock.lock();
|
||||
try {
|
||||
disposed = true;
|
||||
dirtyPixels.clear();
|
||||
pendingChanges = false;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
ToolTipManager.sharedInstance().unregisterComponent(this);
|
||||
view.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToolTipText(MouseEvent event) {
|
||||
if (disposed) {
|
||||
return null;
|
||||
}
|
||||
Rectangle area = fit(bounds, getWidth(), getHeight());
|
||||
if (!area.contains(event.getX(), event.getY())) {
|
||||
return null;
|
||||
}
|
||||
int chunkX = (int) (bounds.minX() + (long) (event.getX() - area.x) * bounds.width() / area.width);
|
||||
int chunkZ = (int) (bounds.minZ() + (long) (event.getY() - area.y) * bounds.height() / area.height);
|
||||
return IrisLanguage.plain(DesktopUiMessages.PREGEN_CHUNK_COORDINATES,
|
||||
MessageArgument.trusted("x", chunkX), MessageArgument.trusted("z", chunkZ));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package art.arcane.iris.studio.view;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.BitSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public final class PregenMapState {
|
||||
private static final int MAX_RASTER_SIZE = 1024;
|
||||
static final Color WAITING = new Color(29, 35, 46);
|
||||
|
||||
private final PregenRenderSnapshot.Bounds bounds;
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final int[] pixels;
|
||||
private final List<View> views = new ArrayList<>(1);
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
PregenMapState(PregenRenderSnapshot.Bounds bounds) {
|
||||
this.bounds = Objects.requireNonNull(bounds);
|
||||
width = (int) Math.min(MAX_RASTER_SIZE, bounds.width());
|
||||
height = (int) Math.min(MAX_RASTER_SIZE, bounds.height());
|
||||
pixels = new int[width * height];
|
||||
Arrays.fill(pixels, WAITING.getRGB());
|
||||
}
|
||||
|
||||
PregenRenderSnapshot.Bounds bounds() {
|
||||
return bounds;
|
||||
}
|
||||
|
||||
int width() {
|
||||
return width;
|
||||
}
|
||||
|
||||
int height() {
|
||||
return height;
|
||||
}
|
||||
|
||||
void submit(int chunkX, int chunkZ, Color color) {
|
||||
if (!bounds.contains(chunkX, chunkZ)) {
|
||||
return;
|
||||
}
|
||||
int x = (int) (((long) chunkX - bounds.minX()) * width / bounds.width());
|
||||
int z = (int) (((long) chunkZ - bounds.minZ()) * height / bounds.height());
|
||||
int index = z * width + x;
|
||||
int rgb = color.getRGB();
|
||||
lock.lock();
|
||||
try {
|
||||
if (pixels[index] == rgb) {
|
||||
return;
|
||||
}
|
||||
pixels[index] = rgb;
|
||||
for (int viewIndex = 0; viewIndex < views.size(); viewIndex++) {
|
||||
View view = views.get(viewIndex);
|
||||
view.dirty.set(index);
|
||||
view.pending = true;
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
View attach(int[] target) {
|
||||
if (target.length != pixels.length) {
|
||||
throw new IllegalArgumentException("Map image must match the retained raster dimensions");
|
||||
}
|
||||
View view = new View(target);
|
||||
lock.lock();
|
||||
try {
|
||||
System.arraycopy(pixels, 0, target, 0, pixels.length);
|
||||
views.add(view);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
final class View {
|
||||
private final int[] target;
|
||||
private final BitSet dirty = new BitSet(pixels.length);
|
||||
private volatile boolean pending;
|
||||
private boolean disposed;
|
||||
|
||||
private View(int[] target) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
boolean flush() {
|
||||
if (!pending) {
|
||||
return false;
|
||||
}
|
||||
lock.lock();
|
||||
try {
|
||||
if (disposed || !pending) {
|
||||
return false;
|
||||
}
|
||||
for (int index = dirty.nextSetBit(0); index >= 0; index = dirty.nextSetBit(index + 1)) {
|
||||
target[index] = pixels[index];
|
||||
}
|
||||
dirty.clear();
|
||||
pending = false;
|
||||
return true;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
void close() {
|
||||
lock.lock();
|
||||
try {
|
||||
disposed = true;
|
||||
pending = false;
|
||||
dirty.clear();
|
||||
views.remove(this);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,4 +20,6 @@ package art.arcane.iris.studio.view;
|
||||
|
||||
public interface PregenRenderSource {
|
||||
PregenRenderSnapshot renderSnapshot();
|
||||
|
||||
PregenMapState renderMapState();
|
||||
}
|
||||
|
||||
@@ -35,16 +35,22 @@ import javax.swing.JFrame;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JProgressBar;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.Scrollable;
|
||||
import javax.swing.JTextArea;
|
||||
import javax.swing.KeyStroke;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.Timer;
|
||||
import javax.swing.plaf.basic.BasicButtonUI;
|
||||
import javax.swing.plaf.basic.BasicGraphicsUtils;
|
||||
import javax.swing.plaf.basic.BasicProgressBarUI;
|
||||
import javax.swing.plaf.basic.BasicScrollBarUI;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Color;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.GridBagConstraints;
|
||||
import java.awt.GridBagLayout;
|
||||
import java.awt.Insets;
|
||||
import java.awt.Font;
|
||||
import java.awt.Frame;
|
||||
import java.awt.GridLayout;
|
||||
@@ -85,7 +91,7 @@ public final class PregenRenderer extends JPanel {
|
||||
private final JLabel worldLabel = label("", new Font(Font.SANS_SERIF, Font.BOLD, 21), TEXT);
|
||||
private final JLabel phaseLabel = label("", BODY_FONT, MUTED);
|
||||
private final JLabel countLabel = label("", BODY_FONT, TEXT);
|
||||
private final JLabel percentLabel = label("", BODY_FONT, MUTED);
|
||||
private final JLabel percentLabel = label("", new Font(Font.SANS_SERIF, Font.BOLD, 24), TEXT);
|
||||
private final JLabel currentValue = label("", VALUE_FONT, TEXT);
|
||||
private final JLabel overallValue = label("", VALUE_FONT, TEXT);
|
||||
private final JLabel thirtyValue = label("", VALUE_FONT, TEXT);
|
||||
@@ -95,7 +101,8 @@ public final class PregenRenderer extends JPanel {
|
||||
private final JLabel memoryValue = label("", VALUE_FONT, TEXT);
|
||||
private final JLabel pressureValue = label("", VALUE_FONT, TEXT);
|
||||
private final JLabel methodLabel = label("", LABEL_FONT, MUTED);
|
||||
private final JLabel failureLabel = label("", LABEL_FONT, ERROR);
|
||||
private final JTextArea failureLabel = wrappedText("", BODY_FONT, ERROR, SURFACE);
|
||||
private String controlFailure;
|
||||
private final JButton pauseButton = new JButton();
|
||||
private final JProgressBar progressBar = new JProgressBar(0, 10_000);
|
||||
private volatile boolean renderingEnabled;
|
||||
@@ -106,14 +113,18 @@ public final class PregenRenderer extends JPanel {
|
||||
private PregenRenderer(PregenRenderSource source, Runnable onPause) {
|
||||
this.source = Objects.requireNonNull(source, "Generation view source");
|
||||
this.onPause = onPause;
|
||||
failureLabel.setFocusable(true);
|
||||
PregenRenderSnapshot initial = source.renderSnapshot();
|
||||
map = new PregenMapPanel(initial.bounds());
|
||||
map = new PregenMapPanel(source.renderMapState());
|
||||
setLayout(new BorderLayout(0, 18));
|
||||
setBackground(BACKGROUND);
|
||||
setBorder(BorderFactory.createEmptyBorder(20, 24, 18, 24));
|
||||
add(header(), BorderLayout.NORTH);
|
||||
add(mapArea(initial.bounds()), BorderLayout.CENTER);
|
||||
add(statusArea(), BorderLayout.SOUTH);
|
||||
JPanel content = panel(new BorderLayout(18, 0), BACKGROUND);
|
||||
content.add(mapArea(initial.bounds()), BorderLayout.CENTER);
|
||||
content.add(statusArea(), BorderLayout.EAST);
|
||||
add(content, BorderLayout.CENTER);
|
||||
add(wrappedText(text(DesktopUiMessages.PREGEN_WINDOW_HINT), LABEL_FONT, MUTED, BACKGROUND), BorderLayout.SOUTH);
|
||||
installPauseControl();
|
||||
repaintTimer = new Timer(IrisSettings.get().getGui().isMaximumPregenGuiFPS() ? 4 : 250,
|
||||
event -> refresh());
|
||||
@@ -130,8 +141,8 @@ public final class PregenRenderer extends JPanel {
|
||||
renderer.frame = frame;
|
||||
renderer.renderingEnabled = true;
|
||||
frame.setContentPane(renderer);
|
||||
frame.setSize(1040, 900);
|
||||
frame.setMinimumSize(new Dimension(760, 660));
|
||||
frame.setSize(1040, 760);
|
||||
frame.setMinimumSize(new Dimension(760, 620));
|
||||
frame.addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(WindowEvent event) {
|
||||
@@ -144,10 +155,6 @@ public final class PregenRenderer extends JPanel {
|
||||
return renderer;
|
||||
}
|
||||
|
||||
public void submit(int x, int z, Color color) {
|
||||
map.submit(x, z, color);
|
||||
}
|
||||
|
||||
public boolean isVisibleFrame() {
|
||||
return renderingEnabled && !disposed;
|
||||
}
|
||||
@@ -199,36 +206,89 @@ public final class PregenRenderer extends JPanel {
|
||||
MessageArgument.trusted("maxX", Form.f(bounds.maxX())),
|
||||
MessageArgument.trusted("minZ", Form.f(bounds.minZ())),
|
||||
MessageArgument.trusted("maxZ", Form.f(bounds.maxZ()))), LABEL_FONT, MUTED);
|
||||
area.add(coordinates, BorderLayout.SOUTH);
|
||||
JPanel mapFooter = panel(new BorderLayout(0, 10), BACKGROUND);
|
||||
mapFooter.add(coordinates, BorderLayout.NORTH);
|
||||
mapFooter.add(legend(), BorderLayout.CENTER);
|
||||
area.add(mapFooter, BorderLayout.SOUTH);
|
||||
return area;
|
||||
}
|
||||
|
||||
private JPanel statusArea() {
|
||||
JPanel metrics = panel(new GridLayout(2, 4, 24, 20), SURFACE);
|
||||
metrics.add(metric(DesktopUiMessages.PREGEN_CURRENT, currentValue));
|
||||
metrics.add(metric(DesktopUiMessages.PREGEN_OVERALL, overallValue));
|
||||
metrics.add(metric(DesktopUiMessages.PREGEN_THIRTY, thirtyValue));
|
||||
metrics.add(metric(DesktopUiMessages.PREGEN_SIXTY, sixtyValue));
|
||||
metrics.add(metric(DesktopUiMessages.PREGEN_ETA, etaValue));
|
||||
metrics.add(metric(DesktopUiMessages.PREGEN_ELAPSED, elapsedValue));
|
||||
metrics.add(metric(DesktopUiMessages.PREGEN_MEMORY_LABEL, memoryValue));
|
||||
metrics.add(metric(DesktopUiMessages.PREGEN_PRESSURE, pressureValue));
|
||||
JPanel method = panel(new BorderLayout(16, 0), SURFACE);
|
||||
method.add(methodLabel, BorderLayout.CENTER);
|
||||
method.add(failureLabel, BorderLayout.EAST);
|
||||
JPanel status = panel(new BorderLayout(0, 18), SURFACE);
|
||||
status.setBorder(BorderFactory.createEmptyBorder(18, 18, 16, 18));
|
||||
status.add(metrics, BorderLayout.CENTER);
|
||||
status.add(method, BorderLayout.SOUTH);
|
||||
JPanel footer = panel(new BorderLayout(0, 12), BACKGROUND);
|
||||
footer.add(legend(), BorderLayout.NORTH);
|
||||
footer.add(status, BorderLayout.CENTER);
|
||||
return footer;
|
||||
private JScrollPane statusArea() {
|
||||
JPanel status = new StatusPanel();
|
||||
status.setBackground(SURFACE);
|
||||
status.setBorder(BorderFactory.createEmptyBorder(18, 16, 16, 16));
|
||||
GridBagConstraints constraints = new GridBagConstraints();
|
||||
constraints.gridx = 0;
|
||||
constraints.gridy = 0;
|
||||
constraints.weightx = 1;
|
||||
constraints.fill = GridBagConstraints.HORIZONTAL;
|
||||
constraints.anchor = GridBagConstraints.NORTHWEST;
|
||||
constraints.insets = new Insets(0, 0, 18, 0);
|
||||
status.add(failureLabel, constraints);
|
||||
constraints.gridy++;
|
||||
currentValue.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 25));
|
||||
etaValue.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 23));
|
||||
status.add(metric(DesktopUiMessages.PREGEN_CURRENT, currentValue), constraints);
|
||||
constraints.gridy++;
|
||||
status.add(metric(DesktopUiMessages.PREGEN_ETA, etaValue), constraints);
|
||||
constraints.gridy++;
|
||||
constraints.insets = new Insets(0, 0, 10, 0);
|
||||
status.add(metricRow(DesktopUiMessages.PREGEN_OVERALL, overallValue), constraints);
|
||||
constraints.gridy++;
|
||||
status.add(metricRow(DesktopUiMessages.PREGEN_THIRTY, thirtyValue), constraints);
|
||||
constraints.gridy++;
|
||||
status.add(metricRow(DesktopUiMessages.PREGEN_SIXTY, sixtyValue), constraints);
|
||||
constraints.gridy++;
|
||||
status.add(metricRow(DesktopUiMessages.PREGEN_ELAPSED, elapsedValue), constraints);
|
||||
constraints.gridy++;
|
||||
status.add(metricRow(DesktopUiMessages.PREGEN_MEMORY_LABEL, memoryValue), constraints);
|
||||
constraints.gridy++;
|
||||
status.add(metricRow(DesktopUiMessages.PREGEN_PRESSURE, pressureValue), constraints);
|
||||
constraints.gridy++;
|
||||
status.add(methodLabel, constraints);
|
||||
constraints.gridy++;
|
||||
constraints.weighty = 1;
|
||||
status.add(panel(new BorderLayout(), SURFACE), constraints);
|
||||
JScrollPane scroll = new JScrollPane(status);
|
||||
scroll.setBorder(BorderFactory.createLineBorder(BORDER));
|
||||
scroll.getViewport().setBackground(SURFACE);
|
||||
scroll.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
|
||||
scroll.setPreferredSize(new Dimension(276, 1));
|
||||
scroll.setMinimumSize(new Dimension(260, 0));
|
||||
scroll.getVerticalScrollBar().setUnitIncrement(16);
|
||||
scroll.getVerticalScrollBar().setUI(new BasicScrollBarUI() {
|
||||
@Override
|
||||
protected void configureScrollBarColors() {
|
||||
thumbColor = BORDER;
|
||||
thumbDarkShadowColor = BORDER;
|
||||
thumbHighlightColor = BORDER;
|
||||
thumbLightShadowColor = BORDER;
|
||||
trackColor = FIELD;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JButton createDecreaseButton(int orientation) {
|
||||
return scrollEnd();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JButton createIncreaseButton(int orientation) {
|
||||
return scrollEnd();
|
||||
}
|
||||
|
||||
private JButton scrollEnd() {
|
||||
JButton button = new JButton();
|
||||
button.setPreferredSize(new Dimension(0, 0));
|
||||
button.setFocusable(false);
|
||||
return button;
|
||||
}
|
||||
});
|
||||
return scroll;
|
||||
}
|
||||
|
||||
private JPanel legend() {
|
||||
JPanel legend = panel(new FlowLayout(FlowLayout.LEFT, 0, 0), BACKGROUND);
|
||||
legend.add(legendItem(DesktopUiMessages.PREGEN_WAITING, PregenMapPanel.WAITING));
|
||||
JPanel legend = panel(new GridLayout(0, 2, 12, 7), BACKGROUND);
|
||||
legend.add(legendItem(DesktopUiMessages.PREGEN_WAITING, PregenMapState.WAITING));
|
||||
legend.add(legendItem(DesktopUiMessages.PREGEN_GENERATING, GENERATING));
|
||||
legend.add(legendItem(DesktopUiMessages.PREGEN_READY, GENERATED));
|
||||
legend.add(legendItem(DesktopUiMessages.PREGEN_EXISTING, EXISTS));
|
||||
@@ -238,24 +298,32 @@ public final class PregenRenderer extends JPanel {
|
||||
}
|
||||
|
||||
private JPanel legendItem(TextKey key, Color color) {
|
||||
JPanel item = panel(new FlowLayout(FlowLayout.LEFT, 6, 0), BACKGROUND);
|
||||
JPanel swatch = new JPanel();
|
||||
swatch.setBackground(color);
|
||||
JPanel item = panel(new BorderLayout(7, 0), BACKGROUND);
|
||||
JPanel swatch = panel(new BorderLayout(), color);
|
||||
swatch.setPreferredSize(new Dimension(10, 10));
|
||||
swatch.setBorder(BorderFactory.createLineBorder(BORDER));
|
||||
item.add(swatch);
|
||||
item.add(label(text(key), LABEL_FONT, MUTED));
|
||||
item.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 13));
|
||||
JPanel marker = panel(new BorderLayout(), BACKGROUND);
|
||||
marker.add(swatch, BorderLayout.NORTH);
|
||||
item.add(marker, BorderLayout.WEST);
|
||||
item.add(wrappedText(text(key), LABEL_FONT, MUTED, BACKGROUND), BorderLayout.CENTER);
|
||||
return item;
|
||||
}
|
||||
|
||||
private JPanel metric(TextKey title, JLabel value) {
|
||||
JPanel metric = panel(new BorderLayout(0, 5), SURFACE);
|
||||
metric.add(label(text(title), LABEL_FONT, MUTED), BorderLayout.NORTH);
|
||||
metric.add(wrappedText(text(title), LABEL_FONT, MUTED, SURFACE), BorderLayout.NORTH);
|
||||
metric.add(value, BorderLayout.CENTER);
|
||||
return metric;
|
||||
}
|
||||
|
||||
private JPanel metricRow(TextKey title, JLabel value) {
|
||||
JPanel row = panel(new BorderLayout(8, 0), SURFACE);
|
||||
value.setFont(BODY_FONT);
|
||||
row.add(wrappedText(text(title), LABEL_FONT, MUTED, SURFACE), BorderLayout.CENTER);
|
||||
row.add(value, BorderLayout.EAST);
|
||||
return row;
|
||||
}
|
||||
|
||||
private void installPauseControl() {
|
||||
pauseButton.setUI(new BasicButtonUI() {
|
||||
@Override
|
||||
@@ -288,6 +356,14 @@ public final class PregenRenderer extends JPanel {
|
||||
}
|
||||
});
|
||||
pauseButton.addActionListener(event -> togglePause());
|
||||
pauseButton.getInputMap(JComponent.WHEN_FOCUSED).put(
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "activatePause");
|
||||
pauseButton.getActionMap().put("activatePause", new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent event) {
|
||||
pauseButton.doClick();
|
||||
}
|
||||
});
|
||||
getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_P, 0, true), "pause");
|
||||
getActionMap().put("pause", new AbstractAction() {
|
||||
@Override
|
||||
@@ -309,10 +385,14 @@ public final class PregenRenderer extends JPanel {
|
||||
}
|
||||
try {
|
||||
onPause.run();
|
||||
controlFailure = null;
|
||||
displayedSnapshot = null;
|
||||
refresh();
|
||||
} catch (RuntimeException failure) {
|
||||
IrisLogging.reportError("Unable to change pregeneration pause state.", failure);
|
||||
failureLabel.setText(text(DesktopUiMessages.PREGEN_CONTROL_FAILED));
|
||||
controlFailure = text(DesktopUiMessages.PREGEN_CONTROL_FAILED) + "\n"
|
||||
+ Objects.toString(failure.getMessage(), failure.getClass().getSimpleName());
|
||||
updateStatus(source.renderSnapshot());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,6 +415,7 @@ public final class PregenRenderer extends JPanel {
|
||||
boolean initializing = snapshot.phase() == PregenRenderSnapshot.Phase.INITIALIZING;
|
||||
boolean paused = snapshot.phase() == PregenRenderSnapshot.Phase.PAUSED;
|
||||
worldLabel.setText(progress.worldName() == null ? text(DesktopUiMessages.PREGEN_TITLE) : progress.worldName());
|
||||
worldLabel.setToolTipText(worldLabel.getText());
|
||||
phaseLabel.setText(text(switch (snapshot.phase()) {
|
||||
case INITIALIZING -> DesktopUiMessages.PREGEN_INITIALIZING;
|
||||
case GENERATING -> DesktopUiMessages.PREGEN_GENERATING;
|
||||
@@ -351,6 +432,8 @@ public final class PregenRenderer extends JPanel {
|
||||
percentLabel.setText(text(DesktopUiMessages.PREGEN_PERCENT,
|
||||
MessageArgument.trusted("percent", Form.pc(Math.max(0D, Math.min(1D, progress.percent() / 100D)), 1))));
|
||||
progressBar.setValue((int) Math.max(0D, Math.min(10_000D, progress.percent() * 100D)));
|
||||
progressBar.getAccessibleContext().setAccessibleName(countLabel.getText());
|
||||
map.getAccessibleContext().setAccessibleDescription(countLabel.getText() + ". " + phaseLabel.getText());
|
||||
pauseButton.setText(text(paused ? DesktopUiMessages.PREGEN_RESUME : DesktopUiMessages.PREGEN_PAUSE));
|
||||
pauseButton.setToolTipText(text(paused ? DesktopUiMessages.PREGEN_RESUME_HINT : DesktopUiMessages.PREGEN_PAUSE_HINT));
|
||||
pauseButton.setEnabled(onPause != null && switch (snapshot.phase()) {
|
||||
@@ -361,7 +444,9 @@ public final class PregenRenderer extends JPanel {
|
||||
overallValue.setText(initializing ? pending() : rate(progress.overallChunksPerSecond()));
|
||||
thirtyValue.setText(initializing ? pending() : rate(progress.thirtySecondChunksPerSecond()));
|
||||
sixtyValue.setText(initializing ? pending() : rate(progress.sixtySecondChunksPerSecond()));
|
||||
etaValue.setText(initializing || paused || progress.eta() < 0
|
||||
etaValue.setText((snapshot.phase() != PregenRenderSnapshot.Phase.GENERATING
|
||||
&& snapshot.phase() != PregenRenderSnapshot.Phase.SAVING
|
||||
&& snapshot.phase() != PregenRenderSnapshot.Phase.COMPLETED) || progress.eta() < 0
|
||||
|| (progress.eta() == 0 && progress.chunksRemaining() > 0)
|
||||
? pending() : Form.duration(progress.eta(), 2));
|
||||
elapsedValue.setText(initializing ? pending() : Form.duration(progress.elapsed(), 2));
|
||||
@@ -375,12 +460,20 @@ public final class PregenRenderer extends JPanel {
|
||||
method = text(DesktopUiMessages.PREGEN_CACHED, MessageArgument.untrusted("method", method));
|
||||
}
|
||||
methodLabel.setText(text(DesktopUiMessages.PREGEN_METHOD, MessageArgument.untrusted("method", method)));
|
||||
failureLabel.setText(progress.failed() > 0 ? text(DesktopUiMessages.PREGEN_FAILED,
|
||||
MessageArgument.trusted("count", Form.f(progress.failed()))) : "");
|
||||
failureLabel.setToolTipText(snapshot.failure());
|
||||
methodLabel.setToolTipText(methodLabel.getText());
|
||||
String failureSummary = progress.failed() > 0 ? text(DesktopUiMessages.PREGEN_FAILED,
|
||||
MessageArgument.trusted("count", Form.f(progress.failed()))) : "";
|
||||
if (snapshot.failure() != null) {
|
||||
failureLabel.setText(text(DesktopUiMessages.PREGEN_ERROR_DETAILS));
|
||||
failureSummary += (failureSummary.isEmpty() ? "" : "\n")
|
||||
+ text(DesktopUiMessages.PREGEN_ERROR_DETAILS) + "\n" + snapshot.failure();
|
||||
}
|
||||
if (controlFailure != null) {
|
||||
failureSummary += (failureSummary.isEmpty() ? "" : "\n") + controlFailure;
|
||||
}
|
||||
if (!failureSummary.equals(failureLabel.getText())) {
|
||||
failureLabel.setText(failureSummary);
|
||||
}
|
||||
failureLabel.setVisible(!failureSummary.isEmpty());
|
||||
}
|
||||
|
||||
private void setRenderingEnabled(boolean enabled) {
|
||||
@@ -418,6 +511,19 @@ public final class PregenRenderer extends JPanel {
|
||||
return label;
|
||||
}
|
||||
|
||||
private static JTextArea wrappedText(String text, Font font, Color foreground, Color background) {
|
||||
JTextArea area = new JTextArea(text);
|
||||
area.setEditable(false);
|
||||
area.setFocusable(false);
|
||||
area.setLineWrap(true);
|
||||
area.setWrapStyleWord(true);
|
||||
area.setFont(font);
|
||||
area.setForeground(foreground);
|
||||
area.setBackground(background);
|
||||
area.setBorder(BorderFactory.createEmptyBorder());
|
||||
return area;
|
||||
}
|
||||
|
||||
private static String pending() {
|
||||
return text(DesktopUiMessages.PREGEN_METHOD_PENDING);
|
||||
}
|
||||
@@ -429,4 +535,35 @@ public final class PregenRenderer extends JPanel {
|
||||
private static String text(TextKey key, MessageArgument... arguments) {
|
||||
return IrisLanguage.plain(key, arguments);
|
||||
}
|
||||
|
||||
private static final class StatusPanel extends JPanel implements Scrollable {
|
||||
private StatusPanel() {
|
||||
super(new GridBagLayout());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getPreferredScrollableViewportSize() {
|
||||
return getPreferredSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getScrollableUnitIncrement(Rectangle visible, int orientation, int direction) {
|
||||
return 16;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getScrollableBlockIncrement(Rectangle visible, int orientation, int direction) {
|
||||
return Math.max(16, visible.height - 16);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getScrollableTracksViewportWidth() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getScrollableTracksViewportHeight() {
|
||||
return getParent() != null && getPreferredSize().height < getParent().getHeight();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
||||
private final List<Runnable> whenDone = new CopyOnWriteArrayList<>();
|
||||
private final IrisPregenerator pregenerator;
|
||||
private final PregenRenderSnapshot.Bounds bounds;
|
||||
private final PregenMapState mapState;
|
||||
private final Engine engine;
|
||||
private final ExecutorService service;
|
||||
private final Thread worker;
|
||||
@@ -98,6 +99,7 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
||||
this.pregenerator = new IrisPregenerator(task, method, this);
|
||||
int[] chunkBounds = task.chunkBounds();
|
||||
bounds = new PregenRenderSnapshot.Bounds(chunkBounds[0], chunkBounds[1], chunkBounds[2], chunkBounds[3]);
|
||||
mapState = new PregenMapState(bounds);
|
||||
lastTotalChunks = task.chunkCount();
|
||||
lastChunksRemaining = lastTotalChunks;
|
||||
publishView(PregenRenderSnapshot.Phase.INITIALIZING);
|
||||
@@ -371,23 +373,13 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
||||
}
|
||||
|
||||
public void drawRegion(int x, int z, Color color) {
|
||||
PregenRenderer activeRenderer = renderer;
|
||||
if (activeRenderer == null || closed) {
|
||||
return;
|
||||
if (!closed) {
|
||||
task.iterateChunks(x, z, (chunkX, chunkZ) -> mapState.submit(chunkX, chunkZ, color));
|
||||
}
|
||||
task.iterateChunks(x, z, (chunkX, chunkZ) -> activeRenderer.submit(chunkX, chunkZ, color));
|
||||
}
|
||||
|
||||
public void draw(int x, int z, Color color) {
|
||||
try {
|
||||
PregenRenderer activeRenderer = renderer;
|
||||
if (activeRenderer != null) {
|
||||
activeRenderer.submit(x, z, color);
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError(error);
|
||||
IrisLogging.error("Failed to draw pregen");
|
||||
}
|
||||
mapState.submit(x, z, color);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
@@ -588,6 +580,11 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
||||
return renderSnapshot;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PregenMapState renderMapState() {
|
||||
return mapState;
|
||||
}
|
||||
|
||||
private synchronized void publishView(PregenRenderSnapshot.Phase phase) {
|
||||
if (failure != null) {
|
||||
phase = PregenRenderSnapshot.Phase.ERROR;
|
||||
@@ -603,12 +600,9 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
||||
}
|
||||
|
||||
private void drawChunkPreview(int x, int z, Color statusColor) {
|
||||
PregenRenderer activeRenderer = renderer;
|
||||
if (activeRenderer == null) {
|
||||
return;
|
||||
}
|
||||
draw(x, z, statusColor);
|
||||
if (!activeRenderer.isVisibleFrame() || service.isShutdown()) {
|
||||
PregenRenderer activeRenderer = renderer;
|
||||
if (activeRenderer == null || !activeRenderer.isVisibleFrame() || service.isShutdown()) {
|
||||
return;
|
||||
}
|
||||
if (engine != null) {
|
||||
|
||||
@@ -20,6 +20,7 @@ public final class BoundaryColumnGeometry implements NativeBlockColumn {
|
||||
private final List<Voxel> palette;
|
||||
private final int[] runEnds;
|
||||
private final short[] paletteIndices;
|
||||
private final int highestSolidOffset;
|
||||
|
||||
public BoundaryColumnGeometry(int minimumY, List<Voxel> palette, int[] runEnds, short[] paletteIndices) {
|
||||
this.minimumY = minimumY;
|
||||
@@ -27,6 +28,7 @@ public final class BoundaryColumnGeometry implements NativeBlockColumn {
|
||||
this.runEnds = Objects.requireNonNull(runEnds, "Geometry run ends").clone();
|
||||
this.paletteIndices = Objects.requireNonNull(paletteIndices, "Geometry palette indices").clone();
|
||||
validate();
|
||||
highestSolidOffset = findHighestSolidOffset();
|
||||
}
|
||||
|
||||
public static BoundaryColumnGeometry empty() {
|
||||
@@ -112,20 +114,16 @@ public final class BoundaryColumnGeometry implements NativeBlockColumn {
|
||||
}
|
||||
|
||||
public boolean hasSolidAbove(int worldY) {
|
||||
long offset = (long) worldY - minimumY;
|
||||
if (offset >= height() - 1L) {
|
||||
return false;
|
||||
}
|
||||
int startRun = offset < 0 ? 0 : runAt((int) offset);
|
||||
for (int run = startRun; run < runEnds.length; run++) {
|
||||
if (runEnds[run] <= offset + 1L) {
|
||||
continue;
|
||||
return highestSolidOffset >= 0 && (long) worldY - minimumY < highestSolidOffset;
|
||||
}
|
||||
|
||||
private int findHighestSolidOffset() {
|
||||
for (int run = runEnds.length - 1; run >= 0; run--) {
|
||||
if (palette.get(paletteIndices[run]).phase() == Phase.SOLID) {
|
||||
return true;
|
||||
return runEnds[run] - 1;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return -1;
|
||||
}
|
||||
|
||||
public List<Voxel> voxels() {
|
||||
|
||||
@@ -1869,6 +1869,8 @@ status = "{name} | X: {x} Z: {z} | Zoom: {zoom} | Wert: {value} | {fps}
|
||||
"category.other" = "Sonstige"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "Chunk X {x} · Z {z}"
|
||||
window_hint = "Beim Schließen dieses Fensters läuft die Generierung weiter."
|
||||
initializing = "Initialisierung..."
|
||||
title = "Vorgenerierungsansicht"
|
||||
method_pending = "Ausstehend"
|
||||
|
||||
@@ -1869,6 +1869,8 @@ status = "{name} | X: {x} Z: {z} | Zoom: {zoom} | Valor: {value} | {fps
|
||||
"category.other" = "Otros"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "Chunk X {x} · Z {z}"
|
||||
window_hint = "Cerrar esta ventana no detiene la generación."
|
||||
initializing = "Inicializando..."
|
||||
title = "Vista de pregen"
|
||||
method_pending = "Pendiente"
|
||||
|
||||
@@ -1874,6 +1874,8 @@ status = "{name} | X: {x} Z: {z} Zoom: {zoom} Arvo: {value} | {fps} FPS"
|
||||
"category.other" = "Muut"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "Chunk X {x} · Z {z}"
|
||||
window_hint = "Generointi jatkuu, vaikka suljet tämän ikkunan."
|
||||
initializing = "Alustetaan..."
|
||||
title = "Pregen- näkymä"
|
||||
method_pending = "Odotetaan"
|
||||
|
||||
@@ -1869,6 +1869,8 @@ status = "{name} | X: {x} Z: {z} | Zoom : {zoom} | Valeur : {value} | {
|
||||
"category.other" = "Autres"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "Chunk X {x} · Z {z}"
|
||||
window_hint = "La génération continue après la fermeture de cette fenêtre."
|
||||
initializing = "Initialisation..."
|
||||
title = "Vue de pregen"
|
||||
method_pending = "En attente"
|
||||
|
||||
@@ -1874,6 +1874,8 @@ status = "{name} | X: {x} Z: {z} זום: {zoom} ערך: {value} | {fps}
|
||||
"category.other" = "אחרים"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "צ'אנק X {x} · Z {z}"
|
||||
window_hint = "היצירה נמשכת גם לאחר סגירת החלון."
|
||||
initializing = "הדמיה..."
|
||||
title = "תצוגה מקדימה"
|
||||
method_pending = "עקבו"
|
||||
|
||||
@@ -1869,6 +1869,8 @@ status = "{name} | X: {x} Z: {z} | Zoom: {zoom} | Valore: {value} | {fps}
|
||||
"category.other" = "Altri"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "Chunk X {x} · Z {z}"
|
||||
window_hint = "La generazione continua dopo la chiusura di questa finestra."
|
||||
initializing = "Inizializzazione..."
|
||||
title = "Vista pregenerazione"
|
||||
method_pending = "In attesa"
|
||||
|
||||
@@ -1872,6 +1872,8 @@ status = "{name} | X: {x} Z: {z} | ズーム: {zoom} | 値: {value} | {
|
||||
"category.other" = "その他"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "チャンク X {x} · Z {z}"
|
||||
window_hint = "このウィンドウを閉じても生成は続行されます。"
|
||||
initializing = "初期化中..."
|
||||
title = "事前生成ビュー"
|
||||
method_pending = "保留中"
|
||||
|
||||
@@ -1872,6 +1872,8 @@ status = "{name} | X: {x} Z: {z} | 줌: {zoom} | 가치: {value} | {fps}
|
||||
"category.other" = "기타"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "청크 X {x} · Z {z}"
|
||||
window_hint = "이 창을 닫아도 생성은 계속됩니다."
|
||||
initializing = "초기화..."
|
||||
title = "Pregen 보기"
|
||||
method_pending = "대기 중"
|
||||
|
||||
@@ -1874,6 +1874,8 @@ status = "{name} | X: {x} Z: {z} 124; didinimas: {zoom} SmithKline 124; Ver
|
||||
"category.other" = "Kita"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "Chunk X {x} · Z {z}"
|
||||
window_hint = "Uždarius šį langą generavimas tęsiamas."
|
||||
initializing = "Inicijuojama..."
|
||||
title = "Comment"
|
||||
method_pending = "Laukiama"
|
||||
|
||||
@@ -1869,6 +1869,8 @@ status = "{name} | X: {x} Z: {z} Zoom: {zoom} Waarde: {value} | {fps} FP
|
||||
"category.other" = "Andere"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "Chunk X {x} · Z {z}"
|
||||
window_hint = "Het genereren gaat door wanneer je dit venster sluit."
|
||||
initializing = "Initialiseren..."
|
||||
title = "Beeld instellen"
|
||||
method_pending = "In afwachting"
|
||||
|
||||
@@ -1874,6 +1874,8 @@ status = "{name} | X: {x} Z: {z} | Zoom: {zoom} | Wartość: {value} |
|
||||
"category.other" = "Inne"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "Chunk X {x} · Z {z}"
|
||||
window_hint = "Generowanie trwa dalej po zamknięciu tego okna."
|
||||
initializing = "Inicjowanie..."
|
||||
title = "Widok z góry"
|
||||
method_pending = "Oczekiwanie"
|
||||
|
||||
@@ -1869,6 +1869,8 @@ status = "{name} | X: {x} Z: {z} □ Zoom: {zoom} Valor: {value} | {fps}
|
||||
"category.other" = "Outros"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "Chunk X {x} · Z {z}"
|
||||
window_hint = "A geração continua após fechar esta janela."
|
||||
initializing = "Inicializando..."
|
||||
title = "Visão Pregen"
|
||||
method_pending = "Pendente"
|
||||
|
||||
@@ -1874,6 +1874,8 @@ status = "{name} | X: {x} Z: {z} Зум: {zoom} Величина: {value}
|
||||
"category.other" = "другой"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "Чанк X {x} · Z {z}"
|
||||
window_hint = "После закрытия этого окна генерация продолжится."
|
||||
initializing = "инициировать..."
|
||||
title = "Прегеновский взгляд"
|
||||
method_pending = "В ожидании"
|
||||
|
||||
@@ -1874,6 +1874,8 @@ status = "{name} | X: {x} Z: {z} yakınlaştırma: {zoom} Değer: {value}
|
||||
"category.other" = "Diğer"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "Chunk X {x} · Z {z}"
|
||||
window_hint = "Bu pencere kapatılsa da oluşturma devam eder."
|
||||
initializing = "Başlangıç..."
|
||||
title = "Prejen View"
|
||||
method_pending = "Pending Ping"
|
||||
|
||||
@@ -1872,6 +1872,8 @@ status = "{name} | X: {x} Z: {z} Phóng to- thu nhỏ {zoom} Giá trị {va
|
||||
"category.other" = "Khác"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "Chunk X {x} · Z {z}"
|
||||
window_hint = "Quá trình tạo vẫn tiếp tục khi đóng cửa sổ này."
|
||||
initializing = "Đang sở khởi..."
|
||||
title = "Xem mẫu"
|
||||
method_pending = "Đang đợi"
|
||||
|
||||
@@ -1872,6 +1872,8 @@ status = "{name} | X: {x} Z: {z} 缩放 : {zoom} 数值 : {value} | {fps}
|
||||
"category.other" = "其他人员"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "区块 X {x} · Z {z}"
|
||||
window_hint = "关闭此窗口后仍会继续生成。"
|
||||
initializing = "初始化..."
|
||||
title = "Pregen 视图"
|
||||
method_pending = "待决"
|
||||
|
||||
@@ -1872,6 +1872,8 @@ status = "{name} | X: {x} Z: {z} 縮放 : {zoom} 數值 : {value} | {fps}
|
||||
"category.other" = "其他人員"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "區塊 X {x} · Z {z}"
|
||||
window_hint = "關閉此視窗後仍會繼續生成。"
|
||||
initializing = "初始化..."
|
||||
title = "Pregen 檢視"
|
||||
method_pending = "待決"
|
||||
|
||||
@@ -18,7 +18,7 @@ import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyPlanHashTest {
|
||||
private static final String EXPECTED_PLAN_DIGEST = "e54461c339854842f7fc7b2e85bd700b57942a2c691f5a249d8e498729378296";
|
||||
private static final String EXPECTED_PLAN_DIGEST = "ad4e63b9081878d0eac05c2aeacc80afa18a7f2bf4e06c18490211e7ae91fe99";
|
||||
|
||||
private static final List<HydrologyTileKey> TILES = List.of(
|
||||
new HydrologyTileKey(0, 0),
|
||||
|
||||
+123
@@ -2,9 +2,13 @@ package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@@ -92,6 +96,125 @@ public final class HydrologyRegionalConnectivityTest {
|
||||
assertTrue(flatChannel().connected(course, bridged, SETTINGS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void separatedIntervalsCanConnectAroundAnAdjacentWaterfall() {
|
||||
HydrologyRegionalConnectivity water = flatChannel();
|
||||
water.addFluidColumn(1, 0, 67, 70);
|
||||
water.addFluidColumn(1, 1, 60, 70);
|
||||
assertTrue(water.connected(course(), ocean(), SETTINGS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anIsolatedUpperIntervalStillRejectsTheCourse() {
|
||||
HydrologyRegionalConnectivity water = flatChannel();
|
||||
water.addFluidColumn(1, 0, 67, 70);
|
||||
water.addFluidColumn(1, 1, 60, 63);
|
||||
assertFalse(water.connected(course(), ocean(), SETTINGS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aHigherReceivingIntervalCanJoinTheOceanAboveAnotherInterval() {
|
||||
HydrologyRegionalConnectivity water = flatChannel();
|
||||
water.addFluidColumn(2, 0, 40, 45);
|
||||
water.addFluidColumn(2, 1, 40, 63);
|
||||
assertTrue(water.connected(course(), ocean(), SETTINGS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void intervalConnectivityMatchesVoxelFloodFillRegardlessOfInsertionOrder() {
|
||||
Random random = new Random(438761L);
|
||||
int connected = 0;
|
||||
int disconnected = 0;
|
||||
for (int trial = 0; trial < 250; trial++) {
|
||||
ArrayList<int[]> intervals = new ArrayList<>();
|
||||
boolean[][][] voxels = new boolean[3][3][13];
|
||||
for (int x = 0; x < 3; x++) {
|
||||
for (int z = 0; z < 3; z++) {
|
||||
intervals.add(new int[]{x, z, 60, 63});
|
||||
if (x == 2 && z == 0) {
|
||||
continue;
|
||||
}
|
||||
if (random.nextBoolean()) {
|
||||
int bed = 64 + random.nextInt(3);
|
||||
intervals.add(new int[]{x, z, bed, bed + 1 + random.nextInt(3)});
|
||||
}
|
||||
if (random.nextBoolean()) {
|
||||
intervals.add(new int[]{x, z, 60, 68 + random.nextInt(3)});
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int[] interval : intervals) {
|
||||
for (int y = interval[2] + 1; y <= interval[3]; y++) {
|
||||
voxels[interval[0]][interval[1]][y - 58] = true;
|
||||
}
|
||||
}
|
||||
boolean expected = voxelConnected(voxels);
|
||||
if (expected) {
|
||||
connected++;
|
||||
} else {
|
||||
disconnected++;
|
||||
}
|
||||
for (int order = 0; order < 2; order++) {
|
||||
Collections.shuffle(intervals, random);
|
||||
HydrologyRegionalConnectivity water = new HydrologyRegionalConnectivity();
|
||||
for (int[] interval : intervals) {
|
||||
water.addFluidColumn(interval[0], interval[1], interval[2], interval[3]);
|
||||
}
|
||||
assertEquals("Trial " + trial + ", order " + order, expected,
|
||||
water.connected(course(), ocean(), SETTINGS));
|
||||
}
|
||||
}
|
||||
assertTrue(connected > 0);
|
||||
assertTrue(disconnected > 0);
|
||||
}
|
||||
|
||||
private static boolean voxelConnected(boolean[][][] voxels) {
|
||||
int height = voxels[0][0].length;
|
||||
int[] queue = new int[3 * 3 * height];
|
||||
boolean[] visited = new boolean[queue.length];
|
||||
int total = 0;
|
||||
int first = -1;
|
||||
for (int x = 0; x < 3; x++) {
|
||||
for (int z = 0; z < 3; z++) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
if (voxels[x][z][y]) {
|
||||
total++;
|
||||
first = (x * 3 + z) * height + y;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (first < 0) {
|
||||
return false;
|
||||
}
|
||||
int[][] offsets = {{1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, {0, -1, 0}, {0, 0, 1}, {0, 0, -1}};
|
||||
int read = 0;
|
||||
int write = 1;
|
||||
queue[0] = first;
|
||||
visited[first] = true;
|
||||
while (read < write) {
|
||||
int current = queue[read++];
|
||||
int y = current % height;
|
||||
int z = current / height % 3;
|
||||
int x = current / height / 3;
|
||||
for (int[] offset : offsets) {
|
||||
int nextX = x + offset[0];
|
||||
int nextZ = z + offset[1];
|
||||
int nextY = y + offset[2];
|
||||
if (nextX < 0 || nextX >= 3 || nextZ < 0 || nextZ >= 3 || nextY < 0 || nextY >= height
|
||||
|| !voxels[nextX][nextZ][nextY]) {
|
||||
continue;
|
||||
}
|
||||
int next = (nextX * 3 + nextZ) * height + nextY;
|
||||
if (!visited[next]) {
|
||||
visited[next] = true;
|
||||
queue[write++] = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
return write == total;
|
||||
}
|
||||
|
||||
private static HydrologyRegionalConnectivity flatChannel() {
|
||||
HydrologyRegionalConnectivity water = new HydrologyRegionalConnectivity();
|
||||
for (int x = 0; x < 3; x++) {
|
||||
|
||||
+17
@@ -12,6 +12,23 @@ import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyRegionalMorphologyTest {
|
||||
@Test
|
||||
public void regionalWidthRestoresAuthoredVariationWithoutChangingFixedRangesOrConfines() {
|
||||
HydrologyGeometrySampler.Request request = new HydrologyGeometrySampler.Request(
|
||||
HydrologyGeometrySampler.Field.SURFACE_WIDTH, "water", 0, 0, 0L, 4, 12);
|
||||
int narrow = HydrologyRegionalPlanner.regionalWidth(request, 4, 0.5D, 1D);
|
||||
int wide = HydrologyRegionalPlanner.regionalWidth(request, 12, 0.5D, 1D);
|
||||
assertTrue(wide - narrow >= 5);
|
||||
for (double flow : List.of(0D, 0.5D, 1D)) {
|
||||
assertEquals(4, HydrologyRegionalPlanner.regionalWidth(request, 8, flow, 0D));
|
||||
int width = HydrologyRegionalPlanner.regionalWidth(request, 8, flow, 1D);
|
||||
assertTrue(width >= 4 && width <= 12);
|
||||
HydrologyGeometrySampler.Request fixed = new HydrologyGeometrySampler.Request(
|
||||
HydrologyGeometrySampler.Field.SURFACE_WIDTH, "water", 0, 0, 0L, 8, 8);
|
||||
assertEquals(8, HydrologyRegionalPlanner.regionalWidth(fixed, 8, flow, 1D));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flatOpenTerrainRetainsFullFreedomWithoutSamplingDuringQueries() {
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
|
||||
+6
-2
@@ -33,7 +33,7 @@ public class HydrologyRegionalWidthAdaptationTest {
|
||||
double fraction = 0.25D + 0.75D * flow.fraction(point.x(), point.z());
|
||||
double accumulated = 4D + 4D * fraction;
|
||||
int authored = 4 + Math.floorMod(point.x() / 256, 5);
|
||||
int expected = (int) StrictMath.round(accumulated * 0.75D + authored * 0.25D);
|
||||
int expected = (int) StrictMath.round(accumulated * 0.35D + authored * 0.65D);
|
||||
int actual = width(planner, context, point.x());
|
||||
assertEquals(expected, actual);
|
||||
assertTrue(actual >= previous);
|
||||
@@ -104,7 +104,11 @@ public class HydrologyRegionalWidthAdaptationTest {
|
||||
.build(71L, 91L, "default", guide, SurfaceTerminal.SINKHOLE, 40, 512);
|
||||
|
||||
assertTrue(built.accepted());
|
||||
assertEquals(10D, storedWidth(built.segments(), 512), 0D);
|
||||
HydrologyTerrainSampler plainTerrain = (x, z) -> HydrologyTerrainSample.openLand(80, 0D, "land");
|
||||
SurfaceCourseResult plain = new SurfaceCourseBuilder(SETTINGS.surface(), plainTerrain, geometry, SETTINGS.seaLevel())
|
||||
.build(71L, 91L, "default", guide, SurfaceTerminal.SINKHOLE, 40, 512);
|
||||
assertTrue(plain.accepted());
|
||||
assertEquals(storedWidth(plain.segments(), 512) * 1.25D, storedWidth(built.segments(), 512), 1.0E-9D);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+129
-36
@@ -9,9 +9,11 @@ import art.arcane.iris.generation.hydrology.HydrologyTerrainSample;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTerrainSampler;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@@ -25,19 +27,22 @@ public class ChannelProfileBuilderTest {
|
||||
@Test
|
||||
public void headwaterOpensAsASpringPoolThatNarrowsToTheCruiseWidth() {
|
||||
SurfaceCenterline centerline = straight(400);
|
||||
ChannelProfile profile = builder(1D, 1D).build(centerline, "water", false);
|
||||
ChannelProfile profile = builder(1D, 1D).build(centerline, "water", false, 0L);
|
||||
|
||||
assertEquals(400, profile.size());
|
||||
assertEquals(3D, profile.halfWidth()[200], 1.0E-9D);
|
||||
assertEquals(3D, profile.halfWidth()[399], 1.0E-9D);
|
||||
assertTrue(profile.halfWidth()[200] >= 2D && profile.halfWidth()[200] <= 4D);
|
||||
assertTrue(profile.halfWidth()[399] >= 2D && profile.halfWidth()[399] <= 4D);
|
||||
assertEquals(3D, profile.depth()[200], 1.0E-9D);
|
||||
assertEquals(3D * 2.5D, profile.halfWidth()[0], 1.0E-9D);
|
||||
assertEquals(baseWidth(0) * 1.9D, profile.halfWidth()[0], 1.0E-9D);
|
||||
assertEquals(baseWidth(6) * 2.5D, profile.halfWidth()[6], 1.0E-9D);
|
||||
assertEquals(4D, profile.depth()[0], 1.0E-9D);
|
||||
for (int station = 1; station <= 24; station++) {
|
||||
assertTrue(profile.halfWidth()[station] <= profile.halfWidth()[station - 1]);
|
||||
double previousRatio = profile.halfWidth()[station - 1] / baseWidth(station - 1);
|
||||
double ratio = profile.halfWidth()[station] / baseWidth(station);
|
||||
assertTrue(station <= 6 ? ratio >= previousRatio : ratio <= previousRatio);
|
||||
assertTrue(profile.depth()[station] <= profile.depth()[station - 1]);
|
||||
}
|
||||
assertEquals(3D, profile.halfWidth()[24], 1.0E-9D);
|
||||
assertEquals(baseWidth(24), profile.halfWidth()[24], 1.0E-9D);
|
||||
assertEquals(3D, profile.depth()[24], 1.0E-9D);
|
||||
}
|
||||
|
||||
@@ -46,24 +51,24 @@ public class ChannelProfileBuilderTest {
|
||||
SurfaceCenterline centerline = straight(400);
|
||||
HydrologyPlannerSettings.Surface defaults = HydrologyPlannerSettings.defaults().surface();
|
||||
HydrologyPlannerSettings.Inlet inlet = defaults.banks().inlet();
|
||||
ChannelProfile profile = builder(1D, 1D).build(centerline, "water", true);
|
||||
ChannelProfile profile = builder(1D, 1D).build(centerline, "water", true, 0L);
|
||||
ChannelProfile plain = builder(defaults.banks().withInlet(HydrologyPlannerSettings.Inlet.none()))
|
||||
.build(centerline, "water", true);
|
||||
.build(centerline, "water", true, 0L);
|
||||
|
||||
assertEquals(64, inlet.length());
|
||||
int flareStart = 400 - inlet.length();
|
||||
assertEquals(3D * defaults.banks().mouthFlareRatio(), profile.halfWidth()[399], 1.0E-9D);
|
||||
assertEquals(plain.halfWidth()[399] * defaults.banks().mouthFlareRatio(), profile.halfWidth()[399], 1.0E-9D);
|
||||
assertEquals(3D + inlet.depth(), profile.depth()[399], 1.0E-9D);
|
||||
assertEquals(3D, profile.halfWidth()[flareStart], 1.0E-9D);
|
||||
assertEquals(plain.halfWidth()[flareStart], profile.halfWidth()[flareStart], 1.0E-9D);
|
||||
assertEquals(3D, profile.depth()[flareStart], 1.0E-9D);
|
||||
assertTrue(profile.halfWidth()[flareStart + 1] > 3D);
|
||||
assertTrue(profile.halfWidth()[flareStart + 1] > plain.halfWidth()[flareStart + 1]);
|
||||
assertTrue(profile.depth()[flareStart + 1] > 3D);
|
||||
assertEquals(3D, profile.halfWidth()[300], 1.0E-9D);
|
||||
assertEquals(plain.halfWidth()[300], profile.halfWidth()[300], 1.0E-9D);
|
||||
for (int station = flareStart + 1; station < 400; station++) {
|
||||
assertTrue(profile.halfWidth()[station] >= profile.halfWidth()[station - 1]);
|
||||
assertTrue(profile.halfWidth()[station] / plain.halfWidth()[station] >= profile.halfWidth()[station - 1] / plain.halfWidth()[station - 1]);
|
||||
assertTrue(profile.depth()[station] >= profile.depth()[station - 1]);
|
||||
}
|
||||
assertEquals(3D, plain.halfWidth()[399], 1.0E-9D);
|
||||
assertEquals(baseWidth(399), plain.halfWidth()[399], 1.0E-9D);
|
||||
assertEquals(3D, plain.depth()[399], 1.0E-9D);
|
||||
}
|
||||
|
||||
@@ -75,39 +80,41 @@ public class ChannelProfileBuilderTest {
|
||||
HydrologyTerrainSampler coast = (int x, int z) -> x >= 380
|
||||
? HydrologyTerrainSample.ocean(50, "ocean")
|
||||
: HydrologyTerrainSample.openLand(80, 0D, "land");
|
||||
ChannelProfile profile = new ChannelProfileBuilder(defaults, coast, CONSTANT_GEOMETRY).build(centerline, "water", true);
|
||||
ChannelProfile profile = new ChannelProfileBuilder(defaults, coast, CONSTANT_GEOMETRY).build(centerline, "water", true, 0L);
|
||||
|
||||
HydrologyPlannerSettings.Surface plainSurface = withBanks(defaults, defaults.banks().withInlet(HydrologyPlannerSettings.Inlet.none()));
|
||||
ChannelProfile plain = new ChannelProfileBuilder(plainSurface, coast, CONSTANT_GEOMETRY).build(centerline, "water", true, 0L);
|
||||
int flareStart = 380 - inlet.length();
|
||||
assertEquals(3D, profile.halfWidth()[flareStart], 1.0E-9D);
|
||||
assertTrue(profile.halfWidth()[flareStart + 1] > 3D);
|
||||
assertEquals(3D * defaults.banks().mouthFlareRatio(), profile.halfWidth()[379], 1.0E-9D);
|
||||
assertEquals(plain.halfWidth()[flareStart], profile.halfWidth()[flareStart], 1.0E-9D);
|
||||
assertTrue(profile.halfWidth()[flareStart + 1] > plain.halfWidth()[flareStart + 1]);
|
||||
assertEquals(plain.halfWidth()[379] * defaults.banks().mouthFlareRatio(), profile.halfWidth()[379], 1.0E-9D);
|
||||
assertEquals(3D + inlet.depth(), profile.depth()[379], 1.0E-9D);
|
||||
assertEquals(3D * defaults.banks().mouthFlareRatio(), profile.halfWidth()[399], 1.0E-9D);
|
||||
assertEquals(3D, profile.halfWidth()[300], 1.0E-9D);
|
||||
assertEquals(plain.halfWidth()[399] * defaults.banks().mouthFlareRatio(), profile.halfWidth()[399], 1.0E-9D);
|
||||
assertEquals(plain.halfWidth()[300], profile.halfWidth()[300], 1.0E-9D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void policyMultipliersScaleWidthDepthAndBank() {
|
||||
SurfaceCenterline centerline = straight(400);
|
||||
ChannelProfile profile = builder(2D, 0.5D).build(centerline, "water", false);
|
||||
ChannelProfile profile = builder(2D, 0.5D).build(centerline, "water", false, 0L);
|
||||
|
||||
assertEquals(6D, profile.halfWidth()[200], 1.0E-9D);
|
||||
assertEquals(baseWidth(200) * 2D, profile.halfWidth()[200], 1.0E-9D);
|
||||
assertEquals(1.5D, profile.depth()[200], 1.0E-9D);
|
||||
assertEquals(1.25D, profile.bankMultiplier()[200], 1.0E-9D);
|
||||
assertEquals(6D * 1.25D + 2D, profile.collar(200, 0.25D), 1.0E-9D);
|
||||
assertEquals(baseWidth(200) * 2D * 1.25D + 2D, profile.collar(200, 0.25D), 1.0E-9D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void groundFallingAwayAcrossThePoolShrinksTheSpringPool() {
|
||||
SurfaceCenterline centerline = straight(400);
|
||||
ChannelProfile sloped = builder(1D, 1D, (int x, int z) -> 80 - Math.abs(z) / 2).build(centerline, "water", false);
|
||||
ChannelProfile cliff = builder(1D, 1D, (int x, int z) -> 80 - Math.abs(z) * 8).build(centerline, "water", false);
|
||||
ChannelProfile sloped = builder(1D, 1D, (int x, int z) -> 80 - Math.abs(z) / 2).build(centerline, "water", false, 0L);
|
||||
ChannelProfile cliff = builder(1D, 1D, (int x, int z) -> 80 - Math.abs(z) * 8).build(centerline, "water", false, 0L);
|
||||
|
||||
assertTrue(sloped.halfWidth()[0] > 3D);
|
||||
assertTrue(sloped.halfWidth()[0] < 3D * 2.5D);
|
||||
assertEquals(3D, cliff.halfWidth()[0], 1.0E-9D);
|
||||
assertTrue(sloped.halfWidth()[0] > baseWidth(0));
|
||||
assertTrue(sloped.halfWidth()[0] < baseWidth(0) * 2.5D);
|
||||
assertEquals(baseWidth(0), cliff.halfWidth()[0], 1.0E-9D);
|
||||
assertEquals(4D, cliff.depth()[0], 1.0E-9D);
|
||||
assertEquals(3D, sloped.halfWidth()[24], 1.0E-9D);
|
||||
assertEquals(baseWidth(24), sloped.halfWidth()[24], 1.0E-9D);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -119,16 +126,17 @@ public class ChannelProfileBuilderTest {
|
||||
};
|
||||
SurfaceCenterline centerline = straight(400);
|
||||
ChannelProfile sharp = builder(new HydrologyPlannerSettings.Channel(0, 0.6D, 1.4D, 1D), stepped)
|
||||
.build(centerline, "water", false);
|
||||
.build(centerline, "water", false, 0L);
|
||||
ChannelProfile smoothed = builder(HydrologyPlannerSettings.Channel.defaults(), stepped)
|
||||
.build(centerline, "water", false);
|
||||
.build(centerline, "water", false, 0L);
|
||||
|
||||
assertEquals(2D, sharp.halfWidth()[199], 1.0E-9D);
|
||||
assertEquals(4D, sharp.halfWidth()[200], 1.0E-9D);
|
||||
assertTrue(sharp.halfWidth()[200] > 2D && sharp.halfWidth()[200] <= 4D);
|
||||
assertEquals(2D, sharp.halfWidth()[100], 1.0E-9D);
|
||||
assertEquals(4D, sharp.halfWidth()[300], 1.0E-9D);
|
||||
assertTrue(sharp.halfWidth()[300] > 2D && sharp.halfWidth()[300] <= 4D);
|
||||
assertTrue(smoothed.halfWidth()[199] > 2D);
|
||||
assertTrue(smoothed.halfWidth()[200] < 4D);
|
||||
assertTrue(smoothed.halfWidth()[200] < sharp.halfWidth()[200]);
|
||||
assertEquals(ChannelProfileBuilder.smooth(sharp.halfWidth(), 16)[200], smoothed.halfWidth()[200], 1.0E-9D);
|
||||
assertEquals(2D, smoothed.halfWidth()[100], 1.0E-9D);
|
||||
}
|
||||
|
||||
@@ -136,9 +144,9 @@ public class ChannelProfileBuilderTest {
|
||||
public void springExtraDepthDeepensTheSpringPool() {
|
||||
SurfaceCenterline centerline = straight(400);
|
||||
ChannelProfile flat = builder(new HydrologyPlannerSettings.Channel(16, 0.6D, 1.4D, 0D), CONSTANT_GEOMETRY)
|
||||
.build(centerline, "water", false);
|
||||
.build(centerline, "water", false, 0L);
|
||||
ChannelProfile deep = builder(new HydrologyPlannerSettings.Channel(16, 0.6D, 1.4D, 3D), CONSTANT_GEOMETRY)
|
||||
.build(centerline, "water", false);
|
||||
.build(centerline, "water", false, 0L);
|
||||
|
||||
assertEquals(3D, flat.depth()[0], 1.0E-9D);
|
||||
assertEquals(3D, flat.depth()[200], 1.0E-9D);
|
||||
@@ -147,6 +155,91 @@ public class ChannelProfileBuilderTest {
|
||||
assertEquals(3D, deep.depth()[200], 1.0E-9D);
|
||||
}
|
||||
|
||||
private static double baseWidth(int station) {
|
||||
HydrologyPlannerSettings.Banks defaults = HydrologyPlannerSettings.defaults().surface().banks();
|
||||
HydrologyPlannerSettings.Banks plain = new HydrologyPlannerSettings.Banks(
|
||||
defaults.sink(), defaults.blendSlope(), defaults.minimumBlendWidth(), defaults.maximumBlendWidth(),
|
||||
defaults.roughness(), defaults.roughnessWavelength(), defaults.cascadeRun(), defaults.waterfallMinimumDrop(),
|
||||
defaults.mouthFlareRatio(), HydrologyPlannerSettings.Inlet.none(), 1D, defaults.springLength(),
|
||||
defaults.exposeCutStrata(), defaults.erosion(), defaults.ponds(), defaults.channel(), defaults.flow());
|
||||
return builder(plain).build(straight(400), "water", false, 0L).halfWidth()[station];
|
||||
}
|
||||
|
||||
private static HydrologyPlannerSettings.Surface withBanks(HydrologyPlannerSettings.Surface surface,
|
||||
HydrologyPlannerSettings.Banks banks) {
|
||||
return new HydrologyPlannerSettings.Surface(surface.enabled(), surface.sources(), surface.minimumWidth(),
|
||||
surface.maximumWidth(), surface.minimumDepth(), surface.maximumDepth(), surface.maximumIncision(),
|
||||
surface.shoreWidth(), banks);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reachVariationIsBoundedRepeatableAndSharedAcrossCourseWindows() {
|
||||
SurfaceCenterline complete = straight(512);
|
||||
ChannelProfile first = builder(1D, 1D).build(complete, "water", false, 71L);
|
||||
ChannelProfile repeated = builder(1D, 1D).build(complete, "water", false, 71L);
|
||||
ChannelProfile otherSeed = builder(1D, 1D).build(complete, "water", false, 72L);
|
||||
assertArrayEquals(first.halfWidth(), repeated.halfWidth(), 0D);
|
||||
assertTrue(!Arrays.equals(first.halfWidth(), otherSeed.halfWidth()));
|
||||
SurfaceCenterline window = SurfaceCenterline.densify(List.of(
|
||||
new HydrologyPoint(128, 80, 0), new HydrologyPoint(511, 80, 0)));
|
||||
ChannelProfile shifted = builder(1D, 1D).build(window, "water", false, 71L);
|
||||
double minimum = Double.POSITIVE_INFINITY;
|
||||
double maximum = 0D;
|
||||
for (int station = 192; station < 352; station++) {
|
||||
assertEquals(first.halfWidth()[station], shifted.halfWidth()[station - 128], 0D);
|
||||
double width = first.halfWidth()[station] * 2D;
|
||||
assertTrue(width >= 4D && width <= 8D);
|
||||
minimum = Math.min(minimum, width);
|
||||
maximum = Math.max(maximum, width);
|
||||
}
|
||||
assertTrue(maximum - minimum > 1D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fixedWidthSpringReachesItsConfiguredPeakEarlyAndPreservesDepth() {
|
||||
HydrologyPlannerSettings.Surface defaults = HydrologyPlannerSettings.defaults().surface();
|
||||
HydrologyPlannerSettings.Surface fixed = new HydrologyPlannerSettings.Surface(defaults.enabled(), defaults.sources(),
|
||||
6, 6, defaults.minimumDepth(), defaults.maximumDepth(), defaults.maximumIncision(), defaults.shoreWidth(), defaults.banks());
|
||||
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(80, 0D, "land");
|
||||
ChannelProfile profile = new ChannelProfileBuilder(fixed, terrain, CONSTANT_GEOMETRY)
|
||||
.build(straight(400), "water", false, 71L);
|
||||
assertEquals(3D * 1.9D, profile.halfWidth()[0], 0D);
|
||||
assertEquals(3D * defaults.banks().springWidthRatio(), profile.halfWidth()[6], 0D);
|
||||
assertEquals(3D, profile.halfWidth()[24], 0D);
|
||||
assertEquals(4D, profile.depth()[0], 0D);
|
||||
assertEquals(3D, profile.depth()[24], 0D);
|
||||
for (int station = 0; station < 24; station++) {
|
||||
assertTrue(profile.halfWidth()[station] <= 3D * defaults.banks().springWidthRatio());
|
||||
}
|
||||
HydrologyPlannerSettings.Banks banks = defaults.banks();
|
||||
HydrologyPlannerSettings.Banks noSpringWidth = new HydrologyPlannerSettings.Banks(banks.sink(), banks.blendSlope(),
|
||||
banks.minimumBlendWidth(), banks.maximumBlendWidth(), banks.roughness(), banks.roughnessWavelength(),
|
||||
banks.cascadeRun(), banks.waterfallMinimumDrop(), banks.mouthFlareRatio(), banks.inlet(), 1D, banks.springLength(),
|
||||
banks.exposeCutStrata(), banks.erosion(), banks.ponds(), banks.channel(), banks.flow());
|
||||
ChannelProfile narrow = new ChannelProfileBuilder(withBanks(fixed, noSpringWidth), terrain, CONSTANT_GEOMETRY)
|
||||
.build(straight(400), "water", false, 71L);
|
||||
for (int station = 0; station < 400; station++) {
|
||||
assertEquals(3D, narrow.halfWidth()[station], 0D);
|
||||
}
|
||||
assertArrayEquals(profile.depth(), narrow.depth(), 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fixedWidthRangesAndMinimumConfinementRemainUnmodulated() {
|
||||
HydrologyPlannerSettings.Surface defaults = HydrologyPlannerSettings.defaults().surface();
|
||||
HydrologyPlannerSettings.Surface fixed = new HydrologyPlannerSettings.Surface(defaults.enabled(), defaults.sources(),
|
||||
6, 6, defaults.minimumDepth(), defaults.maximumDepth(), defaults.maximumIncision(), defaults.shoreWidth(), defaults.banks());
|
||||
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(80, 0D, "land");
|
||||
ChannelProfileBuilder fixedBuilder = new ChannelProfileBuilder(fixed, terrain, CONSTANT_GEOMETRY);
|
||||
assertArrayEquals(fixedBuilder.build(straight(400), "water", false, 71L).halfWidth(),
|
||||
fixedBuilder.build(straight(400), "water", false, 72L).halfWidth(), 0D);
|
||||
ChannelProfileBuilder minimumBuilder = new ChannelProfileBuilder(defaults, terrain, request -> request.minimum());
|
||||
assertArrayEquals(minimumBuilder.build(straight(400), "water", false, 71L).halfWidth(),
|
||||
minimumBuilder.build(straight(400), "water", false, 72L).halfWidth(), 0D);
|
||||
assertArrayEquals(fixedBuilder.build(straight(400), null, false, 71L).halfWidth(),
|
||||
fixedBuilder.build(straight(400), "", false, 71L).halfWidth(), 0D);
|
||||
}
|
||||
|
||||
private static ChannelProfileBuilder builder(
|
||||
HydrologyPlannerSettings.Channel channel,
|
||||
HydrologyGeometrySampler geometry
|
||||
|
||||
+90
-15
@@ -21,6 +21,7 @@ import org.junit.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
@@ -47,6 +48,79 @@ public class ErosionFieldCompilerTest {
|
||||
default -> request.minimum();
|
||||
};
|
||||
|
||||
@Test
|
||||
public void shapedEndpointPondsStayConnectedInsideTheirValidatedCircularEnvelope() {
|
||||
HydrologyPlannerSettings.Ponds ponds = new HydrologyPlannerSettings.Ponds(
|
||||
new HydrologyPlannerSettings.Pond(true, 10, 10, 3),
|
||||
new HydrologyPlannerSettings.Pond(true, 10, 10, 3));
|
||||
HydrologyPlannerSettings.Surface surface = zeroRoughnessSurface(0, HydrologyPlannerSettings.Erosion.defaults(), ponds);
|
||||
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(80, 0D, "land");
|
||||
for (boolean rotated : List.of(false, true)) {
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(List.of(new HydrologyPoint(0, 80, 0),
|
||||
new HydrologyPoint(rotated ? 0 : 127, 80, rotated ? 127 : 0)));
|
||||
double[] widths = new double[centerline.size()];
|
||||
double[] depths = new double[centerline.size()];
|
||||
double[] banks = new double[centerline.size()];
|
||||
int[] heads = new int[centerline.size()];
|
||||
Arrays.fill(widths, 2D);
|
||||
Arrays.fill(depths, 2D);
|
||||
Arrays.fill(banks, 1D);
|
||||
Arrays.fill(heads, 80);
|
||||
ChannelProfile channel = new ChannelProfile(widths, depths, banks);
|
||||
ValleyProfile valley = ValleyProfile.fromHeads(heads, heads.length);
|
||||
ErosionFieldCompiler compiler = new ErosionFieldCompiler(rasterSettings(surface), terrain);
|
||||
ErosionField field = compiler.compile(1234L, centerline, channel, valley, SurfaceTerminal.SINKHOLE, 0);
|
||||
assertNull(field.rejection());
|
||||
assertEquals(0, field.uncontainedWetCells());
|
||||
assertChannelContained(new Compiled(field, valley));
|
||||
assertEquals(SurfaceRole.CHANNEL, field.column(0, 0).role());
|
||||
SurfaceColumn end = field.column(rotated ? 0 : 127, rotated ? 127 : 0);
|
||||
assertEquals(SurfaceRole.CHANNEL, end.role());
|
||||
assertTrue(end.height() < end.headY());
|
||||
assertTrue(field.column(0, 0).height() < field.column(0, 0).headY());
|
||||
SurfaceColumn narrowSide = field.column(rotated ? 9 : 0, rotated ? 0 : 9);
|
||||
assertTrue(narrowSide == null || narrowSide.role() != SurfaceRole.CHANNEL);
|
||||
Set<Long> wet = new HashSet<>();
|
||||
for (SurfaceColumn column : field.columns().values()) {
|
||||
if (column.role() != SurfaceRole.CHANNEL || column.height() >= column.headY() || column.apron()) {
|
||||
continue;
|
||||
}
|
||||
wet.add(RiverFootprint.pack(column.x(), column.z()));
|
||||
int along = rotated ? column.z() : column.x();
|
||||
int across = rotated ? column.x() : column.z();
|
||||
if (along < 0 || along > 127) {
|
||||
int endpoint = along < 0 ? 0 : 127;
|
||||
assertTrue(StrictMath.hypot(along - endpoint, across) <= 10.25D);
|
||||
}
|
||||
}
|
||||
Set<Long> reached = new HashSet<>();
|
||||
ArrayDeque<Long> pending = new ArrayDeque<>();
|
||||
pending.add(RiverFootprint.pack(0, 0));
|
||||
reached.add(RiverFootprint.pack(0, 0));
|
||||
int[][] offsets = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
while (!pending.isEmpty()) {
|
||||
long current = pending.removeFirst();
|
||||
for (int[] offset : offsets) {
|
||||
long next = RiverFootprint.pack(RiverFootprint.unpackX(current) + offset[0],
|
||||
RiverFootprint.unpackZ(current) + offset[1]);
|
||||
if (wet.contains(next) && reached.add(next)) {
|
||||
pending.add(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
assertEquals(wet, reached);
|
||||
SurfaceBounds bounds = rotated ? new SurfaceBounds(-12, -12, 12, 64)
|
||||
: new SurfaceBounds(-12, -12, 64, 12);
|
||||
ErosionField clipped = compiler.compile(1234L, centerline, channel, valley, SurfaceTerminal.SINKHOLE, 0,
|
||||
ponds, SurfaceRasterContext.bounded(bounds));
|
||||
for (SurfaceColumn column : field.columns().values()) {
|
||||
if (bounds.contains(column.x(), column.z())) {
|
||||
assertEquals(column, clipped.column(column.x(), column.z()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void wetIncisionPreflightMatchesFullVerdictsAcrossPondsCrossingsStepsAndPolicies() {
|
||||
int accepted = 0;
|
||||
@@ -253,13 +327,13 @@ public class ErosionFieldCompilerTest {
|
||||
sampleCounts.add(samples.get());
|
||||
}
|
||||
assertEquals(List.of(
|
||||
"66595850077d44389c5c78594276de4568f85f08854d3333137dfc05ebf3455a",
|
||||
"66595850077d44389c5c78594276de4568f85f08854d3333137dfc05ebf3455a",
|
||||
"66595850077d44389c5c78594276de4568f85f08854d3333137dfc05ebf3455a",
|
||||
"744b15e684ba996f9c730ab1409ada2643985c902a6da1aadb5cc2b4882d640b",
|
||||
"2e7e4af1e433ccd0a7392f0d2e93bb85ac122088cd2cf8d55107f7dcfb4ec283",
|
||||
"7beee0b78c37b1287510fa0719375e14e0f0013eb8af81df8e20a3b5628612b7",
|
||||
"ad35016194318efa42dc862df4e2bb3d9ec398d3bacec79588576d74bfd1723a",
|
||||
"1cf278d948dd80523cb79dceeabf5e3846a28c8ecd2595247f97de147b502aea",
|
||||
"1cf278d948dd80523cb79dceeabf5e3846a28c8ecd2595247f97de147b502aea",
|
||||
"1cf278d948dd80523cb79dceeabf5e3846a28c8ecd2595247f97de147b502aea",
|
||||
"4632eab466aaf31be342e60e7ea285bd112564356e364e7876563c373027ab54",
|
||||
"f64cb9e63d53728d55be395cf7f0cd5c88ef0402441434a64f79323d579f8a43",
|
||||
"2da402c95dbd9d0568fec47479a943a020d1304a386653ba5b696d0e885dfff2",
|
||||
"db2cd5eb7fd1d53fd9de167cda8e6c214652585162a937511c51596215b75fbf",
|
||||
"a2fe6d20c87be6b6ebf1f4503da75ee767e65d5c8e8c43d5c344957f48f3085a"), fingerprints);
|
||||
int[] previousSamples = {16724, 16724, 16724, 19594, 15164, 15464, 16724, 7367};
|
||||
int[] maximumSamples = {15500, 15500, 15500, 17000, 3000, 3300, 5000, 7367};
|
||||
@@ -400,7 +474,7 @@ public class ErosionFieldCompilerTest {
|
||||
HydrologyPlannerSettings.Surface surface = zeroRoughnessSurface();
|
||||
HydrologyTerrainSampler hillside = (int x, int z) -> HydrologyTerrainSample.openLand(90 + Math.max(0, z) / 2, 0D, "land");
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(List.of(new HydrologyPoint(-150, 0, 0), new HydrologyPoint(150, 0, 0)));
|
||||
ChannelProfile channel = new ChannelProfileBuilder(surface, hillside, CONSTANT_GEOMETRY).build(centerline, "water", false);
|
||||
ChannelProfile channel = new ChannelProfileBuilder(surface, hillside, CONSTANT_GEOMETRY).build(centerline, "water", false, 0L);
|
||||
ValleyProfile valley = new ValleyProfileSolver(surface, hillside, SEA_LEVEL, 64).solve(centerline, channel, SurfaceTerminal.SINKHOLE, 40);
|
||||
ErosionFieldCompiler compiler = new ErosionFieldCompiler(rasterSettings(surface), hillside);
|
||||
ErosionField complete = compiler.compile(42L, centerline, channel, valley, SurfaceTerminal.SINKHOLE, 8);
|
||||
@@ -745,7 +819,7 @@ public class ErosionFieldCompilerTest {
|
||||
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(stations - 1, 0, 0));
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(path);
|
||||
ChannelProfile channel = new ChannelProfileBuilder(surface, sampler, CONSTANT_GEOMETRY)
|
||||
.build(centerline, "water", terminal == SurfaceTerminal.OCEAN_MOUTH);
|
||||
.build(centerline, "water", terminal == SurfaceTerminal.OCEAN_MOUTH, 0L);
|
||||
ValleyProfile valley = new ValleyProfileSolver(surface, sampler, SEA_LEVEL, 64)
|
||||
.solve(centerline, channel, terminal, terminalHead);
|
||||
assertNull(valley.rejection());
|
||||
@@ -992,7 +1066,7 @@ public class ErosionFieldCompilerTest {
|
||||
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(259, 0, 0));
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(path);
|
||||
ChannelProfile channel = new ChannelProfileBuilder(surface, sampler, CONSTANT_GEOMETRY)
|
||||
.build(centerline, "water", true);
|
||||
.build(centerline, "water", true, 0L);
|
||||
ValleyProfile valley = new ValleyProfileSolver(surface, sampler, SEA_LEVEL, 64)
|
||||
.solve(centerline, channel, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
|
||||
assertNull(valley.rejection());
|
||||
@@ -1069,16 +1143,17 @@ public class ErosionFieldCompilerTest {
|
||||
assertEquals(SurfaceRole.CHANNEL, centre.role());
|
||||
assertEquals(80, centre.headY());
|
||||
assertTrue(centre.height() <= 77);
|
||||
SurfaceColumn behind = compiled.field().column(-9, 0);
|
||||
SurfaceColumn behind = compiled.field().column(-7, 0);
|
||||
assertNotNull(behind);
|
||||
assertEquals(SurfaceRole.CHANNEL, behind.role());
|
||||
assertEquals(80, behind.headY());
|
||||
SurfaceColumn beside = compiled.field().column(0, 9);
|
||||
SurfaceColumn beside = compiled.field().column(0, 6);
|
||||
assertEquals(SurfaceRole.CHANNEL, beside.role());
|
||||
assertEquals(0, behind.station());
|
||||
SurfaceColumn plainBehind = plain.field().column(-9, 0);
|
||||
SurfaceColumn plainBehind = plain.field().column(-7, 0);
|
||||
assertTrue(plainBehind == null || plainBehind.role() != SurfaceRole.CHANNEL);
|
||||
assertEquals(SurfaceRole.SHORE, compiled.field().column(-11, 0).role());
|
||||
SurfaceColumn outside = compiled.field().column(-11, 0);
|
||||
assertTrue(outside == null || outside.role() != SurfaceRole.CHANNEL);
|
||||
assertNotNull(compiled.field().column(150, 0));
|
||||
assertEquals(SurfaceRole.CHANNEL, compiled.field().column(150, 0).role());
|
||||
assertTrue(compiled.field().column(150, 4).role() == SurfaceRole.SHORE);
|
||||
@@ -1129,7 +1204,7 @@ public class ErosionFieldCompilerTest {
|
||||
assertChannelContained(compiled);
|
||||
assertBoundedBankFill(compiled, 8);
|
||||
assertEquals(80, compiled.field().column(0, 0).headY());
|
||||
SurfaceColumn nearRim = compiled.field().column(0, 4);
|
||||
SurfaceColumn nearRim = compiled.field().column(0, 3);
|
||||
assertEquals(SurfaceRole.CHANNEL, nearRim.role());
|
||||
assertTrue(compiled.field().columns().values().stream()
|
||||
.anyMatch(column -> column.height() > column.terrain().naturalHeight()));
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package art.arcane.iris.generation.hydrology.surface;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTerrainSample;
|
||||
import art.arcane.iris.generation.hydrology.RiverFootprint;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Random;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class SurfaceFootprintOrderingTest {
|
||||
@Test
|
||||
public void sparseStationsPreserveCoordinateOrderAcrossSignedBoundaries() {
|
||||
Random random = new Random(9482741L);
|
||||
HydrologyTerrainSample terrain = HydrologyTerrainSample.openLand(90, 0D, "land");
|
||||
for (int stationCount : new int[]{1, 2, 17, 1024}) {
|
||||
Long2ObjectOpenHashMap<SurfaceColumn> columns = new Long2ObjectOpenHashMap<>();
|
||||
for (int index = 0; index < 4096; index++) {
|
||||
int x = index - 2048;
|
||||
int z = random.nextInt();
|
||||
int station = random.nextBoolean() ? stationCount - 1 : random.nextInt(stationCount);
|
||||
SurfaceColumn column = new SurfaceColumn(x, z, terrain, station, SurfaceRole.CHANNEL,
|
||||
64, 68, false);
|
||||
columns.put(RiverFootprint.pack(x, z), column);
|
||||
}
|
||||
ErosionField field = new ErosionField(columns, 0, null, 0, 0L);
|
||||
ArrayList<SurfaceColumn> expected = new ArrayList<>(columns.values());
|
||||
expected.sort(Comparator.comparingInt(SurfaceColumn::station)
|
||||
.thenComparingLong(column -> RiverFootprint.pack(column.x(), column.z())));
|
||||
assertEquals(expected, Arrays.asList(SurfaceFootprintCompiler.orderedColumns(field, stationCount)));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anEmptyFieldNeedsNoColumns() {
|
||||
ErosionField empty = new ErosionField(new Long2ObjectOpenHashMap<>(), 0, null, 0, 0L);
|
||||
assertEquals(0, SurfaceFootprintCompiler.orderedColumns(empty, 0).length);
|
||||
assertEquals(0, SurfaceFootprintCompiler.orderedColumns(empty, 1024).length);
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -29,7 +29,7 @@ public class ValleyProfileSolverTest {
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(List.of(
|
||||
new HydrologyPoint(0, 0, 0), new HydrologyPoint(48, 0, 48)));
|
||||
ChannelProfile channel = new ChannelProfileBuilder(HydrologyPlannerSettings.defaults().surface(),
|
||||
terrain, CONSTANT_GEOMETRY).build(centerline, "water", false);
|
||||
terrain, CONSTANT_GEOMETRY).build(centerline, "water", false, 0L);
|
||||
|
||||
ValleyProfile valley = solver(terrain).solve(centerline, channel, SurfaceTerminal.SINKHOLE, 40);
|
||||
|
||||
@@ -55,7 +55,7 @@ public class ValleyProfileSolverTest {
|
||||
new HydrologyPoint(0, 0, 0), new HydrologyPoint(80, 0, 80)));
|
||||
HydrologyPlannerSettings.Surface surface = HydrologyPlannerSettings.defaults().surface();
|
||||
ChannelProfile channel = new ChannelProfileBuilder(surface, terrain, CONSTANT_GEOMETRY)
|
||||
.build(centerline, "water", false);
|
||||
.build(centerline, "water", false, 0L);
|
||||
|
||||
ValleyProfile valley = new ValleyProfileSolver(surface, terrain, SEA_LEVEL, 67)
|
||||
.solve(centerline, channel, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
|
||||
@@ -482,7 +482,7 @@ public class ValleyProfileSolverTest {
|
||||
|
||||
private static ChannelProfile channel(int stations, HydrologyTerrainSampler sampler) {
|
||||
return new ChannelProfileBuilder(HydrologyPlannerSettings.defaults().surface(), sampler, CONSTANT_GEOMETRY)
|
||||
.build(straight(stations), "water", false);
|
||||
.build(straight(stations), "water", false, 0L);
|
||||
}
|
||||
|
||||
private static SurfaceCenterline straight(int stations) {
|
||||
|
||||
+2
-2
@@ -277,7 +277,7 @@ public class IrisGeneratorSurfaceDetailTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getNoise(long superSeed, double x, double z, IrisData data) {
|
||||
public double getNoise(long superSeed, double x, double z, IrisData data, Engine engine) {
|
||||
return sampler.sample(superSeed, x, z);
|
||||
}
|
||||
}
|
||||
@@ -290,7 +290,7 @@ public class IrisGeneratorSurfaceDetailTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getNoise(long superSeed, double x, double z, IrisData data) {
|
||||
public double getNoise(long superSeed, double x, double z, IrisData data, Engine engine) {
|
||||
return sampler.applyAsDouble(data);
|
||||
}
|
||||
}
|
||||
|
||||
+60
-8
@@ -3,6 +3,7 @@ package art.arcane.iris.generation.noise;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.volmlib.util.noise.CNG;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.junit.Test;
|
||||
|
||||
@@ -14,12 +15,16 @@ import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.never;
|
||||
|
||||
public final class IrisNoiseGeneratorCacheTest {
|
||||
private static final long GENERATOR_SALT = 33_955_677L;
|
||||
@@ -30,17 +35,17 @@ public final class IrisNoiseGeneratorCacheTest {
|
||||
public void callerSeedsDoNotShareAFirstWinnerGenerator() {
|
||||
GeneratorFixture fixture = new GeneratorFixture();
|
||||
|
||||
assertSame(fixture.firstGenerator, fixture.generator.getGenerator(11L, fixture.data));
|
||||
assertSame(fixture.secondGenerator, fixture.generator.getGenerator(29L, fixture.data));
|
||||
assertSame(fixture.firstGenerator, fixture.generator.getGenerator(11L, fixture.data));
|
||||
assertSame(fixture.firstGenerator, fixture.generator.getGenerator(11L, fixture.data, fixture.engine));
|
||||
assertSame(fixture.secondGenerator, fixture.generator.getGenerator(29L, fixture.data, fixture.engine));
|
||||
assertSame(fixture.firstGenerator, fixture.generator.getGenerator(11L, fixture.data, fixture.engine));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reverseInitializationOrderKeepsSeedAssignments() {
|
||||
GeneratorFixture fixture = new GeneratorFixture();
|
||||
|
||||
assertSame(fixture.secondGenerator, fixture.generator.getGenerator(29L, fixture.data));
|
||||
assertSame(fixture.firstGenerator, fixture.generator.getGenerator(11L, fixture.data));
|
||||
assertSame(fixture.secondGenerator, fixture.generator.getGenerator(29L, fixture.data, fixture.engine));
|
||||
assertSame(fixture.firstGenerator, fixture.generator.getGenerator(11L, fixture.data, fixture.engine));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -51,11 +56,11 @@ public final class IrisNoiseGeneratorCacheTest {
|
||||
try {
|
||||
Future<CNG> first = executor.submit(() -> {
|
||||
assertTrue(start.await(5L, TimeUnit.SECONDS));
|
||||
return fixture.generator.getGenerator(11L, fixture.data);
|
||||
return fixture.generator.getGenerator(11L, fixture.data, fixture.engine);
|
||||
});
|
||||
Future<CNG> second = executor.submit(() -> {
|
||||
assertTrue(start.await(5L, TimeUnit.SECONDS));
|
||||
return fixture.generator.getGenerator(29L, fixture.data);
|
||||
return fixture.generator.getGenerator(29L, fixture.data, fixture.engine);
|
||||
});
|
||||
start.countDown();
|
||||
|
||||
@@ -66,6 +71,53 @@ public final class IrisNoiseGeneratorCacheTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void heightResolvesEngineOnceAcrossFracturesAndSurfaceCorners() {
|
||||
IrisData data = mock(IrisData.class);
|
||||
Engine engine = mock(Engine.class);
|
||||
when(data.getEngine()).thenReturn(engine);
|
||||
IrisNoiseGenerator child = new IrisNoiseGenerator().setStyle(NoiseStyle.SIMPLEX.style());
|
||||
IrisNoiseGenerator layer = new IrisNoiseGenerator().setStyle(NoiseStyle.SIMPLEX.style())
|
||||
.setFracture(new KList<>(child));
|
||||
IrisGenerator generator = new IrisGenerator().setComposite(new KList<>(layer, child))
|
||||
.setSurfaceDetail(0.5D);
|
||||
generator.setLoader(data);
|
||||
|
||||
double first = generator.getHeight(13D, -21D, 71L);
|
||||
verify(data, times(1)).getEngine();
|
||||
assertEquals(Double.doubleToRawLongBits(first),
|
||||
Double.doubleToRawLongBits(generator.getHeight(13D, -21D, 71L)));
|
||||
verify(data, times(2)).getEngine();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void changedEngineUsesDistinctConstructionAndRestoresPreviousGenerator() {
|
||||
IrisData data = mock(IrisData.class);
|
||||
Engine firstEngine = mock(Engine.class);
|
||||
Engine secondEngine = mock(Engine.class);
|
||||
IrisGeneratorStyle style = mock(IrisGeneratorStyle.class);
|
||||
CNG first = mock(CNG.class);
|
||||
CNG second = mock(CNG.class);
|
||||
IrisNoiseGenerator layer = new IrisNoiseGenerator().setStyle(style);
|
||||
when(data.getEngine()).thenReturn(firstEngine, secondEngine, firstEngine);
|
||||
when(style.createForLayer(any(RNG.class), same(data), eq(1), same(firstEngine))).thenReturn(first);
|
||||
when(style.createForLayer(any(RNG.class), same(data), eq(1), same(secondEngine))).thenReturn(second);
|
||||
|
||||
assertSame(first, layer.getGenerator(11L, data, data.getEngine()));
|
||||
assertSame(second, layer.getGenerator(11L, data, data.getEngine()));
|
||||
assertSame(first, layer.getGenerator(11L, data, data.getEngine()));
|
||||
verify(style, times(1)).createForLayer(any(RNG.class), same(data), eq(1), same(firstEngine));
|
||||
verify(style, times(1)).createForLayer(any(RNG.class), same(data), eq(1), same(secondEngine));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void disabledNoiseDoesNotResolveEngine() {
|
||||
IrisData data = mock(IrisData.class);
|
||||
IrisNoiseGenerator layer = new IrisNoiseGenerator(false).setOffsetY(0.37D);
|
||||
assertEquals(0.37D, layer.getNoise(11L, 13D, -21D, data), 0D);
|
||||
verify(data, never()).getEngine();
|
||||
}
|
||||
|
||||
private static final class GeneratorFixture {
|
||||
private final IrisData data = mock(IrisData.class);
|
||||
private final Engine engine = mock(Engine.class);
|
||||
@@ -83,7 +135,7 @@ public final class IrisNoiseGeneratorCacheTest {
|
||||
11L + GENERATOR_SALT - CONFIGURED_SEED, firstGenerator,
|
||||
29L + GENERATOR_SALT - CONFIGURED_SEED, secondGenerator
|
||||
);
|
||||
when(style.createForLayer(any(RNG.class), same(data), eq(OCTAVES))).thenAnswer(invocation -> {
|
||||
when(style.createForLayer(any(RNG.class), same(data), eq(OCTAVES), same(engine))).thenAnswer(invocation -> {
|
||||
RNG rng = invocation.getArgument(0);
|
||||
return generators.get(rng.getSeed());
|
||||
});
|
||||
|
||||
@@ -83,7 +83,7 @@ public class SchemaBuilderIdentityTest {
|
||||
@ClassRule
|
||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
|
||||
private static final String EXPECTED_SCHEMA_DIGEST = "767e66a7a52bdd6ae9ad3f0ccfdcaf2cb509fc89c70bfffb4dcd54b565e643f9";
|
||||
private static final String EXPECTED_SCHEMA_DIGEST = "1720dcd78ce7d4ef6fc601cb8d8703d42a35625e8038ab04e98db03514df5959";
|
||||
|
||||
private static final List<Class<?>> SCHEMA_ROOTS = List.of(
|
||||
IrisDimension.class,
|
||||
|
||||
@@ -7,12 +7,14 @@ import org.junit.Test;
|
||||
import javax.swing.SwingUtilities;
|
||||
import java.awt.Color;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class PregenMapPanelTest {
|
||||
@@ -78,6 +80,58 @@ public class PregenMapPanelTest {
|
||||
assertEquals(new Rectangle(251, 1, 300, 600), tall);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hoverCoordinatesFollowPaintedRectangularBoundsAndLetterboxing() throws Exception {
|
||||
PregenMapPanel wide = panel(new PregenRenderSnapshot.Bounds(-2, -3, 1, -2));
|
||||
SwingUtilities.invokeAndWait(() -> wide.setSize(802, 602));
|
||||
assertEquals("Chunk X -2 · Z -3", tooltip(wide, 1, 101));
|
||||
assertEquals("Chunk X -2 · Z -3", tooltip(wide, 200, 300));
|
||||
assertEquals("Chunk X -1 · Z -2", tooltip(wide, 201, 301));
|
||||
assertEquals("Chunk X 1 · Z -2", tooltip(wide, 800, 500));
|
||||
assertNull(tooltip(wide, 0, 300));
|
||||
assertNull(tooltip(wide, 801, 300));
|
||||
assertNull(tooltip(wide, 400, 100));
|
||||
assertNull(tooltip(wide, 400, 501));
|
||||
SwingUtilities.invokeAndWait(wide::disposeMap);
|
||||
assertNull(tooltip(wide, 400, 300));
|
||||
|
||||
PregenMapPanel tall = panel(new PregenRenderSnapshot.Bounds(-7, -2, -6, 1));
|
||||
SwingUtilities.invokeAndWait(() -> tall.setSize(802, 602));
|
||||
assertEquals("Chunk X -7 · Z -2", tooltip(tall, 251, 1));
|
||||
assertEquals("Chunk X -6 · Z 1", tooltip(tall, 550, 600));
|
||||
assertNull(tooltip(tall, 250, 300));
|
||||
assertNull(tooltip(tall, 551, 300));
|
||||
SwingUtilities.invokeAndWait(tall::disposeMap);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hoverCoordinatesPreserveSingleChunkAndExtremeIntegerBoundsAfterResize() throws Exception {
|
||||
PregenMapPanel single = panel(new PregenRenderSnapshot.Bounds(5, -9, 5, -9));
|
||||
SwingUtilities.invokeAndWait(() -> single.setSize(64, 64));
|
||||
assertEquals("Chunk X 5 · Z -9", tooltip(single, 1, 1));
|
||||
assertEquals("Chunk X 5 · Z -9", tooltip(single, 62, 62));
|
||||
SwingUtilities.invokeAndWait(() -> single.setSize(128, 64));
|
||||
assertNull(tooltip(single, 1, 1));
|
||||
assertEquals("Chunk X 5 · Z -9", tooltip(single, 33, 1));
|
||||
SwingUtilities.invokeAndWait(single::disposeMap);
|
||||
|
||||
PregenMapPanel extreme = panel(new PregenRenderSnapshot.Bounds(
|
||||
Integer.MIN_VALUE, -10, Integer.MAX_VALUE, 10));
|
||||
SwingUtilities.invokeAndWait(() -> extreme.setSize(1026, 3));
|
||||
assertEquals("Chunk X -2147483648 · Z -10", tooltip(extreme, 1, 1));
|
||||
assertEquals("Chunk X 0 · Z -10", tooltip(extreme, 513, 1));
|
||||
assertEquals("Chunk X 2143289344 · Z -10", tooltip(extreme, 1024, 1));
|
||||
assertNull(tooltip(extreme, 1025, 1));
|
||||
SwingUtilities.invokeAndWait(extreme::disposeMap);
|
||||
}
|
||||
|
||||
private static String tooltip(PregenMapPanel panel, int x, int y) throws Exception {
|
||||
String[] result = new String[1];
|
||||
SwingUtilities.invokeAndWait(() -> result[0] = panel.getToolTipText(
|
||||
new MouseEvent(panel, MouseEvent.MOUSE_MOVED, 0L, 0, x, y, 0, false)));
|
||||
return result[0];
|
||||
}
|
||||
|
||||
@Test
|
||||
public void arithmeticTaskBoundsMatchActualRectangularTraversal() {
|
||||
PregenTask task = PregenTask.builder().center(new Position2(-17, 35)).radiusX(9).radiusZ(37).build();
|
||||
@@ -93,7 +147,7 @@ public class PregenMapPanelTest {
|
||||
|
||||
private static PregenMapPanel panel(PregenRenderSnapshot.Bounds bounds) throws Exception {
|
||||
PregenMapPanel[] result = new PregenMapPanel[1];
|
||||
SwingUtilities.invokeAndWait(() -> result[0] = new PregenMapPanel(bounds));
|
||||
SwingUtilities.invokeAndWait(() -> result[0] = new PregenMapPanel(new PregenMapState(bounds)));
|
||||
return result[0];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
package art.arcane.iris.studio.view;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.world.pregen.CachedPregenMethod;
|
||||
import art.arcane.iris.world.pregen.PregenCache;
|
||||
import art.arcane.iris.world.pregen.PregenSavedChunkStatus;
|
||||
import art.arcane.iris.world.pregen.PregenTask;
|
||||
import art.arcane.iris.world.pregen.PregeneratorMethod;
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import javax.swing.SwingUtilities;
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class PregenMapRetentionTest {
|
||||
@Test
|
||||
public void existingStartingChunksPublishedBeforeAttachmentAppearInActualPaintAndReopenedView() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.job.onChunkExistsInRegionGen(-16, -16);
|
||||
fixture.job.onChunkGenerated(-17, -16, false);
|
||||
verifyNoInteractions(fixture.engine);
|
||||
|
||||
PregenMapPanel first = panel(fixture.state);
|
||||
assertEquals(PregenRenderer.EXISTS.getRGB(), paint(first, fixture.state).getRGB(17, 17));
|
||||
assertEquals(PregenRenderer.GENERATED.getRGB(), paint(first, fixture.state).getRGB(16, 17));
|
||||
SwingUtilities.invokeAndWait(first::disposeMap);
|
||||
fixture.job.onChunkGenerated(-16, -17, true);
|
||||
PregenMapPanel reopened = panel(fixture.state);
|
||||
assertEquals(PregenRenderer.EXISTS.getRGB(), paint(reopened, fixture.state).getRGB(17, 17));
|
||||
assertEquals(PregenRenderer.GENERATED.getRGB(), paint(reopened, fixture.state).getRGB(17, 16));
|
||||
SwingUtilities.invokeAndWait(reopened::disposeMap);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whollyCachedNegativeRegionPublishesAllChunksBeforeGuiOpens() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
PregenCache cache = mock(PregenCache.class);
|
||||
when(cache.sync()).thenReturn(cache);
|
||||
when(cache.isRegionCached(-1, -1)).thenReturn(true);
|
||||
PregeneratorMethod backend = mock(PregeneratorMethod.class);
|
||||
CachedPregenMethod cached = new CachedPregenMethod(new CachedPregenMethod.Configuration(
|
||||
backend, cache, fixture.task, new PregenSavedChunkStatus((x, z) -> true)));
|
||||
try (MockedStatic<IrisServices> services = mockStatic(IrisServices.class)) {
|
||||
cached.generateRegion(-1, -1, fixture.job);
|
||||
}
|
||||
verifyNoInteractions(backend);
|
||||
PregenMapPanel panel = panel(fixture.state);
|
||||
BufferedImage rendered = paint(panel, fixture.state);
|
||||
for (int x = 1; x < 32; x++) {
|
||||
for (int z = 1; z < 32; z++) {
|
||||
assertEquals(PregenRenderer.GENERATED.getRGB(), rendered.getRGB(x + 1, z + 1));
|
||||
}
|
||||
}
|
||||
SwingUtilities.invokeAndWait(panel::disposeMap);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shutdownSkippedRegionsStayWaitingInsteadOfBeingPaintedAsExisting() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.job.onRegionSkipped(-1, -1);
|
||||
PregenMapPanel panel = panel(fixture.state);
|
||||
assertEquals(PregenMapState.WAITING.getRGB(), paint(panel, fixture.state).getRGB(17, 17));
|
||||
SwingUtilities.invokeAndWait(panel::disposeMap);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentPublicationAndIndependentViewsKeepTheFinalRaster() throws Exception {
|
||||
PregenMapState state = new PregenMapState(new PregenRenderSnapshot.Bounds(-8, -8, 7, 7));
|
||||
PregenMapPanel first = panel(state);
|
||||
PregenMapPanel second = panel(state);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
ExecutorService worker = Executors.newSingleThreadExecutor();
|
||||
try {
|
||||
Future<?> updates = worker.submit(() -> {
|
||||
started.countDown();
|
||||
for (int round = 0; round < 100; round++) {
|
||||
for (int x = -8; x < 8; x++) {
|
||||
for (int z = -8; z < 8; z++) {
|
||||
state.submit(x, z, (round & 1) == 0 ? Color.RED : Color.GREEN);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||
for (int round = 0; round < 10; round++) {
|
||||
SwingUtilities.invokeAndWait(first::flush);
|
||||
}
|
||||
updates.get(5, TimeUnit.SECONDS);
|
||||
BufferedImage one = paint(first, state);
|
||||
BufferedImage two = paint(second, state);
|
||||
for (int x = 2; x < 16; x++) {
|
||||
for (int z = 2; z < 16; z++) {
|
||||
assertEquals(Color.GREEN.getRGB(), one.getRGB(x, z));
|
||||
assertEquals(one.getRGB(x, z), two.getRGB(x, z));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
worker.shutdownNow();
|
||||
SwingUtilities.invokeAndWait(first::disposeMap);
|
||||
SwingUtilities.invokeAndWait(second::disposeMap);
|
||||
}
|
||||
}
|
||||
|
||||
private static PregenMapPanel panel(PregenMapState state) throws Exception {
|
||||
PregenMapPanel[] result = new PregenMapPanel[1];
|
||||
SwingUtilities.invokeAndWait(() -> result[0] = new PregenMapPanel(state));
|
||||
return result[0];
|
||||
}
|
||||
|
||||
private static BufferedImage paint(PregenMapPanel panel, PregenMapState state) throws Exception {
|
||||
BufferedImage image = new BufferedImage(state.width() + 2, state.height() + 2, BufferedImage.TYPE_INT_RGB);
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
panel.flush();
|
||||
panel.setSize(image.getWidth(), image.getHeight());
|
||||
Graphics2D graphics = image.createGraphics();
|
||||
try {
|
||||
panel.paint(graphics);
|
||||
} finally {
|
||||
graphics.dispose();
|
||||
}
|
||||
});
|
||||
return image;
|
||||
}
|
||||
|
||||
private static final class Fixture {
|
||||
private final PregeneratorJob job = mock(PregeneratorJob.class, CALLS_REAL_METHODS);
|
||||
private final Engine engine = mock(Engine.class);
|
||||
private final PregenTask task = PregenTask.builder().center(new Position2(-256, -256))
|
||||
.radiusX(256).radiusZ(256).build();
|
||||
private final PregenMapState state;
|
||||
|
||||
private Fixture() throws Exception {
|
||||
int[] bounds = task.chunkBounds();
|
||||
state = new PregenMapState(new PregenRenderSnapshot.Bounds(bounds[0], bounds[1], bounds[2], bounds[3]));
|
||||
set("mapState", state);
|
||||
set("task", task);
|
||||
set("engine", engine);
|
||||
}
|
||||
|
||||
private void set(String name, Object value) throws Exception {
|
||||
Field field = PregeneratorJob.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(job, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package art.arcane.iris.studio.view;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.localization.DesktopUiMessages;
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JTextArea;
|
||||
import javax.swing.KeyStroke;
|
||||
import javax.swing.SwingUtilities;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
|
||||
public class PregenRendererTest {
|
||||
@Test
|
||||
public void keyboardAndButtonActivationToggleExactlyOnceAndTerminalControlsAreDisabled() throws Exception {
|
||||
onEdt(() -> {
|
||||
Source source = new Source();
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
PregenRenderer renderer = renderer(source, () -> {
|
||||
calls.incrementAndGet();
|
||||
source.snapshot = snapshot(source.snapshot.phase() == PregenRenderSnapshot.Phase.PAUSED
|
||||
? PregenRenderSnapshot.Phase.GENERATING : PregenRenderSnapshot.Phase.PAUSED, null, 0);
|
||||
});
|
||||
try {
|
||||
JButton button = field(renderer, "pauseButton", JButton.class);
|
||||
Object pauseAction = renderer.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
|
||||
.get(KeyStroke.getKeyStroke(KeyEvent.VK_P, 0, true));
|
||||
renderer.getActionMap().get(pauseAction).actionPerformed(new ActionEvent(renderer, 0, "P"));
|
||||
assertEquals(1, calls.get());
|
||||
assertEquals(PregenRenderSnapshot.Phase.PAUSED, source.snapshot.phase());
|
||||
Object enterAction = button.getInputMap(JComponent.WHEN_FOCUSED)
|
||||
.get(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0));
|
||||
button.getActionMap().get(enterAction).actionPerformed(new ActionEvent(button, 0, "Enter"));
|
||||
assertEquals(2, calls.get());
|
||||
button.doClick(0);
|
||||
assertEquals(3, calls.get());
|
||||
for (PregenRenderSnapshot.Phase phase : new PregenRenderSnapshot.Phase[]{
|
||||
PregenRenderSnapshot.Phase.STOPPING, PregenRenderSnapshot.Phase.COMPLETED,
|
||||
PregenRenderSnapshot.Phase.ERROR}) {
|
||||
source.snapshot = snapshot(phase, null, 0);
|
||||
refresh(renderer);
|
||||
assertFalse(button.isEnabled());
|
||||
renderer.getActionMap().get(pauseAction).actionPerformed(new ActionEvent(renderer, 0, "P"));
|
||||
button.getActionMap().get(enterAction).actionPerformed(new ActionEvent(button, 0, "Enter"));
|
||||
button.doClick(0);
|
||||
assertEquals(3, calls.get());
|
||||
}
|
||||
} finally {
|
||||
renderer.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initializingAndPausedEtaRemainPending() throws Exception {
|
||||
onEdt(() -> {
|
||||
Source source = new Source();
|
||||
source.snapshot = snapshot(PregenRenderSnapshot.Phase.INITIALIZING, null, 0);
|
||||
PregenRenderer renderer = renderer(source, () -> { });
|
||||
try {
|
||||
JLabel eta = field(renderer, "etaValue", JLabel.class);
|
||||
String pending = IrisLanguage.plain(DesktopUiMessages.PREGEN_METHOD_PENDING);
|
||||
assertEquals(pending, eta.getText());
|
||||
source.snapshot = snapshot(PregenRenderSnapshot.Phase.PAUSED, null, 0);
|
||||
refresh(renderer);
|
||||
assertEquals(pending, eta.getText());
|
||||
for (PregenRenderSnapshot.Phase phase : new PregenRenderSnapshot.Phase[]{
|
||||
PregenRenderSnapshot.Phase.ERROR, PregenRenderSnapshot.Phase.STOPPING}) {
|
||||
source.snapshot = snapshot(phase, null, 0);
|
||||
refresh(renderer);
|
||||
assertEquals(pending, eta.getText());
|
||||
}
|
||||
source.snapshot = snapshot(PregenRenderSnapshot.Phase.GENERATING, null, 0);
|
||||
refresh(renderer);
|
||||
assertFalse(pending.equals(eta.getText()));
|
||||
} finally {
|
||||
renderer.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failureDetailsAndPauseFailureRemainVisibleUntilSuccessfulRetry() throws Exception {
|
||||
onEdt(() -> {
|
||||
Source source = new Source();
|
||||
source.snapshot = snapshot(PregenRenderSnapshot.Phase.GENERATING, "chunk save failed", 1);
|
||||
AtomicBoolean failPause = new AtomicBoolean(true);
|
||||
PregenRenderer renderer = renderer(source, () -> {
|
||||
if (failPause.get()) {
|
||||
throw new IllegalStateException("scheduler refused pause");
|
||||
}
|
||||
});
|
||||
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class, invocation -> {
|
||||
if (invocation.getMethod().getName().equals("reportError")
|
||||
&& invocation.getMethod().getParameterCount() == 2
|
||||
&& invocation.getMethod().getParameterTypes()[0] == String.class
|
||||
&& invocation.getMethod().getParameterTypes()[1] == Throwable.class) {
|
||||
return null;
|
||||
}
|
||||
return invocation.callRealMethod();
|
||||
})) {
|
||||
JButton button = field(renderer, "pauseButton", JButton.class);
|
||||
JTextArea details = field(renderer, "failureLabel", JTextArea.class);
|
||||
assertTrue(details.getLineWrap());
|
||||
assertTrue(details.getText().contains("chunk save failed"));
|
||||
int selectionStart = details.getText().indexOf("chunk save failed");
|
||||
details.select(selectionStart, selectionStart + "chunk save failed".length());
|
||||
PregenRenderSnapshot previous = source.snapshot;
|
||||
source.snapshot = new PregenRenderSnapshot(previous.bounds(), previous.progress(),
|
||||
previous.phase(), previous.cached(), previous.usedMemoryBytes() + 1024,
|
||||
previous.memoryUsage(), previous.allocationBytesPerSecond(), previous.failure());
|
||||
refresh(renderer);
|
||||
assertEquals("chunk save failed", details.getSelectedText());
|
||||
assertEquals(selectionStart, details.getSelectionStart());
|
||||
button.doClick(0);
|
||||
assertTrue(details.getText().contains("scheduler refused pause"));
|
||||
source.snapshot = snapshot(PregenRenderSnapshot.Phase.GENERATING, null, 2);
|
||||
refresh(renderer);
|
||||
assertTrue(details.getText().contains("scheduler refused pause"));
|
||||
failPause.set(false);
|
||||
button.doClick(0);
|
||||
assertFalse(details.getText().contains("scheduler refused pause"));
|
||||
} finally {
|
||||
renderer.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static PregenRenderer renderer(Source source, Runnable pause) throws Exception {
|
||||
Constructor<PregenRenderer> constructor = PregenRenderer.class.getDeclaredConstructor(
|
||||
PregenRenderSource.class, Runnable.class);
|
||||
constructor.setAccessible(true);
|
||||
return constructor.newInstance(source, pause);
|
||||
}
|
||||
|
||||
private static void refresh(PregenRenderer renderer) throws Exception {
|
||||
Method refresh = PregenRenderer.class.getDeclaredMethod("refresh");
|
||||
refresh.setAccessible(true);
|
||||
refresh.invoke(renderer);
|
||||
}
|
||||
|
||||
private static <T> T field(PregenRenderer renderer, String name, Class<T> type) throws Exception {
|
||||
Field field = PregenRenderer.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
return type.cast(field.get(renderer));
|
||||
}
|
||||
|
||||
private static void onEdt(CheckedTask task) throws Exception {
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
try (MockedStatic<IrisSettings> settings = mockStatic(IrisSettings.class)) {
|
||||
settings.when(IrisSettings::get).thenReturn(new IrisSettings());
|
||||
task.run();
|
||||
} catch (Exception failure) {
|
||||
throw new IllegalStateException(failure);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static PregenRenderSnapshot snapshot(PregenRenderSnapshot.Phase phase, String failure, long failed) {
|
||||
return new PregenRenderSnapshot(new PregenRenderSnapshot.Bounds(-32, -32, 32, 32),
|
||||
new PregeneratorJob.PregenProgress(25, 25, 100, 20, 18, 17, 16,
|
||||
75, 4, 3000, 1000, "native", phase == PregenRenderSnapshot.Phase.PAUSED,
|
||||
failed, "Example world", "example"), phase, false, 1024, 0.25, 256, failure);
|
||||
}
|
||||
|
||||
private interface CheckedTask {
|
||||
void run() throws Exception;
|
||||
}
|
||||
|
||||
private static final class Source implements PregenRenderSource {
|
||||
private PregenRenderSnapshot snapshot = snapshot(PregenRenderSnapshot.Phase.GENERATING, null, 0);
|
||||
private final PregenMapState map = new PregenMapState(snapshot.bounds());
|
||||
|
||||
@Override
|
||||
public PregenRenderSnapshot renderSnapshot() {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PregenMapState renderMapState() {
|
||||
return map;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ public class PregeneratorJobSavedBiomeTest {
|
||||
fixture.job.onChunkGenerated(2, -3, false);
|
||||
assertEquals(1, fixture.tasks.size());
|
||||
fixture.tasks.removeFirst().run();
|
||||
verify(fixture.renderer).submit(anyInt(), anyInt(), any());
|
||||
assertEquals(PregenRenderer.GENERATED.getRGB(), fixture.mapPixels[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -92,11 +92,16 @@ public class PregeneratorJobSavedBiomeTest {
|
||||
private final Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
|
||||
private final PregenRenderer renderer = mock(PregenRenderer.class);
|
||||
private final ArrayList<Runnable> tasks = new ArrayList<>();
|
||||
private final int[] mapPixels = new int[1];
|
||||
|
||||
private Fixture() throws Exception {
|
||||
PregenMapState state = new PregenMapState(new PregenRenderSnapshot.Bounds(2, -3, 2, -3));
|
||||
PregenMapState.View view = state.attach(mapPixels);
|
||||
setField("mapState", state);
|
||||
ExecutorService service = mock(ExecutorService.class);
|
||||
doAnswer(invocation -> {
|
||||
tasks.add(invocation.getArgument(0));
|
||||
view.flush();
|
||||
return null;
|
||||
}).when(service).execute(any(Runnable.class));
|
||||
when(renderer.isVisibleFrame()).thenReturn(true);
|
||||
|
||||
@@ -24,6 +24,50 @@ public class BoundaryColumnGeometryTest {
|
||||
private static final BoundaryColumnGeometry.Voxel AIR = new BoundaryColumnGeometry.Voxel(
|
||||
"minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false);
|
||||
|
||||
@Test
|
||||
public void solidAboveMatchesDenseColumnsWithProtectionAndExtremeWorldCoordinates() {
|
||||
BoundaryColumnGeometry.Voxel protectedSolid = new BoundaryColumnGeometry.Voxel(
|
||||
"minecraft:gold_block", BoundaryColumnGeometry.Phase.SOLID, "", true);
|
||||
BoundaryColumnGeometry.Voxel fluid = new BoundaryColumnGeometry.Voxel(
|
||||
"minecraft:water", BoundaryColumnGeometry.Phase.FLUID, "minecraft:water", false);
|
||||
Random random = new Random(83761L);
|
||||
for (int height : new int[]{0, 1, 2, 3, 64, 257, 4096, BoundaryColumnGeometry.MAXIMUM_HEIGHT}) {
|
||||
for (int minimumY : new int[]{Integer.MIN_VALUE, -2032, -64, 0,
|
||||
Integer.MAX_VALUE - BoundaryColumnGeometry.MAXIMUM_HEIGHT}) {
|
||||
ArrayList<BoundaryColumnGeometry.Voxel> voxels = new ArrayList<>(height);
|
||||
for (int offset = 0; offset < height; offset++) {
|
||||
voxels.add(switch (random.nextInt(8)) {
|
||||
case 0 -> STONE;
|
||||
case 1 -> protectedSolid;
|
||||
case 2 -> fluid;
|
||||
default -> AIR;
|
||||
});
|
||||
}
|
||||
BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(minimumY, voxels);
|
||||
for (int index = 0; index < 128; index++) {
|
||||
long offset = index < 4 ? new long[]{-1, 0, height - 1L, height}[index]
|
||||
: random.nextInt(Math.max(1, height + 2)) - 1L;
|
||||
long worldY = (long) minimumY + offset;
|
||||
if (worldY < Integer.MIN_VALUE || worldY > Integer.MAX_VALUE) {
|
||||
continue;
|
||||
}
|
||||
boolean expected = false;
|
||||
for (int above = (int) Math.max(0L, offset + 1L); above < height; above++) {
|
||||
if (voxels.get(above).phase() == BoundaryColumnGeometry.Phase.SOLID) {
|
||||
expected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertEquals(expected, geometry.hasSolidAbove((int) worldY));
|
||||
}
|
||||
assertFalse(geometry.hasSolidAbove(Integer.MAX_VALUE));
|
||||
}
|
||||
}
|
||||
assertFalse(BoundaryColumnGeometry.empty().hasSolidAbove(Integer.MIN_VALUE));
|
||||
assertFalse(BoundaryColumnGeometry.fromVoxels(-64, List.of(AIR, fluid)).hasSolidAbove(Integer.MIN_VALUE));
|
||||
assertTrue(BoundaryColumnGeometry.fromVoxels(-64, List.of(protectedSolid)).hasSolidAbove(-65));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preservesEveryVerticalRunIncludingCavesAndIslands() {
|
||||
BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-4,
|
||||
|
||||
Reference in New Issue
Block a user