mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-28 23:10:32 +00:00
Refine hydrology and Studio
- Add dimension-wide hydrology density controls. - Validate pond containment and shorelines at block resolution. - Prioritize requested water plans and deduplicate prefetch work. - Keep completed water previews visible while details load. - Add pregeneration map navigation and Studio theme controls. - Reject pack mutations during generation-registry capture.
This commit is contained in:
1 parent
1d182769d8
commit
494cdb1a5c
61 files changed
+1624
-325
No files matched your search
+66
@@ -53,6 +53,7 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.AbstractExecutorService;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -79,8 +80,10 @@ import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
@@ -296,6 +299,31 @@ public class HydrologyRuntimeTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 180_000L)
|
||||
public void chunksSharePrefetchAndRetryAfterEvictionOrFailure() {
|
||||
FakePlanners planners = new FakePlanners(settings());
|
||||
HydrologyPlanCache cache = spy(new HydrologyPlanCache(planners.settings, planners::region, planners::window,
|
||||
pool(planners, 2), () -> false));
|
||||
AtomicInteger prefetches = new AtomicInteger();
|
||||
doAnswer(invocation -> prefetches.incrementAndGet() == 2
|
||||
? CompletableFuture.failedFuture(new IllegalStateException("prefetch failed")) : CompletableFuture.completedFuture(null))
|
||||
.when(cache).prepareWindowsAround(anyInt(), anyInt(), anyInt(), anyInt(), anyInt());
|
||||
AtomicLong evictions = new AtomicLong();
|
||||
doAnswer(invocation -> evictions.get()).when(cache).windowEvictions();
|
||||
HydrologyRuntime runtime = new HydrologyRuntime(planners.settings, cache, planners::natural, () -> false, planners.ticker::get);
|
||||
runtimes.add(runtime);
|
||||
runtime.raster(0, 0);
|
||||
runtime.raster(1, 0);
|
||||
assertEquals(1, prefetches.get());
|
||||
evictions.incrementAndGet();
|
||||
runtime.raster(2, 0);
|
||||
assertEquals(2, prefetches.get());
|
||||
runtime.raster(3, 0);
|
||||
assertEquals(3, prefetches.get());
|
||||
runtime.raster(4, 0);
|
||||
assertEquals(3, prefetches.get());
|
||||
}
|
||||
|
||||
@Test(timeout = 180_000L)
|
||||
public void preparePregenerationPlansRegionsInOrder() throws Exception {
|
||||
FakePlanners planners = new FakePlanners(settings());
|
||||
@@ -702,6 +730,44 @@ public class HydrologyRuntimeTest {
|
||||
verify(engine, never()).getComplex();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zeroDensitySkipsRuntimePlanningAfterValidatingSettings() {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
IrisDimension dimension = new IrisDimension();
|
||||
dimension.setHydrology(new IrisHydrology().setEnabled(true).setDensity(0D));
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
when(engine.getSeedManager()).thenReturn(new SeedManager(1337L));
|
||||
when(engine.getHeight()).thenReturn(384);
|
||||
|
||||
assertDisabled(HydrologyRuntime.create(engine, complex));
|
||||
verifyNoInteractions(complex);
|
||||
verify(engine, never()).getPlatformHooks();
|
||||
|
||||
dimension.getHydrology().setDensity(-1D);
|
||||
assertThrows(IllegalArgumentException.class, () -> HydrologyRuntime.create(engine, complex));
|
||||
}
|
||||
|
||||
@Test(timeout = 15_000L)
|
||||
public void nestedPreviewScopesStayNonblockingAndRestoreDemandSampling() throws Exception {
|
||||
try (FakeHydrology water = new FakeHydrology(() -> false, (x, z) -> 60, 40, 63)) {
|
||||
HydrologyRuntime runtime = spy(water.runtime);
|
||||
water.hold();
|
||||
try (HydrologyRuntime.PreviewScope outer = runtime.preview()) {
|
||||
assertTrue(runtime.raster(0, 0).provisional());
|
||||
try (HydrologyRuntime.PreviewScope inner = runtime.preview()) {
|
||||
assertTrue(runtime.raster(1, 0).provisional());
|
||||
}
|
||||
assertTrue(runtime.raster(0, 1).provisional());
|
||||
}
|
||||
water.release();
|
||||
clearInvocations(runtime);
|
||||
|
||||
assertFalse(runtime.raster(400, 400).provisional());
|
||||
verify(runtime, never()).rasterPreview(anyInt(), anyInt());
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000L)
|
||||
public void createSavesPlansUnderTheWorldFolder() throws Exception {
|
||||
Path worldFolder = temporaryFolder.newFolder("world").toPath();
|
||||
|
||||
@@ -4,6 +4,7 @@ import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
@@ -32,6 +33,35 @@ public class HeightLatticeTest {
|
||||
private static final int AREA = 3 * WINDOW;
|
||||
private static final int DISTINCT_ACROSS_NINE = 25 * WINDOW * WINDOW;
|
||||
|
||||
@Test
|
||||
public void neighbouringCellsSpreadAcrossConcurrentMapBuckets() {
|
||||
int[] buckets = new int[65_536];
|
||||
int occupied = 0;
|
||||
int largest = 0;
|
||||
Set<Long> identities = new HashSet<>();
|
||||
for (int z = -96; z < 96; z++) {
|
||||
for (int x = -96; x < 96; x++) {
|
||||
long key = HeightLattice.key(x, z);
|
||||
assertTrue(identities.add(key));
|
||||
int hash = Long.hashCode(key);
|
||||
int bucket = (hash ^ (hash >>> 16)) & (buckets.length - 1);
|
||||
if (buckets[bucket]++ == 0) {
|
||||
occupied++;
|
||||
}
|
||||
largest = Math.max(largest, buckets[bucket]);
|
||||
}
|
||||
}
|
||||
assertTrue("occupied buckets " + occupied, occupied > 25_000);
|
||||
assertTrue("largest collision chain " + largest, largest < 12);
|
||||
identities.clear();
|
||||
int[] extremes = {Integer.MIN_VALUE, Integer.MIN_VALUE + 1, -1, 0, 1, Integer.MAX_VALUE - 1, Integer.MAX_VALUE};
|
||||
for (int x : extremes) {
|
||||
for (int z : extremes) {
|
||||
assertTrue("distinct coordinate " + x + "," + z, identities.add(HeightLattice.key(x, z)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void samplesEachCellOnceAcrossNineOverlappingWindows() {
|
||||
CountingSource source = new CountingSource();
|
||||
|
||||
+42
-1
@@ -317,6 +317,41 @@ public class HydrologyPlanCacheTest {
|
||||
assertEquals(26, planners.windowCalls.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void speculativePlanningLeavesWorkersForNewAndPromotedDemand() throws Exception {
|
||||
for (int threads : List.of(2, 4)) {
|
||||
FakePlanners planners = new FakePlanners(unpersisted());
|
||||
Gate gate = new Gate();
|
||||
planners.hook = key -> {
|
||||
if (key instanceof RegionKey region && Math.abs(region.rx()) < 10 && Math.abs(region.rz()) < 10) {
|
||||
gate.pass();
|
||||
}
|
||||
};
|
||||
ExecutorService workers = pool(threads);
|
||||
HydrologyPlanCache cache = cache(planners, workers, () -> false);
|
||||
CompletableFuture<Void> prepared = cache.prepareWindowsAround(0, 0, 31, 31, 2);
|
||||
try {
|
||||
await(() -> gate.arrivals() >= threads / 2, "speculative planning reaches its concurrency limit");
|
||||
WindowKey demanded = new WindowKey(400, 400);
|
||||
assertEquals(demanded, cache.demandWindow(demanded).get(5L, TimeUnit.SECONDS).plan().key());
|
||||
assertEquals("prefetch leaves workers available for demand", threads / 2, gate.arrivals());
|
||||
|
||||
WindowKey promoted = new WindowKey(800, 800);
|
||||
CompletableFuture<IndexedWindow> prefetched = cache.prefetchWindow(promoted);
|
||||
workers.submit(() -> null).get(5L, TimeUnit.SECONDS);
|
||||
assertFalse("the prefetch waits for speculative capacity", prefetched.isDone());
|
||||
IndexedWindow ready = cache.demandWindow(promoted).get(5L, TimeUnit.SECONDS);
|
||||
assertEquals(promoted, ready.plan().key());
|
||||
assertSame("demand promotes the queued prefetch", ready, prefetched.get(5L, TimeUnit.SECONDS));
|
||||
assertEquals(threads / 2, gate.arrivals());
|
||||
} finally {
|
||||
gate.release();
|
||||
}
|
||||
prepared.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
assertEquals("all prepared windows finish after demand", 27, planners.windowCalls.size());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plansWithRealPlannersAndReloadsFromStore() throws Exception {
|
||||
HydrologySettings settings = settings(new IrisHydrology());
|
||||
@@ -412,13 +447,14 @@ public class HydrologyPlanCacheTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void leastRecentlyUsedWindowIsEvicted() {
|
||||
public void leastRecentlyUsedWindowIsEvicted() throws Exception {
|
||||
FakePlanners planners = new FakePlanners(unpersisted());
|
||||
HydrologyPlanCache cache = cache(planners, pool(4), () -> false);
|
||||
WindowKey kept = new WindowKey(0, 0);
|
||||
WindowKey stale = new WindowKey(1, 0);
|
||||
IndexedWindow keptWindow = cache.window(kept);
|
||||
cache.window(stale);
|
||||
assertEquals(0L, cache.windowEvictions());
|
||||
List<WindowKey> requested = new ArrayList<>(List.of(kept, stale));
|
||||
for (int index = 0; index < 300; index++) {
|
||||
WindowKey key = new WindowKey(index, 7);
|
||||
@@ -435,8 +471,13 @@ public class HydrologyPlanCacheTest {
|
||||
}
|
||||
}
|
||||
assertTrue("the cache stays bounded: " + ready, ready < requested.size() / 2);
|
||||
await(() -> cache.windowEvictions() == requested.size() - 128, "all excess windows are evicted");
|
||||
long evictions = cache.windowEvictions();
|
||||
assertSame(keptWindow, cache.window(kept));
|
||||
assertEquals("a resident cache hit does not evict a window", evictions, cache.windowEvictions());
|
||||
cache.window(stale);
|
||||
assertEquals("an evicted window is planned again on demand", 2, planners.windowCalls(stale));
|
||||
await(() -> cache.windowEvictions() == evictions + 1, "replanning an evicted window advances the eviction count");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+14
-1
@@ -30,7 +30,20 @@ public class HydrologySettingsTest {
|
||||
private static final int SEA_LEVEL = 62;
|
||||
private static final int MIN_Y = -64;
|
||||
private static final int MAX_Y = 320;
|
||||
private static final long DEFAULT_FINGERPRINT = 3973071586549334460L;
|
||||
private static final long DEFAULT_FINGERPRINT = 8818472145657699942L;
|
||||
|
||||
@Test
|
||||
public void densityIsBoundedAndChangesThePlanIdentity() {
|
||||
HydrologySettings full = compile(new IrisHydrology());
|
||||
HydrologySettings half = compile(new IrisHydrology().setDensity(0.5D));
|
||||
assertEquals(1f, full.density(), 0f);
|
||||
assertEquals(0.5f, half.density(), 0f);
|
||||
assertTrue(full.fingerprint() != half.fingerprint());
|
||||
assertEquals(full.tiers(), half.tiers());
|
||||
for (double invalid : new double[]{-0.01D, 1.01D, Double.NaN, Double.POSITIVE_INFINITY}) {
|
||||
assertRejected(new IrisHydrology().setDensity(invalid), "density");
|
||||
}
|
||||
}
|
||||
|
||||
@Rule
|
||||
public final TemporaryFolder temporary = new TemporaryFolder();
|
||||
|
||||
@@ -34,6 +34,14 @@ public class RegionPlannerTest {
|
||||
private static final RegionKey ORIGIN = new RegionKey(0, 0);
|
||||
private static final RegionKey EAST = new RegionKey(1, 0);
|
||||
|
||||
@Test
|
||||
public void zeroDensityRemovesMajorChannelsAndLakes() {
|
||||
HydrologySettings settings = HydrologySettings.compile(new IrisHydrology().setDensity(0D), SEED, SEA_LEVEL,
|
||||
SyntheticTerrain.MIN_Y, SyntheticTerrain.MAX_Y);
|
||||
assertTrue(planner(settings, SyntheticTerrain.coneIsland(2048D, 2048D, 1700D, 240D, 6)).plan(ORIGIN).majorReaches().isEmpty());
|
||||
assertTrue(planner(settings, SyntheticTerrain.crater(2048D, 2048D, 150, 700D)).plan(ORIGIN).lakes().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void coneIslandMajorReachesEndInOcean() {
|
||||
HydrologySettings settings = settings();
|
||||
|
||||
@@ -41,6 +41,14 @@ public class WindowPlannerTest {
|
||||
private static final WindowKey LAND = new WindowKey(4, 4);
|
||||
private static final int SEAM_DISTANCE = 256;
|
||||
|
||||
@Test
|
||||
public void zeroDensityRemovesLocalChannelsAndPonds() {
|
||||
HydrologySettings settings = settings(new IrisHydrology().setDensity(0D));
|
||||
WindowPlan plan = planner(settings, SyntheticTerrain.trunkValley()).plan(LAND, noRegions(LAND));
|
||||
assertTrue(plan.reaches().isEmpty());
|
||||
assertTrue(plan.ponds().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void burnedTrunkCapturesLocalDrainage() {
|
||||
HydrologySettings settings = settings(new IrisHydrology());
|
||||
|
||||
+39
@@ -28,12 +28,51 @@ import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class CavePoolFillerTest {
|
||||
private static final long SEED = 1337L;
|
||||
private static final int FLOOR = 50;
|
||||
|
||||
@Test
|
||||
public void globalDensityThinsCandidateColumnsBeforeSamplingCaves() {
|
||||
IrisHydrology config = new IrisHydrology().setDensity(0.5D);
|
||||
config.getUnderground().getCavePools().setDensity(8D);
|
||||
CavePoolFiller filler = new CavePoolFiller(HydrologySettings.compile(config, SEED, 63, 0, 384));
|
||||
CarvedTerrain carved = mock(CarvedTerrain.class);
|
||||
MantleWriter writer = mock(MantleWriter.class);
|
||||
Set<Long> columns = new HashSet<>();
|
||||
when(carved.surface(anyInt(), anyInt())).thenAnswer(invocation -> {
|
||||
int x = invocation.getArgument(0);
|
||||
int z = invocation.getArgument(1);
|
||||
columns.add(key(x, 0, z));
|
||||
return 0;
|
||||
});
|
||||
for (int chunkX = 0; chunkX < 1024; chunkX++) {
|
||||
filler.fill(chunkX, 0, carved, writer, SEED);
|
||||
}
|
||||
assertTrue("half density samples approximately half the candidate columns", columns.size() > 3600 && columns.size() < 4500);
|
||||
Set<Long> repeated = new HashSet<>(columns);
|
||||
columns.clear();
|
||||
for (int chunkX = 1023; chunkX >= 0; chunkX--) {
|
||||
filler.fill(chunkX, 0, carved, writer, SEED);
|
||||
}
|
||||
assertEquals(repeated, columns);
|
||||
verifyNoInteractions(writer);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zeroGlobalDensityDoesNotReadCaves() {
|
||||
IrisHydrology config = new IrisHydrology().setDensity(0D);
|
||||
config.getUnderground().getCavePools().setDensity(8D);
|
||||
CavePoolFiller filler = new CavePoolFiller(HydrologySettings.compile(config, SEED, 63, 0, 384));
|
||||
CarvedTerrain carved = mock(CarvedTerrain.class);
|
||||
MantleWriter writer = mock(MantleWriter.class);
|
||||
filler.fill(0, 0, carved, writer, SEED);
|
||||
verifyNoInteractions(carved, writer);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void poolLevelIsHighestContainedLevelMinusFreeboard() {
|
||||
VoxelWorld world = roomWithSill();
|
||||
|
||||
+25
@@ -21,6 +21,31 @@ import static org.junit.Assert.assertTrue;
|
||||
public class SeaInletPlannerTest {
|
||||
private static final int SEA = InletTerrain.SEA;
|
||||
|
||||
@Test
|
||||
public void globalDensityThinsInletSitesWithoutChangingTheirGeometry() {
|
||||
InletTerrain terrain = InletTerrain.cliff(30D);
|
||||
IrisHydrology full = new IrisHydrology();
|
||||
IrisHydrology half = new IrisHydrology().setDensity(0.5D);
|
||||
full.getUnderground().getSeaInlets().setSpacing(256);
|
||||
half.getUnderground().getSeaInlets().setSpacing(256);
|
||||
int baseline = 0;
|
||||
int retained = 0;
|
||||
for (int offset = 0; offset < 32; offset++) {
|
||||
InletTerrain shifted = terrain.shifted(0, offset * 512);
|
||||
List<SeaInlet> all = shifted.plan(InletTerrain.settings(full));
|
||||
List<SeaInlet> reduced = shifted.plan(InletTerrain.settings(half));
|
||||
baseline += all.size();
|
||||
retained += reduced.size();
|
||||
for (SeaInlet inlet : reduced) {
|
||||
assertTrue("retained inlets keep the same full passage", all.contains(inlet));
|
||||
}
|
||||
assertEquals(reduced, shifted.plan(InletTerrain.settings(half)));
|
||||
}
|
||||
assertTrue("cliff provides enough candidates", baseline >= 32);
|
||||
assertTrue("half density retains approximately half the sites", retained > baseline * 0.3 && retained < baseline * 0.7);
|
||||
assertTrue(terrain.plan(InletTerrain.settings(new IrisHydrology().setDensity(0D))).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inletStartsAtOceanCell() {
|
||||
InletTerrain terrain = InletTerrain.cliff(30D);
|
||||
|
||||
+121
-2
@@ -10,15 +10,24 @@ import art.arcane.iris.generation.hydrology.grid.HydrologySampleBuffer;
|
||||
import art.arcane.iris.generation.hydrology.grid.HydrologyTerrainSource;
|
||||
import art.arcane.iris.generation.hydrology.grid.PriorityFlood;
|
||||
import art.arcane.iris.generation.hydrology.plan.HydrologySettings;
|
||||
import art.arcane.iris.generation.hydrology.plan.IndexedWindow;
|
||||
import art.arcane.iris.generation.hydrology.plan.RegionKey;
|
||||
import art.arcane.iris.generation.hydrology.plan.RegionPlan;
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowKey;
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowPlan;
|
||||
import art.arcane.iris.generation.hydrology.raster.ChunkRasterizer;
|
||||
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
|
||||
import art.arcane.iris.generation.hydrology.raster.ReachIndex;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.BitSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.DoubleBinaryOperator;
|
||||
@@ -38,6 +47,77 @@ public class PondPlannerTest {
|
||||
private static final float BOWL_SPILL = 112f;
|
||||
private static final WindowKey ORIGIN = new WindowKey(0, 0);
|
||||
|
||||
@Test
|
||||
public void smallSeededPondFollowsItsBowlAtBlockResolution() {
|
||||
HydrologySettings settings = settings(config -> {
|
||||
seededOnly(config);
|
||||
config.getPonds().getSeeded().setDensity(1D).setRadius(new KList<>(8, 8)).setDepth(new KList<>(6, 6))
|
||||
.setAlongChannels(false);
|
||||
config.getPonds().setFreeboard(new KList<>(1, 1));
|
||||
config.getPonds().getShore().setRoundness(0D);
|
||||
}, MIN_Y, MAX_Y);
|
||||
List<Pond> ponds = plan(settings, new Terrain((x, z) -> 100D, 1f, ALLOW_ALL, null, MIN_Y, MAX_Y),
|
||||
0, 0, 64, 64, new BitSet(), List.of());
|
||||
Pond pond = single(ponds);
|
||||
PondMask mask = pond.mask();
|
||||
assertEquals(1, mask.step());
|
||||
int wet = 0;
|
||||
int minX = Integer.MAX_VALUE;
|
||||
int minZ = Integer.MAX_VALUE;
|
||||
int maxX = Integer.MIN_VALUE;
|
||||
int maxZ = Integer.MIN_VALUE;
|
||||
for (int j = 0; j < mask.depth(); j++) {
|
||||
for (int i = 0; i < mask.width(); i++) {
|
||||
if (mask.node(i, j)) {
|
||||
wet++;
|
||||
minX = Math.min(minX, i);
|
||||
minZ = Math.min(minZ, j);
|
||||
maxX = Math.max(maxX, i);
|
||||
maxZ = Math.max(maxZ, j);
|
||||
}
|
||||
}
|
||||
}
|
||||
int rectangle = (maxX - minX + 1) * (maxZ - minZ + 1);
|
||||
assertTrue("rounded bowl occupies " + wet + " of " + rectangle + " bounding blocks", wet < rectangle * 0.9D);
|
||||
assertFalse(mask.node(minX, minZ));
|
||||
assertFalse(mask.node(maxX, maxZ));
|
||||
assertTrue(mask.contains(pond.floorX(), pond.floorZ()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void globalDensityHalvesSaturatedSeededSiteAcceptance() {
|
||||
HydrologySettings full = settings(config -> {
|
||||
seededOnly(config);
|
||||
config.getPonds().getSeeded().setDensity(16D).setAlongChannels(false);
|
||||
}, MIN_Y, MAX_Y);
|
||||
HydrologySettings half = settings(config -> {
|
||||
seededOnly(config);
|
||||
config.setDensity(0.5D);
|
||||
config.getPonds().getSeeded().setDensity(16D).setAlongChannels(false);
|
||||
}, MIN_Y, MAX_Y);
|
||||
Terrain terrain = new Terrain((x, z) -> 100D, 1f, ALLOW_ALL, null, MIN_Y, MAX_Y);
|
||||
Set<Long> all = seededSites(full, terrain);
|
||||
Set<Long> reduced = seededSites(half, terrain);
|
||||
assertTrue("full candidates " + all.size(), all.size() >= 450);
|
||||
assertTrue(all.containsAll(reduced));
|
||||
double fraction = reduced.size() / (double) all.size();
|
||||
assertTrue("retained fraction " + fraction, fraction > 0.44D && fraction < 0.56D);
|
||||
}
|
||||
|
||||
private static Set<Long> seededSites(HydrologySettings settings, Terrain terrain) {
|
||||
Set<Long> sites = new HashSet<>();
|
||||
for (int window = 0; window < 32; window++) {
|
||||
int x = window * WindowKey.SIZE;
|
||||
Sampled sampled = sample(terrain, 8, x, 0, 64, 64);
|
||||
Arrays.fill(sampled.samples().pondDensity(), 100f);
|
||||
CupSet cups = new PondPlanner(settings).subtractCups(sampled.grid(), new WindowKey(window, 0), sampled.samples(), new BitSet());
|
||||
for (CupSet.Cup cup : cups.within(x, 0, x + WindowKey.SIZE, WindowKey.SIZE)) {
|
||||
sites.add(((long) cup.x() << 32) | (cup.z() & 0xFFFFFFFFL));
|
||||
}
|
||||
}
|
||||
return sites;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void naturalBasinPondSitsBelowSpill() {
|
||||
HydrologySettings settings = settings(PondPlannerTest::naturalOnly, MIN_Y, MAX_Y);
|
||||
@@ -187,6 +267,7 @@ public class PondPlannerTest {
|
||||
assertTrue(pond.mask().contains(200, 256));
|
||||
assertTrue(pond.mask().contains(312, 256));
|
||||
assertContained(pond, terrain);
|
||||
assertRasterRimContained(settings, pond, terrain);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -527,6 +608,43 @@ public class PondPlannerTest {
|
||||
return new PondMask(x, 208, 16, 1, 7, new long[]{0b1111111L});
|
||||
}
|
||||
|
||||
private static void assertRasterRimContained(HydrologySettings settings, Pond pond, DoubleBinaryOperator natural) {
|
||||
Map<Long, ChunkWaterRaster> rasters = new HashMap<>();
|
||||
ChunkRasterizer rasterizer = new ChunkRasterizer(settings);
|
||||
int raised = 0;
|
||||
int[][] sides = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
for (int[] wet : wetNodes(pond.mask())) {
|
||||
for (int[] side : sides) {
|
||||
int x = wet[0] + side[0];
|
||||
int z = wet[1] + side[1];
|
||||
if (pond.mask().contains(x, z)) {
|
||||
continue;
|
||||
}
|
||||
long key = ((long) (x >> 4) << 32) | ((z >> 4) & 0xFFFFFFFFL);
|
||||
ChunkWaterRaster raster = rasters.get(key);
|
||||
if (raster == null) {
|
||||
WindowPlan window = new WindowPlan(WindowKey.containing(x, z), List.of(), List.of(pond), List.of());
|
||||
IndexedWindow indexed = new IndexedWindow(window, ReachIndex.build(List.of(), List.of(pond)));
|
||||
List<RegionPlan> regions = new ArrayList<>();
|
||||
for (RegionKey region : ChunkRasterizer.lakeRegions(x >> 4, z >> 4)) {
|
||||
regions.add(new RegionPlan(region, List.of(), List.of()));
|
||||
}
|
||||
raster = rasterizer.rasterize(x >> 4, z >> 4, (px, pz) -> (int) Math.round(natural.applyAsDouble(px, pz)),
|
||||
List.of(indexed), regions);
|
||||
rasters.put(key, raster);
|
||||
}
|
||||
int ground = (int) Math.round(natural.applyAsDouble(x, z));
|
||||
int actual = raster.terrainHeight(x & 15, z & 15);
|
||||
assertTrue("dry rim contains the water at " + x + "," + z, actual >= pond.level());
|
||||
assertTrue("rim fill remains bounded at " + x + "," + z, actual <= ground + settings.deformation().maxFill());
|
||||
if (actual > ground) {
|
||||
raised++;
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue("sub-lattice rim needs real raster berms", raised > 0);
|
||||
}
|
||||
|
||||
private static Pond lake(PondMask mask) {
|
||||
return new Pond(42L, PondKind.LAKE, 110, 100, mask.originX(), mask.originZ(), 111f, mask, 0L, 0, "water", mask.originX(), mask.originZ());
|
||||
}
|
||||
@@ -548,6 +666,7 @@ public class PondPlannerTest {
|
||||
private static void assertContained(Pond pond, DoubleBinaryOperator natural) {
|
||||
PondMask mask = pond.mask();
|
||||
int step = mask.step();
|
||||
int maxFill = new IrisHydrology().getDeformation().getMaxFill();
|
||||
List<int[]> nodes = wetNodes(mask);
|
||||
assertFalse(nodes.isEmpty());
|
||||
assertTrue("floor " + pond.floor() + " level " + pond.level(), pond.floor() < pond.level());
|
||||
@@ -557,8 +676,8 @@ public class PondPlannerTest {
|
||||
int x = node[0] + direction[0];
|
||||
int z = node[1] + direction[1];
|
||||
if (!mask.contains(x, z)) {
|
||||
float edge = (float) natural.applyAsDouble(x, z);
|
||||
assertTrue("edge " + x + "," + z + " natural " + edge + " level " + pond.level(), edge >= pond.level());
|
||||
long edge = Math.round(natural.applyAsDouble(x, z));
|
||||
assertTrue("edge " + x + "," + z + " natural " + edge + " level " + pond.level(), edge + maxFill >= pond.level());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -23,6 +23,10 @@ import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.studio.map.components.MapLayer;
|
||||
import art.arcane.iris.studio.map.components.MapTile;
|
||||
import art.arcane.iris.studio.map.components.MapTileRenderer;
|
||||
import art.arcane.iris.studio.map.components.TileType;
|
||||
import art.arcane.iris.structure.object.IObjectPlacer;
|
||||
import art.arcane.iris.testsupport.FakeHydrology;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
@@ -44,12 +48,14 @@ import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
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.assertNotNull;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
@@ -208,6 +214,35 @@ public class IrisComplexHeightStreamTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 15_000L)
|
||||
public void mapHeightPreviewReturnsWhilePlanningAndRefreshesToFinalTerrain() throws Exception {
|
||||
try (HydrologyFixture fixture = new HydrologyFixture(hydrologyPack())) {
|
||||
fixture.hydrology.hold();
|
||||
MapTileRenderer renderer = new MapTileRenderer(fixture.engine);
|
||||
MapLayer layer = MapLayer.of(TileType.HEIGHT);
|
||||
long before = ProvisionalSampling.revision();
|
||||
|
||||
MapTile preview = renderer.renderTile(0D, 0D, 1D, 16, layer, true, () -> false);
|
||||
|
||||
assertEquals(HydrologyFixture.NATURAL, preview.heightAt(8, 8), 0D);
|
||||
assertNotEquals(before, ProvisionalSampling.revision());
|
||||
fixture.hydrology.release();
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10L);
|
||||
MapTile settled;
|
||||
do {
|
||||
assertTrue("Map height preview never settled", System.nanoTime() < deadline);
|
||||
Thread.sleep(10L);
|
||||
before = ProvisionalSampling.revision();
|
||||
settled = renderer.renderTile(0D, 0D, 1D, 16, layer, true, () -> false);
|
||||
} while (before != ProvisionalSampling.revision());
|
||||
|
||||
double terrain = fixture.complex.getHeightStream().getDouble(8D, 8D);
|
||||
assertNotEquals(HydrologyFixture.NATURAL, terrain, 0D);
|
||||
assertEquals(terrain, settled.heightAt(8, 8), 0D);
|
||||
assertEquals(before, ProvisionalSampling.revision());
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 120_000L)
|
||||
public void waterStreamsFollowTheRaster() throws Exception {
|
||||
try (HydrologyFixture fixture = new HydrologyFixture(hydrologyPack())) {
|
||||
|
||||
@@ -83,7 +83,7 @@ public class SchemaBuilderIdentityTest {
|
||||
@ClassRule
|
||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
|
||||
private static final String EXPECTED_SCHEMA_DIGEST = "dd1ea7f0361dfad24c4800e75f1e26e951b4f775aa1b541328724a8635afeeca";
|
||||
private static final String EXPECTED_SCHEMA_DIGEST = "6935e8d3914e5cde852522151d87fe898f79de3b394826e544c5c5bfbfceba57";
|
||||
|
||||
private static final List<Class<?>> SCHEMA_ROOTS = List.of(
|
||||
IrisDimension.class,
|
||||
|
||||
@@ -13,6 +13,8 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -102,6 +104,42 @@ public class VisionRenderControllerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zoomRetainsCompleteWaterTilesWhileDetailedTilesAreProvisional() throws Exception {
|
||||
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
||||
MapTile overview = mock(MapTile.class);
|
||||
MapTile incomplete = mock(MapTile.class);
|
||||
when(renderer.renderTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean(), any()))
|
||||
.thenAnswer(invocation -> {
|
||||
double scale = invocation.getArgument(2);
|
||||
if (scale < 4D) {
|
||||
ProvisionalSampling.mark();
|
||||
return incomplete;
|
||||
}
|
||||
return overview;
|
||||
});
|
||||
try (VisionRenderController controller = new VisionRenderController(() -> { })) {
|
||||
VisionRenderController.Frame coarse = controller.request(new VisionRenderController.RenderSpec(renderer,
|
||||
MapLayer.of(TileType.RIVER), 1L, 128D, 128D, 4D, 64, 64, false));
|
||||
await(() -> controller.progress(coarse).ready() == 1);
|
||||
|
||||
VisionRenderController.Frame fine = controller.request(spec(renderer, 1L));
|
||||
await(() -> fine.tiles().getFirst().tile() != null);
|
||||
VisionRenderController.VisibleTile visible = fine.tiles().getFirst();
|
||||
assertTrue(visible.provisional());
|
||||
assertEquals(0, controller.progress(fine).ready());
|
||||
VisionRenderController.Fallback retained = controller.fallback(fine, visible);
|
||||
assertNotNull(retained);
|
||||
assertSame(overview, retained.pieces().getFirst().tile());
|
||||
assertEquals(16, retained.pieces().getFirst().sourceSize());
|
||||
|
||||
VisionRenderController.Frame returned = controller.request(new VisionRenderController.RenderSpec(renderer,
|
||||
MapLayer.of(TileType.RIVER), 1L, 128D, 128D, 4D, 64, 64, false));
|
||||
assertSame(overview, returned.tiles().getFirst().tile());
|
||||
assertFalse(returned.tiles().getFirst().provisional());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closeRejectsLatePublication() throws Exception {
|
||||
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
||||
|
||||
@@ -2,8 +2,10 @@ package art.arcane.iris.studio.render;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
|
||||
import art.arcane.iris.generation.hydrology.engine.HydrologyPlanPreview;
|
||||
import art.arcane.iris.generation.hydrology.plan.HydrologySettings;
|
||||
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
|
||||
import art.arcane.iris.generation.hydrology.water.Pond;
|
||||
import art.arcane.iris.generation.hydrology.water.Reach;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
@@ -93,6 +95,34 @@ public class IrisRendererRiverTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zoomKeepsResidentWaterPlansUntilDetailedRastersAreReady() {
|
||||
HydrologyRuntime runtime = mock(HydrologyRuntime.class);
|
||||
Pond pond = WaterScene.pond(32L, 62, 16, 16, 10);
|
||||
when(runtime.enabled()).thenReturn(true);
|
||||
when(runtime.planPreview(anyInt(), anyInt(), anyInt(), anyInt()))
|
||||
.thenReturn(new HydrologyPlanPreview(List.of(), List.of(pond), List.of(), List.of(), true));
|
||||
when(runtime.rasterPreview(anyInt(), anyInt())).thenReturn(ChunkWaterRaster.PROVISIONAL);
|
||||
IrisRenderer renderer = new IrisRenderer(engine(runtime));
|
||||
|
||||
BufferedImage overview = renderer.renderStudio(0D, 0D, 256D, 64, RenderType.RIVER, () -> false);
|
||||
int water = IrisRenderer.hydrologyFeatureColor(HydrologyFeatureType.POND);
|
||||
assertEquals(water, overview.getRGB(4, 4));
|
||||
|
||||
for (int step : new int[]{2, 1}) {
|
||||
long before = ProvisionalSampling.revision();
|
||||
BufferedImage detailed = renderer.renderStudio(0D, 0D, step * 64D, 64, RenderType.RIVER, () -> false);
|
||||
assertEquals(water, detailed.getRGB(16 / step, 16 / step));
|
||||
assertNotEquals(before, ProvisionalSampling.revision());
|
||||
}
|
||||
|
||||
when(runtime.rasterPreview(anyInt(), anyInt())).thenReturn(ChunkWaterRaster.EMPTY);
|
||||
long before = ProvisionalSampling.revision();
|
||||
BufferedImage settled = renderer.renderStudio(0D, 0D, 64D, 64, RenderType.RIVER, () -> false);
|
||||
assertEquals(NO_RIVER, settled.getRGB(16, 16));
|
||||
assertEquals(before, ProvisionalSampling.revision());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void channelPixelsShowTheirTier() throws Exception {
|
||||
HydrologySettings settings = FakeHydrology.settings(SEA_LEVEL);
|
||||
|
||||
@@ -6,8 +6,10 @@ import org.junit.Test;
|
||||
|
||||
import javax.swing.SwingUtilities;
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.MouseWheelEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
@@ -125,6 +127,82 @@ public class PregenMapPanelTest {
|
||||
SwingUtilities.invokeAndWait(extreme::disposeMap);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void wheelZoomPreservesTheChunkUnderThePointerAndRetainedPixels() throws Exception {
|
||||
PregenMapPanel panel = panel(new PregenRenderSnapshot.Bounds(-32, -32, 31, 31));
|
||||
SwingUtilities.invokeAndWait(() -> panel.setSize(802, 602));
|
||||
panel.submit(-7, -19, Color.GREEN);
|
||||
flush(panel);
|
||||
BufferedImage retained = image(panel);
|
||||
String before = tooltip(panel, 337, 123);
|
||||
SwingUtilities.invokeAndWait(() -> panel.dispatchEvent(new MouseWheelEvent(panel,
|
||||
MouseEvent.MOUSE_WHEEL, 1L, 0, 337, 123, 0, false,
|
||||
MouseWheelEvent.WHEEL_UNIT_SCROLL, 3, -4)));
|
||||
assertEquals(before, tooltip(panel, 337, 123));
|
||||
assertTrue(panel.zoom() > 1D);
|
||||
assertTrue(retained == image(panel));
|
||||
assertEquals(Color.GREEN.getRGB(), image(panel).getRGB(25, 13));
|
||||
SwingUtilities.invokeAndWait(panel::disposeMap);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void draggingPansTheMapAndResetFitsBesideTheSidebar() throws Exception {
|
||||
PregenMapPanel panel = panel(new PregenRenderSnapshot.Bounds(-32, -32, 31, 31));
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
panel.setSize(1000, 600);
|
||||
panel.setCoveredLeftWidth(300);
|
||||
});
|
||||
String initial = tooltip(panel, 650, 300);
|
||||
assertNull(tooltip(panel, 250, 300));
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
panel.dispatchEvent(new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, 1L,
|
||||
MouseEvent.BUTTON1_DOWN_MASK, 650, 300, 1, false, MouseEvent.BUTTON1));
|
||||
panel.dispatchEvent(new MouseEvent(panel, MouseEvent.MOUSE_DRAGGED, 2L,
|
||||
MouseEvent.BUTTON1_DOWN_MASK, 710, 350, 0, false, MouseEvent.NOBUTTON));
|
||||
panel.dispatchEvent(new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, 3L,
|
||||
0, 710, 350, 1, false, MouseEvent.BUTTON1));
|
||||
});
|
||||
assertEquals(initial, tooltip(panel, 710, 350));
|
||||
assertFalse(initial.equals(tooltip(panel, 650, 300)));
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
panel.zoomIn();
|
||||
panel.resetView();
|
||||
});
|
||||
assertEquals(1D, panel.zoom(), 0D);
|
||||
assertEquals(initial, tooltip(panel, 650, 300));
|
||||
SwingUtilities.invokeAndWait(panel::disposeMap);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zoomBoundsAndGridPaintingHandleExtremeMapCoordinates() throws Exception {
|
||||
PregenMapPanel panel = panel(new PregenRenderSnapshot.Bounds(
|
||||
Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE));
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
panel.setSize(820, 520);
|
||||
panel.toggleGrid();
|
||||
for (int index = 0; index < 100; index++) {
|
||||
panel.zoomIn();
|
||||
}
|
||||
assertEquals(128D, panel.zoom(), 0D);
|
||||
BufferedImage target = new BufferedImage(820, 520, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D canvas = target.createGraphics();
|
||||
try {
|
||||
panel.paint(canvas);
|
||||
panel.panBy(1_000_000, -1_000_000);
|
||||
panel.paint(canvas);
|
||||
for (int index = 0; index < 100; index++) {
|
||||
panel.zoomOut();
|
||||
}
|
||||
assertEquals(0.25D, panel.zoom(), 0D);
|
||||
panel.resetView();
|
||||
panel.paint(canvas);
|
||||
} finally {
|
||||
canvas.dispose();
|
||||
panel.disposeMap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static String tooltip(PregenMapPanel panel, int x, int y) throws Exception {
|
||||
String[] result = new String[1];
|
||||
SwingUtilities.invokeAndWait(() -> result[0] = panel.getToolTipText(
|
||||
|
||||
@@ -11,8 +11,12 @@ import javax.swing.JButton;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JTextArea;
|
||||
import javax.swing.JToggleButton;
|
||||
import javax.swing.JViewport;
|
||||
import javax.swing.KeyStroke;
|
||||
import javax.swing.SwingUtilities;
|
||||
import java.awt.Component;
|
||||
import java.awt.Container;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.lang.reflect.Constructor;
|
||||
@@ -27,6 +31,50 @@ import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
|
||||
public class PregenRendererTest {
|
||||
@Test
|
||||
public void smallWindowOpensAtTheProgressHeaderWithoutScrollingAway() throws Exception {
|
||||
PregenRenderer[] result = new PregenRenderer[1];
|
||||
onEdt(() -> {
|
||||
result[0] = renderer(new Source(), () -> { });
|
||||
result[0].setSize(820, 520);
|
||||
layout(result[0]);
|
||||
});
|
||||
try {
|
||||
onEdt(() -> layout(result[0]));
|
||||
onEdt(() -> assertScrollAtTop(result[0]));
|
||||
} finally {
|
||||
onEdt(result[0]::close);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mapShortcutsZoomPanResetAndKeepGridControlSynchronized() throws Exception {
|
||||
onEdt(() -> {
|
||||
PregenRenderer renderer = renderer(new Source(), () -> { });
|
||||
try {
|
||||
PregenMapPanel map = field(renderer, "map", PregenMapPanel.class);
|
||||
map.setSize(1000, 600);
|
||||
map.setCoveredLeftWidth(300);
|
||||
shortcut(renderer, KeyEvent.VK_EQUALS);
|
||||
assertTrue(map.zoom() > 1D);
|
||||
shortcut(renderer, KeyEvent.VK_LEFT);
|
||||
shortcut(renderer, KeyEvent.VK_R);
|
||||
assertEquals(1D, map.zoom(), 0D);
|
||||
shortcut(renderer, KeyEvent.VK_G);
|
||||
assertTrue(map.isGridVisible());
|
||||
JToggleButton grid = field(renderer, "gridToggle", JToggleButton.class);
|
||||
assertTrue(grid.isSelected());
|
||||
grid.doClick(0);
|
||||
assertFalse(map.isGridVisible());
|
||||
assertFalse(grid.isSelected());
|
||||
shortcut(renderer, KeyEvent.VK_SLASH);
|
||||
shortcut(renderer, KeyEvent.VK_ESCAPE);
|
||||
} finally {
|
||||
renderer.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void keyboardAndButtonActivationToggleExactlyOnceAndTerminalControlsAreDisabled() throws Exception {
|
||||
onEdt(() -> {
|
||||
@@ -155,6 +203,32 @@ public class PregenRendererTest {
|
||||
refresh.invoke(renderer);
|
||||
}
|
||||
|
||||
private static void shortcut(PregenRenderer renderer, int keyCode) {
|
||||
Object action = renderer.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
|
||||
.get(KeyStroke.getKeyStroke(keyCode, 0));
|
||||
renderer.getActionMap().get(action).actionPerformed(new ActionEvent(renderer, 0, "shortcut"));
|
||||
}
|
||||
|
||||
private static void layout(Container container) {
|
||||
container.doLayout();
|
||||
for (Component component : container.getComponents()) {
|
||||
if (component instanceof Container child) {
|
||||
layout(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertScrollAtTop(Container container) {
|
||||
for (Component component : container.getComponents()) {
|
||||
if (component instanceof JViewport viewport) {
|
||||
assertEquals(0, viewport.getViewPosition().y);
|
||||
}
|
||||
if (component instanceof Container child) {
|
||||
assertScrollAtTop(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> T field(PregenRenderer renderer, String name, Class<T> type) throws Exception {
|
||||
Field field = PregenRenderer.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
|
||||
@@ -68,6 +68,11 @@ public class IrisRiverSchemaTest {
|
||||
JSONObject fluid = referencedProperties(definitions, properties.getJSONObject("fluids").getJSONObject("items"));
|
||||
|
||||
assertEquals("boolean", properties.getJSONObject("enabled").getString("type"));
|
||||
JSONObject density = properties.getJSONObject("density");
|
||||
assertEquals("number", density.getString("type"));
|
||||
assertEquals(0D, density.getDouble("minimum"), 0D);
|
||||
assertEquals(1D, density.getDouble("maximum"), 0D);
|
||||
assertTrue(density.getString("description").contains("Default Value is 1.0"));
|
||||
assertEquals("boolean", properties.getJSONObject("persist").getString("type"));
|
||||
assertEquals(0, properties.getJSONObject("prefetchRings").getInt("minimum"));
|
||||
assertEquals(4, properties.getJSONObject("prefetchRings").getInt("maximum"));
|
||||
|
||||
+43
-3
@@ -34,6 +34,8 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -895,7 +897,7 @@ public class GenerationRegistryContractFactoryTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsAChangedPackBeforeCapture() throws Exception {
|
||||
public void rejectsAPackChangedBeforeCapture() throws Exception {
|
||||
File pack = temporary.newFolder("changed-pack");
|
||||
String original = GenerationPackFingerprint.compute(
|
||||
pack.toPath(),
|
||||
@@ -904,20 +906,58 @@ public class GenerationRegistryContractFactoryTest {
|
||||
Files.writeString(pack.toPath().resolve("dimension.json"), "{}", StandardCharsets.UTF_8);
|
||||
IrisData data = mock(IrisData.class);
|
||||
when(data.getDataFolder()).thenReturn(pack);
|
||||
IrisDimension dimension = spy(new IrisDimension());
|
||||
dimension.setLoadKey("overworld");
|
||||
stubDimensions(data, dimension);
|
||||
doReturn(new KList<>()).when(dimension).getReachableBiomes(any());
|
||||
doReturn(new KList<>()).when(dimension).getAllRegions(any());
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
when(registries.blockTypeKeys()).thenReturn(new KList<>());
|
||||
when(registries.entityKeys()).thenReturn(new KList<>());
|
||||
|
||||
assertThrows(
|
||||
IOException.class,
|
||||
() -> GenerationRegistryContractFactory.create(
|
||||
data,
|
||||
new IrisDimension(),
|
||||
dimension,
|
||||
original,
|
||||
DataVersion.getLatest().get(),
|
||||
mock(PlatformRegistries.class),
|
||||
registries,
|
||||
new FakeGenerationRegistry("runtime")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsSameSizePackMutationDuringCaptureWithPreservedTimestamp() throws Exception {
|
||||
File pack = temporary.newFolder("mutating-pack");
|
||||
Path content = pack.toPath().resolve("content.json");
|
||||
Files.writeString(content, "{\"version\":1}", StandardCharsets.UTF_8);
|
||||
FileTime modified = Files.getLastModifiedTime(content);
|
||||
String original = GenerationPackFingerprint.compute(pack.toPath(), GenerationPackFingerprint.CURRENT_VERSION);
|
||||
IrisData data = mock(IrisData.class);
|
||||
when(data.getDataFolder()).thenReturn(pack);
|
||||
IrisDimension dimension = spy(new IrisDimension());
|
||||
dimension.setLoadKey("overworld");
|
||||
stubDimensions(data, dimension);
|
||||
doReturn(new KList<>()).when(dimension).getReachableBiomes(any());
|
||||
doReturn(new KList<>()).when(dimension).getAllRegions(any());
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
when(registries.blockTypeKeys()).thenAnswer(ignored -> {
|
||||
Files.writeString(content, "{\"version\":2}", StandardCharsets.UTF_8);
|
||||
Files.setLastModifiedTime(content, modified);
|
||||
return new KList<>();
|
||||
});
|
||||
when(registries.entityKeys()).thenReturn(new KList<>());
|
||||
|
||||
IOException failure = assertThrows(IOException.class, () -> GenerationRegistryContractFactory.create(
|
||||
data, dimension, original, DataVersion.getLatest().get(), registries, new FakeGenerationRegistry("runtime")));
|
||||
|
||||
assertEquals("Iris generation registry capture rejected a changed pack.", failure.getMessage());
|
||||
assertEquals(modified, Files.getLastModifiedTime(content));
|
||||
assertEquals("{\"version\":2}", Files.readString(content));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsAnUnloadableAdditionalDimensionBeforeCapturingItsRegistry() throws Exception {
|
||||
File pack = temporary.newFolder("missing-secondary-dimension");
|
||||
|
||||
Reference in new issue
Block a user