mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 05:50:39 +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
@@ -17,6 +17,11 @@ public class IrisHydrology {
|
|||||||
@Description("Turns rivers, streams, ponds, lakes and underground water on for this dimension.")
|
@Description("Turns rivers, streams, ponds, lakes and underground water on for this dimension.")
|
||||||
private boolean enabled = false;
|
private boolean enabled = false;
|
||||||
|
|
||||||
|
@MinNumber(0)
|
||||||
|
@MaxNumber(1)
|
||||||
|
@Description("Fraction of eligible water features to keep after local placement probabilities. 0.5 keeps half the candidate river sources, ponds, lakes and underground water features without changing their configured sizes.")
|
||||||
|
private double density = 1D;
|
||||||
|
|
||||||
@Description("How water drains across the terrain before rivers and ponds are placed.")
|
@Description("How water drains across the terrain before rivers and ponds are placed.")
|
||||||
private IrisHydrologyDrainage drainage = new IrisHydrologyDrainage();
|
private IrisHydrologyDrainage drainage = new IrisHydrologyDrainage();
|
||||||
|
|
||||||
|
|||||||
+67
-8
@@ -60,6 +60,7 @@ public final class HydrologyRuntime {
|
|||||||
private static final int CHUNKS_PER_WINDOW = WindowKey.SIZE >> 4;
|
private static final int CHUNKS_PER_WINDOW = WindowKey.SIZE >> 4;
|
||||||
public static final int MAX_PREGENERATION_REGIONS = 4096;
|
public static final int MAX_PREGENERATION_REGIONS = 4096;
|
||||||
private static final int MAX_PREVIEW_REQUESTS = 64;
|
private static final int MAX_PREVIEW_REQUESTS = 64;
|
||||||
|
private static final int PREFETCH_CAPACITY = 16;
|
||||||
private static final HydrologyRuntime DISABLED = new HydrologyRuntime();
|
private static final HydrologyRuntime DISABLED = new HydrologyRuntime();
|
||||||
|
|
||||||
private final HydrologySettings settings;
|
private final HydrologySettings settings;
|
||||||
@@ -74,6 +75,7 @@ public final class HydrologyRuntime {
|
|||||||
private final RasterFailures windowPreviewFailures;
|
private final RasterFailures windowPreviewFailures;
|
||||||
private final RasterFailures regionPreviewFailures;
|
private final RasterFailures regionPreviewFailures;
|
||||||
private final Set<Object> previews;
|
private final Set<Object> previews;
|
||||||
|
private final LinkedHashMap<WindowKey, WindowPrefetch> prefetches;
|
||||||
private final HydrologyBinding binding;
|
private final HydrologyBinding binding;
|
||||||
private final Function<PreparedHydrologyCacheIdentity, HydrologyPlanStore> stores;
|
private final Function<PreparedHydrologyCacheIdentity, HydrologyPlanStore> stores;
|
||||||
private final boolean caveFluidPolicies;
|
private final boolean caveFluidPolicies;
|
||||||
@@ -94,6 +96,7 @@ public final class HydrologyRuntime {
|
|||||||
this.windowPreviewFailures = null;
|
this.windowPreviewFailures = null;
|
||||||
this.regionPreviewFailures = null;
|
this.regionPreviewFailures = null;
|
||||||
this.previews = null;
|
this.previews = null;
|
||||||
|
this.prefetches = null;
|
||||||
this.binding = HydrologyBinding.UNBOUND;
|
this.binding = HydrologyBinding.UNBOUND;
|
||||||
this.stores = identity -> null;
|
this.stores = identity -> null;
|
||||||
this.caveFluidPolicies = false;
|
this.caveFluidPolicies = false;
|
||||||
@@ -119,6 +122,7 @@ public final class HydrologyRuntime {
|
|||||||
this.windowPreviewFailures = new RasterFailures(ticker);
|
this.windowPreviewFailures = new RasterFailures(ticker);
|
||||||
this.regionPreviewFailures = new RasterFailures(ticker);
|
this.regionPreviewFailures = new RasterFailures(ticker);
|
||||||
this.previews = new HashSet<>();
|
this.previews = new HashSet<>();
|
||||||
|
this.prefetches = new LinkedHashMap<>(PREFETCH_CAPACITY * 2, 0.75f, true);
|
||||||
this.binding = Objects.requireNonNull(binding, "binding");
|
this.binding = Objects.requireNonNull(binding, "binding");
|
||||||
this.stores = Objects.requireNonNull(stores, "stores");
|
this.stores = Objects.requireNonNull(stores, "stores");
|
||||||
this.caveFluidPolicies = caveFluidPolicies;
|
this.caveFluidPolicies = caveFluidPolicies;
|
||||||
@@ -135,6 +139,9 @@ public final class HydrologyRuntime {
|
|||||||
engine.getHeight());
|
engine.getHeight());
|
||||||
boolean caveFluidPolicies = settings.underground().cavePools().enabled()
|
boolean caveFluidPolicies = settings.underground().cavePools().enabled()
|
||||||
&& requireBiomeFluids(dimension.getReachableBiomes(engine), settings);
|
&& requireBiomeFluids(dimension.getReachableBiomes(engine), settings);
|
||||||
|
if (settings.density() == 0D) {
|
||||||
|
return DISABLED;
|
||||||
|
}
|
||||||
HydrologyBinding binding = binding(engine, complex);
|
HydrologyBinding binding = binding(engine, complex);
|
||||||
IrisHydrologyTerrainSource source = new IrisHydrologyTerrainSource(complex, dimension, settings, MultiBurst.hydrology,
|
IrisHydrologyTerrainSource source = new IrisHydrologyTerrainSource(complex, dimension, settings, MultiBurst.hydrology,
|
||||||
MultiBurst.hydrology.parallelism(), binding);
|
MultiBurst.hydrology.parallelism(), binding);
|
||||||
@@ -203,16 +210,24 @@ public final class HydrologyRuntime {
|
|||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PreviewScope preview() {
|
||||||
|
return new PreviewScope(cache == null ? null : last.get());
|
||||||
|
}
|
||||||
|
|
||||||
public ChunkWaterRaster raster(int chunkX, int chunkZ) {
|
public ChunkWaterRaster raster(int chunkX, int chunkZ) {
|
||||||
if (cache == null) {
|
if (cache == null) {
|
||||||
return ChunkWaterRaster.EMPTY;
|
return ChunkWaterRaster.EMPTY;
|
||||||
}
|
}
|
||||||
|
LastRaster slot = last.get();
|
||||||
|
if (slot.preview) {
|
||||||
|
return rasterPreview(chunkX, chunkZ);
|
||||||
|
}
|
||||||
if (mainThread.getAsBoolean()) {
|
if (mainThread.getAsBoolean()) {
|
||||||
return rasterSnapshot(chunkX, chunkZ);
|
return rasterSnapshot(chunkX, chunkZ);
|
||||||
}
|
}
|
||||||
requireOpen();
|
requireOpen();
|
||||||
long key = key(chunkX, chunkZ);
|
long key = key(chunkX, chunkZ);
|
||||||
ChunkWaterRaster known = remembered(key);
|
ChunkWaterRaster known = remembered(key, slot);
|
||||||
return known != null ? known : remember(key, planned(chunkX, chunkZ, key));
|
return known != null ? known : remember(key, planned(chunkX, chunkZ, key));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,7 +239,7 @@ public final class HydrologyRuntime {
|
|||||||
return ChunkWaterRaster.PROVISIONAL;
|
return ChunkWaterRaster.PROVISIONAL;
|
||||||
}
|
}
|
||||||
long key = key(chunkX, chunkZ);
|
long key = key(chunkX, chunkZ);
|
||||||
ChunkWaterRaster known = remembered(key);
|
ChunkWaterRaster known = remembered(key, last.get());
|
||||||
if (known == null) {
|
if (known == null) {
|
||||||
ChunkWaterRaster landed = queue(chunkX, chunkZ, key, false);
|
ChunkWaterRaster landed = queue(chunkX, chunkZ, key, false);
|
||||||
return landed == null ? ChunkWaterRaster.PROVISIONAL : landed;
|
return landed == null ? ChunkWaterRaster.PROVISIONAL : landed;
|
||||||
@@ -240,7 +255,7 @@ public final class HydrologyRuntime {
|
|||||||
return ChunkWaterRaster.PROVISIONAL;
|
return ChunkWaterRaster.PROVISIONAL;
|
||||||
}
|
}
|
||||||
long key = key(chunkX, chunkZ);
|
long key = key(chunkX, chunkZ);
|
||||||
ChunkWaterRaster known = remembered(key);
|
ChunkWaterRaster known = remembered(key, last.get());
|
||||||
if (known == null) {
|
if (known == null) {
|
||||||
ChunkWaterRaster landed = queue(chunkX, chunkZ, key, true);
|
ChunkWaterRaster landed = queue(chunkX, chunkZ, key, true);
|
||||||
return landed == null ? ChunkWaterRaster.PROVISIONAL : landed;
|
return landed == null ? ChunkWaterRaster.PROVISIONAL : landed;
|
||||||
@@ -426,6 +441,9 @@ public final class HydrologyRuntime {
|
|||||||
synchronized (rasters) {
|
synchronized (rasters) {
|
||||||
rasters.clear();
|
rasters.clear();
|
||||||
}
|
}
|
||||||
|
synchronized (prefetches) {
|
||||||
|
prefetches.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HydrologyBinding binding(Engine engine, IrisComplex complex) {
|
private static HydrologyBinding binding(Engine engine, IrisComplex complex) {
|
||||||
@@ -532,7 +550,7 @@ public final class HydrologyRuntime {
|
|||||||
queued.remove(key);
|
queued.remove(key);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
ChunkWaterRaster landed = remembered(key);
|
ChunkWaterRaster landed = remembered(key, last.get());
|
||||||
if (landed != null) {
|
if (landed != null) {
|
||||||
finish(key, preview);
|
finish(key, preview);
|
||||||
return landed;
|
return landed;
|
||||||
@@ -632,8 +650,22 @@ public final class HydrologyRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void prefetch(int chunkX, int chunkZ) {
|
private void prefetch(int chunkX, int chunkZ) {
|
||||||
if (settings.prefetchRings() > 0) {
|
if (settings.prefetchRings() <= 0) {
|
||||||
cache.prepareWindowsAround(chunkX, chunkZ, chunkX, chunkZ, 1 + settings.prefetchRings());
|
return;
|
||||||
|
}
|
||||||
|
WindowKey owner = WindowKey.containing(chunkX << 4, chunkZ << 4);
|
||||||
|
synchronized (prefetches) {
|
||||||
|
long evictions = cache.windowEvictions();
|
||||||
|
WindowPrefetch known = prefetches.get(owner);
|
||||||
|
if (known != null && !known.prepared().isCompletedExceptionally()
|
||||||
|
&& (!known.prepared().isDone() || known.evictions() == evictions)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CompletableFuture<Void> prepared = cache.prepareWindowsAround(chunkX, chunkZ, chunkX, chunkZ, 1 + settings.prefetchRings());
|
||||||
|
prefetches.put(owner, new WindowPrefetch(prepared, evictions));
|
||||||
|
if (prefetches.size() > PREFETCH_CAPACITY) {
|
||||||
|
prefetches.pollFirstEntry();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -648,8 +680,7 @@ public final class HydrologyRuntime {
|
|||||||
return keys;
|
return keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChunkWaterRaster remembered(long key) {
|
private ChunkWaterRaster remembered(long key, LastRaster slot) {
|
||||||
LastRaster slot = last.get();
|
|
||||||
if (slot.raster != null && slot.key == key) {
|
if (slot.raster != null && slot.key == key) {
|
||||||
return slot.raster;
|
return slot.raster;
|
||||||
}
|
}
|
||||||
@@ -712,9 +743,13 @@ public final class HydrologyRuntime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record WindowPrefetch(CompletableFuture<Void> prepared, long evictions) {
|
||||||
|
}
|
||||||
|
|
||||||
private static final class LastRaster {
|
private static final class LastRaster {
|
||||||
private long key;
|
private long key;
|
||||||
private ChunkWaterRaster raster;
|
private ChunkWaterRaster raster;
|
||||||
|
private boolean preview;
|
||||||
|
|
||||||
private void remember(long key, ChunkWaterRaster raster) {
|
private void remember(long key, ChunkWaterRaster raster) {
|
||||||
this.key = key;
|
this.key = key;
|
||||||
@@ -722,6 +757,30 @@ public final class HydrologyRuntime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static final class PreviewScope implements AutoCloseable {
|
||||||
|
private final LastRaster slot;
|
||||||
|
private final boolean previous;
|
||||||
|
private boolean closed;
|
||||||
|
|
||||||
|
private PreviewScope(LastRaster slot) {
|
||||||
|
this.slot = slot;
|
||||||
|
this.previous = slot != null && slot.preview;
|
||||||
|
if (slot != null) {
|
||||||
|
slot.preview = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
if (!closed) {
|
||||||
|
closed = true;
|
||||||
|
if (slot != null) {
|
||||||
|
slot.preview = previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final class RasterLru extends LinkedHashMap<Long, ChunkWaterRaster> {
|
private static final class RasterLru extends LinkedHashMap<Long, ChunkWaterRaster> {
|
||||||
private RasterLru() {
|
private RasterLru() {
|
||||||
super(RASTER_CAPACITY * 2, 0.75f, true);
|
super(RASTER_CAPACITY * 2, 0.75f, true);
|
||||||
|
|||||||
@@ -225,8 +225,11 @@ public final class HeightLattice {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Long key(int cellX, int cellZ) {
|
static long key(int cellX, int cellZ) {
|
||||||
return ((long) cellX << 32) | (cellZ & 0xFFFFFFFFL);
|
long packed = ((long) cellX << 32) | (cellZ & 0xFFFFFFFFL);
|
||||||
|
long mixed = (packed ^ (packed >>> 30)) * 0xbf58476d1ce4e5b9L;
|
||||||
|
mixed = (mixed ^ (mixed >>> 27)) * 0x94d049bb133111ebL;
|
||||||
|
return mixed ^ (mixed >>> 31);
|
||||||
}
|
}
|
||||||
|
|
||||||
private record Slot(CompletableFuture<HydrologySampleBuffer> batch, int index) {
|
private record Slot(CompletableFuture<HydrologySampleBuffer> batch, int index) {
|
||||||
|
|||||||
+68
-21
@@ -1,5 +1,6 @@
|
|||||||
package art.arcane.iris.generation.hydrology.plan;
|
package art.arcane.iris.generation.hydrology.plan;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||||
import art.arcane.iris.generation.hydrology.raster.ReachIndex;
|
import art.arcane.iris.generation.hydrology.raster.ReachIndex;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
|
||||||
@@ -20,7 +21,9 @@ import java.util.concurrent.CompletionException;
|
|||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.ForkJoinPool;
|
||||||
import java.util.concurrent.RejectedExecutionException;
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
@@ -46,6 +49,7 @@ public final class HydrologyPlanCache {
|
|||||||
private volatile HydrologyPlanStore store;
|
private volatile HydrologyPlanStore store;
|
||||||
private int writing;
|
private int writing;
|
||||||
private final ExecutorService pool;
|
private final ExecutorService pool;
|
||||||
|
private final int speculativeParallelism;
|
||||||
private final BooleanSupplier mainThread;
|
private final BooleanSupplier mainThread;
|
||||||
private final Object lock = new Object();
|
private final Object lock = new Object();
|
||||||
private final PriorityQueue<Job> queue = new PriorityQueue<>(Comparator.comparingLong((Job job) -> job.priority).thenComparingLong(job -> job.order));
|
private final PriorityQueue<Job> queue = new PriorityQueue<>(Comparator.comparingLong((Job job) -> job.priority).thenComparingLong(job -> job.order));
|
||||||
@@ -56,6 +60,7 @@ public final class HydrologyPlanCache {
|
|||||||
private final Shelf<RegionKey, RegionPlan> regions = new Shelf<>(REGION_CAPACITY, this::startRegion);
|
private final Shelf<RegionKey, RegionPlan> regions = new Shelf<>(REGION_CAPACITY, this::startRegion);
|
||||||
private final Shelf<WindowKey, IndexedWindow> windows = new Shelf<>(WINDOW_CAPACITY, this::startWindow);
|
private final Shelf<WindowKey, IndexedWindow> windows = new Shelf<>(WINDOW_CAPACITY, this::startWindow);
|
||||||
private final Map<Object, Set<Slot<?, ?>>> epochSlots = new HashMap<>();
|
private final Map<Object, Set<Slot<?, ?>>> epochSlots = new HashMap<>();
|
||||||
|
private int runningSpeculative;
|
||||||
private volatile boolean closed;
|
private volatile boolean closed;
|
||||||
|
|
||||||
public HydrologyPlanCache(HydrologySettings settings, Function<RegionKey, RegionPlan> regionPlanner,
|
public HydrologyPlanCache(HydrologySettings settings, Function<RegionKey, RegionPlan> regionPlanner,
|
||||||
@@ -64,6 +69,7 @@ public final class HydrologyPlanCache {
|
|||||||
this.windowPlanner = Objects.requireNonNull(windowPlanner, "windowPlanner");
|
this.windowPlanner = Objects.requireNonNull(windowPlanner, "windowPlanner");
|
||||||
this.persist = Objects.requireNonNull(settings, "settings").persist();
|
this.persist = Objects.requireNonNull(settings, "settings").persist();
|
||||||
this.pool = Objects.requireNonNull(pool, "pool");
|
this.pool = Objects.requireNonNull(pool, "pool");
|
||||||
|
this.speculativeParallelism = speculativeParallelism(pool);
|
||||||
this.mainThread = Objects.requireNonNull(mainThread, "mainThread");
|
this.mainThread = Objects.requireNonNull(mainThread, "mainThread");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +108,10 @@ public final class HydrologyPlanCache {
|
|||||||
return windows.ready(Objects.requireNonNull(key, "key"));
|
return windows.ready(Objects.requireNonNull(key, "key"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long windowEvictions() {
|
||||||
|
return windows.evictions.get();
|
||||||
|
}
|
||||||
|
|
||||||
public RegionPlan region(RegionKey key) {
|
public RegionPlan region(RegionKey key) {
|
||||||
Objects.requireNonNull(key, "key");
|
Objects.requireNonNull(key, "key");
|
||||||
requireOffMainThread("region " + key.rx() + "," + key.rz());
|
requireOffMainThread("region " + key.rx() + "," + key.rz());
|
||||||
@@ -179,11 +189,7 @@ public final class HydrologyPlanCache {
|
|||||||
dropped.run();
|
dropped.run();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
dispatch();
|
||||||
pool.execute(this::runNext);
|
|
||||||
} catch (RejectedExecutionException rejected) {
|
|
||||||
abandonQueue(rejected);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private CompletableFuture<Void> prepare(int minChunkX, int minChunkZ, int maxChunkX, int maxChunkZ, int rings, Interest interest) {
|
private CompletableFuture<Void> prepare(int minChunkX, int minChunkZ, int maxChunkX, int maxChunkZ, int rings, Interest interest) {
|
||||||
@@ -288,7 +294,17 @@ public final class HydrologyPlanCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private long speculativePriority() {
|
private long speculativePriority() {
|
||||||
return -(speculations.incrementAndGet() << SPECULATIVE_SHIFT);
|
return Long.MAX_VALUE - (speculations.incrementAndGet() << SPECULATIVE_SHIFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int speculativeParallelism(ExecutorService pool) {
|
||||||
|
int parallelism = switch (pool) {
|
||||||
|
case MultiBurst burst -> burst.parallelism();
|
||||||
|
case ForkJoinPool forkJoin -> forkJoin.getParallelism();
|
||||||
|
case ThreadPoolExecutor threads -> threads.getCorePoolSize();
|
||||||
|
default -> 1;
|
||||||
|
};
|
||||||
|
return Math.max(1, parallelism / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int frontCells(int unit, int cell) {
|
private static int frontCells(int unit, int cell) {
|
||||||
@@ -457,29 +473,34 @@ public final class HydrologyPlanCache {
|
|||||||
slot.fail(new CancellationException("Hydrology plan cache is closed."));
|
slot.fail(new CancellationException("Hydrology plan cache is closed."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
dispatch();
|
||||||
pool.execute(this::runNext);
|
|
||||||
} catch (RejectedExecutionException rejected) {
|
|
||||||
abandonQueue(rejected);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void runNext() {
|
private void runNext() {
|
||||||
Job job;
|
Job job;
|
||||||
|
boolean speculative;
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
job = queue.poll();
|
job = queue.peek();
|
||||||
if (job != null && job.slot != null) {
|
if (job == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
speculative = job.priority >= 0;
|
||||||
|
if (speculative && runningSpeculative >= speculativeParallelism) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queue.remove();
|
||||||
|
if (speculative) {
|
||||||
|
runningSpeculative++;
|
||||||
|
}
|
||||||
|
if (job.slot != null) {
|
||||||
job.slot.job = null;
|
job.slot.job = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (job == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (closed) {
|
|
||||||
job.drop(new CancellationException("Hydrology plan cache is closed."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
|
if (closed) {
|
||||||
|
job.drop(new CancellationException("Hydrology plan cache is closed."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
job.body.run();
|
job.body.run();
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
if (job.slot != null) {
|
if (job.slot != null) {
|
||||||
@@ -487,6 +508,25 @@ public final class HydrologyPlanCache {
|
|||||||
} else if (!closed) {
|
} else if (!closed) {
|
||||||
IrisLogging.reportError("A speculative hydrology task failed.", failure);
|
IrisLogging.reportError("A speculative hydrology task failed.", failure);
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
if (speculative) {
|
||||||
|
boolean pending;
|
||||||
|
synchronized (lock) {
|
||||||
|
runningSpeculative--;
|
||||||
|
pending = !closed && !queue.isEmpty();
|
||||||
|
}
|
||||||
|
if (pending) {
|
||||||
|
dispatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dispatch() {
|
||||||
|
try {
|
||||||
|
pool.execute(this::runNext);
|
||||||
|
} catch (RejectedExecutionException rejected) {
|
||||||
|
abandonQueue(rejected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,8 +553,12 @@ public final class HydrologyPlanCache {
|
|||||||
slot.priority = priority;
|
slot.priority = priority;
|
||||||
Job job = slot.job;
|
Job job = slot.job;
|
||||||
if (job != null && queue.remove(job)) {
|
if (job != null && queue.remove(job)) {
|
||||||
|
boolean demanded = job.priority >= 0 && priority < 0;
|
||||||
job.priority = priority;
|
job.priority = priority;
|
||||||
queue.add(job);
|
queue.add(job);
|
||||||
|
if (demanded) {
|
||||||
|
dispatch();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (Slot<RegionKey, RegionPlan> need : slot.needs) {
|
for (Slot<RegionKey, RegionPlan> need : slot.needs) {
|
||||||
promoteLocked(need, priority);
|
promoteLocked(need, priority);
|
||||||
@@ -597,6 +641,7 @@ public final class HydrologyPlanCache {
|
|||||||
|
|
||||||
private final class Shelf<K, V> {
|
private final class Shelf<K, V> {
|
||||||
private final ConcurrentHashMap<K, Slot<K, V>> slots = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<K, Slot<K, V>> slots = new ConcurrentHashMap<>();
|
||||||
|
private final AtomicLong evictions = new AtomicLong();
|
||||||
private final int capacity;
|
private final int capacity;
|
||||||
private final BiConsumer<K, Slot<K, V>> start;
|
private final BiConsumer<K, Slot<K, V>> start;
|
||||||
|
|
||||||
@@ -658,7 +703,9 @@ public final class HydrologyPlanCache {
|
|||||||
ready.sort(Comparator.comparingLong(Aged::used));
|
ready.sort(Comparator.comparingLong(Aged::used));
|
||||||
for (int index = 0; index < excess; index++) {
|
for (int index = 0; index < excess; index++) {
|
||||||
Slot<K, V> slot = ready.get(index).slot();
|
Slot<K, V> slot = ready.get(index).slot();
|
||||||
slots.remove(slot.key, slot);
|
if (slots.remove(slot.key, slot)) {
|
||||||
|
evictions.incrementAndGet();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-1
@@ -50,6 +50,7 @@ public final class HydrologySettings {
|
|||||||
private final int minY;
|
private final int minY;
|
||||||
private final int maxY;
|
private final int maxY;
|
||||||
private final boolean enabled;
|
private final boolean enabled;
|
||||||
|
private final float density;
|
||||||
private final Drainage drainage;
|
private final Drainage drainage;
|
||||||
private final List<HydrologyTier> tiers;
|
private final List<HydrologyTier> tiers;
|
||||||
private final Ponds ponds;
|
private final Ponds ponds;
|
||||||
@@ -67,6 +68,10 @@ public final class HydrologySettings {
|
|||||||
this.minY = minY;
|
this.minY = minY;
|
||||||
this.maxY = maxY;
|
this.maxY = maxY;
|
||||||
this.enabled = config.isEnabled();
|
this.enabled = config.isEnabled();
|
||||||
|
if (!Double.isFinite(config.getDensity()) || config.getDensity() < 0D || config.getDensity() > 1D) {
|
||||||
|
throw new IllegalArgumentException("Hydrology density must be within 0..1, got " + config.getDensity());
|
||||||
|
}
|
||||||
|
this.density = (float) config.getDensity();
|
||||||
this.drainage = drainage(config.getDrainage());
|
this.drainage = drainage(config.getDrainage());
|
||||||
this.tiers = tiers(config.getTiers());
|
this.tiers = tiers(config.getTiers());
|
||||||
this.ponds = ponds(config.getPonds());
|
this.ponds = ponds(config.getPonds());
|
||||||
@@ -126,6 +131,10 @@ public final class HydrologySettings {
|
|||||||
return enabled;
|
return enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public float density() {
|
||||||
|
return density;
|
||||||
|
}
|
||||||
|
|
||||||
public Drainage drainage() {
|
public Drainage drainage() {
|
||||||
return drainage;
|
return drainage;
|
||||||
}
|
}
|
||||||
@@ -458,7 +467,7 @@ public final class HydrologySettings {
|
|||||||
|
|
||||||
private long computeFingerprint() {
|
private long computeFingerprint() {
|
||||||
Fingerprint hash = new Fingerprint().add(seed).add(seaLevel).add(minY).add(maxY).add(enabled)
|
Fingerprint hash = new Fingerprint().add(seed).add(seaLevel).add(minY).add(maxY).add(enabled)
|
||||||
.add(prefetchRings).add(persist);
|
.add(density).add(prefetchRings).add(persist);
|
||||||
addDrainage(hash);
|
addDrainage(hash);
|
||||||
addTiers(hash);
|
addTiers(hash);
|
||||||
addPonds(hash);
|
addPonds(hash);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package art.arcane.iris.generation.hydrology.plan;
|
|||||||
import art.arcane.iris.generation.hydrology.grid.Basin;
|
import art.arcane.iris.generation.hydrology.grid.Basin;
|
||||||
import art.arcane.iris.generation.hydrology.grid.BasinExtractor;
|
import art.arcane.iris.generation.hydrology.grid.BasinExtractor;
|
||||||
import art.arcane.iris.generation.hydrology.grid.Breacher;
|
import art.arcane.iris.generation.hydrology.grid.Breacher;
|
||||||
|
import art.arcane.iris.generation.hydrology.grid.CellHash;
|
||||||
import art.arcane.iris.generation.hydrology.grid.ChannelHeads;
|
import art.arcane.iris.generation.hydrology.grid.ChannelHeads;
|
||||||
import art.arcane.iris.generation.hydrology.grid.D8Router;
|
import art.arcane.iris.generation.hydrology.grid.D8Router;
|
||||||
import art.arcane.iris.generation.hydrology.grid.DemGrid;
|
import art.arcane.iris.generation.hydrology.grid.DemGrid;
|
||||||
@@ -28,6 +29,7 @@ public final class RegionPlanner {
|
|||||||
private static final int SPAN = 3 * REGION_CELLS;
|
private static final int SPAN = 3 * REGION_CELLS;
|
||||||
private static final float BASIN_DEPTH = 0.5f;
|
private static final float BASIN_DEPTH = 0.5f;
|
||||||
private static final int BREACH_PASSES = 8;
|
private static final int BREACH_PASSES = 8;
|
||||||
|
private static final long SALT_HEAD = 0x4d414a4f524844L;
|
||||||
|
|
||||||
private final HydrologySettings settings;
|
private final HydrologySettings settings;
|
||||||
private final HeightLattice lattice;
|
private final HeightLattice lattice;
|
||||||
@@ -127,7 +129,7 @@ public final class RegionPlanner {
|
|||||||
float[] accumulation = FlowAccumulator.accumulate(receivers, filled, weights, settings.drainage().saturationCatchment());
|
float[] accumulation = FlowAccumulator.accumulate(receivers, filled, weights, settings.drainage().saturationCatchment());
|
||||||
BitSet candidates = ChannelHeads.candidates(surface, accumulation, samples.permission(), settings.majorTier().minCatchment(), settings.seaLevel());
|
BitSet candidates = ChannelHeads.candidates(surface, accumulation, samples.permission(), settings.majorTier().minCatchment(), settings.seaLevel());
|
||||||
BitSet channels = ChannelHeads.keep(surface, samples.height(), receivers, candidates, settings.drainage().headRelief(), cell -> false,
|
BitSet channels = ChannelHeads.keep(surface, samples.height(), receivers, candidates, settings.drainage().headRelief(), cell -> false,
|
||||||
cell -> true);
|
cell -> CellHash.unit(CellHash.mix(settings.seed(), SALT_HEAD, surface.blockX(cell), surface.blockZ(cell))) < settings.density());
|
||||||
return shaper.coarsePaths(surface, receivers, accumulation, channels, settings.seed());
|
return shaper.coarsePaths(surface, receivers, accumulation, channels, settings.seed());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,11 +94,18 @@ public final class WindowPlanner {
|
|||||||
BitSet seeds = seeds(samples.ocean(), trunks);
|
BitSet seeds = seeds(samples.ocean(), trunks);
|
||||||
DemGrid preliminary = new DemGrid(raw.originX(), raw.originZ(), CELL, SPAN, SPAN);
|
DemGrid preliminary = new DemGrid(raw.originX(), raw.originZ(), CELL, SPAN, SPAN);
|
||||||
System.arraycopy(raw.values(), 0, preliminary.values(), 0, SPAN * SPAN);
|
System.arraycopy(raw.values(), 0, preliminary.values(), 0, SPAN * SPAN);
|
||||||
BitSet channelHint = drain(preliminary, seeds, samples, trunks, lakes).channels();
|
Drainage initial = drain(preliminary, seeds, samples, trunks, lakes);
|
||||||
|
BitSet channelHint = (BitSet) initial.channels().clone();
|
||||||
channelHint.andNot(trunks.cells());
|
channelHint.andNot(trunks.cells());
|
||||||
CupSet cups = ponds.subtractCups(raw, key, samples, channelHint);
|
CupSet cups = ponds.subtractCups(raw, key, samples, channelHint);
|
||||||
trunks.descend(raw.values());
|
Drainage drainage;
|
||||||
Drainage drainage = drain(raw, seeds, samples, trunks, lakes);
|
if (cups.size() == 0) {
|
||||||
|
raw = preliminary;
|
||||||
|
drainage = initial;
|
||||||
|
} else {
|
||||||
|
trunks.descend(raw.values());
|
||||||
|
drainage = drain(raw, seeds, samples, trunks, lakes);
|
||||||
|
}
|
||||||
List<Pond> water = new ArrayList<>(ponds.ponds(raw, drainage.filled(), drainage.basins(), lakes, fine, cups));
|
List<Pond> water = new ArrayList<>(ponds.ponds(raw, drainage.filled(), drainage.basins(), lakes, fine, cups));
|
||||||
water.addAll(lakes);
|
water.addAll(lakes);
|
||||||
ShapedNetwork network = shaper.shape(drainage.surface(), drainage.receivers(), drainage.accumulation(), drainage.channels(), water,
|
ShapedNetwork network = shaper.shape(drainage.surface(), drainage.receivers(), drainage.accumulation(), drainage.channels(), water,
|
||||||
@@ -231,10 +238,8 @@ public final class WindowPlanner {
|
|||||||
|
|
||||||
private boolean keepHead(DemGrid surface, int cell, float rainfall) {
|
private boolean keepHead(DemGrid surface, int cell, float rainfall) {
|
||||||
HydrologySettings.Drainage drainage = settings.drainage();
|
HydrologySettings.Drainage drainage = settings.drainage();
|
||||||
if (!drainage.wetnessThinning()) {
|
float keep = drainage.wetnessThinning() ? Math.clamp(Math.max(drainage.minimumHeadKeep(), rainfall), 0f, 1f) : 1f;
|
||||||
return true;
|
keep *= settings.density();
|
||||||
}
|
|
||||||
float keep = Math.max(drainage.minimumHeadKeep(), rainfall);
|
|
||||||
return CellHash.unit(CellHash.mix(settings.seed(), SALT_HEAD, surface.blockX(cell), surface.blockZ(cell))) < keep;
|
return CellHash.unit(CellHash.mix(settings.seed(), SALT_HEAD, surface.blockX(cell), surface.blockZ(cell))) < keep;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,13 +28,20 @@ final class MaskEdges {
|
|||||||
int firstJ = Math.max(-margin, Math.floorDiv(minZ - mask.originZ() + half, step));
|
int firstJ = Math.max(-margin, Math.floorDiv(minZ - mask.originZ() + half, step));
|
||||||
int lastJ = Math.min(mask.depth() - 1 + margin, Math.floorDiv(maxZ - mask.originZ() + half, step));
|
int lastJ = Math.min(mask.depth() - 1 + margin, Math.floorDiv(maxZ - mask.originZ() + half, step));
|
||||||
for (int j = firstJ; j <= lastJ; j++) {
|
for (int j = firstJ; j <= lastJ; j++) {
|
||||||
|
int start = Integer.MIN_VALUE;
|
||||||
for (int i = firstI; i <= lastI; i++) {
|
for (int i = firstI; i <= lastI; i++) {
|
||||||
if (edge(mask, i, j)) {
|
if (edge(mask, i, j)) {
|
||||||
int x = mask.originX() + i * step - half;
|
if (start == Integer.MIN_VALUE) {
|
||||||
int z = mask.originZ() + j * step - half;
|
start = i;
|
||||||
append(x, z, x + step - 1, z + step - 1);
|
}
|
||||||
|
} else if (start != Integer.MIN_VALUE) {
|
||||||
|
appendRun(mask, start, i, j);
|
||||||
|
start = Integer.MIN_VALUE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (start != Integer.MIN_VALUE) {
|
||||||
|
appendRun(mask, start, lastI + 1, j);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
slots++;
|
slots++;
|
||||||
starts[slots] = count;
|
starts[slots] = count;
|
||||||
@@ -77,6 +84,14 @@ final class MaskEdges {
|
|||||||
squares[count++] = maxZ;
|
squares[count++] = maxZ;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void appendRun(PondMask mask, int first, int end, int row) {
|
||||||
|
int step = mask.step();
|
||||||
|
int half = step >> 1;
|
||||||
|
int x = mask.originX() + first * step - half;
|
||||||
|
int z = mask.originZ() + row * step - half;
|
||||||
|
append(x, z, mask.originX() + end * step - half - 1, z + step - 1);
|
||||||
|
}
|
||||||
|
|
||||||
enum Kind {
|
enum Kind {
|
||||||
WATER,
|
WATER,
|
||||||
BANK,
|
BANK,
|
||||||
|
|||||||
+7
-1
@@ -19,6 +19,7 @@ public final class CavePoolFiller {
|
|||||||
private static final long SALT_COLUMN = 0x434156434f4c00L;
|
private static final long SALT_COLUMN = 0x434156434f4c00L;
|
||||||
private static final long SALT_FLOOR = 0x4341564652L << 20;
|
private static final long SALT_FLOOR = 0x4341564652L << 20;
|
||||||
private static final long SALT_FREEBOARD = 0x43415646524244L;
|
private static final long SALT_FREEBOARD = 0x43415646524244L;
|
||||||
|
private static final long SALT_DENSITY = 0x43415644454e53L;
|
||||||
private static final HydrologyCaveCell GUARD = HydrologyCaveCell.of(HydrologyCaveAction.SEAL_GUARD);
|
private static final HydrologyCaveCell GUARD = HydrologyCaveCell.of(HydrologyCaveAction.SEAL_GUARD);
|
||||||
private static final CaveVoxel[] VOXELS = CaveVoxel.values();
|
private static final CaveVoxel[] VOXELS = CaveVoxel.values();
|
||||||
private static final int[] DX = {1, -1, 0, 0, 0, 0};
|
private static final int[] DX = {1, -1, 0, 0, 0, 0};
|
||||||
@@ -31,18 +32,23 @@ public final class CavePoolFiller {
|
|||||||
|
|
||||||
private final HydrologySettings.CavePools pools;
|
private final HydrologySettings.CavePools pools;
|
||||||
private final Set<String> fluids;
|
private final Set<String> fluids;
|
||||||
|
private final float density;
|
||||||
|
|
||||||
public CavePoolFiller(HydrologySettings settings) {
|
public CavePoolFiller(HydrologySettings settings) {
|
||||||
this.pools = settings.underground().cavePools();
|
this.pools = settings.underground().cavePools();
|
||||||
this.fluids = settings.fluids().keySet();
|
this.fluids = settings.fluids().keySet();
|
||||||
|
this.density = settings.density();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void fill(int chunkX, int chunkZ, CarvedTerrain carved, MantleWriter writer, long seed) {
|
public void fill(int chunkX, int chunkZ, CarvedTerrain carved, MantleWriter writer, long seed) {
|
||||||
if (!pools.enabled()) {
|
if (!pools.enabled() || density == 0f) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int attempts = attempts(seed, chunkX, chunkZ);
|
int attempts = attempts(seed, chunkX, chunkZ);
|
||||||
for (int attempt = 0; attempt < attempts; attempt++) {
|
for (int attempt = 0; attempt < attempts; attempt++) {
|
||||||
|
if (density < 1f && CellHash.unit(CellHash.mix(seed, SALT_DENSITY + attempt, chunkX, chunkZ)) >= density) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
long column = CellHash.mix(seed, SALT_COLUMN + attempt, chunkX, chunkZ);
|
long column = CellHash.mix(seed, SALT_COLUMN + attempt, chunkX, chunkZ);
|
||||||
int x = (chunkX << 4) + (int) (column & 15);
|
int x = (chunkX << 4) + (int) (column & 15);
|
||||||
int z = (chunkZ << 4) + (int) ((column >>> 4) & 15);
|
int z = (chunkZ << 4) + (int) ((column >>> 4) & 15);
|
||||||
|
|||||||
+5
-1
@@ -32,6 +32,7 @@ public final class SeaInletPlanner {
|
|||||||
private static final long SALT_RADIUS = 0x53454152414449L;
|
private static final long SALT_RADIUS = 0x53454152414449L;
|
||||||
private static final long SALT_HEADROOM = 0x53454148454144L;
|
private static final long SALT_HEADROOM = 0x53454148454144L;
|
||||||
private static final long SALT_DEPTH = 0x53454144455054L;
|
private static final long SALT_DEPTH = 0x53454144455054L;
|
||||||
|
private static final long SALT_DENSITY = 0x53454144454e53L;
|
||||||
|
|
||||||
private final HydrologySettings settings;
|
private final HydrologySettings settings;
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ public final class SeaInletPlanner {
|
|||||||
public List<SeaInlet> plan(DemGrid raw, HydrologySampleBuffer samples, int[] receivers, float[] accumulation, ShapedNetwork network,
|
public List<SeaInlet> plan(DemGrid raw, HydrologySampleBuffer samples, int[] receivers, float[] accumulation, ShapedNetwork network,
|
||||||
long seed) {
|
long seed) {
|
||||||
HydrologySettings.SeaInlets config = settings.underground().seaInlets();
|
HydrologySettings.SeaInlets config = settings.underground().seaInlets();
|
||||||
if (!config.enabled()) {
|
if (!config.enabled() || settings.density() == 0f) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
int count = raw.values().length;
|
int count = raw.values().length;
|
||||||
@@ -101,6 +102,9 @@ public final class SeaInletPlanner {
|
|||||||
private SeaInlet site(Terrain terrain, HydrologySettings.SeaInlets config, long key, List<Integer> candidates) {
|
private SeaInlet site(Terrain terrain, HydrologySettings.SeaInlets config, long key, List<Integer> candidates) {
|
||||||
int siteX = (int) (key >> 32);
|
int siteX = (int) (key >> 32);
|
||||||
int siteZ = (int) key;
|
int siteZ = (int) key;
|
||||||
|
if (settings.density() < 1f && CellHash.unit(CellHash.mix(terrain.seed, SALT_DENSITY, siteX, siteZ)) >= settings.density()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
double targetX = (siteX + CellHash.unit(CellHash.mix(terrain.seed, SALT_SITE_X, siteX, siteZ))) * config.spacing();
|
double targetX = (siteX + CellHash.unit(CellHash.mix(terrain.seed, SALT_SITE_X, siteX, siteZ))) * config.spacing();
|
||||||
double targetZ = (siteZ + CellHash.unit(CellHash.mix(terrain.seed, SALT_SITE_Z, siteX, siteZ))) * config.spacing();
|
double targetZ = (siteZ + CellHash.unit(CellHash.mix(terrain.seed, SALT_SITE_Z, siteX, siteZ))) * config.spacing();
|
||||||
DemGrid grid = terrain.grid;
|
DemGrid grid = terrain.grid;
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ final class CupCarver {
|
|||||||
}
|
}
|
||||||
float slope = slope(raw, samples, cell);
|
float slope = slope(raw, samples, cell);
|
||||||
if (suitable(samples, cell, slope)
|
if (suitable(samples, cell, slope)
|
||||||
&& CellHash.unit(CellHash.mix(seed, SALT_SITE, siteX, siteZ)) < chance * samples.pondDensity()[cell]) {
|
&& CellHash.unit(CellHash.mix(seed, SALT_SITE, siteX, siteZ))
|
||||||
|
< Math.clamp(chance * samples.pondDensity()[cell], 0D, 1D) * settings.density()) {
|
||||||
addCup(raw, samples, cell, slope, siteX, siteZ, false, out);
|
addCup(raw, samples, cell, slope, siteX, siteZ, false, out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,7 +90,7 @@ final class CupCarver {
|
|||||||
}
|
}
|
||||||
float slope = slope(raw, samples, cell);
|
float slope = slope(raw, samples, cell);
|
||||||
double roll = CellHash.unit(CellHash.mix(settings.seed(), SALT_CHAIN, raw.blockX(cell), raw.blockZ(cell)));
|
double roll = CellHash.unit(CellHash.mix(settings.seed(), SALT_CHAIN, raw.blockX(cell), raw.blockZ(cell)));
|
||||||
if (suitable(samples, cell, slope) && roll < chance * samples.pondDensity()[cell]) {
|
if (suitable(samples, cell, slope) && roll < Math.clamp(chance * samples.pondDensity()[cell], 0D, 1D) * settings.density()) {
|
||||||
sites.add(new Site(roll, cell, slope));
|
sites.add(new Site(roll, cell, slope));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ public final class PondPlanner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new Pond(pond.id(), pond.kind(), level, (int) Math.floor(lowest), pond.spillX(), pond.spillZ(), pond.spillHeight(),
|
return new Pond(pond.id(), pond.kind(), level, (int) Math.floor(lowest), pond.spillX(), pond.spillZ(), pond.spillHeight(),
|
||||||
mask(box, wet), pond.outletReachId(), pond.damRaise(), pond.fluid(), pond.floorX(), pond.floorZ());
|
mask(box, ground, wet, level), pond.outletReachId(), pond.damRaise(), pond.fluid(), pond.floorX(), pond.floorZ());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -223,13 +223,16 @@ public final class PondPlanner {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (kind == PondKind.NATURAL && CellHash.unit(CellHash.mix(settings.seed(), SALT_KEEP, floorX, floorZ))
|
if (kind == PondKind.NATURAL && CellHash.unit(CellHash.mix(settings.seed(), SALT_KEEP, floorX, floorZ))
|
||||||
>= settings.ponds().natural().keep() * floor.pondDensity()[0]) {
|
>= Math.clamp(settings.ponds().natural().keep() * floor.pondDensity()[0], 0f, 1f) * settings.density()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return new Candidate(basin, kind, floorX, floorZ, floor.rainfall()[0], fluid(floor));
|
return new Candidate(basin, kind, floorX, floorZ, floor.rainfall()[0], fluid(floor));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Pond lake(LakeCandidate candidate, HeightLattice fine) {
|
private Pond lake(LakeCandidate candidate, HeightLattice fine) {
|
||||||
|
if (CellHash.unit(CellHash.mix(settings.seed(), SALT_KEEP, candidate.floorX(), candidate.floorZ())) >= settings.density()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
HydrologySampleBuffer floor = sampleAt(fine, candidate.floorX(), candidate.floorZ());
|
HydrologySampleBuffer floor = sampleAt(fine, candidate.floorX(), candidate.floorZ());
|
||||||
if ((floor.permission()[0] & HydrologySampleBuffer.ALLOW_PONDS) == 0) {
|
if ((floor.permission()[0] & HydrologySampleBuffer.ALLOW_PONDS) == 0) {
|
||||||
return null;
|
return null;
|
||||||
@@ -347,36 +350,31 @@ public final class PondPlanner {
|
|||||||
level--;
|
level--;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
int limit = rim == null ? level : Math.min(level, rimGround(rim, box, wet) + settings.deformation().maxFill());
|
PondMask mask = mask(box, ground, wet, level);
|
||||||
|
int limit = rim == null ? level : Math.min(level, rimGround(rim, mask) + settings.deformation().maxFill());
|
||||||
if (limit >= level) {
|
if (limit >= level) {
|
||||||
return pond(site, level, box, ground, wet, spill, damRaise);
|
return pond(site, level, box, ground, wet, spill, damRaise, mask);
|
||||||
}
|
}
|
||||||
level = limit;
|
level = limit;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int rimGround(HeightLattice lattice, Box box, boolean[] wet) {
|
private static int rimGround(HeightLattice lattice, PondMask mask) {
|
||||||
int half = box.step() >> 1;
|
|
||||||
Set<Long> columns = new LinkedHashSet<>();
|
Set<Long> columns = new LinkedHashSet<>();
|
||||||
for (int node = 0; node < wet.length; node++) {
|
for (int j = 0; j < mask.depth(); j++) {
|
||||||
if (!wet[node]) {
|
for (int i = 0; i < mask.width(); i++) {
|
||||||
continue;
|
if (!mask.node(i, j)) {
|
||||||
}
|
|
||||||
int i = node % box.width();
|
|
||||||
int j = node / box.width();
|
|
||||||
int x = box.x(node);
|
|
||||||
int z = box.z(node);
|
|
||||||
for (int axis = 0; axis < AXIS_X.length; axis++) {
|
|
||||||
int ni = i + AXIS_X[axis];
|
|
||||||
int nj = j + AXIS_Z[axis];
|
|
||||||
if (ni >= 0 && nj >= 0 && ni < box.width() && nj < box.depth() && wet[nj * box.width() + ni]) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (int along = -half; along < half; along++) {
|
for (int axis = 0; axis < AXIS_X.length; axis++) {
|
||||||
int columnX = AXIS_X[axis] == 0 ? x + along : AXIS_X[axis] > 0 ? x + half : x - half - 1;
|
int ni = i + AXIS_X[axis];
|
||||||
int columnZ = AXIS_Z[axis] == 0 ? z + along : AXIS_Z[axis] > 0 ? z + half : z - half - 1;
|
int nj = j + AXIS_Z[axis];
|
||||||
columns.add(((long) columnX << 32) | (columnZ & 0xFFFFFFFFL));
|
if (!mask.node(ni, nj)) {
|
||||||
|
int x = mask.originX() + ni;
|
||||||
|
int z = mask.originZ() + nj;
|
||||||
|
columns.add(((long) x << 32) | (z & 0xFFFFFFFFL));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -397,7 +395,7 @@ public final class PondPlanner {
|
|||||||
return xs.length == 0 ? Integer.MAX_VALUE : (int) Math.floor(lowest);
|
return xs.length == 0 ? Integer.MAX_VALUE : (int) Math.floor(lowest);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Pond pond(Site site, int level, Box box, float[] ground, boolean[] wet, Spill spill, int damRaise) {
|
private Pond pond(Site site, int level, Box box, float[] ground, boolean[] wet, Spill spill, int damRaise, PondMask mask) {
|
||||||
float lowest = Float.POSITIVE_INFINITY;
|
float lowest = Float.POSITIVE_INFINITY;
|
||||||
for (int node = 0; node < wet.length; node++) {
|
for (int node = 0; node < wet.length; node++) {
|
||||||
if (wet[node]) {
|
if (wet[node]) {
|
||||||
@@ -406,7 +404,7 @@ public final class PondPlanner {
|
|||||||
}
|
}
|
||||||
long id = CellHash.mix(settings.seed(), salt(site.kind()), site.floorX(), site.floorZ());
|
long id = CellHash.mix(settings.seed(), salt(site.kind()), site.floorX(), site.floorZ());
|
||||||
return new Pond(id, site.kind(), level, (int) Math.floor(lowest), spill.x(), spill.z(), spill.height(),
|
return new Pond(id, site.kind(), level, (int) Math.floor(lowest), spill.x(), spill.z(), spill.height(),
|
||||||
mask(box, wet), 0L, damRaise, site.fluid(), site.floorX(), site.floorZ());
|
mask, 0L, damRaise, site.fluid(), site.floorX(), site.floorZ());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean covered(DemGrid raw, Basin basin, List<Pond> lakes) {
|
private static boolean covered(DemGrid raw, Basin basin, List<Pond> lakes) {
|
||||||
@@ -681,7 +679,7 @@ public final class PondPlanner {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PondMask mask(Box box, boolean[] wet) {
|
private static PondMask mask(Box box, float[] ground, boolean[] wet, int level) {
|
||||||
int minI = Integer.MAX_VALUE;
|
int minI = Integer.MAX_VALUE;
|
||||||
int minJ = Integer.MAX_VALUE;
|
int minJ = Integer.MAX_VALUE;
|
||||||
int maxI = Integer.MIN_VALUE;
|
int maxI = Integer.MIN_VALUE;
|
||||||
@@ -698,6 +696,9 @@ public final class PondPlanner {
|
|||||||
minJ = Math.max(0, minJ - 1);
|
minJ = Math.max(0, minJ - 1);
|
||||||
maxI = Math.min(box.width() - 1, maxI + 1);
|
maxI = Math.min(box.width() - 1, maxI + 1);
|
||||||
maxJ = Math.min(box.depth() - 1, maxJ + 1);
|
maxJ = Math.min(box.depth() - 1, maxJ + 1);
|
||||||
|
if (box.step() < LAKE_STEP) {
|
||||||
|
return shorelineMask(box, ground, wet, level, minI, minJ, maxI, maxJ);
|
||||||
|
}
|
||||||
int width = maxI - minI + 1;
|
int width = maxI - minI + 1;
|
||||||
int depth = maxJ - minJ + 1;
|
int depth = maxJ - minJ + 1;
|
||||||
long[] bits = new long[(width * depth + 63) >>> 6];
|
long[] bits = new long[(width * depth + 63) >>> 6];
|
||||||
@@ -712,6 +713,46 @@ public final class PondPlanner {
|
|||||||
return new PondMask(box.originX() + minI * box.step(), box.originZ() + minJ * box.step(), box.step(), width, depth, bits);
|
return new PondMask(box.originX() + minI * box.step(), box.originZ() + minJ * box.step(), box.step(), width, depth, bits);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static PondMask shorelineMask(Box box, float[] ground, boolean[] wet, int level, int minI, int minJ, int maxI, int maxJ) {
|
||||||
|
int step = box.step();
|
||||||
|
int half = step >> 1;
|
||||||
|
int width = (maxI - minI + 1) * step;
|
||||||
|
int depth = (maxJ - minJ + 1) * step;
|
||||||
|
long[] bits = new long[(width * depth + 63) >>> 6];
|
||||||
|
for (int z = 0; z < depth; z++) {
|
||||||
|
int sampleZ = minJ * step + z - half;
|
||||||
|
int j = Math.floorDiv(sampleZ, step);
|
||||||
|
if (j < 0 || j + 1 >= box.depth()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
float v = Math.floorMod(sampleZ, step) / (float) step;
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
int sampleX = minI * step + x - half;
|
||||||
|
int i = Math.floorDiv(sampleX, step);
|
||||||
|
if (i < 0 || i + 1 >= box.width()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
float u = Math.floorMod(sampleX, step) / (float) step;
|
||||||
|
int node = j * box.width() + i;
|
||||||
|
if (!wet[node] && !wet[node + 1] && !wet[node + box.width()] && !wet[node + box.width() + 1]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
float first = lifted(box, ground, node) * (1f - u) + lifted(box, ground, node + 1) * u;
|
||||||
|
float second = lifted(box, ground, node + box.width()) * (1f - u)
|
||||||
|
+ lifted(box, ground, node + box.width() + 1) * u;
|
||||||
|
if (first * (1f - v) + second * v < level) {
|
||||||
|
int index = z * width + x;
|
||||||
|
bits[index >>> 6] |= 1L << index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new PondMask(box.originX() + minI * step - half, box.originZ() + minJ * step - half, 1, width, depth, bits);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float lifted(Box box, float[] ground, int node) {
|
||||||
|
return ground[node] - box.lift()[node];
|
||||||
|
}
|
||||||
|
|
||||||
private int freeboard(Site site) {
|
private int freeboard(Site site) {
|
||||||
return CupCarver.pick(settings.ponds().freeboard(),
|
return CupCarver.pick(settings.ponds().freeboard(),
|
||||||
CellHash.mix(settings.seed(), SALT_FREEBOARD, site.floorX(), site.floorZ()));
|
CellHash.mix(settings.seed(), SALT_FREEBOARD, site.floorX(), site.floorZ()));
|
||||||
|
|||||||
@@ -119,6 +119,17 @@ public final class DesktopUiMessages {
|
|||||||
public static final TextKey PREGEN_EXISTING = TextKey.of("iris.desktop.pregen.existing", "Existing");
|
public static final TextKey PREGEN_EXISTING = TextKey.of("iris.desktop.pregen.existing", "Existing");
|
||||||
public static final TextKey PREGEN_NETWORK = TextKey.of("iris.desktop.pregen.network", "Network");
|
public static final TextKey PREGEN_NETWORK = TextKey.of("iris.desktop.pregen.network", "Network");
|
||||||
public static final TextKey PREGEN_TERRAIN_HINT = TextKey.of("iris.desktop.pregen.terrain_hint", "Finished chunks show terrain colors");
|
public static final TextKey PREGEN_TERRAIN_HINT = TextKey.of("iris.desktop.pregen.terrain_hint", "Finished chunks show terrain colors");
|
||||||
|
public static final TextKey PREGEN_GRID = TextKey.of("iris.desktop.pregen.grid", "Grid");
|
||||||
|
public static final TextKey PREGEN_GRID_HINT = TextKey.of("iris.desktop.pregen.grid_hint", "Show chunk and region borders (G)");
|
||||||
|
public static final TextKey PREGEN_FIT = TextKey.of("iris.desktop.pregen.fit", "Reset view");
|
||||||
|
public static final TextKey PREGEN_FIT_HINT = TextKey.of("iris.desktop.pregen.fit_hint", "Fit the whole generation area (R)");
|
||||||
|
public static final TextKey PREGEN_ZOOM_IN = TextKey.of("iris.desktop.pregen.zoom_in", "Zoom in (+)");
|
||||||
|
public static final TextKey PREGEN_ZOOM_OUT = TextKey.of("iris.desktop.pregen.zoom_out", "Zoom out (-)");
|
||||||
|
public static final TextKey PREGEN_ZOOM = TextKey.of("iris.desktop.pregen.zoom", "Zoom {zoom}");
|
||||||
|
public static final TextKey PREGEN_SHORTCUTS = TextKey.of("iris.desktop.pregen.shortcuts", "Shortcuts");
|
||||||
|
public static final TextKey PREGEN_SHORTCUTS_HINT = TextKey.of("iris.desktop.pregen.shortcuts_hint", "Show mouse and key shortcuts (/)");
|
||||||
|
public static final TextKey PREGEN_NAVIGATION_HINT = TextKey.of("iris.desktop.pregen.navigation_hint", "Drag to pan · Scroll to zoom · Hover to inspect chunks");
|
||||||
|
public static final TextKey PREGEN_SHORTCUT_DETAILS = TextKey.of("iris.desktop.pregen.shortcut_details", "Drag or arrow keys: pan\nScroll or + / -: zoom\nR or \\: fit generation area\nG: chunk and region grid\nP: pause or resume generation\n/ or Escape: close shortcuts");
|
||||||
public static final TextKey IMAGEMAP_TITLE = TextKey.of("iris.desktop.imagemap.title", "Image Map Studio");
|
public static final TextKey IMAGEMAP_TITLE = TextKey.of("iris.desktop.imagemap.title", "Image Map Studio");
|
||||||
public static final TextKey IMAGEMAP_PRESET = TextKey.of("iris.desktop.imagemap.preset", "Preset:");
|
public static final TextKey IMAGEMAP_PRESET = TextKey.of("iris.desktop.imagemap.preset", "Preset:");
|
||||||
public static final TextKey IMAGEMAP_LOAD = TextKey.of("iris.desktop.imagemap.load", "Load");
|
public static final TextKey IMAGEMAP_LOAD = TextKey.of("iris.desktop.imagemap.load", "Load");
|
||||||
@@ -216,7 +227,9 @@ public final class DesktopUiMessages {
|
|||||||
PREGEN_METHOD, PREGEN_CACHED, PREGEN_FAILED, PREGEN_MAP,
|
PREGEN_METHOD, PREGEN_CACHED, PREGEN_FAILED, PREGEN_MAP,
|
||||||
PREGEN_CHUNK_COORDINATES, PREGEN_WINDOW_HINT,
|
PREGEN_CHUNK_COORDINATES, PREGEN_WINDOW_HINT,
|
||||||
PREGEN_BOUNDS, PREGEN_WAITING, PREGEN_READY, PREGEN_EXISTING,
|
PREGEN_BOUNDS, PREGEN_WAITING, PREGEN_READY, PREGEN_EXISTING,
|
||||||
PREGEN_NETWORK, PREGEN_TERRAIN_HINT,
|
PREGEN_NETWORK, PREGEN_TERRAIN_HINT, PREGEN_GRID, PREGEN_GRID_HINT,
|
||||||
|
PREGEN_FIT, PREGEN_FIT_HINT, PREGEN_ZOOM_IN, PREGEN_ZOOM_OUT, PREGEN_ZOOM,
|
||||||
|
PREGEN_SHORTCUTS, PREGEN_SHORTCUTS_HINT, PREGEN_NAVIGATION_HINT, PREGEN_SHORTCUT_DETAILS,
|
||||||
IMAGEMAP_TITLE, IMAGEMAP_PRESET, IMAGEMAP_LOAD, IMAGEMAP_IMPORT_PNG,
|
IMAGEMAP_TITLE, IMAGEMAP_PRESET, IMAGEMAP_LOAD, IMAGEMAP_IMPORT_PNG,
|
||||||
IMAGEMAP_REPLACE_PNG, IMAGEMAP_PREVIEW, IMAGEMAP_EXPORT, IMAGEMAP_METADATA,
|
IMAGEMAP_REPLACE_PNG, IMAGEMAP_PREVIEW, IMAGEMAP_EXPORT, IMAGEMAP_METADATA,
|
||||||
IMAGEMAP_RESOURCE, IMAGEMAP_COORDINATES, IMAGEMAP_BINDING_KEY, IMAGEMAP_MAP_KEY,
|
IMAGEMAP_RESOURCE, IMAGEMAP_COORDINATES, IMAGEMAP_BINDING_KEY, IMAGEMAP_MAP_KEY,
|
||||||
|
|||||||
@@ -388,10 +388,6 @@ public class StudioSVC implements IrisService {
|
|||||||
fingerprint,
|
fingerprint,
|
||||||
aliasPolicy
|
aliasPolicy
|
||||||
);
|
);
|
||||||
if (!fingerprint.equals(GenerationPackFingerprint.compute(
|
|
||||||
packRoot, GenerationPackFingerprint.CURRENT_VERSION))) {
|
|
||||||
throw new IOException("Generation pack changed during validation: " + packRoot);
|
|
||||||
}
|
|
||||||
return new GenerationCandidate(fingerprint, dimensionContract, registryContract);
|
return new GenerationCandidate(fingerprint, dimensionContract, registryContract);
|
||||||
} finally {
|
} finally {
|
||||||
data.close();
|
data.close();
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import java.awt.event.MouseEvent;
|
|||||||
* Lays an island over content that fills the whole area behind it. Dragging the island's right edge resizes it,
|
* Lays an island over content that fills the whole area behind it. Dragging the island's right edge resizes it,
|
||||||
* always leaving the content some room beside it.
|
* always leaving the content some room beside it.
|
||||||
*/
|
*/
|
||||||
final class IslandOverlay extends JLayeredPane {
|
public final class IslandOverlay extends JLayeredPane {
|
||||||
private static final long serialVersionUID = -4182209135360718342L;
|
private static final long serialVersionUID = -4182209135360718342L;
|
||||||
|
|
||||||
private final IslandPanel island;
|
private final IslandPanel island;
|
||||||
@@ -38,7 +38,7 @@ final class IslandOverlay extends JLayeredPane {
|
|||||||
private final int minimumUncoveredWidth;
|
private final int minimumUncoveredWidth;
|
||||||
private int islandWidth;
|
private int islandWidth;
|
||||||
|
|
||||||
IslandOverlay(IslandPanel island, JComponent background, int islandWidth, int minimumIslandWidth, int minimumUncoveredWidth) {
|
public IslandOverlay(IslandPanel island, JComponent background, int islandWidth, int minimumIslandWidth, int minimumUncoveredWidth) {
|
||||||
this.island = island;
|
this.island = island;
|
||||||
this.background = background;
|
this.background = background;
|
||||||
this.islandWidth = islandWidth;
|
this.islandWidth = islandWidth;
|
||||||
|
|||||||
@@ -34,13 +34,13 @@ import java.util.List;
|
|||||||
* islands theme. The gap belongs to the content behind it, except for the strip along the right edge, which grabs the
|
* islands theme. The gap belongs to the content behind it, except for the strip along the right edge, which grabs the
|
||||||
* island to resize it.
|
* island to resize it.
|
||||||
*/
|
*/
|
||||||
final class IslandPanel extends JPanel implements FloatingPanels.PartlySeeThrough {
|
public final class IslandPanel extends JPanel implements FloatingPanels.PartlySeeThrough {
|
||||||
private static final long serialVersionUID = 7313084027614358519L;
|
private static final long serialVersionUID = 7313084027614358519L;
|
||||||
private static final int GAP = 6;
|
private static final int GAP = 6;
|
||||||
private static final int CORNER_ARC = 12;
|
private static final int CORNER_ARC = 12;
|
||||||
private static final int INNER_PADDING = 4;
|
private static final int INNER_PADDING = 4;
|
||||||
|
|
||||||
IslandPanel(JComponent content) {
|
public IslandPanel(JComponent content) {
|
||||||
super(new BorderLayout());
|
super(new BorderLayout());
|
||||||
setOpaque(false);
|
setOpaque(false);
|
||||||
int inset = GAP + INNER_PADDING;
|
int inset = GAP + INNER_PADDING;
|
||||||
|
|||||||
@@ -724,10 +724,13 @@ final class MapCanvas extends JPanel {
|
|||||||
int tileHeight = Math.max(1, (int) Math.round(worldToScreenZ(worldZ + frameTileSpan)) - screenY);
|
int tileHeight = Math.max(1, (int) Math.round(worldToScreenZ(worldZ + frameTileSpan)) - screenY);
|
||||||
if (tile.tile() != null) {
|
if (tile.tile() != null) {
|
||||||
canvas.drawImage(tile.tile().image(style), screenX, screenY, tileWidth, tileHeight, null);
|
canvas.drawImage(tile.tile().image(style), screenX, screenY, tileWidth, tileHeight, null);
|
||||||
continue;
|
if (!tile.provisional()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
canvas.setColor(((tile.tileX() ^ tile.tileZ()) & 1L) == 0L ? TILE_EMPTY_A : TILE_EMPTY_B);
|
||||||
|
canvas.fillRect(screenX, screenY, tileWidth, tileHeight);
|
||||||
}
|
}
|
||||||
canvas.setColor(((tile.tileX() ^ tile.tileZ()) & 1L) == 0L ? TILE_EMPTY_A : TILE_EMPTY_B);
|
|
||||||
canvas.fillRect(screenX, screenY, tileWidth, tileHeight);
|
|
||||||
drawFallback(canvas, controller.fallback(frame, tile), screenX, screenY, tileWidth, tileHeight);
|
drawFallback(canvas, controller.fallback(frame, tile), screenX, screenY, tileWidth, tileHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ public final class MapHost {
|
|||||||
return GuiHost.isAvailable();
|
return GuiHost.isAvailable();
|
||||||
}
|
}
|
||||||
|
|
||||||
static JFrame createFrame(String title) {
|
public static JFrame createFrame(String title) {
|
||||||
XwaylandWindowManagerHint.apply();
|
XwaylandWindowManagerHint.apply();
|
||||||
StudioTheme.install();
|
StudioTheme.install();
|
||||||
JFrame frame = new JFrame(title);
|
JFrame frame = new JFrame(title);
|
||||||
@@ -73,7 +73,7 @@ public final class MapHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A scaled-up default size can be bigger than a small or low resolution screen.
|
// A scaled-up default size can be bigger than a small or low resolution screen.
|
||||||
static Dimension fitToScreen(Dimension preferred) {
|
public static Dimension fitToScreen(Dimension preferred) {
|
||||||
Rectangle usable = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
|
Rectangle usable = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
|
||||||
return new Dimension(Math.min(preferred.width, usable.width), Math.min(preferred.height, usable.height));
|
return new Dimension(Math.min(preferred.width, usable.width), Math.min(preferred.height, usable.height));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,20 +45,20 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
* The colours of IntelliJ's "New UI", light and dark, shared by FlatLaf's controls and everything the studio windows
|
* The colours of IntelliJ's "New UI", light and dark, shared by FlatLaf's controls and everything the studio windows
|
||||||
* draw themselves. The theme follows the system unless the user picked light or dark.
|
* draw themselves. The theme follows the system unless the user picked light or dark.
|
||||||
*/
|
*/
|
||||||
final class StudioTheme {
|
public final class StudioTheme {
|
||||||
static final Color EDITOR_BACKGROUND = new ThemeColor(0xFFFFFF, 0x1E1F22);
|
public static final Color EDITOR_BACKGROUND = new ThemeColor(0xFFFFFF, 0x1E1F22);
|
||||||
static final Color PANEL_BACKGROUND = new ThemeColor(0xF7F8FA, 0x2B2D30);
|
public static final Color PANEL_BACKGROUND = new ThemeColor(0xF7F8FA, 0x2B2D30);
|
||||||
static final Color HOVER_BACKGROUND = new ThemeColor(0xDFE1E5, 0x393B40);
|
static final Color HOVER_BACKGROUND = new ThemeColor(0xDFE1E5, 0x393B40);
|
||||||
static final Color INACTIVE_SELECTION = new ThemeColor(0xDFE1E5, 0x43454A);
|
static final Color INACTIVE_SELECTION = new ThemeColor(0xDFE1E5, 0x43454A);
|
||||||
static final Color SELECTION = new ThemeColor(0xD4E2FF, 0x2E436E);
|
static final Color SELECTION = new ThemeColor(0xD4E2FF, 0x2E436E);
|
||||||
static final Color CONTROL_BORDER = new ThemeColor(0xC9CCD6, 0x4E5157);
|
public static final Color CONTROL_BORDER = new ThemeColor(0xC9CCD6, 0x4E5157);
|
||||||
static final Color SEPARATOR = new ThemeColor(0xEBECF0, 0x1E1F22);
|
static final Color SEPARATOR = new ThemeColor(0xEBECF0, 0x1E1F22);
|
||||||
static final Color TEXT = new ThemeColor(0x000000, 0xDFE1E5);
|
public static final Color TEXT = new ThemeColor(0x000000, 0xDFE1E5);
|
||||||
static final ThemeColor SECONDARY_TEXT = new ThemeColor(0x6C707E, 0x9DA0A8);
|
public static final ThemeColor SECONDARY_TEXT = new ThemeColor(0x6C707E, 0x9DA0A8);
|
||||||
static final Color MUTED_TEXT = new ThemeColor(0x818594, 0x6F737A);
|
static final Color MUTED_TEXT = new ThemeColor(0x818594, 0x6F737A);
|
||||||
static final Color ACCENT = new Color(0x3574F0);
|
public static final Color ACCENT = new Color(0x3574F0);
|
||||||
static final Color LINK = new ThemeColor(0x315FBD, 0x548AF7);
|
static final Color LINK = new ThemeColor(0x315FBD, 0x548AF7);
|
||||||
static final Color ERROR = new ThemeColor(0xDB3B4B, 0xF75464);
|
public static final Color ERROR = new ThemeColor(0xDB3B4B, 0xF75464);
|
||||||
static final Color WARNING = new ThemeColor(0xC27D04, 0xF2C55C);
|
static final Color WARNING = new ThemeColor(0xC27D04, 0xF2C55C);
|
||||||
static final Color SUCCESS = new ThemeColor(0x208A3C, 0x5FB865);
|
static final Color SUCCESS = new ThemeColor(0x208A3C, 0x5FB865);
|
||||||
static final Color OVERLAY_BACKGROUND = new ThemeColor(new Color(0xFF, 0xFF, 0xFF, 240), new Color(0x2B, 0x2D, 0x30, 236));
|
static final Color OVERLAY_BACKGROUND = new ThemeColor(new Color(0xFF, 0xFF, 0xFF, 240), new Color(0x2B, 0x2D, 0x30, 236));
|
||||||
@@ -231,7 +231,7 @@ final class StudioTheme {
|
|||||||
/**
|
/**
|
||||||
* FlatLaf grows fonts and its own controls to the screen's DPI; every size the studio sets itself has to grow with them.
|
* FlatLaf grows fonts and its own controls to the screen's DPI; every size the studio sets itself has to grow with them.
|
||||||
*/
|
*/
|
||||||
static int scale(int pixels) {
|
public static int scale(int pixels) {
|
||||||
return Math.round(pixels * pixelScale);
|
return Math.round(pixels * pixelScale);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,27 +239,27 @@ final class StudioTheme {
|
|||||||
return pixels * pixelScale;
|
return pixels * pixelScale;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Dimension scaledSize(int width, int height) {
|
public static Dimension scaledSize(int width, int height) {
|
||||||
return new Dimension(scale(width), scale(height));
|
return new Dimension(scale(width), scale(height));
|
||||||
}
|
}
|
||||||
|
|
||||||
static Border emptyBorder(int top, int left, int bottom, int right) {
|
public static Border emptyBorder(int top, int left, int bottom, int right) {
|
||||||
return BorderFactory.createEmptyBorder(scale(top), scale(left), scale(bottom), scale(right));
|
return BorderFactory.createEmptyBorder(scale(top), scale(left), scale(bottom), scale(right));
|
||||||
}
|
}
|
||||||
|
|
||||||
static Font font() {
|
public static Font font() {
|
||||||
return themeFont("defaultFont", Font.PLAIN, 13);
|
return themeFont("defaultFont", Font.PLAIN, 13);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Font smallFont() {
|
public static Font smallFont() {
|
||||||
return themeFont("small.font", Font.PLAIN, 11);
|
return themeFont("small.font", Font.PLAIN, 11);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Font boldFont() {
|
public static Font boldFont() {
|
||||||
return font().deriveFont(Font.BOLD);
|
return font().deriveFont(Font.BOLD);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Font titleFont() {
|
public static Font titleFont() {
|
||||||
return font().deriveFont(Font.BOLD, font().getSize2D() + 1F);
|
return font().deriveFont(Font.BOLD, font().getSize2D() + 1F);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,13 +40,13 @@ import java.awt.Insets;
|
|||||||
/**
|
/**
|
||||||
* Builds the IntelliJ style controls the studio windows share: a flat main toolbar, borderless toolbar buttons and search boxes.
|
* Builds the IntelliJ style controls the studio windows share: a flat main toolbar, borderless toolbar buttons and search boxes.
|
||||||
*/
|
*/
|
||||||
final class StudioToolbar {
|
public final class StudioToolbar {
|
||||||
private static final int TOOLBAR_HEIGHT_PADDING = 5;
|
private static final int TOOLBAR_HEIGHT_PADDING = 5;
|
||||||
|
|
||||||
private StudioToolbar() {
|
private StudioToolbar() {
|
||||||
}
|
}
|
||||||
|
|
||||||
static JToolBar create() {
|
public static JToolBar create() {
|
||||||
JToolBar toolbar = new JToolBar();
|
JToolBar toolbar = new JToolBar();
|
||||||
toolbar.setFloatable(false);
|
toolbar.setFloatable(false);
|
||||||
toolbar.setRollover(true);
|
toolbar.setRollover(true);
|
||||||
@@ -65,7 +65,7 @@ final class StudioToolbar {
|
|||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
|
|
||||||
static JButton button(String text, String tooltip, Runnable action) {
|
public static JButton button(String text, String tooltip, Runnable action) {
|
||||||
JButton button = toolbarButton(text, tooltip);
|
JButton button = toolbarButton(text, tooltip);
|
||||||
button.addActionListener(event -> action.run());
|
button.addActionListener(event -> action.run());
|
||||||
return button;
|
return button;
|
||||||
@@ -74,7 +74,7 @@ final class StudioToolbar {
|
|||||||
/**
|
/**
|
||||||
* Opens a menu to pick light, dark or the system's theme.
|
* Opens a menu to pick light, dark or the system's theme.
|
||||||
*/
|
*/
|
||||||
static JButton themeButton() {
|
public static JButton themeButton() {
|
||||||
JButton button = toolbarButton("Theme", "Light, dark, or whatever the system uses");
|
JButton button = toolbarButton("Theme", "Light, dark, or whatever the system uses");
|
||||||
button.addActionListener(event -> themeMenu().show(button, 0, button.getHeight()));
|
button.addActionListener(event -> themeMenu().show(button, 0, button.getHeight()));
|
||||||
return button;
|
return button;
|
||||||
@@ -102,7 +102,7 @@ final class StudioToolbar {
|
|||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
static JToggleButton toggle(String text, String tooltip) {
|
public static JToggleButton toggle(String text, String tooltip) {
|
||||||
JToggleButton toggle = new JToggleButton(text);
|
JToggleButton toggle = new JToggleButton(text);
|
||||||
toggle.setToolTipText(tooltip);
|
toggle.setToolTipText(tooltip);
|
||||||
toggle.setFocusable(false);
|
toggle.setFocusable(false);
|
||||||
@@ -134,7 +134,7 @@ final class StudioToolbar {
|
|||||||
return panel;
|
return panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
static JScrollPane scroll(JComponent content) {
|
public static JScrollPane scroll(JComponent content) {
|
||||||
JScrollPane scrollPane = new JScrollPane(content);
|
JScrollPane scrollPane = new JScrollPane(content);
|
||||||
scrollPane.setBorder(BorderFactory.createEmptyBorder());
|
scrollPane.setBorder(BorderFactory.createEmptyBorder());
|
||||||
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
|
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
|
||||||
|
|||||||
@@ -578,6 +578,10 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
return tile;
|
return tile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean provisional() {
|
||||||
|
return provisional;
|
||||||
|
}
|
||||||
|
|
||||||
private void setTile(MapTile tile) {
|
private void setTile(MapTile tile) {
|
||||||
this.tile = tile;
|
this.tile = tile;
|
||||||
provisional = false;
|
provisional = false;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ package art.arcane.iris.studio.map.components;
|
|||||||
|
|
||||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
|
||||||
import art.arcane.iris.generation.terrain.InferredType;
|
import art.arcane.iris.generation.terrain.InferredType;
|
||||||
import art.arcane.iris.generation.biome.IrisBiome;
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
@@ -166,7 +167,15 @@ public final class MapTileRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private double[] sampleHeights(TileGrid grid) {
|
private double[] sampleHeights(TileGrid grid) {
|
||||||
return new HeightLattice(grid, engine.getComplex().getHeightStream()).interpolatedHeights();
|
IrisComplex complex = engine.getComplex();
|
||||||
|
IrisDimension dimension = engine.getDimension();
|
||||||
|
HydrologyRuntime water = complex.getHydrologyRuntime();
|
||||||
|
if (water == null || !water.enabled() || dimension != null && dimension.getTerrainTransform() != null) {
|
||||||
|
return new HeightLattice(grid, complex.getHeightStream()).interpolatedHeights();
|
||||||
|
}
|
||||||
|
try (HydrologyRuntime.PreviewScope ignored = water.preview()) {
|
||||||
|
return new HeightLattice(grid, complex.getHeightStream()).interpolatedHeights();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Large uniform squares are filled after a few samples; squares stay small enough to not skip tiny biomes.
|
// Large uniform squares are filled after a few samples; squares stay small enough to not skip tiny biomes.
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ final class WaterMap {
|
|||||||
lakes.add(lake.id());
|
lakes.add(lake.id());
|
||||||
}
|
}
|
||||||
Map<Long, ChunkWaterRaster> rasters = new HashMap<>();
|
Map<Long, ChunkWaterRaster> rasters = new HashMap<>();
|
||||||
|
WaterMap planned = null;
|
||||||
for (int pixelZ = 0; pixelZ < resolution; pixelZ++) {
|
for (int pixelZ = 0; pixelZ < resolution; pixelZ++) {
|
||||||
checkCancelled();
|
checkCancelled();
|
||||||
int minimumZ = (int) Math.floor(startZ + pixelZ * step);
|
int minimumZ = (int) Math.floor(startZ + pixelZ * step);
|
||||||
@@ -102,6 +103,11 @@ final class WaterMap {
|
|||||||
ChunkWaterRaster raster = raster(rasters, x >> 4, z >> 4);
|
ChunkWaterRaster raster = raster(rasters, x >> 4, z >> 4);
|
||||||
if (raster.provisional()) {
|
if (raster.provisional()) {
|
||||||
provisional = true;
|
provisional = true;
|
||||||
|
if (planned == null) {
|
||||||
|
planned = new WaterMap(runtime, resolution, startX, startZ, step, cancelled);
|
||||||
|
planned.drawPlans(plans);
|
||||||
|
}
|
||||||
|
stamp(pixelX, pixelZ, planned.at(pixelX, pixelZ));
|
||||||
} else if (!raster.empty()) {
|
} else if (!raster.empty()) {
|
||||||
stamp(pixelX, pixelZ, columnType(raster, x & 15, z & 15, lakes));
|
stamp(pixelX, pixelZ, columnType(raster, x & 15, z & 15, lakes));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,32 +2,60 @@ package art.arcane.iris.studio.view;
|
|||||||
|
|
||||||
import art.arcane.iris.localization.DesktopUiMessages;
|
import art.arcane.iris.localization.DesktopUiMessages;
|
||||||
import art.arcane.iris.localization.IrisLanguage;
|
import art.arcane.iris.localization.IrisLanguage;
|
||||||
|
import art.arcane.iris.studio.map.StudioTheme;
|
||||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||||
|
|
||||||
import javax.swing.JPanel;
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.SwingUtilities;
|
||||||
import javax.swing.ToolTipManager;
|
import javax.swing.ToolTipManager;
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
|
import java.awt.Cursor;
|
||||||
|
import java.awt.FontMetrics;
|
||||||
import java.awt.Graphics;
|
import java.awt.Graphics;
|
||||||
import java.awt.Graphics2D;
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.Point;
|
||||||
import java.awt.Rectangle;
|
import java.awt.Rectangle;
|
||||||
import java.awt.RenderingHints;
|
import java.awt.RenderingHints;
|
||||||
|
import java.awt.event.MouseAdapter;
|
||||||
import java.awt.event.MouseEvent;
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.awt.event.MouseWheelEvent;
|
||||||
|
import java.awt.geom.AffineTransform;
|
||||||
|
import java.awt.geom.Rectangle2D;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.awt.image.DataBufferInt;
|
import java.awt.image.DataBufferInt;
|
||||||
|
|
||||||
final class PregenMapPanel extends JPanel {
|
final class PregenMapPanel extends JPanel {
|
||||||
|
private static final double MIN_ZOOM = 0.25D;
|
||||||
|
private static final double MAX_ZOOM = 128D;
|
||||||
|
private static final double ZOOM_STEP = 1.25D;
|
||||||
|
|
||||||
private final PregenRenderSnapshot.Bounds bounds;
|
private final PregenRenderSnapshot.Bounds bounds;
|
||||||
private final PregenMapState state;
|
private final PregenMapState state;
|
||||||
private final PregenMapState.View view;
|
private final PregenMapState.View view;
|
||||||
private final BufferedImage image;
|
private final BufferedImage image;
|
||||||
|
private final String[] shortcuts;
|
||||||
private volatile boolean disposed;
|
private volatile boolean disposed;
|
||||||
|
private Runnable viewListener;
|
||||||
|
private Point drag;
|
||||||
|
private Point hover;
|
||||||
|
private double centerX = 0.5D;
|
||||||
|
private double centerZ = 0.5D;
|
||||||
|
private double zoom = 1D;
|
||||||
|
private int coveredLeftWidth;
|
||||||
|
private boolean gridVisible;
|
||||||
|
private boolean helpVisible;
|
||||||
|
|
||||||
PregenMapPanel(PregenMapState state) {
|
PregenMapPanel(PregenMapState state) {
|
||||||
this.state = state;
|
this.state = state;
|
||||||
bounds = state.bounds();
|
bounds = state.bounds();
|
||||||
image = new BufferedImage(state.width(), state.height(), BufferedImage.TYPE_INT_RGB);
|
image = new BufferedImage(state.width(), state.height(), BufferedImage.TYPE_INT_RGB);
|
||||||
|
image.setAccelerationPriority(0F);
|
||||||
view = state.attach(((DataBufferInt) image.getRaster().getDataBuffer()).getData());
|
view = state.attach(((DataBufferInt) image.getRaster().getDataBuffer()).getData());
|
||||||
setBackground(PregenRenderer.BACKGROUND);
|
shortcuts = IrisLanguage.plain(DesktopUiMessages.PREGEN_SHORTCUT_DETAILS).split("\n");
|
||||||
|
setBackground(StudioTheme.EDITOR_BACKGROUND);
|
||||||
|
setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
|
||||||
|
setFocusable(true);
|
||||||
|
installNavigation();
|
||||||
ToolTipManager.sharedInstance().registerComponent(this);
|
ToolTipManager.sharedInstance().registerComponent(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,23 +71,83 @@ final class PregenMapPanel extends JPanel {
|
|||||||
|
|
||||||
void disposeMap() {
|
void disposeMap() {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
|
viewListener = null;
|
||||||
ToolTipManager.sharedInstance().unregisterComponent(this);
|
ToolTipManager.sharedInstance().unregisterComponent(this);
|
||||||
view.close();
|
view.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setViewListener(Runnable listener) {
|
||||||
|
viewListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCoveredLeftWidth(int width) {
|
||||||
|
coveredLeftWidth = Math.max(0, width);
|
||||||
|
changed();
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetView() {
|
||||||
|
centerX = 0.5D;
|
||||||
|
centerZ = 0.5D;
|
||||||
|
zoom = 1D;
|
||||||
|
changed();
|
||||||
|
}
|
||||||
|
|
||||||
|
double zoom() {
|
||||||
|
return zoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
void zoomIn() {
|
||||||
|
zoomAt(ZOOM_STEP, (getWidth() + coveredLeftWidth) / 2D, getHeight() / 2D);
|
||||||
|
}
|
||||||
|
|
||||||
|
void zoomOut() {
|
||||||
|
zoomAt(1D / ZOOM_STEP, (getWidth() + coveredLeftWidth) / 2D, getHeight() / 2D);
|
||||||
|
}
|
||||||
|
|
||||||
|
void zoomAt(double factor, double anchorX, double anchorY) {
|
||||||
|
Rectangle2D.Double before = mapArea();
|
||||||
|
double mapX = (anchorX - before.x) / before.width;
|
||||||
|
double mapZ = (anchorY - before.y) / before.height;
|
||||||
|
zoom = Math.clamp(zoom * factor, MIN_ZOOM, MAX_ZOOM);
|
||||||
|
Rectangle2D.Double after = mapArea();
|
||||||
|
centerX += mapX - (anchorX - after.x) / after.width;
|
||||||
|
centerZ += mapZ - (anchorY - after.y) / after.height;
|
||||||
|
changed();
|
||||||
|
}
|
||||||
|
|
||||||
|
void panBy(double pixelsX, double pixelsY) {
|
||||||
|
Rectangle2D.Double area = mapArea();
|
||||||
|
centerX -= pixelsX / area.width;
|
||||||
|
centerZ -= pixelsY / area.height;
|
||||||
|
changed();
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleGrid() {
|
||||||
|
gridVisible = !gridVisible;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isGridVisible() {
|
||||||
|
return gridVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleHelp() {
|
||||||
|
helpVisible = !helpVisible;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
void dismissHelp() {
|
||||||
|
helpVisible = false;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
String hoverCoordinates() {
|
||||||
|
return hover == null ? null : coordinatesAt(hover.x, hover.y);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getToolTipText(MouseEvent event) {
|
public String getToolTipText(MouseEvent event) {
|
||||||
if (disposed) {
|
return coordinatesAt(event.getX(), event.getY());
|
||||||
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
|
@Override
|
||||||
@@ -67,11 +155,20 @@ final class PregenMapPanel extends JPanel {
|
|||||||
super.paintComponent(graphics);
|
super.paintComponent(graphics);
|
||||||
Graphics2D canvas = (Graphics2D) graphics.create();
|
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||||
try {
|
try {
|
||||||
Rectangle area = fit(bounds, getWidth(), getHeight());
|
Rectangle2D.Double area = mapArea();
|
||||||
canvas.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
|
canvas.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
|
||||||
canvas.drawImage(image, area.x, area.y, area.width, area.height, null);
|
AffineTransform transform = AffineTransform.getTranslateInstance(area.x, area.y);
|
||||||
canvas.setColor(PregenRenderer.BORDER);
|
transform.scale(area.width / image.getWidth(), area.height / image.getHeight());
|
||||||
canvas.drawRect(area.x, area.y, area.width - 1, area.height - 1);
|
canvas.drawImage(image, transform, null);
|
||||||
|
if (gridVisible) {
|
||||||
|
paintGrid(canvas, area);
|
||||||
|
}
|
||||||
|
canvas.setColor(StudioTheme.CONTROL_BORDER);
|
||||||
|
canvas.draw(area);
|
||||||
|
paintHover(canvas, area);
|
||||||
|
if (helpVisible) {
|
||||||
|
paintHelp(canvas);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
canvas.dispose();
|
canvas.dispose();
|
||||||
}
|
}
|
||||||
@@ -85,4 +182,150 @@ final class PregenMapPanel extends JPanel {
|
|||||||
int mapHeight = Math.max(1, (int) Math.floor(bounds.height() * scale));
|
int mapHeight = Math.max(1, (int) Math.floor(bounds.height() * scale));
|
||||||
return new Rectangle((width - mapWidth) / 2, (height - mapHeight) / 2, mapWidth, mapHeight);
|
return new Rectangle((width - mapWidth) / 2, (height - mapHeight) / 2, mapWidth, mapHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Rectangle2D.Double mapArea() {
|
||||||
|
Rectangle fit = fit(bounds, Math.max(1, getWidth() - coveredLeftWidth), getHeight());
|
||||||
|
double width = fit.width * zoom;
|
||||||
|
double height = fit.height * zoom;
|
||||||
|
return new Rectangle2D.Double(coveredLeftWidth + fit.getCenterX() - centerX * width,
|
||||||
|
fit.getCenterY() - centerZ * height, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String coordinatesAt(int x, int y) {
|
||||||
|
if (disposed || x < coveredLeftWidth) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Rectangle2D.Double area = mapArea();
|
||||||
|
if (!area.contains(x, y)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
long chunkX = bounds.minX() + (long) Math.floor((x - area.x) * bounds.width() / area.width);
|
||||||
|
long chunkZ = bounds.minZ() + (long) Math.floor((y - area.y) * bounds.height() / area.height);
|
||||||
|
return IrisLanguage.plain(DesktopUiMessages.PREGEN_CHUNK_COORDINATES,
|
||||||
|
MessageArgument.trusted("x", chunkX), MessageArgument.trusted("z", chunkZ));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void installNavigation() {
|
||||||
|
MouseAdapter navigation = new MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mousePressed(MouseEvent event) {
|
||||||
|
if (SwingUtilities.isLeftMouseButton(event)) {
|
||||||
|
requestFocusInWindow();
|
||||||
|
drag = event.getPoint();
|
||||||
|
setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseReleased(MouseEvent event) {
|
||||||
|
drag = null;
|
||||||
|
setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseDragged(MouseEvent event) {
|
||||||
|
if (drag != null) {
|
||||||
|
hover = event.getPoint();
|
||||||
|
panBy(event.getX() - drag.x, event.getY() - drag.y);
|
||||||
|
drag = hover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseMoved(MouseEvent event) {
|
||||||
|
hover = event.getPoint();
|
||||||
|
changed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseExited(MouseEvent event) {
|
||||||
|
hover = null;
|
||||||
|
changed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseWheelMoved(MouseWheelEvent event) {
|
||||||
|
zoomAt(Math.pow(ZOOM_STEP, -event.getPreciseWheelRotation()), event.getX(), event.getY());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
addMouseListener(navigation);
|
||||||
|
addMouseMotionListener(navigation);
|
||||||
|
addMouseWheelListener(navigation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void paintGrid(Graphics2D canvas, Rectangle2D.Double area) {
|
||||||
|
if (!area.intersects(0, 0, getWidth(), getHeight())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
double chunkWidth = area.width / bounds.width();
|
||||||
|
double chunkHeight = area.height / bounds.height();
|
||||||
|
long step = Math.min(chunkWidth, chunkHeight) >= 24D ? 1 : 32;
|
||||||
|
while (step * Math.min(chunkWidth, chunkHeight) < 24D) {
|
||||||
|
step *= 2;
|
||||||
|
}
|
||||||
|
canvas.setColor(StudioTheme.CONTROL_BORDER);
|
||||||
|
long minX = Math.max(bounds.minX(), (long) Math.floor(bounds.minX() - area.x / chunkWidth));
|
||||||
|
long maxX = Math.min((long) bounds.maxX() + 1, (long) Math.ceil(bounds.minX() + (getWidth() - area.x) / chunkWidth));
|
||||||
|
long minZ = Math.max(bounds.minZ(), (long) Math.floor(bounds.minZ() - area.y / chunkHeight));
|
||||||
|
long maxZ = Math.min((long) bounds.maxZ() + 1, (long) Math.ceil(bounds.minZ() + (getHeight() - area.y) / chunkHeight));
|
||||||
|
int top = (int) Math.max(0, area.y);
|
||||||
|
int bottom = (int) Math.min(getHeight(), area.getMaxY());
|
||||||
|
int left = (int) Math.max(0, area.x);
|
||||||
|
int right = (int) Math.min(getWidth(), area.getMaxX());
|
||||||
|
for (long x = Math.ceilDiv(minX, step) * step; x <= maxX; x += step) {
|
||||||
|
int screenX = (int) Math.round(area.x + (x - bounds.minX()) * chunkWidth);
|
||||||
|
canvas.drawLine(screenX, top, screenX, bottom);
|
||||||
|
}
|
||||||
|
for (long z = Math.ceilDiv(minZ, step) * step; z <= maxZ; z += step) {
|
||||||
|
int screenZ = (int) Math.round(area.y + (z - bounds.minZ()) * chunkHeight);
|
||||||
|
canvas.drawLine(left, screenZ, right, screenZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void paintHover(Graphics2D canvas, Rectangle2D.Double area) {
|
||||||
|
if (hover == null || hover.x < coveredLeftWidth || !area.contains(hover)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
double chunkWidth = area.width / bounds.width();
|
||||||
|
double chunkHeight = area.height / bounds.height();
|
||||||
|
if (Math.min(chunkWidth, chunkHeight) < 4D) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
double x = area.x + Math.floor((hover.x - area.x) / chunkWidth) * chunkWidth;
|
||||||
|
double y = area.y + Math.floor((hover.y - area.y) / chunkHeight) * chunkHeight;
|
||||||
|
canvas.setColor(StudioTheme.ACCENT);
|
||||||
|
canvas.draw(new Rectangle2D.Double(x, y, chunkWidth, chunkHeight));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void paintHelp(Graphics2D canvas) {
|
||||||
|
canvas.setFont(StudioTheme.font());
|
||||||
|
FontMetrics metrics = canvas.getFontMetrics();
|
||||||
|
int padding = StudioTheme.scale(14);
|
||||||
|
int width = 0;
|
||||||
|
for (String line : shortcuts) {
|
||||||
|
width = Math.max(width, metrics.stringWidth(line));
|
||||||
|
}
|
||||||
|
width += padding * 2;
|
||||||
|
int height = metrics.getHeight() * shortcuts.length + padding * 2;
|
||||||
|
int x = Math.max(coveredLeftWidth + padding, getWidth() - width - padding);
|
||||||
|
int arc = StudioTheme.scale(10);
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
canvas.setColor(StudioTheme.PANEL_BACKGROUND);
|
||||||
|
canvas.fillRoundRect(x, padding, width, height, arc, arc);
|
||||||
|
canvas.setColor(StudioTheme.CONTROL_BORDER);
|
||||||
|
canvas.drawRoundRect(x, padding, width, height, arc, arc);
|
||||||
|
canvas.setColor(StudioTheme.TEXT);
|
||||||
|
int baseline = padding * 2 + metrics.getAscent();
|
||||||
|
for (String line : shortcuts) {
|
||||||
|
canvas.drawString(line, x + padding, baseline);
|
||||||
|
baseline += metrics.getHeight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void changed() {
|
||||||
|
if (viewListener != null) {
|
||||||
|
viewListener.run();
|
||||||
|
}
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@ import java.util.concurrent.locks.ReentrantLock;
|
|||||||
|
|
||||||
public final class PregenMapState {
|
public final class PregenMapState {
|
||||||
private static final int MAX_RASTER_SIZE = 1024;
|
private static final int MAX_RASTER_SIZE = 1024;
|
||||||
static final Color WAITING = new Color(29, 35, 46);
|
static final Color WAITING = new Color(57, 59, 64);
|
||||||
|
|
||||||
private final PregenRenderSnapshot.Bounds bounds;
|
private final PregenRenderSnapshot.Bounds bounds;
|
||||||
private final int width;
|
private final int width;
|
||||||
|
|||||||
@@ -22,13 +22,18 @@ import art.arcane.iris.configuration.IrisSettings;
|
|||||||
import art.arcane.iris.localization.DesktopUiMessages;
|
import art.arcane.iris.localization.DesktopUiMessages;
|
||||||
import art.arcane.iris.localization.IrisLanguage;
|
import art.arcane.iris.localization.IrisLanguage;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.iris.studio.map.IslandOverlay;
|
||||||
|
import art.arcane.iris.studio.map.IslandPanel;
|
||||||
|
import art.arcane.iris.studio.map.MapHost;
|
||||||
|
import art.arcane.iris.studio.map.StudioTheme;
|
||||||
|
import art.arcane.iris.studio.map.StudioToolbar;
|
||||||
import art.arcane.volmlib.util.format.Form;
|
import art.arcane.volmlib.util.format.Form;
|
||||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||||
import art.arcane.volmlib.util.localization.TextKey;
|
import art.arcane.volmlib.util.localization.TextKey;
|
||||||
|
|
||||||
import javax.swing.AbstractAction;
|
import javax.swing.AbstractAction;
|
||||||
import javax.swing.AbstractButton;
|
|
||||||
import javax.swing.BorderFactory;
|
import javax.swing.BorderFactory;
|
||||||
|
import javax.swing.Box;
|
||||||
import javax.swing.JButton;
|
import javax.swing.JButton;
|
||||||
import javax.swing.JComponent;
|
import javax.swing.JComponent;
|
||||||
import javax.swing.JFrame;
|
import javax.swing.JFrame;
|
||||||
@@ -36,30 +41,31 @@ import javax.swing.JLabel;
|
|||||||
import javax.swing.JPanel;
|
import javax.swing.JPanel;
|
||||||
import javax.swing.JProgressBar;
|
import javax.swing.JProgressBar;
|
||||||
import javax.swing.JScrollPane;
|
import javax.swing.JScrollPane;
|
||||||
import javax.swing.Scrollable;
|
|
||||||
import javax.swing.JTextArea;
|
import javax.swing.JTextArea;
|
||||||
|
import javax.swing.JToggleButton;
|
||||||
|
import javax.swing.JToolBar;
|
||||||
import javax.swing.KeyStroke;
|
import javax.swing.KeyStroke;
|
||||||
|
import javax.swing.Scrollable;
|
||||||
import javax.swing.SwingUtilities;
|
import javax.swing.SwingUtilities;
|
||||||
import javax.swing.Timer;
|
import javax.swing.Timer;
|
||||||
import javax.swing.plaf.basic.BasicButtonUI;
|
import javax.swing.text.DefaultCaret;
|
||||||
import javax.swing.plaf.basic.BasicGraphicsUtils;
|
import javax.swing.text.JTextComponent;
|
||||||
import javax.swing.plaf.basic.BasicProgressBarUI;
|
|
||||||
import javax.swing.plaf.basic.BasicScrollBarUI;
|
|
||||||
import java.awt.BorderLayout;
|
import java.awt.BorderLayout;
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
|
import java.awt.Component;
|
||||||
import java.awt.Dimension;
|
import java.awt.Dimension;
|
||||||
import java.awt.GridBagConstraints;
|
|
||||||
import java.awt.GridBagLayout;
|
|
||||||
import java.awt.Insets;
|
|
||||||
import java.awt.Font;
|
import java.awt.Font;
|
||||||
import java.awt.Frame;
|
import java.awt.Frame;
|
||||||
|
import java.awt.GridBagConstraints;
|
||||||
|
import java.awt.GridBagLayout;
|
||||||
import java.awt.GridLayout;
|
import java.awt.GridLayout;
|
||||||
import java.awt.Graphics;
|
import java.awt.Insets;
|
||||||
|
import java.awt.KeyboardFocusManager;
|
||||||
import java.awt.LayoutManager;
|
import java.awt.LayoutManager;
|
||||||
import java.awt.Rectangle;
|
import java.awt.Rectangle;
|
||||||
import java.awt.event.ActionEvent;
|
import java.awt.event.ActionEvent;
|
||||||
import java.awt.event.FocusAdapter;
|
import java.awt.event.ComponentAdapter;
|
||||||
import java.awt.event.FocusEvent;
|
import java.awt.event.ComponentEvent;
|
||||||
import java.awt.event.KeyEvent;
|
import java.awt.event.KeyEvent;
|
||||||
import java.awt.event.WindowAdapter;
|
import java.awt.event.WindowAdapter;
|
||||||
import java.awt.event.WindowEvent;
|
import java.awt.event.WindowEvent;
|
||||||
@@ -67,43 +73,43 @@ import java.util.Objects;
|
|||||||
|
|
||||||
public final class PregenRenderer extends JPanel {
|
public final class PregenRenderer extends JPanel {
|
||||||
private static final long serialVersionUID = 2094606939770332040L;
|
private static final long serialVersionUID = 2094606939770332040L;
|
||||||
static final Color BACKGROUND = new Color(12, 15, 22);
|
private static final Color BACKGROUND = StudioTheme.EDITOR_BACKGROUND;
|
||||||
static final Color BORDER = new Color(105, 119, 139);
|
private static final Color BORDER = StudioTheme.CONTROL_BORDER;
|
||||||
private static final Color SURFACE = new Color(17, 20, 29);
|
private static final Color SURFACE = StudioTheme.PANEL_BACKGROUND;
|
||||||
private static final Color FIELD = new Color(29, 34, 47);
|
private static final Color TEXT = StudioTheme.TEXT;
|
||||||
private static final Color TEXT = new Color(230, 234, 242);
|
private static final Color MUTED = StudioTheme.SECONDARY_TEXT;
|
||||||
private static final Color MUTED = new Color(163, 173, 191);
|
private static final Color ACCENT = StudioTheme.ACCENT;
|
||||||
private static final Color ACCENT = new Color(101, 194, 149);
|
private static final Color ERROR = StudioTheme.ERROR;
|
||||||
private static final Color ERROR = new Color(255, 139, 139);
|
|
||||||
static final Color GENERATING = new Color(204, 168, 107);
|
static final Color GENERATING = new Color(204, 168, 107);
|
||||||
static final Color GENERATED = ACCENT;
|
static final Color GENERATED = new Color(101, 194, 149);
|
||||||
static final Color EXISTS = new Color(93, 133, 112);
|
static final Color EXISTS = new Color(93, 133, 112);
|
||||||
static final Color NETWORK = new Color(157, 138, 176);
|
static final Color NETWORK = new Color(157, 138, 176);
|
||||||
static final Color NETWORK_GENERATING = new Color(129, 112, 145);
|
static final Color NETWORK_GENERATING = new Color(129, 112, 145);
|
||||||
private static final Font BODY_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
|
|
||||||
private static final Font LABEL_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 11);
|
|
||||||
private static final Font VALUE_FONT = new Font(Font.SANS_SERIF, Font.BOLD, 15);
|
|
||||||
|
|
||||||
private final PregenRenderSource source;
|
private final PregenRenderSource source;
|
||||||
private final Runnable onPause;
|
private final Runnable onPause;
|
||||||
private final PregenMapPanel map;
|
private final PregenMapPanel map;
|
||||||
private final Timer repaintTimer;
|
private final Timer repaintTimer;
|
||||||
private final JLabel worldLabel = label("", new Font(Font.SANS_SERIF, Font.BOLD, 21), TEXT);
|
private final JLabel worldLabel = label("", StudioTheme.titleFont(), TEXT);
|
||||||
private final JLabel phaseLabel = label("", BODY_FONT, MUTED);
|
private final JLabel phaseLabel = label("", StudioTheme.font(), MUTED);
|
||||||
private final JLabel countLabel = label("", BODY_FONT, TEXT);
|
private final JLabel countLabel = label("", StudioTheme.font(), TEXT);
|
||||||
private final JLabel percentLabel = label("", new Font(Font.SANS_SERIF, Font.BOLD, 24), TEXT);
|
private final JLabel percentLabel = label("", StudioTheme.titleFont(), TEXT);
|
||||||
private final JLabel currentValue = label("", VALUE_FONT, TEXT);
|
private final JLabel currentValue = label("", StudioTheme.boldFont(), TEXT);
|
||||||
private final JLabel overallValue = label("", VALUE_FONT, TEXT);
|
private final JLabel overallValue = label("", StudioTheme.boldFont(), TEXT);
|
||||||
private final JLabel thirtyValue = label("", VALUE_FONT, TEXT);
|
private final JLabel thirtyValue = label("", StudioTheme.boldFont(), TEXT);
|
||||||
private final JLabel sixtyValue = label("", VALUE_FONT, TEXT);
|
private final JLabel sixtyValue = label("", StudioTheme.boldFont(), TEXT);
|
||||||
private final JLabel etaValue = label("", VALUE_FONT, TEXT);
|
private final JLabel etaValue = label("", StudioTheme.boldFont(), TEXT);
|
||||||
private final JLabel elapsedValue = label("", VALUE_FONT, TEXT);
|
private final JLabel elapsedValue = label("", StudioTheme.boldFont(), TEXT);
|
||||||
private final JLabel memoryValue = label("", VALUE_FONT, TEXT);
|
private final JLabel memoryValue = label("", StudioTheme.boldFont(), TEXT);
|
||||||
private final JLabel pressureValue = label("", VALUE_FONT, TEXT);
|
private final JLabel pressureValue = label("", StudioTheme.boldFont(), TEXT);
|
||||||
private final JLabel methodLabel = label("", LABEL_FONT, MUTED);
|
private final JLabel methodLabel = label("", StudioTheme.smallFont(), MUTED);
|
||||||
private final JTextArea failureLabel = wrappedText("", BODY_FONT, ERROR, SURFACE);
|
private final JTextArea failureLabel = wrappedText("", StudioTheme.font(), ERROR, SURFACE);
|
||||||
private String controlFailure;
|
private String controlFailure;
|
||||||
private final JButton pauseButton = new JButton();
|
private final JButton pauseButton = StudioToolbar.button("", "", this::togglePause);
|
||||||
|
private final JToggleButton gridToggle = StudioToolbar.toggle(text(DesktopUiMessages.PREGEN_GRID),
|
||||||
|
text(DesktopUiMessages.PREGEN_GRID_HINT));
|
||||||
|
private final JLabel coordinatesLabel = label("", StudioTheme.smallFont(), MUTED);
|
||||||
|
private final JLabel zoomLabel = label("", StudioTheme.smallFont(), MUTED);
|
||||||
private final JProgressBar progressBar = new JProgressBar(0, 10_000);
|
private final JProgressBar progressBar = new JProgressBar(0, 10_000);
|
||||||
private volatile boolean renderingEnabled;
|
private volatile boolean renderingEnabled;
|
||||||
private volatile boolean disposed;
|
private volatile boolean disposed;
|
||||||
@@ -116,16 +122,23 @@ public final class PregenRenderer extends JPanel {
|
|||||||
failureLabel.setFocusable(true);
|
failureLabel.setFocusable(true);
|
||||||
PregenRenderSnapshot initial = source.renderSnapshot();
|
PregenRenderSnapshot initial = source.renderSnapshot();
|
||||||
map = new PregenMapPanel(source.renderMapState());
|
map = new PregenMapPanel(source.renderMapState());
|
||||||
setLayout(new BorderLayout(0, 18));
|
setLayout(new BorderLayout());
|
||||||
setBackground(BACKGROUND);
|
setBackground(BACKGROUND);
|
||||||
setBorder(BorderFactory.createEmptyBorder(20, 24, 18, 24));
|
map.getAccessibleContext().setAccessibleName(text(DesktopUiMessages.PREGEN_MAP));
|
||||||
add(header(), BorderLayout.NORTH);
|
map.setViewListener(this::updateMapStatus);
|
||||||
JPanel content = panel(new BorderLayout(18, 0), BACKGROUND);
|
IslandPanel sidebar = new IslandPanel(statusArea(initial.bounds()));
|
||||||
content.add(mapArea(initial.bounds()), BorderLayout.CENTER);
|
sidebar.addComponentListener(new ComponentAdapter() {
|
||||||
content.add(statusArea(), BorderLayout.EAST);
|
@Override
|
||||||
add(content, BorderLayout.CENTER);
|
public void componentResized(ComponentEvent event) {
|
||||||
add(wrappedText(text(DesktopUiMessages.PREGEN_WINDOW_HINT), LABEL_FONT, MUTED, BACKGROUND), BorderLayout.SOUTH);
|
map.setCoveredLeftWidth(sidebar.getWidth());
|
||||||
installPauseControl();
|
}
|
||||||
|
});
|
||||||
|
add(toolbar(), BorderLayout.NORTH);
|
||||||
|
add(new IslandOverlay(sidebar, map, StudioTheme.scale(300), StudioTheme.scale(260),
|
||||||
|
StudioTheme.scale(300)), BorderLayout.CENTER);
|
||||||
|
add(statusBar(), BorderLayout.SOUTH);
|
||||||
|
installControls();
|
||||||
|
updateMapStatus();
|
||||||
repaintTimer = new Timer(IrisSettings.get().getGui().isMaximumPregenGuiFPS() ? 4 : 250,
|
repaintTimer = new Timer(IrisSettings.get().getGui().isMaximumPregenGuiFPS() ? 4 : 250,
|
||||||
event -> refresh());
|
event -> refresh());
|
||||||
refresh();
|
refresh();
|
||||||
@@ -135,14 +148,14 @@ public final class PregenRenderer extends JPanel {
|
|||||||
if (!SwingUtilities.isEventDispatchThread()) {
|
if (!SwingUtilities.isEventDispatchThread()) {
|
||||||
throw new IllegalStateException("The pregeneration window must open on the Swing event thread");
|
throw new IllegalStateException("The pregeneration window must open on the Swing event thread");
|
||||||
}
|
}
|
||||||
|
JFrame frame = MapHost.createFrame(title);
|
||||||
PregenRenderer renderer = new PregenRenderer(source, onPause);
|
PregenRenderer renderer = new PregenRenderer(source, onPause);
|
||||||
JFrame frame = new JFrame(title);
|
|
||||||
GuiHost.prepareFrame(frame);
|
|
||||||
renderer.frame = frame;
|
renderer.frame = frame;
|
||||||
renderer.renderingEnabled = true;
|
renderer.renderingEnabled = true;
|
||||||
frame.setContentPane(renderer);
|
frame.setContentPane(renderer);
|
||||||
frame.setSize(1040, 760);
|
frame.setSize(MapHost.fitToScreen(StudioTheme.scaledSize(1240, 800)));
|
||||||
frame.setMinimumSize(new Dimension(760, 620));
|
frame.setMinimumSize(StudioTheme.scaledSize(820, 520));
|
||||||
|
frame.setLocationRelativeTo(null);
|
||||||
frame.addWindowListener(new WindowAdapter() {
|
frame.addWindowListener(new WindowAdapter() {
|
||||||
@Override
|
@Override
|
||||||
public void windowClosed(WindowEvent event) {
|
public void windowClosed(WindowEvent event) {
|
||||||
@@ -172,67 +185,80 @@ public final class PregenRenderer extends JPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private JPanel header() {
|
private JToolBar toolbar() {
|
||||||
JPanel heading = panel(new BorderLayout(16, 0), BACKGROUND);
|
JToolBar toolbar = StudioToolbar.create();
|
||||||
JPanel identity = panel(new GridLayout(2, 1, 0, 4), BACKGROUND);
|
worldLabel.setBorder(StudioTheme.emptyBorder(0, 6, 0, 14));
|
||||||
identity.add(worldLabel);
|
worldLabel.setMaximumSize(StudioTheme.scaledSize(220, 36));
|
||||||
identity.add(phaseLabel);
|
worldLabel.setMinimumSize(StudioTheme.scaledSize(80, 24));
|
||||||
heading.add(identity, BorderLayout.CENTER);
|
toolbar.add(worldLabel);
|
||||||
heading.add(pauseButton, BorderLayout.EAST);
|
toolbar.add(pauseButton);
|
||||||
JPanel counts = panel(new BorderLayout(), BACKGROUND);
|
toolbar.addSeparator();
|
||||||
counts.add(countLabel, BorderLayout.WEST);
|
toolbar.add(StudioToolbar.button(text(DesktopUiMessages.PREGEN_FIT),
|
||||||
counts.add(percentLabel, BorderLayout.EAST);
|
text(DesktopUiMessages.PREGEN_FIT_HINT), map::resetView));
|
||||||
progressBar.setUI(new BasicProgressBarUI());
|
toolbar.add(StudioToolbar.button("−", text(DesktopUiMessages.PREGEN_ZOOM_OUT), map::zoomOut));
|
||||||
progressBar.setForeground(ACCENT);
|
toolbar.add(StudioToolbar.button("+", text(DesktopUiMessages.PREGEN_ZOOM_IN), map::zoomIn));
|
||||||
progressBar.setBackground(FIELD);
|
gridToggle.addActionListener(event -> toggleGrid());
|
||||||
progressBar.setBorderPainted(false);
|
toolbar.add(gridToggle);
|
||||||
progressBar.setPreferredSize(new Dimension(1, 6));
|
toolbar.add(Box.createHorizontalGlue());
|
||||||
progressBar.setFocusable(false);
|
toolbar.add(StudioToolbar.themeButton());
|
||||||
JPanel progress = panel(new BorderLayout(0, 9), BACKGROUND);
|
toolbar.add(StudioToolbar.button(text(DesktopUiMessages.PREGEN_SHORTCUTS),
|
||||||
progress.add(counts, BorderLayout.NORTH);
|
text(DesktopUiMessages.PREGEN_SHORTCUTS_HINT), map::toggleHelp));
|
||||||
progress.add(progressBar, BorderLayout.SOUTH);
|
for (Component control : toolbar.getComponents()) {
|
||||||
JPanel header = panel(new BorderLayout(0, 18), BACKGROUND);
|
if (control instanceof JButton || control instanceof JToggleButton) {
|
||||||
header.add(heading, BorderLayout.NORTH);
|
control.setFocusable(true);
|
||||||
header.add(progress, BorderLayout.SOUTH);
|
}
|
||||||
return header;
|
}
|
||||||
|
return toolbar;
|
||||||
}
|
}
|
||||||
|
|
||||||
private JPanel mapArea(PregenRenderSnapshot.Bounds bounds) {
|
private JPanel statusBar() {
|
||||||
JPanel area = panel(new BorderLayout(0, 8), BACKGROUND);
|
JPanel status = panel(new BorderLayout(12, 0), SURFACE);
|
||||||
map.getAccessibleContext().setAccessibleName(text(DesktopUiMessages.PREGEN_MAP));
|
status.setBorder(StudioTheme.emptyBorder(5, 12, 5, 12));
|
||||||
area.add(map, BorderLayout.CENTER);
|
status.add(coordinatesLabel, BorderLayout.CENTER);
|
||||||
JLabel coordinates = label(text(DesktopUiMessages.PREGEN_BOUNDS,
|
status.add(zoomLabel, BorderLayout.EAST);
|
||||||
MessageArgument.trusted("minX", Form.f(bounds.minX())),
|
status.setToolTipText(text(DesktopUiMessages.PREGEN_WINDOW_HINT));
|
||||||
MessageArgument.trusted("maxX", Form.f(bounds.maxX())),
|
return status;
|
||||||
MessageArgument.trusted("minZ", Form.f(bounds.minZ())),
|
|
||||||
MessageArgument.trusted("maxZ", Form.f(bounds.maxZ()))), LABEL_FONT, MUTED);
|
|
||||||
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 JScrollPane statusArea() {
|
private void updateMapStatus() {
|
||||||
|
String coordinates = map.hoverCoordinates();
|
||||||
|
coordinatesLabel.setText(coordinates == null ? text(DesktopUiMessages.PREGEN_NAVIGATION_HINT) : coordinates);
|
||||||
|
zoomLabel.setText(text(DesktopUiMessages.PREGEN_ZOOM,
|
||||||
|
MessageArgument.trusted("zoom", Form.pc(map.zoom(), 0))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private JScrollPane statusArea(PregenRenderSnapshot.Bounds bounds) {
|
||||||
JPanel status = new StatusPanel();
|
JPanel status = new StatusPanel();
|
||||||
status.setBackground(SURFACE);
|
status.setBackground(SURFACE);
|
||||||
status.setBorder(BorderFactory.createEmptyBorder(18, 16, 16, 16));
|
status.setBorder(StudioTheme.emptyBorder(14, 12, 12, 12));
|
||||||
GridBagConstraints constraints = new GridBagConstraints();
|
GridBagConstraints constraints = new GridBagConstraints();
|
||||||
constraints.gridx = 0;
|
constraints.gridx = 0;
|
||||||
constraints.gridy = 0;
|
constraints.gridy = 0;
|
||||||
constraints.weightx = 1;
|
constraints.weightx = 1;
|
||||||
constraints.fill = GridBagConstraints.HORIZONTAL;
|
constraints.fill = GridBagConstraints.HORIZONTAL;
|
||||||
constraints.anchor = GridBagConstraints.NORTHWEST;
|
constraints.anchor = GridBagConstraints.NORTHWEST;
|
||||||
constraints.insets = new Insets(0, 0, 18, 0);
|
constraints.insets = new Insets(0, 0, StudioTheme.scale(12), 0);
|
||||||
|
status.add(label(text(DesktopUiMessages.PREGEN_TITLE), StudioTheme.titleFont(), TEXT), constraints);
|
||||||
|
constraints.gridy++;
|
||||||
|
status.add(phaseLabel, constraints);
|
||||||
|
constraints.gridy++;
|
||||||
|
JPanel progress = panel(new BorderLayout(0, StudioTheme.scale(8)), SURFACE);
|
||||||
|
progress.add(percentLabel, BorderLayout.NORTH);
|
||||||
|
progressBar.setForeground(ACCENT);
|
||||||
|
progressBar.setBackground(BACKGROUND);
|
||||||
|
progressBar.setBorderPainted(false);
|
||||||
|
progressBar.setPreferredSize(StudioTheme.scaledSize(1, 4));
|
||||||
|
progressBar.setFocusable(false);
|
||||||
|
progress.add(progressBar, BorderLayout.CENTER);
|
||||||
|
progress.add(countLabel, BorderLayout.SOUTH);
|
||||||
|
status.add(progress, constraints);
|
||||||
|
constraints.gridy++;
|
||||||
status.add(failureLabel, constraints);
|
status.add(failureLabel, constraints);
|
||||||
constraints.gridy++;
|
constraints.gridy++;
|
||||||
currentValue.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 25));
|
status.add(metricRow(DesktopUiMessages.PREGEN_CURRENT, currentValue), constraints);
|
||||||
etaValue.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 23));
|
|
||||||
status.add(metric(DesktopUiMessages.PREGEN_CURRENT, currentValue), constraints);
|
|
||||||
constraints.gridy++;
|
constraints.gridy++;
|
||||||
status.add(metric(DesktopUiMessages.PREGEN_ETA, etaValue), constraints);
|
status.add(metricRow(DesktopUiMessages.PREGEN_ETA, etaValue), constraints);
|
||||||
constraints.gridy++;
|
constraints.gridy++;
|
||||||
constraints.insets = new Insets(0, 0, 10, 0);
|
|
||||||
status.add(metricRow(DesktopUiMessages.PREGEN_OVERALL, overallValue), constraints);
|
status.add(metricRow(DesktopUiMessages.PREGEN_OVERALL, overallValue), constraints);
|
||||||
constraints.gridy++;
|
constraints.gridy++;
|
||||||
status.add(metricRow(DesktopUiMessages.PREGEN_THIRTY, thirtyValue), constraints);
|
status.add(metricRow(DesktopUiMessages.PREGEN_THIRTY, thirtyValue), constraints);
|
||||||
@@ -247,47 +273,26 @@ public final class PregenRenderer extends JPanel {
|
|||||||
constraints.gridy++;
|
constraints.gridy++;
|
||||||
status.add(methodLabel, constraints);
|
status.add(methodLabel, constraints);
|
||||||
constraints.gridy++;
|
constraints.gridy++;
|
||||||
|
status.add(legend(), constraints);
|
||||||
|
constraints.gridy++;
|
||||||
|
status.add(wrappedText(text(DesktopUiMessages.PREGEN_BOUNDS,
|
||||||
|
MessageArgument.trusted("minX", Form.f(bounds.minX())),
|
||||||
|
MessageArgument.trusted("maxX", Form.f(bounds.maxX())),
|
||||||
|
MessageArgument.trusted("minZ", Form.f(bounds.minZ())),
|
||||||
|
MessageArgument.trusted("maxZ", Form.f(bounds.maxZ()))), StudioTheme.smallFont(), MUTED, SURFACE), constraints);
|
||||||
|
constraints.gridy++;
|
||||||
|
status.add(wrappedText(text(DesktopUiMessages.PREGEN_WINDOW_HINT), StudioTheme.smallFont(), MUTED, SURFACE), constraints);
|
||||||
|
constraints.gridy++;
|
||||||
constraints.weighty = 1;
|
constraints.weighty = 1;
|
||||||
status.add(panel(new BorderLayout(), SURFACE), constraints);
|
status.add(panel(new BorderLayout(), SURFACE), constraints);
|
||||||
JScrollPane scroll = new JScrollPane(status);
|
JScrollPane scroll = StudioToolbar.scroll(status);
|
||||||
scroll.setBorder(BorderFactory.createLineBorder(BORDER));
|
|
||||||
scroll.getViewport().setBackground(SURFACE);
|
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;
|
return scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
private JPanel legend() {
|
private JPanel legend() {
|
||||||
JPanel legend = panel(new GridLayout(0, 2, 12, 7), BACKGROUND);
|
JPanel legend = panel(new GridLayout(0, 2, StudioTheme.scale(12), StudioTheme.scale(7)), SURFACE);
|
||||||
|
legend.setBorder(StudioTheme.emptyBorder(8, 0, 4, 0));
|
||||||
legend.add(legendItem(DesktopUiMessages.PREGEN_WAITING, PregenMapState.WAITING));
|
legend.add(legendItem(DesktopUiMessages.PREGEN_WAITING, PregenMapState.WAITING));
|
||||||
legend.add(legendItem(DesktopUiMessages.PREGEN_GENERATING, GENERATING));
|
legend.add(legendItem(DesktopUiMessages.PREGEN_GENERATING, GENERATING));
|
||||||
legend.add(legendItem(DesktopUiMessages.PREGEN_READY, GENERATED));
|
legend.add(legendItem(DesktopUiMessages.PREGEN_READY, GENERATED));
|
||||||
@@ -298,64 +303,27 @@ public final class PregenRenderer extends JPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private JPanel legendItem(TextKey key, Color color) {
|
private JPanel legendItem(TextKey key, Color color) {
|
||||||
JPanel item = panel(new BorderLayout(7, 0), BACKGROUND);
|
JPanel item = panel(new BorderLayout(StudioTheme.scale(7), 0), SURFACE);
|
||||||
JPanel swatch = panel(new BorderLayout(), color);
|
JPanel swatch = panel(new BorderLayout(), color);
|
||||||
swatch.setPreferredSize(new Dimension(10, 10));
|
swatch.setPreferredSize(StudioTheme.scaledSize(10, 10));
|
||||||
swatch.setBorder(BorderFactory.createLineBorder(BORDER));
|
swatch.setBorder(BorderFactory.createLineBorder(BORDER));
|
||||||
JPanel marker = panel(new BorderLayout(), BACKGROUND);
|
JPanel marker = panel(new BorderLayout(), SURFACE);
|
||||||
marker.add(swatch, BorderLayout.NORTH);
|
marker.add(swatch, BorderLayout.NORTH);
|
||||||
item.add(marker, BorderLayout.WEST);
|
item.add(marker, BorderLayout.WEST);
|
||||||
item.add(wrappedText(text(key), LABEL_FONT, MUTED, BACKGROUND), BorderLayout.CENTER);
|
item.add(wrappedText(text(key), StudioTheme.smallFont(), MUTED, SURFACE), BorderLayout.CENTER);
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
private JPanel metric(TextKey title, JLabel value) {
|
|
||||||
JPanel metric = panel(new BorderLayout(0, 5), SURFACE);
|
|
||||||
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) {
|
private JPanel metricRow(TextKey title, JLabel value) {
|
||||||
JPanel row = panel(new BorderLayout(8, 0), SURFACE);
|
JPanel row = panel(new BorderLayout(StudioTheme.scale(8), 0), SURFACE);
|
||||||
value.setFont(BODY_FONT);
|
row.add(wrappedText(text(title), StudioTheme.smallFont(), MUTED, SURFACE), BorderLayout.CENTER);
|
||||||
row.add(wrappedText(text(title), LABEL_FONT, MUTED, SURFACE), BorderLayout.CENTER);
|
|
||||||
row.add(value, BorderLayout.EAST);
|
row.add(value, BorderLayout.EAST);
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void installPauseControl() {
|
private void installControls() {
|
||||||
pauseButton.setUI(new BasicButtonUI() {
|
pauseButton.setFocusable(true);
|
||||||
@Override
|
|
||||||
protected void paintText(Graphics graphics, AbstractButton button, Rectangle bounds, String text) {
|
|
||||||
if (button.isEnabled()) {
|
|
||||||
super.paintText(graphics, button, bounds, text);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
graphics.setColor(MUTED);
|
|
||||||
BasicGraphicsUtils.drawStringUnderlineCharAt(graphics, text, button.getDisplayedMnemonicIndex(),
|
|
||||||
bounds.x, bounds.y + graphics.getFontMetrics().getAscent());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
pauseButton.setBackground(FIELD);
|
|
||||||
pauseButton.setForeground(TEXT);
|
|
||||||
pauseButton.setFont(BODY_FONT);
|
|
||||||
pauseButton.putClientProperty("html.disable", Boolean.TRUE);
|
pauseButton.putClientProperty("html.disable", Boolean.TRUE);
|
||||||
pauseButton.setFocusPainted(false);
|
|
||||||
pauseButton.setPreferredSize(new Dimension(108, 46));
|
|
||||||
setPauseBorder(false);
|
|
||||||
pauseButton.addFocusListener(new FocusAdapter() {
|
|
||||||
@Override
|
|
||||||
public void focusGained(FocusEvent event) {
|
|
||||||
setPauseBorder(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void focusLost(FocusEvent event) {
|
|
||||||
setPauseBorder(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
pauseButton.addActionListener(event -> togglePause());
|
|
||||||
pauseButton.getInputMap(JComponent.WHEN_FOCUSED).put(
|
pauseButton.getInputMap(JComponent.WHEN_FOCUSED).put(
|
||||||
KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "activatePause");
|
KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "activatePause");
|
||||||
pauseButton.getActionMap().put("activatePause", new AbstractAction() {
|
pauseButton.getActionMap().put("activatePause", new AbstractAction() {
|
||||||
@@ -364,19 +332,39 @@ public final class PregenRenderer extends JPanel {
|
|||||||
pauseButton.doClick();
|
pauseButton.doClick();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_P, 0, true), "pause");
|
bind(KeyStroke.getKeyStroke(KeyEvent.VK_P, 0, true), "pause", this::togglePause);
|
||||||
getActionMap().put("pause", new AbstractAction() {
|
bind(KeyStroke.getKeyStroke(KeyEvent.VK_R, 0), "reset-view", map::resetView);
|
||||||
|
bind(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SLASH, 0), "reset-zoom", map::resetView);
|
||||||
|
bind(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, 0), "zoom-in", map::zoomIn);
|
||||||
|
bind(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, KeyEvent.SHIFT_DOWN_MASK), "zoom-in-plus", map::zoomIn);
|
||||||
|
bind(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0), "zoom-in-keypad", map::zoomIn);
|
||||||
|
bind(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, 0), "zoom-out", map::zoomOut);
|
||||||
|
bind(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, 0), "zoom-out-keypad", map::zoomOut);
|
||||||
|
bind(KeyStroke.getKeyStroke(KeyEvent.VK_G, 0), "grid", this::toggleGrid);
|
||||||
|
bind(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), "pan-left", () -> map.panBy(64, 0));
|
||||||
|
bind(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), "pan-right", () -> map.panBy(-64, 0));
|
||||||
|
bind(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), "pan-up", () -> map.panBy(0, 64));
|
||||||
|
bind(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), "pan-down", () -> map.panBy(0, -64));
|
||||||
|
bind(KeyStroke.getKeyStroke(KeyEvent.VK_SLASH, 0), "help", map::toggleHelp);
|
||||||
|
bind(KeyStroke.getKeyStroke(KeyEvent.VK_SLASH, KeyEvent.SHIFT_DOWN_MASK), "help-question", map::toggleHelp);
|
||||||
|
bind(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "dismiss", map::dismissHelp);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bind(KeyStroke key, String name, Runnable action) {
|
||||||
|
getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(key, name);
|
||||||
|
getActionMap().put(name, new AbstractAction() {
|
||||||
@Override
|
@Override
|
||||||
public void actionPerformed(ActionEvent event) {
|
public void actionPerformed(ActionEvent event) {
|
||||||
togglePause();
|
if (!(KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() instanceof JTextComponent)) {
|
||||||
|
action.run();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setPauseBorder(boolean focused) {
|
private void toggleGrid() {
|
||||||
pauseButton.setBorder(BorderFactory.createCompoundBorder(
|
map.toggleGrid();
|
||||||
BorderFactory.createLineBorder(focused ? ACCENT : BORDER, 2),
|
gridToggle.setSelected(map.isGridVisible());
|
||||||
BorderFactory.createEmptyBorder(8, 14, 8, 14)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void togglePause() {
|
private void togglePause() {
|
||||||
@@ -512,7 +500,10 @@ public final class PregenRenderer extends JPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static JTextArea wrappedText(String text, Font font, Color foreground, Color background) {
|
private static JTextArea wrappedText(String text, Font font, Color foreground, Color background) {
|
||||||
JTextArea area = new JTextArea(text);
|
JTextArea area = new JTextArea();
|
||||||
|
DefaultCaret caret = (DefaultCaret) area.getCaret();
|
||||||
|
caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
|
||||||
|
area.setText(text);
|
||||||
area.setEditable(false);
|
area.setEditable(false);
|
||||||
area.setFocusable(false);
|
area.setFocusable(false);
|
||||||
area.setLineWrap(true);
|
area.setLineWrap(true);
|
||||||
|
|||||||
+9
-8
@@ -158,13 +158,6 @@ public final class GenerationRegistryContractFactory {
|
|||||||
CustomBiomeAliasPolicy requiredAliasPolicy = Objects.requireNonNull(aliasPolicy, "aliasPolicy");
|
CustomBiomeAliasPolicy requiredAliasPolicy = Objects.requireNonNull(aliasPolicy, "aliasPolicy");
|
||||||
requireRuntimeIdentity(requiredGenerationRegistry);
|
requireRuntimeIdentity(requiredGenerationRegistry);
|
||||||
Path packRoot = requiredData.getDataFolder().toPath().toAbsolutePath().normalize();
|
Path packRoot = requiredData.getDataFolder().toPath().toAbsolutePath().normalize();
|
||||||
String actualPackFingerprint = GenerationPackFingerprint.compute(
|
|
||||||
packRoot,
|
|
||||||
GenerationPackFingerprint.CURRENT_VERSION
|
|
||||||
);
|
|
||||||
if (!requiredPackFingerprint.equals(actualPackFingerprint)) {
|
|
||||||
throw new IOException("Iris generation registry capture rejected a changed pack.");
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<GenerationRegistryContract.PhysicalResourceKey, PlatformGenerationRegistry.Definition> definitions =
|
Map<GenerationRegistryContract.PhysicalResourceKey, PlatformGenerationRegistry.Definition> definitions =
|
||||||
new LinkedHashMap<>();
|
new LinkedHashMap<>();
|
||||||
@@ -221,7 +214,15 @@ public final class GenerationRegistryContractFactory {
|
|||||||
ReferencedPlatformKeys referenced = referencedPlatformKeys(packRoot, requiredRegistries);
|
ReferencedPlatformKeys referenced = referencedPlatformKeys(packRoot, requiredRegistries);
|
||||||
addPlatformDefinitions(definitions, BLOCK_REGISTRY, referenced.blockKeys(), requiredGenerationRegistry);
|
addPlatformDefinitions(definitions, BLOCK_REGISTRY, referenced.blockKeys(), requiredGenerationRegistry);
|
||||||
addPlatformDefinitions(definitions, ENTITY_TYPE_REGISTRY, referenced.entityKeys(), requiredGenerationRegistry);
|
addPlatformDefinitions(definitions, ENTITY_TYPE_REGISTRY, referenced.entityKeys(), requiredGenerationRegistry);
|
||||||
return contract(definitions, generatedSources, requiredGenerationRegistry).withBiomeTags(biomeTags);
|
GenerationRegistryContract captured = contract(definitions, generatedSources, requiredGenerationRegistry).withBiomeTags(biomeTags);
|
||||||
|
String actualPackFingerprint = GenerationPackFingerprint.compute(
|
||||||
|
packRoot,
|
||||||
|
GenerationPackFingerprint.CURRENT_VERSION
|
||||||
|
);
|
||||||
|
if (!requiredPackFingerprint.equals(actualPackFingerprint)) {
|
||||||
|
throw new IOException("Iris generation registry capture rejected a changed pack.");
|
||||||
|
}
|
||||||
|
return captured;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CustomBiomeDefinition customBiomeDefinition(
|
public static CustomBiomeDefinition customBiomeDefinition(
|
||||||
|
|||||||
@@ -1937,6 +1937,17 @@ ready = "Bereit"
|
|||||||
existing = "Vorhanden"
|
existing = "Vorhanden"
|
||||||
network = "Netzwerk"
|
network = "Netzwerk"
|
||||||
terrain_hint = "Fertige Chunks zeigen Geländefarben"
|
terrain_hint = "Fertige Chunks zeigen Geländefarben"
|
||||||
|
grid = "Grid"
|
||||||
|
grid_hint = "Show chunk and region borders (G)"
|
||||||
|
fit = "Reset view"
|
||||||
|
fit_hint = "Fit the whole generation area (R)"
|
||||||
|
zoom_in = "Zoom in (+)"
|
||||||
|
zoom_out = "Zoom out (-)"
|
||||||
|
zoom = "Zoom {zoom}"
|
||||||
|
shortcuts = "Shortcuts"
|
||||||
|
shortcuts_hint = "Show mouse and key shortcuts (/)"
|
||||||
|
navigation_hint = "Drag to pan · Scroll to zoom · Hover to inspect chunks"
|
||||||
|
shortcut_details = "Drag or arrow keys: pan\nScroll or + / -: zoom\nR or \\: fit generation area\nG: chunk and region grid\nP: pause or resume generation\n/ or Escape: close shortcuts"
|
||||||
|
|
||||||
[iris.runtime.what]
|
[iris.runtime.what]
|
||||||
material = "Material: {material}"
|
material = "Material: {material}"
|
||||||
|
|||||||
@@ -1937,6 +1937,17 @@ ready = "Listo"
|
|||||||
existing = "Existente"
|
existing = "Existente"
|
||||||
network = "Red"
|
network = "Red"
|
||||||
terrain_hint = "Los chunks terminados muestran los colores del terreno"
|
terrain_hint = "Los chunks terminados muestran los colores del terreno"
|
||||||
|
grid = "Grid"
|
||||||
|
grid_hint = "Show chunk and region borders (G)"
|
||||||
|
fit = "Reset view"
|
||||||
|
fit_hint = "Fit the whole generation area (R)"
|
||||||
|
zoom_in = "Zoom in (+)"
|
||||||
|
zoom_out = "Zoom out (-)"
|
||||||
|
zoom = "Zoom {zoom}"
|
||||||
|
shortcuts = "Shortcuts"
|
||||||
|
shortcuts_hint = "Show mouse and key shortcuts (/)"
|
||||||
|
navigation_hint = "Drag to pan · Scroll to zoom · Hover to inspect chunks"
|
||||||
|
shortcut_details = "Drag or arrow keys: pan\nScroll or + / -: zoom\nR or \\: fit generation area\nG: chunk and region grid\nP: pause or resume generation\n/ or Escape: close shortcuts"
|
||||||
|
|
||||||
[iris.runtime.what]
|
[iris.runtime.what]
|
||||||
material = "Material: {material}"
|
material = "Material: {material}"
|
||||||
|
|||||||
@@ -1942,6 +1942,17 @@ ready = "Valmis"
|
|||||||
existing = "Olemassa"
|
existing = "Olemassa"
|
||||||
network = "Verkko"
|
network = "Verkko"
|
||||||
terrain_hint = "Valmiit lohkot näyttävät maaston värit"
|
terrain_hint = "Valmiit lohkot näyttävät maaston värit"
|
||||||
|
grid = "Grid"
|
||||||
|
grid_hint = "Show chunk and region borders (G)"
|
||||||
|
fit = "Reset view"
|
||||||
|
fit_hint = "Fit the whole generation area (R)"
|
||||||
|
zoom_in = "Zoom in (+)"
|
||||||
|
zoom_out = "Zoom out (-)"
|
||||||
|
zoom = "Zoom {zoom}"
|
||||||
|
shortcuts = "Shortcuts"
|
||||||
|
shortcuts_hint = "Show mouse and key shortcuts (/)"
|
||||||
|
navigation_hint = "Drag to pan · Scroll to zoom · Hover to inspect chunks"
|
||||||
|
shortcut_details = "Drag or arrow keys: pan\nScroll or + / -: zoom\nR or \\: fit generation area\nG: chunk and region grid\nP: pause or resume generation\n/ or Escape: close shortcuts"
|
||||||
|
|
||||||
[iris.runtime.what]
|
[iris.runtime.what]
|
||||||
material = "Material: {material}"
|
material = "Material: {material}"
|
||||||
|
|||||||
@@ -1937,6 +1937,17 @@ ready = "Prêt"
|
|||||||
existing = "Existant"
|
existing = "Existant"
|
||||||
network = "Réseau"
|
network = "Réseau"
|
||||||
terrain_hint = "Les chunks terminés affichent les couleurs du terrain"
|
terrain_hint = "Les chunks terminés affichent les couleurs du terrain"
|
||||||
|
grid = "Grid"
|
||||||
|
grid_hint = "Show chunk and region borders (G)"
|
||||||
|
fit = "Reset view"
|
||||||
|
fit_hint = "Fit the whole generation area (R)"
|
||||||
|
zoom_in = "Zoom in (+)"
|
||||||
|
zoom_out = "Zoom out (-)"
|
||||||
|
zoom = "Zoom {zoom}"
|
||||||
|
shortcuts = "Shortcuts"
|
||||||
|
shortcuts_hint = "Show mouse and key shortcuts (/)"
|
||||||
|
navigation_hint = "Drag to pan · Scroll to zoom · Hover to inspect chunks"
|
||||||
|
shortcut_details = "Drag or arrow keys: pan\nScroll or + / -: zoom\nR or \\: fit generation area\nG: chunk and region grid\nP: pause or resume generation\n/ or Escape: close shortcuts"
|
||||||
|
|
||||||
[iris.runtime.what]
|
[iris.runtime.what]
|
||||||
material = "Material: {material}"
|
material = "Material: {material}"
|
||||||
|
|||||||
@@ -1942,6 +1942,17 @@ ready = "מוכן"
|
|||||||
existing = "קיים"
|
existing = "קיים"
|
||||||
network = "רשת"
|
network = "רשת"
|
||||||
terrain_hint = "צ׳אנקים שהושלמו מציגים את צבעי השטח"
|
terrain_hint = "צ׳אנקים שהושלמו מציגים את צבעי השטח"
|
||||||
|
grid = "Grid"
|
||||||
|
grid_hint = "Show chunk and region borders (G)"
|
||||||
|
fit = "Reset view"
|
||||||
|
fit_hint = "Fit the whole generation area (R)"
|
||||||
|
zoom_in = "Zoom in (+)"
|
||||||
|
zoom_out = "Zoom out (-)"
|
||||||
|
zoom = "Zoom {zoom}"
|
||||||
|
shortcuts = "Shortcuts"
|
||||||
|
shortcuts_hint = "Show mouse and key shortcuts (/)"
|
||||||
|
navigation_hint = "Drag to pan · Scroll to zoom · Hover to inspect chunks"
|
||||||
|
shortcut_details = "Drag or arrow keys: pan\nScroll or + / -: zoom\nR or \\: fit generation area\nG: chunk and region grid\nP: pause or resume generation\n/ or Escape: close shortcuts"
|
||||||
|
|
||||||
[iris.runtime.what]
|
[iris.runtime.what]
|
||||||
material = "Material: {material}"
|
material = "Material: {material}"
|
||||||
|
|||||||
@@ -1937,6 +1937,17 @@ ready = "Pronto"
|
|||||||
existing = "Esistente"
|
existing = "Esistente"
|
||||||
network = "Rete"
|
network = "Rete"
|
||||||
terrain_hint = "I chunk completati mostrano i colori del terreno"
|
terrain_hint = "I chunk completati mostrano i colori del terreno"
|
||||||
|
grid = "Grid"
|
||||||
|
grid_hint = "Show chunk and region borders (G)"
|
||||||
|
fit = "Reset view"
|
||||||
|
fit_hint = "Fit the whole generation area (R)"
|
||||||
|
zoom_in = "Zoom in (+)"
|
||||||
|
zoom_out = "Zoom out (-)"
|
||||||
|
zoom = "Zoom {zoom}"
|
||||||
|
shortcuts = "Shortcuts"
|
||||||
|
shortcuts_hint = "Show mouse and key shortcuts (/)"
|
||||||
|
navigation_hint = "Drag to pan · Scroll to zoom · Hover to inspect chunks"
|
||||||
|
shortcut_details = "Drag or arrow keys: pan\nScroll or + / -: zoom\nR or \\: fit generation area\nG: chunk and region grid\nP: pause or resume generation\n/ or Escape: close shortcuts"
|
||||||
|
|
||||||
[iris.runtime.what]
|
[iris.runtime.what]
|
||||||
material = "Material: {material}"
|
material = "Material: {material}"
|
||||||
|
|||||||
@@ -1940,6 +1940,17 @@ ready = "準備完了"
|
|||||||
existing = "既存"
|
existing = "既存"
|
||||||
network = "ネットワーク"
|
network = "ネットワーク"
|
||||||
terrain_hint = "完了したチャンクは地形の色で表示されます"
|
terrain_hint = "完了したチャンクは地形の色で表示されます"
|
||||||
|
grid = "Grid"
|
||||||
|
grid_hint = "Show chunk and region borders (G)"
|
||||||
|
fit = "Reset view"
|
||||||
|
fit_hint = "Fit the whole generation area (R)"
|
||||||
|
zoom_in = "Zoom in (+)"
|
||||||
|
zoom_out = "Zoom out (-)"
|
||||||
|
zoom = "Zoom {zoom}"
|
||||||
|
shortcuts = "Shortcuts"
|
||||||
|
shortcuts_hint = "Show mouse and key shortcuts (/)"
|
||||||
|
navigation_hint = "Drag to pan · Scroll to zoom · Hover to inspect chunks"
|
||||||
|
shortcut_details = "Drag or arrow keys: pan\nScroll or + / -: zoom\nR or \\: fit generation area\nG: chunk and region grid\nP: pause or resume generation\n/ or Escape: close shortcuts"
|
||||||
|
|
||||||
[iris.runtime.what]
|
[iris.runtime.what]
|
||||||
material = "Material: {material}"
|
material = "Material: {material}"
|
||||||
|
|||||||
@@ -1940,6 +1940,17 @@ ready = "준비됨"
|
|||||||
existing = "기존"
|
existing = "기존"
|
||||||
network = "네트워크"
|
network = "네트워크"
|
||||||
terrain_hint = "완료된 청크는 지형 색상으로 표시됩니다"
|
terrain_hint = "완료된 청크는 지형 색상으로 표시됩니다"
|
||||||
|
grid = "Grid"
|
||||||
|
grid_hint = "Show chunk and region borders (G)"
|
||||||
|
fit = "Reset view"
|
||||||
|
fit_hint = "Fit the whole generation area (R)"
|
||||||
|
zoom_in = "Zoom in (+)"
|
||||||
|
zoom_out = "Zoom out (-)"
|
||||||
|
zoom = "Zoom {zoom}"
|
||||||
|
shortcuts = "Shortcuts"
|
||||||
|
shortcuts_hint = "Show mouse and key shortcuts (/)"
|
||||||
|
navigation_hint = "Drag to pan · Scroll to zoom · Hover to inspect chunks"
|
||||||
|
shortcut_details = "Drag or arrow keys: pan\nScroll or + / -: zoom\nR or \\: fit generation area\nG: chunk and region grid\nP: pause or resume generation\n/ or Escape: close shortcuts"
|
||||||
|
|
||||||
[iris.runtime.what]
|
[iris.runtime.what]
|
||||||
material = "Material: {material}"
|
material = "Material: {material}"
|
||||||
|
|||||||
@@ -1942,6 +1942,17 @@ ready = "Paruošta"
|
|||||||
existing = "Esamos"
|
existing = "Esamos"
|
||||||
network = "Tinklas"
|
network = "Tinklas"
|
||||||
terrain_hint = "Baigtos sritys rodo reljefo spalvas"
|
terrain_hint = "Baigtos sritys rodo reljefo spalvas"
|
||||||
|
grid = "Grid"
|
||||||
|
grid_hint = "Show chunk and region borders (G)"
|
||||||
|
fit = "Reset view"
|
||||||
|
fit_hint = "Fit the whole generation area (R)"
|
||||||
|
zoom_in = "Zoom in (+)"
|
||||||
|
zoom_out = "Zoom out (-)"
|
||||||
|
zoom = "Zoom {zoom}"
|
||||||
|
shortcuts = "Shortcuts"
|
||||||
|
shortcuts_hint = "Show mouse and key shortcuts (/)"
|
||||||
|
navigation_hint = "Drag to pan · Scroll to zoom · Hover to inspect chunks"
|
||||||
|
shortcut_details = "Drag or arrow keys: pan\nScroll or + / -: zoom\nR or \\: fit generation area\nG: chunk and region grid\nP: pause or resume generation\n/ or Escape: close shortcuts"
|
||||||
|
|
||||||
[iris.runtime.what]
|
[iris.runtime.what]
|
||||||
material = "Material: {material}"
|
material = "Material: {material}"
|
||||||
|
|||||||
@@ -1937,6 +1937,17 @@ ready = "Gereed"
|
|||||||
existing = "Bestaand"
|
existing = "Bestaand"
|
||||||
network = "Netwerk"
|
network = "Netwerk"
|
||||||
terrain_hint = "Voltooide chunks tonen terreinkleuren"
|
terrain_hint = "Voltooide chunks tonen terreinkleuren"
|
||||||
|
grid = "Grid"
|
||||||
|
grid_hint = "Show chunk and region borders (G)"
|
||||||
|
fit = "Reset view"
|
||||||
|
fit_hint = "Fit the whole generation area (R)"
|
||||||
|
zoom_in = "Zoom in (+)"
|
||||||
|
zoom_out = "Zoom out (-)"
|
||||||
|
zoom = "Zoom {zoom}"
|
||||||
|
shortcuts = "Shortcuts"
|
||||||
|
shortcuts_hint = "Show mouse and key shortcuts (/)"
|
||||||
|
navigation_hint = "Drag to pan · Scroll to zoom · Hover to inspect chunks"
|
||||||
|
shortcut_details = "Drag or arrow keys: pan\nScroll or + / -: zoom\nR or \\: fit generation area\nG: chunk and region grid\nP: pause or resume generation\n/ or Escape: close shortcuts"
|
||||||
|
|
||||||
[iris.runtime.what]
|
[iris.runtime.what]
|
||||||
material = "Material: {material}"
|
material = "Material: {material}"
|
||||||
|
|||||||
@@ -1942,6 +1942,17 @@ ready = "Gotowe"
|
|||||||
existing = "Istniejące"
|
existing = "Istniejące"
|
||||||
network = "Sieć"
|
network = "Sieć"
|
||||||
terrain_hint = "Gotowe chunki pokazują kolory terenu"
|
terrain_hint = "Gotowe chunki pokazują kolory terenu"
|
||||||
|
grid = "Grid"
|
||||||
|
grid_hint = "Show chunk and region borders (G)"
|
||||||
|
fit = "Reset view"
|
||||||
|
fit_hint = "Fit the whole generation area (R)"
|
||||||
|
zoom_in = "Zoom in (+)"
|
||||||
|
zoom_out = "Zoom out (-)"
|
||||||
|
zoom = "Zoom {zoom}"
|
||||||
|
shortcuts = "Shortcuts"
|
||||||
|
shortcuts_hint = "Show mouse and key shortcuts (/)"
|
||||||
|
navigation_hint = "Drag to pan · Scroll to zoom · Hover to inspect chunks"
|
||||||
|
shortcut_details = "Drag or arrow keys: pan\nScroll or + / -: zoom\nR or \\: fit generation area\nG: chunk and region grid\nP: pause or resume generation\n/ or Escape: close shortcuts"
|
||||||
|
|
||||||
[iris.runtime.what]
|
[iris.runtime.what]
|
||||||
material = "Material: {material}"
|
material = "Material: {material}"
|
||||||
|
|||||||
@@ -1937,6 +1937,17 @@ ready = "Pronto"
|
|||||||
existing = "Existente"
|
existing = "Existente"
|
||||||
network = "Rede"
|
network = "Rede"
|
||||||
terrain_hint = "Os chunks concluídos mostram as cores do terreno"
|
terrain_hint = "Os chunks concluídos mostram as cores do terreno"
|
||||||
|
grid = "Grid"
|
||||||
|
grid_hint = "Show chunk and region borders (G)"
|
||||||
|
fit = "Reset view"
|
||||||
|
fit_hint = "Fit the whole generation area (R)"
|
||||||
|
zoom_in = "Zoom in (+)"
|
||||||
|
zoom_out = "Zoom out (-)"
|
||||||
|
zoom = "Zoom {zoom}"
|
||||||
|
shortcuts = "Shortcuts"
|
||||||
|
shortcuts_hint = "Show mouse and key shortcuts (/)"
|
||||||
|
navigation_hint = "Drag to pan · Scroll to zoom · Hover to inspect chunks"
|
||||||
|
shortcut_details = "Drag or arrow keys: pan\nScroll or + / -: zoom\nR or \\: fit generation area\nG: chunk and region grid\nP: pause or resume generation\n/ or Escape: close shortcuts"
|
||||||
|
|
||||||
[iris.runtime.what]
|
[iris.runtime.what]
|
||||||
material = "Material: {material}"
|
material = "Material: {material}"
|
||||||
|
|||||||
@@ -1942,6 +1942,17 @@ ready = "Готово"
|
|||||||
existing = "Существующие"
|
existing = "Существующие"
|
||||||
network = "Сеть"
|
network = "Сеть"
|
||||||
terrain_hint = "Готовые чанки отображают цвета ландшафта"
|
terrain_hint = "Готовые чанки отображают цвета ландшафта"
|
||||||
|
grid = "Grid"
|
||||||
|
grid_hint = "Show chunk and region borders (G)"
|
||||||
|
fit = "Reset view"
|
||||||
|
fit_hint = "Fit the whole generation area (R)"
|
||||||
|
zoom_in = "Zoom in (+)"
|
||||||
|
zoom_out = "Zoom out (-)"
|
||||||
|
zoom = "Zoom {zoom}"
|
||||||
|
shortcuts = "Shortcuts"
|
||||||
|
shortcuts_hint = "Show mouse and key shortcuts (/)"
|
||||||
|
navigation_hint = "Drag to pan · Scroll to zoom · Hover to inspect chunks"
|
||||||
|
shortcut_details = "Drag or arrow keys: pan\nScroll or + / -: zoom\nR or \\: fit generation area\nG: chunk and region grid\nP: pause or resume generation\n/ or Escape: close shortcuts"
|
||||||
|
|
||||||
[iris.runtime.what]
|
[iris.runtime.what]
|
||||||
material = "Material: {material}"
|
material = "Material: {material}"
|
||||||
|
|||||||
@@ -1942,6 +1942,17 @@ ready = "Hazır"
|
|||||||
existing = "Mevcut"
|
existing = "Mevcut"
|
||||||
network = "Ağ"
|
network = "Ağ"
|
||||||
terrain_hint = "Tamamlanan chunklar arazi renklerini gösterir"
|
terrain_hint = "Tamamlanan chunklar arazi renklerini gösterir"
|
||||||
|
grid = "Grid"
|
||||||
|
grid_hint = "Show chunk and region borders (G)"
|
||||||
|
fit = "Reset view"
|
||||||
|
fit_hint = "Fit the whole generation area (R)"
|
||||||
|
zoom_in = "Zoom in (+)"
|
||||||
|
zoom_out = "Zoom out (-)"
|
||||||
|
zoom = "Zoom {zoom}"
|
||||||
|
shortcuts = "Shortcuts"
|
||||||
|
shortcuts_hint = "Show mouse and key shortcuts (/)"
|
||||||
|
navigation_hint = "Drag to pan · Scroll to zoom · Hover to inspect chunks"
|
||||||
|
shortcut_details = "Drag or arrow keys: pan\nScroll or + / -: zoom\nR or \\: fit generation area\nG: chunk and region grid\nP: pause or resume generation\n/ or Escape: close shortcuts"
|
||||||
|
|
||||||
[iris.runtime.what]
|
[iris.runtime.what]
|
||||||
material = "Material: {material}"
|
material = "Material: {material}"
|
||||||
|
|||||||
@@ -1940,6 +1940,17 @@ ready = "Sẵn sàng"
|
|||||||
existing = "Có sẵn"
|
existing = "Có sẵn"
|
||||||
network = "Mạng"
|
network = "Mạng"
|
||||||
terrain_hint = "Chunk hoàn tất hiển thị màu địa hình"
|
terrain_hint = "Chunk hoàn tất hiển thị màu địa hình"
|
||||||
|
grid = "Grid"
|
||||||
|
grid_hint = "Show chunk and region borders (G)"
|
||||||
|
fit = "Reset view"
|
||||||
|
fit_hint = "Fit the whole generation area (R)"
|
||||||
|
zoom_in = "Zoom in (+)"
|
||||||
|
zoom_out = "Zoom out (-)"
|
||||||
|
zoom = "Zoom {zoom}"
|
||||||
|
shortcuts = "Shortcuts"
|
||||||
|
shortcuts_hint = "Show mouse and key shortcuts (/)"
|
||||||
|
navigation_hint = "Drag to pan · Scroll to zoom · Hover to inspect chunks"
|
||||||
|
shortcut_details = "Drag or arrow keys: pan\nScroll or + / -: zoom\nR or \\: fit generation area\nG: chunk and region grid\nP: pause or resume generation\n/ or Escape: close shortcuts"
|
||||||
|
|
||||||
[iris.runtime.what]
|
[iris.runtime.what]
|
||||||
material = "Material: {material}"
|
material = "Material: {material}"
|
||||||
|
|||||||
@@ -1940,6 +1940,17 @@ ready = "就绪"
|
|||||||
existing = "已有"
|
existing = "已有"
|
||||||
network = "网络"
|
network = "网络"
|
||||||
terrain_hint = "已完成的区块显示地形颜色"
|
terrain_hint = "已完成的区块显示地形颜色"
|
||||||
|
grid = "Grid"
|
||||||
|
grid_hint = "Show chunk and region borders (G)"
|
||||||
|
fit = "Reset view"
|
||||||
|
fit_hint = "Fit the whole generation area (R)"
|
||||||
|
zoom_in = "Zoom in (+)"
|
||||||
|
zoom_out = "Zoom out (-)"
|
||||||
|
zoom = "Zoom {zoom}"
|
||||||
|
shortcuts = "Shortcuts"
|
||||||
|
shortcuts_hint = "Show mouse and key shortcuts (/)"
|
||||||
|
navigation_hint = "Drag to pan · Scroll to zoom · Hover to inspect chunks"
|
||||||
|
shortcut_details = "Drag or arrow keys: pan\nScroll or + / -: zoom\nR or \\: fit generation area\nG: chunk and region grid\nP: pause or resume generation\n/ or Escape: close shortcuts"
|
||||||
|
|
||||||
[iris.runtime.what]
|
[iris.runtime.what]
|
||||||
material = "Material: {material}"
|
material = "Material: {material}"
|
||||||
|
|||||||
@@ -1940,6 +1940,17 @@ ready = "就緒"
|
|||||||
existing = "既有"
|
existing = "既有"
|
||||||
network = "網路"
|
network = "網路"
|
||||||
terrain_hint = "已完成的區塊顯示地形顏色"
|
terrain_hint = "已完成的區塊顯示地形顏色"
|
||||||
|
grid = "Grid"
|
||||||
|
grid_hint = "Show chunk and region borders (G)"
|
||||||
|
fit = "Reset view"
|
||||||
|
fit_hint = "Fit the whole generation area (R)"
|
||||||
|
zoom_in = "Zoom in (+)"
|
||||||
|
zoom_out = "Zoom out (-)"
|
||||||
|
zoom = "Zoom {zoom}"
|
||||||
|
shortcuts = "Shortcuts"
|
||||||
|
shortcuts_hint = "Show mouse and key shortcuts (/)"
|
||||||
|
navigation_hint = "Drag to pan · Scroll to zoom · Hover to inspect chunks"
|
||||||
|
shortcut_details = "Drag or arrow keys: pan\nScroll or + / -: zoom\nR or \\: fit generation area\nG: chunk and region grid\nP: pause or resume generation\n/ or Escape: close shortcuts"
|
||||||
|
|
||||||
[iris.runtime.what]
|
[iris.runtime.what]
|
||||||
material = "Material: {material}"
|
material = "Material: {material}"
|
||||||
|
|||||||
+66
@@ -53,6 +53,7 @@ import java.util.Optional;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.AbstractExecutorService;
|
import java.util.concurrent.AbstractExecutorService;
|
||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.ExecutorService;
|
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.argThat;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.clearInvocations;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.spy;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.verifyNoInteractions;
|
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)
|
@Test(timeout = 180_000L)
|
||||||
public void preparePregenerationPlansRegionsInOrder() throws Exception {
|
public void preparePregenerationPlansRegionsInOrder() throws Exception {
|
||||||
FakePlanners planners = new FakePlanners(settings());
|
FakePlanners planners = new FakePlanners(settings());
|
||||||
@@ -702,6 +730,44 @@ public class HydrologyRuntimeTest {
|
|||||||
verify(engine, never()).getComplex();
|
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)
|
@Test(timeout = 300_000L)
|
||||||
public void createSavesPlansUnderTheWorldFolder() throws Exception {
|
public void createSavesPlansUnderTheWorldFolder() throws Exception {
|
||||||
Path worldFolder = temporaryFolder.newFolder("world").toPath();
|
Path worldFolder = temporaryFolder.newFolder("world").toPath();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import org.junit.Test;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -32,6 +33,35 @@ public class HeightLatticeTest {
|
|||||||
private static final int AREA = 3 * WINDOW;
|
private static final int AREA = 3 * WINDOW;
|
||||||
private static final int DISTINCT_ACROSS_NINE = 25 * WINDOW * 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
|
@Test
|
||||||
public void samplesEachCellOnceAcrossNineOverlappingWindows() {
|
public void samplesEachCellOnceAcrossNineOverlappingWindows() {
|
||||||
CountingSource source = new CountingSource();
|
CountingSource source = new CountingSource();
|
||||||
|
|||||||
+42
-1
@@ -317,6 +317,41 @@ public class HydrologyPlanCacheTest {
|
|||||||
assertEquals(26, planners.windowCalls.size());
|
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
|
@Test
|
||||||
public void plansWithRealPlannersAndReloadsFromStore() throws Exception {
|
public void plansWithRealPlannersAndReloadsFromStore() throws Exception {
|
||||||
HydrologySettings settings = settings(new IrisHydrology());
|
HydrologySettings settings = settings(new IrisHydrology());
|
||||||
@@ -412,13 +447,14 @@ public class HydrologyPlanCacheTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void leastRecentlyUsedWindowIsEvicted() {
|
public void leastRecentlyUsedWindowIsEvicted() throws Exception {
|
||||||
FakePlanners planners = new FakePlanners(unpersisted());
|
FakePlanners planners = new FakePlanners(unpersisted());
|
||||||
HydrologyPlanCache cache = cache(planners, pool(4), () -> false);
|
HydrologyPlanCache cache = cache(planners, pool(4), () -> false);
|
||||||
WindowKey kept = new WindowKey(0, 0);
|
WindowKey kept = new WindowKey(0, 0);
|
||||||
WindowKey stale = new WindowKey(1, 0);
|
WindowKey stale = new WindowKey(1, 0);
|
||||||
IndexedWindow keptWindow = cache.window(kept);
|
IndexedWindow keptWindow = cache.window(kept);
|
||||||
cache.window(stale);
|
cache.window(stale);
|
||||||
|
assertEquals(0L, cache.windowEvictions());
|
||||||
List<WindowKey> requested = new ArrayList<>(List.of(kept, stale));
|
List<WindowKey> requested = new ArrayList<>(List.of(kept, stale));
|
||||||
for (int index = 0; index < 300; index++) {
|
for (int index = 0; index < 300; index++) {
|
||||||
WindowKey key = new WindowKey(index, 7);
|
WindowKey key = new WindowKey(index, 7);
|
||||||
@@ -435,8 +471,13 @@ public class HydrologyPlanCacheTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
assertTrue("the cache stays bounded: " + ready, ready < requested.size() / 2);
|
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);
|
cache.window(stale);
|
||||||
assertEquals("an evicted window is planned again on demand", 2, planners.windowCalls(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
|
@Test
|
||||||
|
|||||||
+14
-1
@@ -30,7 +30,20 @@ public class HydrologySettingsTest {
|
|||||||
private static final int SEA_LEVEL = 62;
|
private static final int SEA_LEVEL = 62;
|
||||||
private static final int MIN_Y = -64;
|
private static final int MIN_Y = -64;
|
||||||
private static final int MAX_Y = 320;
|
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
|
@Rule
|
||||||
public final TemporaryFolder temporary = new TemporaryFolder();
|
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 ORIGIN = new RegionKey(0, 0);
|
||||||
private static final RegionKey EAST = new RegionKey(1, 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
|
@Test
|
||||||
public void coneIslandMajorReachesEndInOcean() {
|
public void coneIslandMajorReachesEndInOcean() {
|
||||||
HydrologySettings settings = settings();
|
HydrologySettings settings = settings();
|
||||||
|
|||||||
@@ -41,6 +41,14 @@ public class WindowPlannerTest {
|
|||||||
private static final WindowKey LAND = new WindowKey(4, 4);
|
private static final WindowKey LAND = new WindowKey(4, 4);
|
||||||
private static final int SEAM_DISTANCE = 256;
|
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
|
@Test
|
||||||
public void burnedTrunkCapturesLocalDrainage() {
|
public void burnedTrunkCapturesLocalDrainage() {
|
||||||
HydrologySettings settings = settings(new IrisHydrology());
|
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.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.doAnswer;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class CavePoolFillerTest {
|
public class CavePoolFillerTest {
|
||||||
private static final long SEED = 1337L;
|
private static final long SEED = 1337L;
|
||||||
private static final int FLOOR = 50;
|
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
|
@Test
|
||||||
public void poolLevelIsHighestContainedLevelMinusFreeboard() {
|
public void poolLevelIsHighestContainedLevelMinusFreeboard() {
|
||||||
VoxelWorld world = roomWithSill();
|
VoxelWorld world = roomWithSill();
|
||||||
|
|||||||
+25
@@ -21,6 +21,31 @@ import static org.junit.Assert.assertTrue;
|
|||||||
public class SeaInletPlannerTest {
|
public class SeaInletPlannerTest {
|
||||||
private static final int SEA = InletTerrain.SEA;
|
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
|
@Test
|
||||||
public void inletStartsAtOceanCell() {
|
public void inletStartsAtOceanCell() {
|
||||||
InletTerrain terrain = InletTerrain.cliff(30D);
|
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.HydrologyTerrainSource;
|
||||||
import art.arcane.iris.generation.hydrology.grid.PriorityFlood;
|
import art.arcane.iris.generation.hydrology.grid.PriorityFlood;
|
||||||
import art.arcane.iris.generation.hydrology.plan.HydrologySettings;
|
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.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 art.arcane.volmlib.util.collection.KList;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.BitSet;
|
import java.util.BitSet;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.DoubleBinaryOperator;
|
import java.util.function.DoubleBinaryOperator;
|
||||||
@@ -38,6 +47,77 @@ public class PondPlannerTest {
|
|||||||
private static final float BOWL_SPILL = 112f;
|
private static final float BOWL_SPILL = 112f;
|
||||||
private static final WindowKey ORIGIN = new WindowKey(0, 0);
|
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
|
@Test
|
||||||
public void naturalBasinPondSitsBelowSpill() {
|
public void naturalBasinPondSitsBelowSpill() {
|
||||||
HydrologySettings settings = settings(PondPlannerTest::naturalOnly, MIN_Y, MAX_Y);
|
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(200, 256));
|
||||||
assertTrue(pond.mask().contains(312, 256));
|
assertTrue(pond.mask().contains(312, 256));
|
||||||
assertContained(pond, terrain);
|
assertContained(pond, terrain);
|
||||||
|
assertRasterRimContained(settings, pond, terrain);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -527,6 +608,43 @@ public class PondPlannerTest {
|
|||||||
return new PondMask(x, 208, 16, 1, 7, new long[]{0b1111111L});
|
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) {
|
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());
|
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) {
|
private static void assertContained(Pond pond, DoubleBinaryOperator natural) {
|
||||||
PondMask mask = pond.mask();
|
PondMask mask = pond.mask();
|
||||||
int step = mask.step();
|
int step = mask.step();
|
||||||
|
int maxFill = new IrisHydrology().getDeformation().getMaxFill();
|
||||||
List<int[]> nodes = wetNodes(mask);
|
List<int[]> nodes = wetNodes(mask);
|
||||||
assertFalse(nodes.isEmpty());
|
assertFalse(nodes.isEmpty());
|
||||||
assertTrue("floor " + pond.floor() + " level " + pond.level(), pond.floor() < pond.level());
|
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 x = node[0] + direction[0];
|
||||||
int z = node[1] + direction[1];
|
int z = node[1] + direction[1];
|
||||||
if (!mask.contains(x, z)) {
|
if (!mask.contains(x, z)) {
|
||||||
float edge = (float) natural.applyAsDouble(x, z);
|
long edge = Math.round(natural.applyAsDouble(x, z));
|
||||||
assertTrue("edge " + x + "," + z + " natural " + edge + " level " + pond.level(), edge >= pond.level());
|
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.IrisPlatforms;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.spi.IrisServices;
|
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.structure.object.IObjectPlacer;
|
||||||
import art.arcane.iris.testsupport.FakeHydrology;
|
import art.arcane.iris.testsupport.FakeHydrology;
|
||||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||||
@@ -44,12 +48,14 @@ import java.util.EnumSet;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
import static org.junit.Assert.assertNotSame;
|
import static org.junit.Assert.assertNotSame;
|
||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertSame;
|
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)
|
@Test(timeout = 120_000L)
|
||||||
public void waterStreamsFollowTheRaster() throws Exception {
|
public void waterStreamsFollowTheRaster() throws Exception {
|
||||||
try (HydrologyFixture fixture = new HydrologyFixture(hydrologyPack())) {
|
try (HydrologyFixture fixture = new HydrologyFixture(hydrologyPack())) {
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ public class SchemaBuilderIdentityTest {
|
|||||||
@ClassRule
|
@ClassRule
|
||||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
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(
|
private static final List<Class<?>> SCHEMA_ROOTS = List.of(
|
||||||
IrisDimension.class,
|
IrisDimension.class,
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||||||
import java.util.function.BooleanSupplier;
|
import java.util.function.BooleanSupplier;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
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.assertSame;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
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
|
@Test
|
||||||
public void closeRejectsLatePublication() throws Exception {
|
public void closeRejectsLatePublication() throws Exception {
|
||||||
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
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.biome.IrisBiome;
|
||||||
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
|
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.plan.HydrologySettings;
|
||||||
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
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.Pond;
|
||||||
import art.arcane.iris.generation.hydrology.water.Reach;
|
import art.arcane.iris.generation.hydrology.water.Reach;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
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
|
@Test
|
||||||
public void channelPixelsShowTheirTier() throws Exception {
|
public void channelPixelsShowTheirTier() throws Exception {
|
||||||
HydrologySettings settings = FakeHydrology.settings(SEA_LEVEL);
|
HydrologySettings settings = FakeHydrology.settings(SEA_LEVEL);
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import org.junit.Test;
|
|||||||
|
|
||||||
import javax.swing.SwingUtilities;
|
import javax.swing.SwingUtilities;
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
import java.awt.Rectangle;
|
import java.awt.Rectangle;
|
||||||
import java.awt.event.MouseEvent;
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.awt.event.MouseWheelEvent;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
|
||||||
@@ -125,6 +127,82 @@ public class PregenMapPanelTest {
|
|||||||
SwingUtilities.invokeAndWait(extreme::disposeMap);
|
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 {
|
private static String tooltip(PregenMapPanel panel, int x, int y) throws Exception {
|
||||||
String[] result = new String[1];
|
String[] result = new String[1];
|
||||||
SwingUtilities.invokeAndWait(() -> result[0] = panel.getToolTipText(
|
SwingUtilities.invokeAndWait(() -> result[0] = panel.getToolTipText(
|
||||||
|
|||||||
@@ -11,8 +11,12 @@ import javax.swing.JButton;
|
|||||||
import javax.swing.JComponent;
|
import javax.swing.JComponent;
|
||||||
import javax.swing.JLabel;
|
import javax.swing.JLabel;
|
||||||
import javax.swing.JTextArea;
|
import javax.swing.JTextArea;
|
||||||
|
import javax.swing.JToggleButton;
|
||||||
|
import javax.swing.JViewport;
|
||||||
import javax.swing.KeyStroke;
|
import javax.swing.KeyStroke;
|
||||||
import javax.swing.SwingUtilities;
|
import javax.swing.SwingUtilities;
|
||||||
|
import java.awt.Component;
|
||||||
|
import java.awt.Container;
|
||||||
import java.awt.event.ActionEvent;
|
import java.awt.event.ActionEvent;
|
||||||
import java.awt.event.KeyEvent;
|
import java.awt.event.KeyEvent;
|
||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
@@ -27,6 +31,50 @@ import static org.junit.Assert.assertTrue;
|
|||||||
import static org.mockito.Mockito.mockStatic;
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
|
||||||
public class PregenRendererTest {
|
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
|
@Test
|
||||||
public void keyboardAndButtonActivationToggleExactlyOnceAndTerminalControlsAreDisabled() throws Exception {
|
public void keyboardAndButtonActivationToggleExactlyOnceAndTerminalControlsAreDisabled() throws Exception {
|
||||||
onEdt(() -> {
|
onEdt(() -> {
|
||||||
@@ -155,6 +203,32 @@ public class PregenRendererTest {
|
|||||||
refresh.invoke(renderer);
|
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 {
|
private static <T> T field(PregenRenderer renderer, String name, Class<T> type) throws Exception {
|
||||||
Field field = PregenRenderer.class.getDeclaredField(name);
|
Field field = PregenRenderer.class.getDeclaredField(name);
|
||||||
field.setAccessible(true);
|
field.setAccessible(true);
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ public class IrisRiverSchemaTest {
|
|||||||
JSONObject fluid = referencedProperties(definitions, properties.getJSONObject("fluids").getJSONObject("items"));
|
JSONObject fluid = referencedProperties(definitions, properties.getJSONObject("fluids").getJSONObject("items"));
|
||||||
|
|
||||||
assertEquals("boolean", properties.getJSONObject("enabled").getString("type"));
|
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("boolean", properties.getJSONObject("persist").getString("type"));
|
||||||
assertEquals(0, properties.getJSONObject("prefetchRings").getInt("minimum"));
|
assertEquals(0, properties.getJSONObject("prefetchRings").getInt("minimum"));
|
||||||
assertEquals(4, properties.getJSONObject("prefetchRings").getInt("maximum"));
|
assertEquals(4, properties.getJSONObject("prefetchRings").getInt("maximum"));
|
||||||
|
|||||||
+43
-3
@@ -34,6 +34,8 @@ import java.io.File;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.attribute.FileTime;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -895,7 +897,7 @@ public class GenerationRegistryContractFactoryTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void rejectsAChangedPackBeforeCapture() throws Exception {
|
public void rejectsAPackChangedBeforeCapture() throws Exception {
|
||||||
File pack = temporary.newFolder("changed-pack");
|
File pack = temporary.newFolder("changed-pack");
|
||||||
String original = GenerationPackFingerprint.compute(
|
String original = GenerationPackFingerprint.compute(
|
||||||
pack.toPath(),
|
pack.toPath(),
|
||||||
@@ -904,20 +906,58 @@ public class GenerationRegistryContractFactoryTest {
|
|||||||
Files.writeString(pack.toPath().resolve("dimension.json"), "{}", StandardCharsets.UTF_8);
|
Files.writeString(pack.toPath().resolve("dimension.json"), "{}", StandardCharsets.UTF_8);
|
||||||
IrisData data = mock(IrisData.class);
|
IrisData data = mock(IrisData.class);
|
||||||
when(data.getDataFolder()).thenReturn(pack);
|
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(
|
assertThrows(
|
||||||
IOException.class,
|
IOException.class,
|
||||||
() -> GenerationRegistryContractFactory.create(
|
() -> GenerationRegistryContractFactory.create(
|
||||||
data,
|
data,
|
||||||
new IrisDimension(),
|
dimension,
|
||||||
original,
|
original,
|
||||||
DataVersion.getLatest().get(),
|
DataVersion.getLatest().get(),
|
||||||
mock(PlatformRegistries.class),
|
registries,
|
||||||
new FakeGenerationRegistry("runtime")
|
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
|
@Test
|
||||||
public void rejectsAnUnloadableAdditionalDimensionBeforeCapturingItsRegistry() throws Exception {
|
public void rejectsAnUnloadableAdditionalDimensionBeforeCapturingItsRegistry() throws Exception {
|
||||||
File pack = temporary.newFolder("missing-secondary-dimension");
|
File pack = temporary.newFolder("missing-secondary-dimension");
|
||||||
|
|||||||
Reference in new issue
Block a user