mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-28 06:50:02 +00:00
Refine terrain and pregeneration
- Add coherent river width variation and shape source and terminal pools. - Preserve separated hydrology intervals when checking river connectivity. - Reduce footprint sorting, engine context lookup, and terrain blending work. - Reuse modded snapshot validation after content and registry checks. - Explain production pack update staging and restart requirements. - Retain pregeneration map updates before its desktop window opens. - Improve pregeneration layout, coordinate hover, keyboard controls, and error details. - Add the new desktop labels to every supported language catalog. - Add the buildPsychoLT entry point for verified platform outputs. - Cover terrain, validation, and desktop behavior with regression tests.
This commit is contained in:
56 files changed
+1656
-280
No files matched your search
+1
-1
@@ -589,7 +589,7 @@ public record HydrologyPlannerSettings(
|
||||
}
|
||||
}
|
||||
|
||||
/** A pond at one end of a surface course: a round bowl holding the course's head at that end. */
|
||||
/** A pond at one end of a surface course: a shaped bowl holding the course's head at that end. */
|
||||
public record Pond(boolean enabled, int minimumRadius, int maximumRadius, int depth) {
|
||||
public Pond {
|
||||
if (minimumRadius < 1 || maximumRadius < minimumRadius || depth < 1) {
|
||||
|
||||
+82
-3
@@ -2,6 +2,8 @@ package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceCenterline;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceLayerColumn;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue;
|
||||
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayList;
|
||||
@@ -71,10 +73,16 @@ final class HydrologyRegionalConnectivity {
|
||||
}
|
||||
|
||||
boolean connected(RiverCourse course, HydrologyTerrainSampler sampler, HydrologyPlannerSettings settings) {
|
||||
if (water.isEmpty() || !disjoint.isEmpty()) {
|
||||
if (water.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
LongOpenHashSet visited = new LongOpenHashSet(water.size());
|
||||
if (!disjoint.isEmpty()) {
|
||||
if (!connectedIntervals(visited)) {
|
||||
return false;
|
||||
}
|
||||
return reachesTerminal(course, sampler, settings, visited);
|
||||
}
|
||||
LongArrayFIFOQueue queue = new LongArrayFIFOQueue();
|
||||
long start = water.keySet().iterator().nextLong();
|
||||
queue.enqueue(start);
|
||||
@@ -102,6 +110,65 @@ final class HydrologyRegionalConnectivity {
|
||||
if (visited.size() != water.size()) {
|
||||
return false;
|
||||
}
|
||||
return reachesTerminal(course, sampler, settings, visited);
|
||||
}
|
||||
|
||||
private boolean connectedIntervals(LongOpenHashSet visited) {
|
||||
int count = water.size();
|
||||
for (LongArrayList intervals : disjoint.values()) {
|
||||
count += intervals.size() - 1;
|
||||
}
|
||||
long[] columns = new long[count];
|
||||
long[] intervals = new long[count];
|
||||
Long2IntOpenHashMap starts = new Long2IntOpenHashMap(water.size());
|
||||
starts.defaultReturnValue(-1);
|
||||
int index = 0;
|
||||
for (long column : water.keySet()) {
|
||||
starts.put(column, index);
|
||||
LongArrayList layers = disjoint.get(column);
|
||||
int size = layers == null ? 1 : layers.size();
|
||||
for (int layer = 0; layer < size; layer++) {
|
||||
columns[index] = column;
|
||||
intervals[index++] = layers == null ? water.get(column) : layers.getLong(layer);
|
||||
}
|
||||
}
|
||||
boolean[] reached = new boolean[count];
|
||||
IntArrayFIFOQueue queue = new IntArrayFIFOQueue();
|
||||
queue.enqueue(0);
|
||||
reached[0] = true;
|
||||
visited.add(columns[0]);
|
||||
int reachedCount = 1;
|
||||
while (!queue.isEmpty()) {
|
||||
int current = queue.dequeueInt();
|
||||
int currentBed = (int) (intervals[current] >> 32);
|
||||
int currentHead = (int) intervals[current];
|
||||
int x = RiverFootprint.unpackX(columns[current]);
|
||||
int z = RiverFootprint.unpackZ(columns[current]);
|
||||
for (int[] offset : NEIGHBORS) {
|
||||
long nextColumn = RiverFootprint.pack(x + offset[0], z + offset[1]);
|
||||
int start = starts.get(nextColumn);
|
||||
if (start < 0) {
|
||||
continue;
|
||||
}
|
||||
LongArrayList layers = disjoint.get(nextColumn);
|
||||
int end = start + (layers == null ? 1 : layers.size());
|
||||
for (int next = start; next < end; next++) {
|
||||
if (reached[next] || Math.max(currentBed, (int) (intervals[next] >> 32))
|
||||
>= Math.min(currentHead, (int) intervals[next])) {
|
||||
continue;
|
||||
}
|
||||
reached[next] = true;
|
||||
reachedCount++;
|
||||
visited.add(nextColumn);
|
||||
queue.enqueue(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
return reachedCount == count;
|
||||
}
|
||||
|
||||
private boolean reachesTerminal(RiverCourse course, HydrologyTerrainSampler sampler,
|
||||
HydrologyPlannerSettings settings, LongOpenHashSet visited) {
|
||||
HydraulicSegment first = course.segments().getFirst();
|
||||
HydraulicSegment last = course.segments().getLast();
|
||||
return reaches(last, false, sampler, settings, visited)
|
||||
@@ -154,8 +221,20 @@ final class HydrologyRegionalConnectivity {
|
||||
private boolean touchesOwnedWater(int x, int z, int seaLevel, LongOpenHashSet visited) {
|
||||
for (int[] offset : NEIGHBORS) {
|
||||
long key = RiverFootprint.pack(x + offset[0], z + offset[1]);
|
||||
if (visited.contains(key) && (int) water.get(key) == seaLevel) {
|
||||
return true;
|
||||
if (!visited.contains(key)) {
|
||||
continue;
|
||||
}
|
||||
LongArrayList layers = disjoint.get(key);
|
||||
if (layers == null) {
|
||||
if ((int) water.get(key) == seaLevel) {
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (int index = 0; index < layers.size(); index++) {
|
||||
if ((int) layers.getLong(index) == seaLevel) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
+12
-5
@@ -31,6 +31,7 @@ final class HydrologyRegionalPlanner {
|
||||
private static final long BASIN_SALT = 0x524547424153494eL;
|
||||
private static final long COURSE_SALT = 0x524547434f555253L;
|
||||
private static final long NODE_SALT = 0x5245474e4f444553L;
|
||||
private static final double AUTHORED_WIDTH_WEIGHT = 0.65D;
|
||||
private static final int MAXIMUM_SOURCE_TRIALS = 24;
|
||||
private static final int SOURCE_TRIAL_WINDOW = 16;
|
||||
private static final int MAXIMUM_DIAGNOSTICS = 64;
|
||||
@@ -579,11 +580,17 @@ final class HydrologyRegionalPlanner {
|
||||
if (request.field() != HydrologyGeometrySampler.Field.SURFACE_WIDTH) {
|
||||
return planner.geometrySampler.sample(request);
|
||||
}
|
||||
int authored = planner.geometrySampler.sample(request);
|
||||
double fraction = 0.25D + 0.75D * context.flow().fraction(request.x(), request.z());
|
||||
double sampled = request.minimum() + (request.maximum() - request.minimum()) * fraction;
|
||||
double width = sampled * 0.75D + authored * 0.25D;
|
||||
double freedom = context.morphology().freedomAt(request.x(), request.z());
|
||||
return regionalWidth(request, planner.geometrySampler.sample(request),
|
||||
context.flow().fraction(request.x(), request.z()),
|
||||
context.morphology().freedomAt(request.x(), request.z()));
|
||||
}
|
||||
|
||||
static int regionalWidth(HydrologyGeometrySampler.Request request, int authored,
|
||||
double flow, double freedom) {
|
||||
double fraction = 0.25D + 0.75D * flow;
|
||||
double range = (double) request.maximum() - request.minimum();
|
||||
double sampled = request.minimum() + range * fraction;
|
||||
double width = sampled * (1D - AUTHORED_WIDTH_WEIGHT) + authored * AUTHORED_WIDTH_WEIGHT;
|
||||
double constrained = request.minimum() + (width - request.minimum()) * freedom;
|
||||
return Math.max(request.minimum(), Math.min(request.maximum(), (int) StrictMath.round(constrained)));
|
||||
}
|
||||
|
||||
+4
-4
@@ -16,7 +16,7 @@ import lombok.experimental.Accessors;
|
||||
@Description("Controls the wet channel of a surface river.")
|
||||
@Data
|
||||
public class IrisSurfaceRiverChannelConfig {
|
||||
@Description("Wet channel width in blocks.")
|
||||
@Description("Wet channel width range in blocks. Authored noise and coherent reach variation shape the width; equal endpoints disable the additional variation.")
|
||||
private IrisStyledRange width = range(4D, 8D, 1024D);
|
||||
|
||||
@Description("Wet bed depth in blocks at the channel center.")
|
||||
@@ -44,17 +44,17 @@ public class IrisSurfaceRiverChannelConfig {
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(4)
|
||||
@Description("Width of the spring pool at the headwater relative to the channel width; 1 starts the river at its normal width.")
|
||||
@Description("Peak width of the spring pool relative to the channel width, reached near the first quarter of springLength; 1 keeps the source at normal channel width.")
|
||||
private double springWidthRatio = 2.5D;
|
||||
|
||||
@MinNumber(4)
|
||||
@MaxNumber(96)
|
||||
@Description("Blocks over which the spring pool narrows back to the channel width.")
|
||||
@Description("Blocks over which the source widens into its spring pool and narrows back to the channel width.")
|
||||
private int springLength = 24;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(64)
|
||||
@Description("Stations along the course the sampled width and depth are averaged over, so the channel changes size gradually; 0 follows the sampled values exactly at every station.")
|
||||
@Description("Stations along the course the varied width and sampled depth are averaged over; 0 retains their unsmoothed values at each station.")
|
||||
private int smoothingRadius = 16;
|
||||
|
||||
@MinNumber(0.2)
|
||||
|
||||
+35
-3
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.generation.hydrology.surface;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.HydrologyGeometrySampler;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyHash;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTerrainSample;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTerrainSampler;
|
||||
@@ -8,6 +9,13 @@ import art.arcane.iris.generation.hydrology.HydrologyTerrainSampler;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class ChannelProfileBuilder {
|
||||
private static final long WIDTH_REACH_SALT = 0x5749445448524541L;
|
||||
private static final double WIDTH_REACH_STRENGTH = 0.45D;
|
||||
private static final double SPRING_OPENING_FRACTION = 0.6D;
|
||||
private static final double SPRING_PEAK_PROGRESS = 0.25D;
|
||||
private static final int MINIMUM_WIDTH_WAVELENGTH = 32;
|
||||
private static final int MAXIMUM_WIDTH_WAVELENGTH = 128;
|
||||
|
||||
private final HydrologyPlannerSettings.Surface surface;
|
||||
private final HydrologyTerrainSampler sampler;
|
||||
private final HydrologyGeometrySampler geometry;
|
||||
@@ -25,13 +33,18 @@ public final class ChannelProfileBuilder {
|
||||
public ChannelProfile build(
|
||||
SurfaceCenterline centerline,
|
||||
String profileKey,
|
||||
boolean directOcean
|
||||
boolean directOcean,
|
||||
long worldSeed
|
||||
) {
|
||||
profileKey = profileKey == null ? "" : profileKey;
|
||||
int count = centerline.size();
|
||||
double[] width = new double[count];
|
||||
double[] depth = new double[count];
|
||||
double[] bank = new double[count];
|
||||
int coast = count;
|
||||
long widthSeed = HydrologyHash.mix(worldSeed, WIDTH_REACH_SALT, profileKey.hashCode());
|
||||
int widthWavelength = Math.max(MINIMUM_WIDTH_WAVELENGTH,
|
||||
Math.min(MAXIMUM_WIDTH_WAVELENGTH, surface.maximumWidth() * 12));
|
||||
for (int station = 0; station < count; station++) {
|
||||
int stationX = centerline.x()[station];
|
||||
int stationZ = centerline.z()[station];
|
||||
@@ -60,7 +73,12 @@ public final class ChannelProfileBuilder {
|
||||
double widthMultiplier = terrain == null ? 1D : terrain.widthMultiplier();
|
||||
double depthMultiplier = terrain == null ? 1D : terrain.depthMultiplier();
|
||||
bank[station] = terrain == null ? 1D : terrain.bankMultiplier();
|
||||
width[station] = clamp(sampledWidth * widthMultiplier, surface.minimumWidth(), surface.maximumWidth() * 2D);
|
||||
double variedWidth = sampledWidth;
|
||||
if (surface.maximumWidth() > surface.minimumWidth() && sampledWidth > surface.minimumWidth()) {
|
||||
variedWidth = reachWidth(sampledWidth, surface.minimumWidth(), surface.maximumWidth(),
|
||||
SurfaceNoise.signed(widthSeed, stationX, stationZ, widthWavelength));
|
||||
}
|
||||
width[station] = clamp(variedWidth * widthMultiplier, surface.minimumWidth(), surface.maximumWidth() * 2D);
|
||||
depth[station] = clamp(sampledDepth * depthMultiplier, 1D, surface.maximumDepth() * 2D);
|
||||
}
|
||||
HydrologyPlannerSettings.Channel channel = surface.banks().channel();
|
||||
@@ -74,7 +92,12 @@ public final class ChannelProfileBuilder {
|
||||
for (int station = 0; station < spring; station++) {
|
||||
double remaining = 1D - SurfaceNoise.smoothStep(station / (double) spring);
|
||||
double localRatio = 1D + (springRatio - 1D) * springRoom(centerline, station, smoothWidth[station] * springRatio / 2D);
|
||||
smoothWidth[station] *= 1D + (localRatio - 1D) * remaining;
|
||||
double progress = station / (double) spring;
|
||||
double opening = progress <= SPRING_PEAK_PROGRESS
|
||||
? SPRING_OPENING_FRACTION + (1D - SPRING_OPENING_FRACTION)
|
||||
* SurfaceNoise.smoothStep(progress / SPRING_PEAK_PROGRESS)
|
||||
: 1D - SurfaceNoise.smoothStep((progress - SPRING_PEAK_PROGRESS) / (1D - SPRING_PEAK_PROGRESS));
|
||||
smoothWidth[station] *= 1D + (localRatio - 1D) * opening;
|
||||
smoothDepth[station] += remaining * channel.springExtraDepth();
|
||||
}
|
||||
// The inlet: over its length before the coast the channel widens toward the mouth flare and
|
||||
@@ -146,6 +169,15 @@ public final class ChannelProfileBuilder {
|
||||
return smoothed;
|
||||
}
|
||||
|
||||
static double reachWidth(double authored, int minimum, int maximum, double reach) {
|
||||
double range = (double) maximum - minimum;
|
||||
if (range <= 0D || authored <= minimum) {
|
||||
return authored;
|
||||
}
|
||||
double freedom = Math.min(1D, (authored - minimum) / (range * 0.25D));
|
||||
return clamp(authored + range * WIDTH_REACH_STRENGTH * freedom * reach, minimum, maximum);
|
||||
}
|
||||
|
||||
private static double clamp(double value, double minimum, double maximum) {
|
||||
return Math.max(minimum, Math.min(maximum, value));
|
||||
}
|
||||
|
||||
+24
-15
@@ -28,6 +28,11 @@ public final class ErosionFieldCompiler {
|
||||
private static final long SOURCE_POND_SALT = 0x504f4e4453524345L;
|
||||
private static final long TERMINAL_POND_SALT = 0x504f4e4454524dL;
|
||||
private static final int POND_RIM_SAMPLES_PER_BLOCK = 8;
|
||||
private static final long POND_ASPECT_SALT = 0x504f4e4441535045L;
|
||||
private static final double POND_LONG_AXIS_RATIO = 0.85D;
|
||||
private static final double POND_OFFSET_RATIO = 0.15D;
|
||||
private static final double POND_MINIMUM_SHORT_AXIS_RATIO = 0.6D;
|
||||
private static final double POND_SHORT_AXIS_RANGE = 0.2D;
|
||||
private static final double EPSILON = 1.0E-9D;
|
||||
|
||||
private final HydrologyPlannerSettings.Surface surface;
|
||||
@@ -174,22 +179,20 @@ public final class ErosionFieldCompiler {
|
||||
continue;
|
||||
}
|
||||
long key = RiverFootprint.pack(cellX, cellZ);
|
||||
int index = indices.get(key);
|
||||
int existing = index < 0 ? -1 : stations.getInt(index);
|
||||
double existingDistance = index < 0 ? 0D : distance.getDouble(index);
|
||||
int index = indices.putIfAbsent(key, stations.size());
|
||||
if (index < 0) {
|
||||
stations.add(station);
|
||||
distance.add(cellDistance);
|
||||
continue;
|
||||
}
|
||||
int existing = stations.getInt(index);
|
||||
double existingDistance = distance.getDouble(index);
|
||||
// A cell on a station's cross-section row ties between the two segments meeting
|
||||
// there; the later station owns it so the row uses the head its own ring bounded.
|
||||
if (existing < 0
|
||||
|| cellDistance < existingDistance - EPSILON
|
||||
if (cellDistance < existingDistance - EPSILON
|
||||
|| Math.abs(cellDistance - existingDistance) <= EPSILON && station > existing) {
|
||||
if (index < 0) {
|
||||
indices.put(key, stations.size());
|
||||
stations.add(station);
|
||||
distance.add(cellDistance);
|
||||
} else {
|
||||
stations.set(index, station);
|
||||
distance.set(index, cellDistance);
|
||||
}
|
||||
stations.set(index, station);
|
||||
distance.set(index, cellDistance);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,7 +355,7 @@ public final class ErosionFieldCompiler {
|
||||
}
|
||||
|
||||
/**
|
||||
* A round bowl around an end station holding that station's head: wet inside its outline, a shore
|
||||
* A shaped bowl around an end station holding that station's head: wet inside its outline, a shore
|
||||
* ring and an eroded rim outside it like the channel's own. Channel water inside the bowl joins
|
||||
* the pond at the pond's level; dry channel columns give way to the bowl. The radius is chosen per
|
||||
* course within the configured range and shrinks where the ground around the rim falls below the
|
||||
@@ -399,9 +402,15 @@ public final class ErosionFieldCompiler {
|
||||
double maximumBlendWidth = Math.min(banks.maximumBlendWidth(),
|
||||
Math.max(0.25D, erosion.excavation().maximumWidth() - shore));
|
||||
int reach = (int) StrictMath.ceil(radius * (1D + roughness) + shore + banks.maximumBlendWidth()) + 1;
|
||||
double tangentX = centerline.tangentX()[station];
|
||||
double tangentZ = centerline.tangentZ()[station];
|
||||
double offset = radius * POND_OFFSET_RATIO;
|
||||
double minor = POND_MINIMUM_SHORT_AXIS_RATIO + POND_SHORT_AXIS_RANGE * HydrologyHash.unit(HydrologyHash.mix(seed, POND_ASPECT_SALT));
|
||||
for (int deltaZ = -reach; deltaZ <= reach; deltaZ++) {
|
||||
for (int deltaX = -reach; deltaX <= reach; deltaX++) {
|
||||
double distance = Math.sqrt((double) deltaX * deltaX + (double) deltaZ * deltaZ);
|
||||
double along = (deltaX * tangentX + deltaZ * tangentZ - offset) / POND_LONG_AXIS_RATIO;
|
||||
double across = (-deltaX * tangentZ + deltaZ * tangentX) / minor;
|
||||
double distance = Math.sqrt(along * along + across * across);
|
||||
if (distance > reach) {
|
||||
continue;
|
||||
}
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ public final class SurfaceCourseBuilder {
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(path);
|
||||
boolean directOcean = terminal == SurfaceTerminal.OCEAN_MOUTH;
|
||||
ChannelProfile channel = new ChannelProfileBuilder(surface, sampler, geometry)
|
||||
.build(centerline, profileKey, directOcean);
|
||||
.build(centerline, profileKey, directOcean, worldSeed);
|
||||
ValleyProfile valley = new RiverProfileSolver(new RiverProfileSolver.Options(surface, seaLevel), sampler)
|
||||
.solve(path, centerline, channel, terminal, terminalHead, minimumCourseLength);
|
||||
if (!valley.accepted()) {
|
||||
|
||||
+28
-4
@@ -21,12 +21,15 @@ import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class SurfaceFootprintCompiler {
|
||||
private static final long COURSE_SEED_SALT = 0x535552464143455fL;
|
||||
private static final Comparator<SurfaceColumn> COLUMN_POSITION_ORDER = Comparator.comparingLong(
|
||||
column -> RiverFootprint.pack(column.x(), column.z()));
|
||||
|
||||
private final HydrologyPlannerSettings settings;
|
||||
private final HydrologyTerrainSampler sampler;
|
||||
@@ -175,11 +178,9 @@ public final class SurfaceFootprintCompiler {
|
||||
seed, centerline, run.channel(), run.valley(), run.terminal(),
|
||||
settings.outlets().maximumOceanApron(), run.ponds(), context,
|
||||
run.field());
|
||||
ArrayList<SurfaceColumn> ordered = new ArrayList<>(field.columns().values());
|
||||
ordered.sort(Comparator.comparingInt(SurfaceColumn::station)
|
||||
.thenComparingLong((SurfaceColumn column) -> RiverFootprint.pack(column.x(), column.z())));
|
||||
SurfaceColumn[] ordered = orderedColumns(field, centerline.size());
|
||||
SurfaceFeatureRefs features = new SurfaceFeatureRefs(course.id());
|
||||
ArrayList<SurfaceLayerColumn> columns = new ArrayList<>(ordered.size());
|
||||
ArrayList<SurfaceLayerColumn> columns = new ArrayList<>(ordered.length);
|
||||
for (SurfaceColumn column : ordered) {
|
||||
if (bounds != null && !bounds.contains(column.x(), column.z())) {
|
||||
continue;
|
||||
@@ -199,6 +200,29 @@ public final class SurfaceFootprintCompiler {
|
||||
return new SurfaceFootprint(columns, field.uncontainedWetCells(), field.rejection(), field.rejectionDetail(), field.bankExcavation());
|
||||
}
|
||||
|
||||
static SurfaceColumn[] orderedColumns(ErosionField field, int stationCount) {
|
||||
int[] ends = new int[stationCount + 1];
|
||||
for (SurfaceColumn column : field.columns().values()) {
|
||||
ends[column.station() + 1]++;
|
||||
}
|
||||
for (int station = 1; station < ends.length; station++) {
|
||||
ends[station] += ends[station - 1];
|
||||
}
|
||||
SurfaceColumn[] ordered = new SurfaceColumn[field.size()];
|
||||
for (SurfaceColumn column : field.columns().values()) {
|
||||
ordered[ends[column.station()]++] = column;
|
||||
}
|
||||
int start = 0;
|
||||
for (int station = 0; station < stationCount; station++) {
|
||||
int end = ends[station];
|
||||
if (end - start > 1) {
|
||||
Arrays.sort(ordered, start, end, COLUMN_POSITION_ORDER);
|
||||
}
|
||||
start = end;
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
|
||||
private SurfaceRunBoundary runBoundary(RiverCourse course, int first, int after,
|
||||
SurfaceCenterline centerline, ChannelProfile channel) {
|
||||
SurfaceRunBoundary.Plane start = null;
|
||||
|
||||
@@ -200,38 +200,37 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
if (!Double.isFinite(surfaceDetail) || surfaceDetail < 0D || surfaceDetail > 1D) {
|
||||
throw new IllegalArgumentException("Generator surfaceDetail must be finite and between 0 and 1");
|
||||
}
|
||||
if (surfaceDetail == 1D || composite.isEmpty()) {
|
||||
return sampleHeight(rx, rz, superSeed);
|
||||
if (composite.isEmpty()) {
|
||||
return 0D;
|
||||
}
|
||||
IrisData data = getLoader();
|
||||
Engine engine = data == null ? null : data.getEngine();
|
||||
if (surfaceDetail == 1D) {
|
||||
return sampleHeight(rx, rz, superSeed, data, engine);
|
||||
}
|
||||
SurfaceCacheStripe[] stripes = surfaceCache(data, engine).stripes();
|
||||
double lowerX = Math.floor(rx / SURFACE_GRID) * SURFACE_GRID;
|
||||
double lowerZ = Math.floor(rz / SURFACE_GRID) * SURFACE_GRID;
|
||||
double dx = (rx - lowerX) / SURFACE_GRID;
|
||||
double dz = (rz - lowerZ) / SURFACE_GRID;
|
||||
double northWest = cachedSurfaceHeight(lowerX, lowerZ, superSeed, stripes);
|
||||
double northWest = cachedSurfaceHeight(lowerX, lowerZ, superSeed, stripes, data, engine);
|
||||
if (dx == 0D && dz == 0D) {
|
||||
return northWest;
|
||||
}
|
||||
double north = dx == 0D ? northWest : IrisInterpolation.lerp(northWest,
|
||||
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ, superSeed, stripes), dx);
|
||||
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ, superSeed, stripes, data, engine), dx);
|
||||
double smooth = north;
|
||||
if (dz != 0D) {
|
||||
double southWest = cachedSurfaceHeight(lowerX, lowerZ + SURFACE_GRID, superSeed, stripes);
|
||||
double southWest = cachedSurfaceHeight(lowerX, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine);
|
||||
double south = dx == 0D ? southWest : IrisInterpolation.lerp(southWest,
|
||||
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID, superSeed, stripes), dx);
|
||||
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine), dx);
|
||||
smooth = IrisInterpolation.lerp(north, south, dz);
|
||||
}
|
||||
return surfaceDetail == 0D ? smooth
|
||||
: smooth + (sampleHeight(rx, rz, superSeed) - smooth) * surfaceDetail;
|
||||
: smooth + (sampleHeight(rx, rz, superSeed, data, engine) - smooth) * surfaceDetail;
|
||||
}
|
||||
|
||||
private double sampleHeight(double rx, double rz, long superSeed) {
|
||||
if (composite.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private double sampleHeight(double rx, double rz, long superSeed, IrisData data, Engine engine) {
|
||||
int hc = (int) ((cliffHeightMin * 10) + 10 + cliffHeightMax * getSeed() + offsetX + offsetZ);
|
||||
double h = multiplicitive ? 1 : 0;
|
||||
double tp = 0;
|
||||
@@ -240,18 +239,18 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
|
||||
if (composite.size() == 1) {
|
||||
if (multiplicitive) {
|
||||
h *= composite.get(0).getNoise(getSeed() + superSeed + hc, sampleX, sampleZ, getLoader());
|
||||
h *= composite.get(0).getNoise(getSeed() + superSeed + hc, sampleX, sampleZ, data, engine);
|
||||
} else {
|
||||
tp += composite.get(0).getOpacity();
|
||||
h += composite.get(0).getNoise(getSeed() + superSeed + hc, sampleX, sampleZ, getLoader());
|
||||
h += composite.get(0).getNoise(getSeed() + superSeed + hc, sampleX, sampleZ, data, engine);
|
||||
}
|
||||
} else {
|
||||
for (IrisNoiseGenerator i : composite) {
|
||||
if (multiplicitive) {
|
||||
h *= i.getNoise(getSeed() + superSeed + hc, sampleX, sampleZ, getLoader());
|
||||
h *= i.getNoise(getSeed() + superSeed + hc, sampleX, sampleZ, data, engine);
|
||||
} else {
|
||||
tp += i.getOpacity();
|
||||
h += i.getNoise(getSeed() + superSeed + hc, sampleX, sampleZ, getLoader());
|
||||
h += i.getNoise(getSeed() + superSeed + hc, sampleX, sampleZ, data, engine);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -262,15 +261,15 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
v = 0;
|
||||
}
|
||||
|
||||
v = hasCliffs() ? cliff(rx, rz, v, superSeed + 294596 + hc) : v;
|
||||
v = hasCliffs() ? cliff(rx, rz, v, superSeed + 294596 + hc, data, engine) : v;
|
||||
v = hasCellCracks() ? cell(rx, rz, v, superSeed + 48622 + hc) : v;
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
private double cachedSurfaceHeight(double x, double z, long superSeed, SurfaceCacheStripe[] stripes) {
|
||||
private double cachedSurfaceHeight(double x, double z, long superSeed, SurfaceCacheStripe[] stripes, IrisData data, Engine engine) {
|
||||
if (x != (int) x || z != (int) z) {
|
||||
return sampleHeight(x, z, superSeed);
|
||||
return sampleHeight(x, z, superSeed, data, engine);
|
||||
}
|
||||
long key = ((long) (int) x << 32) | ((int) z & 0xffffffffL);
|
||||
long mixed = key ^ (key >>> 33);
|
||||
@@ -282,7 +281,7 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
if (cached != null) {
|
||||
return cached.height();
|
||||
}
|
||||
double height = sampleHeight(x, z, superSeed);
|
||||
double height = sampleHeight(x, z, superSeed, data, engine);
|
||||
stripe.put(slot, new SurfaceSample(key, superSeed, height));
|
||||
return height;
|
||||
}
|
||||
@@ -321,15 +320,25 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
}
|
||||
|
||||
public double getCliffHeight(double rx, double rz, double superSeed) {
|
||||
IrisData data = getLoader();
|
||||
return getCliffHeight(rx, rz, superSeed, data, data == null ? null : data.getEngine());
|
||||
}
|
||||
|
||||
private double getCliffHeight(double rx, double rz, double superSeed, IrisData data, Engine engine) {
|
||||
int hc = (int) ((cliffHeightMin * 10) + 10 + cliffHeightMax * getSeed() + offsetX + offsetZ);
|
||||
double sampleX = (rx + offsetX) / zoom;
|
||||
double sampleZ = (rz + offsetZ) / zoom;
|
||||
double h = cliffHeightGenerator.getNoise((long) (getSeed() + superSeed + hc), sampleX, sampleZ, getLoader());
|
||||
double h = cliffHeightGenerator.getNoise((long) (getSeed() + superSeed + hc), sampleX, sampleZ, data, engine);
|
||||
return IrisInterpolation.lerp(cliffHeightMin, cliffHeightMax, h);
|
||||
}
|
||||
|
||||
public double cliff(double rx, double rz, double v, double superSeed) {
|
||||
double cliffHeight = getCliffHeight(rx, rz, superSeed - 34857);
|
||||
IrisData data = getLoader();
|
||||
return cliff(rx, rz, v, superSeed, data, data == null ? null : data.getEngine());
|
||||
}
|
||||
|
||||
private double cliff(double rx, double rz, double v, double superSeed, IrisData data, Engine engine) {
|
||||
double cliffHeight = getCliffHeight(rx, rz, superSeed - 34857, data, engine);
|
||||
return (Math.round((v * 255D) / cliffHeight) * cliffHeight) / 255D;
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,11 @@ public class IrisGeneratorStyle {
|
||||
}
|
||||
|
||||
public CNG createForLayer(RNG rng, IrisData data, int octaveMultiplier) {
|
||||
return createNoCache(rng, data, Math.clamp(octaveMultiplier, 1, 16), 0, false, resolveEngine(data));
|
||||
return createForLayer(rng, data, octaveMultiplier, resolveEngine(data));
|
||||
}
|
||||
|
||||
CNG createForLayer(RNG rng, IrisData data, int octaveMultiplier, Engine engine) {
|
||||
return createNoCache(rng, data, Math.clamp(octaveMultiplier, 1, 16), 0, false, engine);
|
||||
}
|
||||
|
||||
public CNG createForPrebake(RNG rng, IrisData data, int fallbackCacheSize) {
|
||||
|
||||
@@ -102,8 +102,7 @@ public class IrisNoiseGenerator {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
protected CNG getGenerator(long superSeed, IrisData data) {
|
||||
Engine engine = data == null ? null : data.getEngine();
|
||||
CNG getGenerator(long superSeed, IrisData data, Engine engine) {
|
||||
long generatorSeed = superSeed + GENERATOR_SEED_SALT - seed;
|
||||
CachedGenerator recent = recentGenerator.get();
|
||||
if (recent != null && recent.key.matches(data, engine, generatorSeed)) {
|
||||
@@ -112,7 +111,7 @@ public class IrisNoiseGenerator {
|
||||
|
||||
GeneratorKey key = new GeneratorKey(data, engine, generatorSeed);
|
||||
CNG generator = generators.computeIfAbsent(key,
|
||||
ignored -> style.createForLayer(new RNG(generatorSeed), data, octaves));
|
||||
ignored -> style.createForLayer(new RNG(generatorSeed), data, octaves, engine));
|
||||
if (generator != null) {
|
||||
recentGenerator.set(new CachedGenerator(key, generator));
|
||||
}
|
||||
@@ -128,6 +127,14 @@ public class IrisNoiseGenerator {
|
||||
return offsetY;
|
||||
}
|
||||
|
||||
return getNoise(superSeed, xv, zv, data, data == null ? null : data.getEngine());
|
||||
}
|
||||
|
||||
double getNoise(long superSeed, double xv, double zv, IrisData data, Engine engine) {
|
||||
if (!enabled) {
|
||||
return offsetY;
|
||||
}
|
||||
|
||||
double x = xv;
|
||||
double z = zv;
|
||||
int g = 33;
|
||||
@@ -135,13 +142,13 @@ public class IrisNoiseGenerator {
|
||||
for (IrisNoiseGenerator i : fracture) {
|
||||
if (i.isEnabled()) {
|
||||
double fractureOffset = i.getOpacity() / 2D;
|
||||
x += i.getNoise(superSeed + seed + g, xv, zv, data) - fractureOffset;
|
||||
z += i.getNoise(superSeed + seed + g, zv, xv, data) - fractureOffset;
|
||||
x += i.getNoise(superSeed + seed + g, xv, zv, data, engine) - fractureOffset;
|
||||
z += i.getNoise(superSeed + seed + g, zv, xv, data, engine) - fractureOffset;
|
||||
}
|
||||
g += 819;
|
||||
}
|
||||
|
||||
CNG cng = getGenerator(superSeed, data);
|
||||
CNG cng = getGenerator(superSeed, data, engine);
|
||||
double sampleX = (x / zoom) + offsetX;
|
||||
double sampleZ = (z / zoom) + offsetZ;
|
||||
double n = cng.noiseFast2D(sampleX, sampleZ) * opacity;
|
||||
|
||||
@@ -111,6 +111,8 @@ public final class DesktopUiMessages {
|
||||
public static final TextKey PREGEN_CACHED = TextKey.of("iris.desktop.pregen.cached", "{method} · cached");
|
||||
public static final TextKey PREGEN_FAILED = TextKey.of("iris.desktop.pregen.failed", "{count} failed · see server log");
|
||||
public static final TextKey PREGEN_MAP = TextKey.of("iris.desktop.pregen.map", "Generation map");
|
||||
public static final TextKey PREGEN_CHUNK_COORDINATES = TextKey.of("iris.desktop.pregen.chunk_coordinates", "Chunk X {x} · Z {z}");
|
||||
public static final TextKey PREGEN_WINDOW_HINT = TextKey.of("iris.desktop.pregen.window_hint", "Closing this window keeps generation running.");
|
||||
public static final TextKey PREGEN_BOUNDS = TextKey.of("iris.desktop.pregen.bounds", "Chunks X {minX} to {maxX} · Z {minZ} to {maxZ}");
|
||||
public static final TextKey PREGEN_WAITING = TextKey.of("iris.desktop.pregen.waiting", "Waiting");
|
||||
public static final TextKey PREGEN_READY = TextKey.of("iris.desktop.pregen.ready", "Ready");
|
||||
@@ -212,6 +214,7 @@ public final class DesktopUiMessages {
|
||||
PREGEN_SIXTY, PREGEN_RATE, PREGEN_ETA, PREGEN_ELAPSED,
|
||||
PREGEN_MEMORY_LABEL, PREGEN_MEMORY_USAGE, PREGEN_PRESSURE, PREGEN_PRESSURE_VALUE,
|
||||
PREGEN_METHOD, PREGEN_CACHED, PREGEN_FAILED, PREGEN_MAP,
|
||||
PREGEN_CHUNK_COORDINATES, PREGEN_WINDOW_HINT,
|
||||
PREGEN_BOUNDS, PREGEN_WAITING, PREGEN_READY, PREGEN_EXISTING,
|
||||
PREGEN_NETWORK, PREGEN_TERRAIN_HINT,
|
||||
IMAGEMAP_TITLE, IMAGEMAP_PRESET, IMAGEMAP_LOAD, IMAGEMAP_IMPORT_PNG,
|
||||
|
||||
@@ -1,90 +1,65 @@
|
||||
package art.arcane.iris.studio.view;
|
||||
|
||||
import art.arcane.iris.localization.DesktopUiMessages;
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.ToolTipManager;
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferInt;
|
||||
import java.util.Arrays;
|
||||
import java.util.BitSet;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
final class PregenMapPanel extends JPanel {
|
||||
private static final int MAX_RASTER_SIZE = 1024;
|
||||
static final Color WAITING = new Color(29, 35, 46);
|
||||
|
||||
private final PregenRenderSnapshot.Bounds bounds;
|
||||
private final PregenMapState state;
|
||||
private final PregenMapState.View view;
|
||||
private final BufferedImage image;
|
||||
private final int[] imagePixels;
|
||||
private final int[] pendingPixels;
|
||||
private final BitSet dirtyPixels;
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
private volatile boolean pendingChanges;
|
||||
private volatile boolean disposed;
|
||||
|
||||
PregenMapPanel(PregenRenderSnapshot.Bounds bounds) {
|
||||
this.bounds = bounds;
|
||||
int width = (int) Math.min(MAX_RASTER_SIZE, bounds.width());
|
||||
int height = (int) Math.min(MAX_RASTER_SIZE, bounds.height());
|
||||
image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||
imagePixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
|
||||
pendingPixels = new int[imagePixels.length];
|
||||
dirtyPixels = new BitSet(imagePixels.length);
|
||||
Arrays.fill(imagePixels, WAITING.getRGB());
|
||||
Arrays.fill(pendingPixels, WAITING.getRGB());
|
||||
PregenMapPanel(PregenMapState state) {
|
||||
this.state = state;
|
||||
bounds = state.bounds();
|
||||
image = new BufferedImage(state.width(), state.height(), BufferedImage.TYPE_INT_RGB);
|
||||
view = state.attach(((DataBufferInt) image.getRaster().getDataBuffer()).getData());
|
||||
setBackground(PregenRenderer.BACKGROUND);
|
||||
ToolTipManager.sharedInstance().registerComponent(this);
|
||||
}
|
||||
|
||||
void submit(int chunkX, int chunkZ, Color color) {
|
||||
if (disposed || !bounds.contains(chunkX, chunkZ)) {
|
||||
return;
|
||||
}
|
||||
int x = (int) (((long) chunkX - bounds.minX()) * image.getWidth() / bounds.width());
|
||||
int z = (int) (((long) chunkZ - bounds.minZ()) * image.getHeight() / bounds.height());
|
||||
int index = z * image.getWidth() + x;
|
||||
int rgb = color.getRGB();
|
||||
lock.lock();
|
||||
try {
|
||||
if (disposed || pendingPixels[index] == rgb) {
|
||||
return;
|
||||
}
|
||||
pendingPixels[index] = rgb;
|
||||
dirtyPixels.set(index);
|
||||
pendingChanges = true;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
if (!disposed) {
|
||||
state.submit(chunkX, chunkZ, color);
|
||||
}
|
||||
}
|
||||
|
||||
boolean flush() {
|
||||
if (!pendingChanges || disposed) {
|
||||
return false;
|
||||
}
|
||||
lock.lock();
|
||||
try {
|
||||
for (int index = dirtyPixels.nextSetBit(0); index >= 0; index = dirtyPixels.nextSetBit(index + 1)) {
|
||||
imagePixels[index] = pendingPixels[index];
|
||||
}
|
||||
dirtyPixels.clear();
|
||||
pendingChanges = false;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
return true;
|
||||
return !disposed && view.flush();
|
||||
}
|
||||
|
||||
void disposeMap() {
|
||||
lock.lock();
|
||||
try {
|
||||
disposed = true;
|
||||
dirtyPixels.clear();
|
||||
pendingChanges = false;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
disposed = true;
|
||||
ToolTipManager.sharedInstance().unregisterComponent(this);
|
||||
view.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToolTipText(MouseEvent event) {
|
||||
if (disposed) {
|
||||
return null;
|
||||
}
|
||||
Rectangle area = fit(bounds, getWidth(), getHeight());
|
||||
if (!area.contains(event.getX(), event.getY())) {
|
||||
return null;
|
||||
}
|
||||
int chunkX = (int) (bounds.minX() + (long) (event.getX() - area.x) * bounds.width() / area.width);
|
||||
int chunkZ = (int) (bounds.minZ() + (long) (event.getY() - area.y) * bounds.height() / area.height);
|
||||
return IrisLanguage.plain(DesktopUiMessages.PREGEN_CHUNK_COORDINATES,
|
||||
MessageArgument.trusted("x", chunkX), MessageArgument.trusted("z", chunkZ));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package art.arcane.iris.studio.view;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.BitSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public final class PregenMapState {
|
||||
private static final int MAX_RASTER_SIZE = 1024;
|
||||
static final Color WAITING = new Color(29, 35, 46);
|
||||
|
||||
private final PregenRenderSnapshot.Bounds bounds;
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final int[] pixels;
|
||||
private final List<View> views = new ArrayList<>(1);
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
PregenMapState(PregenRenderSnapshot.Bounds bounds) {
|
||||
this.bounds = Objects.requireNonNull(bounds);
|
||||
width = (int) Math.min(MAX_RASTER_SIZE, bounds.width());
|
||||
height = (int) Math.min(MAX_RASTER_SIZE, bounds.height());
|
||||
pixels = new int[width * height];
|
||||
Arrays.fill(pixels, WAITING.getRGB());
|
||||
}
|
||||
|
||||
PregenRenderSnapshot.Bounds bounds() {
|
||||
return bounds;
|
||||
}
|
||||
|
||||
int width() {
|
||||
return width;
|
||||
}
|
||||
|
||||
int height() {
|
||||
return height;
|
||||
}
|
||||
|
||||
void submit(int chunkX, int chunkZ, Color color) {
|
||||
if (!bounds.contains(chunkX, chunkZ)) {
|
||||
return;
|
||||
}
|
||||
int x = (int) (((long) chunkX - bounds.minX()) * width / bounds.width());
|
||||
int z = (int) (((long) chunkZ - bounds.minZ()) * height / bounds.height());
|
||||
int index = z * width + x;
|
||||
int rgb = color.getRGB();
|
||||
lock.lock();
|
||||
try {
|
||||
if (pixels[index] == rgb) {
|
||||
return;
|
||||
}
|
||||
pixels[index] = rgb;
|
||||
for (int viewIndex = 0; viewIndex < views.size(); viewIndex++) {
|
||||
View view = views.get(viewIndex);
|
||||
view.dirty.set(index);
|
||||
view.pending = true;
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
View attach(int[] target) {
|
||||
if (target.length != pixels.length) {
|
||||
throw new IllegalArgumentException("Map image must match the retained raster dimensions");
|
||||
}
|
||||
View view = new View(target);
|
||||
lock.lock();
|
||||
try {
|
||||
System.arraycopy(pixels, 0, target, 0, pixels.length);
|
||||
views.add(view);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
final class View {
|
||||
private final int[] target;
|
||||
private final BitSet dirty = new BitSet(pixels.length);
|
||||
private volatile boolean pending;
|
||||
private boolean disposed;
|
||||
|
||||
private View(int[] target) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
boolean flush() {
|
||||
if (!pending) {
|
||||
return false;
|
||||
}
|
||||
lock.lock();
|
||||
try {
|
||||
if (disposed || !pending) {
|
||||
return false;
|
||||
}
|
||||
for (int index = dirty.nextSetBit(0); index >= 0; index = dirty.nextSetBit(index + 1)) {
|
||||
target[index] = pixels[index];
|
||||
}
|
||||
dirty.clear();
|
||||
pending = false;
|
||||
return true;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
void close() {
|
||||
lock.lock();
|
||||
try {
|
||||
disposed = true;
|
||||
pending = false;
|
||||
dirty.clear();
|
||||
views.remove(this);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,4 +20,6 @@ package art.arcane.iris.studio.view;
|
||||
|
||||
public interface PregenRenderSource {
|
||||
PregenRenderSnapshot renderSnapshot();
|
||||
|
||||
PregenMapState renderMapState();
|
||||
}
|
||||
@@ -35,16 +35,22 @@ import javax.swing.JFrame;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JProgressBar;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.Scrollable;
|
||||
import javax.swing.JTextArea;
|
||||
import javax.swing.KeyStroke;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.Timer;
|
||||
import javax.swing.plaf.basic.BasicButtonUI;
|
||||
import javax.swing.plaf.basic.BasicGraphicsUtils;
|
||||
import javax.swing.plaf.basic.BasicProgressBarUI;
|
||||
import javax.swing.plaf.basic.BasicScrollBarUI;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Color;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.GridBagConstraints;
|
||||
import java.awt.GridBagLayout;
|
||||
import java.awt.Insets;
|
||||
import java.awt.Font;
|
||||
import java.awt.Frame;
|
||||
import java.awt.GridLayout;
|
||||
@@ -85,7 +91,7 @@ public final class PregenRenderer extends JPanel {
|
||||
private final JLabel worldLabel = label("", new Font(Font.SANS_SERIF, Font.BOLD, 21), TEXT);
|
||||
private final JLabel phaseLabel = label("", BODY_FONT, MUTED);
|
||||
private final JLabel countLabel = label("", BODY_FONT, TEXT);
|
||||
private final JLabel percentLabel = label("", BODY_FONT, MUTED);
|
||||
private final JLabel percentLabel = label("", new Font(Font.SANS_SERIF, Font.BOLD, 24), TEXT);
|
||||
private final JLabel currentValue = label("", VALUE_FONT, TEXT);
|
||||
private final JLabel overallValue = label("", VALUE_FONT, TEXT);
|
||||
private final JLabel thirtyValue = label("", VALUE_FONT, TEXT);
|
||||
@@ -95,7 +101,8 @@ public final class PregenRenderer extends JPanel {
|
||||
private final JLabel memoryValue = label("", VALUE_FONT, TEXT);
|
||||
private final JLabel pressureValue = label("", VALUE_FONT, TEXT);
|
||||
private final JLabel methodLabel = label("", LABEL_FONT, MUTED);
|
||||
private final JLabel failureLabel = label("", LABEL_FONT, ERROR);
|
||||
private final JTextArea failureLabel = wrappedText("", BODY_FONT, ERROR, SURFACE);
|
||||
private String controlFailure;
|
||||
private final JButton pauseButton = new JButton();
|
||||
private final JProgressBar progressBar = new JProgressBar(0, 10_000);
|
||||
private volatile boolean renderingEnabled;
|
||||
@@ -106,14 +113,18 @@ public final class PregenRenderer extends JPanel {
|
||||
private PregenRenderer(PregenRenderSource source, Runnable onPause) {
|
||||
this.source = Objects.requireNonNull(source, "Generation view source");
|
||||
this.onPause = onPause;
|
||||
failureLabel.setFocusable(true);
|
||||
PregenRenderSnapshot initial = source.renderSnapshot();
|
||||
map = new PregenMapPanel(initial.bounds());
|
||||
map = new PregenMapPanel(source.renderMapState());
|
||||
setLayout(new BorderLayout(0, 18));
|
||||
setBackground(BACKGROUND);
|
||||
setBorder(BorderFactory.createEmptyBorder(20, 24, 18, 24));
|
||||
add(header(), BorderLayout.NORTH);
|
||||
add(mapArea(initial.bounds()), BorderLayout.CENTER);
|
||||
add(statusArea(), BorderLayout.SOUTH);
|
||||
JPanel content = panel(new BorderLayout(18, 0), BACKGROUND);
|
||||
content.add(mapArea(initial.bounds()), BorderLayout.CENTER);
|
||||
content.add(statusArea(), BorderLayout.EAST);
|
||||
add(content, BorderLayout.CENTER);
|
||||
add(wrappedText(text(DesktopUiMessages.PREGEN_WINDOW_HINT), LABEL_FONT, MUTED, BACKGROUND), BorderLayout.SOUTH);
|
||||
installPauseControl();
|
||||
repaintTimer = new Timer(IrisSettings.get().getGui().isMaximumPregenGuiFPS() ? 4 : 250,
|
||||
event -> refresh());
|
||||
@@ -130,8 +141,8 @@ public final class PregenRenderer extends JPanel {
|
||||
renderer.frame = frame;
|
||||
renderer.renderingEnabled = true;
|
||||
frame.setContentPane(renderer);
|
||||
frame.setSize(1040, 900);
|
||||
frame.setMinimumSize(new Dimension(760, 660));
|
||||
frame.setSize(1040, 760);
|
||||
frame.setMinimumSize(new Dimension(760, 620));
|
||||
frame.addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(WindowEvent event) {
|
||||
@@ -144,10 +155,6 @@ public final class PregenRenderer extends JPanel {
|
||||
return renderer;
|
||||
}
|
||||
|
||||
public void submit(int x, int z, Color color) {
|
||||
map.submit(x, z, color);
|
||||
}
|
||||
|
||||
public boolean isVisibleFrame() {
|
||||
return renderingEnabled && !disposed;
|
||||
}
|
||||
@@ -199,36 +206,89 @@ public final class PregenRenderer extends JPanel {
|
||||
MessageArgument.trusted("maxX", Form.f(bounds.maxX())),
|
||||
MessageArgument.trusted("minZ", Form.f(bounds.minZ())),
|
||||
MessageArgument.trusted("maxZ", Form.f(bounds.maxZ()))), LABEL_FONT, MUTED);
|
||||
area.add(coordinates, BorderLayout.SOUTH);
|
||||
JPanel mapFooter = panel(new BorderLayout(0, 10), BACKGROUND);
|
||||
mapFooter.add(coordinates, BorderLayout.NORTH);
|
||||
mapFooter.add(legend(), BorderLayout.CENTER);
|
||||
area.add(mapFooter, BorderLayout.SOUTH);
|
||||
return area;
|
||||
}
|
||||
|
||||
private JPanel statusArea() {
|
||||
JPanel metrics = panel(new GridLayout(2, 4, 24, 20), SURFACE);
|
||||
metrics.add(metric(DesktopUiMessages.PREGEN_CURRENT, currentValue));
|
||||
metrics.add(metric(DesktopUiMessages.PREGEN_OVERALL, overallValue));
|
||||
metrics.add(metric(DesktopUiMessages.PREGEN_THIRTY, thirtyValue));
|
||||
metrics.add(metric(DesktopUiMessages.PREGEN_SIXTY, sixtyValue));
|
||||
metrics.add(metric(DesktopUiMessages.PREGEN_ETA, etaValue));
|
||||
metrics.add(metric(DesktopUiMessages.PREGEN_ELAPSED, elapsedValue));
|
||||
metrics.add(metric(DesktopUiMessages.PREGEN_MEMORY_LABEL, memoryValue));
|
||||
metrics.add(metric(DesktopUiMessages.PREGEN_PRESSURE, pressureValue));
|
||||
JPanel method = panel(new BorderLayout(16, 0), SURFACE);
|
||||
method.add(methodLabel, BorderLayout.CENTER);
|
||||
method.add(failureLabel, BorderLayout.EAST);
|
||||
JPanel status = panel(new BorderLayout(0, 18), SURFACE);
|
||||
status.setBorder(BorderFactory.createEmptyBorder(18, 18, 16, 18));
|
||||
status.add(metrics, BorderLayout.CENTER);
|
||||
status.add(method, BorderLayout.SOUTH);
|
||||
JPanel footer = panel(new BorderLayout(0, 12), BACKGROUND);
|
||||
footer.add(legend(), BorderLayout.NORTH);
|
||||
footer.add(status, BorderLayout.CENTER);
|
||||
return footer;
|
||||
private JScrollPane statusArea() {
|
||||
JPanel status = new StatusPanel();
|
||||
status.setBackground(SURFACE);
|
||||
status.setBorder(BorderFactory.createEmptyBorder(18, 16, 16, 16));
|
||||
GridBagConstraints constraints = new GridBagConstraints();
|
||||
constraints.gridx = 0;
|
||||
constraints.gridy = 0;
|
||||
constraints.weightx = 1;
|
||||
constraints.fill = GridBagConstraints.HORIZONTAL;
|
||||
constraints.anchor = GridBagConstraints.NORTHWEST;
|
||||
constraints.insets = new Insets(0, 0, 18, 0);
|
||||
status.add(failureLabel, constraints);
|
||||
constraints.gridy++;
|
||||
currentValue.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 25));
|
||||
etaValue.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 23));
|
||||
status.add(metric(DesktopUiMessages.PREGEN_CURRENT, currentValue), constraints);
|
||||
constraints.gridy++;
|
||||
status.add(metric(DesktopUiMessages.PREGEN_ETA, etaValue), constraints);
|
||||
constraints.gridy++;
|
||||
constraints.insets = new Insets(0, 0, 10, 0);
|
||||
status.add(metricRow(DesktopUiMessages.PREGEN_OVERALL, overallValue), constraints);
|
||||
constraints.gridy++;
|
||||
status.add(metricRow(DesktopUiMessages.PREGEN_THIRTY, thirtyValue), constraints);
|
||||
constraints.gridy++;
|
||||
status.add(metricRow(DesktopUiMessages.PREGEN_SIXTY, sixtyValue), constraints);
|
||||
constraints.gridy++;
|
||||
status.add(metricRow(DesktopUiMessages.PREGEN_ELAPSED, elapsedValue), constraints);
|
||||
constraints.gridy++;
|
||||
status.add(metricRow(DesktopUiMessages.PREGEN_MEMORY_LABEL, memoryValue), constraints);
|
||||
constraints.gridy++;
|
||||
status.add(metricRow(DesktopUiMessages.PREGEN_PRESSURE, pressureValue), constraints);
|
||||
constraints.gridy++;
|
||||
status.add(methodLabel, constraints);
|
||||
constraints.gridy++;
|
||||
constraints.weighty = 1;
|
||||
status.add(panel(new BorderLayout(), SURFACE), constraints);
|
||||
JScrollPane scroll = new JScrollPane(status);
|
||||
scroll.setBorder(BorderFactory.createLineBorder(BORDER));
|
||||
scroll.getViewport().setBackground(SURFACE);
|
||||
scroll.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
|
||||
scroll.setPreferredSize(new Dimension(276, 1));
|
||||
scroll.setMinimumSize(new Dimension(260, 0));
|
||||
scroll.getVerticalScrollBar().setUnitIncrement(16);
|
||||
scroll.getVerticalScrollBar().setUI(new BasicScrollBarUI() {
|
||||
@Override
|
||||
protected void configureScrollBarColors() {
|
||||
thumbColor = BORDER;
|
||||
thumbDarkShadowColor = BORDER;
|
||||
thumbHighlightColor = BORDER;
|
||||
thumbLightShadowColor = BORDER;
|
||||
trackColor = FIELD;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JButton createDecreaseButton(int orientation) {
|
||||
return scrollEnd();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JButton createIncreaseButton(int orientation) {
|
||||
return scrollEnd();
|
||||
}
|
||||
|
||||
private JButton scrollEnd() {
|
||||
JButton button = new JButton();
|
||||
button.setPreferredSize(new Dimension(0, 0));
|
||||
button.setFocusable(false);
|
||||
return button;
|
||||
}
|
||||
});
|
||||
return scroll;
|
||||
}
|
||||
|
||||
private JPanel legend() {
|
||||
JPanel legend = panel(new FlowLayout(FlowLayout.LEFT, 0, 0), BACKGROUND);
|
||||
legend.add(legendItem(DesktopUiMessages.PREGEN_WAITING, PregenMapPanel.WAITING));
|
||||
JPanel legend = panel(new GridLayout(0, 2, 12, 7), BACKGROUND);
|
||||
legend.add(legendItem(DesktopUiMessages.PREGEN_WAITING, PregenMapState.WAITING));
|
||||
legend.add(legendItem(DesktopUiMessages.PREGEN_GENERATING, GENERATING));
|
||||
legend.add(legendItem(DesktopUiMessages.PREGEN_READY, GENERATED));
|
||||
legend.add(legendItem(DesktopUiMessages.PREGEN_EXISTING, EXISTS));
|
||||
@@ -238,24 +298,32 @@ public final class PregenRenderer extends JPanel {
|
||||
}
|
||||
|
||||
private JPanel legendItem(TextKey key, Color color) {
|
||||
JPanel item = panel(new FlowLayout(FlowLayout.LEFT, 6, 0), BACKGROUND);
|
||||
JPanel swatch = new JPanel();
|
||||
swatch.setBackground(color);
|
||||
JPanel item = panel(new BorderLayout(7, 0), BACKGROUND);
|
||||
JPanel swatch = panel(new BorderLayout(), color);
|
||||
swatch.setPreferredSize(new Dimension(10, 10));
|
||||
swatch.setBorder(BorderFactory.createLineBorder(BORDER));
|
||||
item.add(swatch);
|
||||
item.add(label(text(key), LABEL_FONT, MUTED));
|
||||
item.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 13));
|
||||
JPanel marker = panel(new BorderLayout(), BACKGROUND);
|
||||
marker.add(swatch, BorderLayout.NORTH);
|
||||
item.add(marker, BorderLayout.WEST);
|
||||
item.add(wrappedText(text(key), LABEL_FONT, MUTED, BACKGROUND), BorderLayout.CENTER);
|
||||
return item;
|
||||
}
|
||||
|
||||
private JPanel metric(TextKey title, JLabel value) {
|
||||
JPanel metric = panel(new BorderLayout(0, 5), SURFACE);
|
||||
metric.add(label(text(title), LABEL_FONT, MUTED), BorderLayout.NORTH);
|
||||
metric.add(wrappedText(text(title), LABEL_FONT, MUTED, SURFACE), BorderLayout.NORTH);
|
||||
metric.add(value, BorderLayout.CENTER);
|
||||
return metric;
|
||||
}
|
||||
|
||||
private JPanel metricRow(TextKey title, JLabel value) {
|
||||
JPanel row = panel(new BorderLayout(8, 0), SURFACE);
|
||||
value.setFont(BODY_FONT);
|
||||
row.add(wrappedText(text(title), LABEL_FONT, MUTED, SURFACE), BorderLayout.CENTER);
|
||||
row.add(value, BorderLayout.EAST);
|
||||
return row;
|
||||
}
|
||||
|
||||
private void installPauseControl() {
|
||||
pauseButton.setUI(new BasicButtonUI() {
|
||||
@Override
|
||||
@@ -288,6 +356,14 @@ public final class PregenRenderer extends JPanel {
|
||||
}
|
||||
});
|
||||
pauseButton.addActionListener(event -> togglePause());
|
||||
pauseButton.getInputMap(JComponent.WHEN_FOCUSED).put(
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "activatePause");
|
||||
pauseButton.getActionMap().put("activatePause", new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent event) {
|
||||
pauseButton.doClick();
|
||||
}
|
||||
});
|
||||
getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_P, 0, true), "pause");
|
||||
getActionMap().put("pause", new AbstractAction() {
|
||||
@Override
|
||||
@@ -309,10 +385,14 @@ public final class PregenRenderer extends JPanel {
|
||||
}
|
||||
try {
|
||||
onPause.run();
|
||||
controlFailure = null;
|
||||
displayedSnapshot = null;
|
||||
refresh();
|
||||
} catch (RuntimeException failure) {
|
||||
IrisLogging.reportError("Unable to change pregeneration pause state.", failure);
|
||||
failureLabel.setText(text(DesktopUiMessages.PREGEN_CONTROL_FAILED));
|
||||
controlFailure = text(DesktopUiMessages.PREGEN_CONTROL_FAILED) + "\n"
|
||||
+ Objects.toString(failure.getMessage(), failure.getClass().getSimpleName());
|
||||
updateStatus(source.renderSnapshot());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,6 +415,7 @@ public final class PregenRenderer extends JPanel {
|
||||
boolean initializing = snapshot.phase() == PregenRenderSnapshot.Phase.INITIALIZING;
|
||||
boolean paused = snapshot.phase() == PregenRenderSnapshot.Phase.PAUSED;
|
||||
worldLabel.setText(progress.worldName() == null ? text(DesktopUiMessages.PREGEN_TITLE) : progress.worldName());
|
||||
worldLabel.setToolTipText(worldLabel.getText());
|
||||
phaseLabel.setText(text(switch (snapshot.phase()) {
|
||||
case INITIALIZING -> DesktopUiMessages.PREGEN_INITIALIZING;
|
||||
case GENERATING -> DesktopUiMessages.PREGEN_GENERATING;
|
||||
@@ -351,6 +432,8 @@ public final class PregenRenderer extends JPanel {
|
||||
percentLabel.setText(text(DesktopUiMessages.PREGEN_PERCENT,
|
||||
MessageArgument.trusted("percent", Form.pc(Math.max(0D, Math.min(1D, progress.percent() / 100D)), 1))));
|
||||
progressBar.setValue((int) Math.max(0D, Math.min(10_000D, progress.percent() * 100D)));
|
||||
progressBar.getAccessibleContext().setAccessibleName(countLabel.getText());
|
||||
map.getAccessibleContext().setAccessibleDescription(countLabel.getText() + ". " + phaseLabel.getText());
|
||||
pauseButton.setText(text(paused ? DesktopUiMessages.PREGEN_RESUME : DesktopUiMessages.PREGEN_PAUSE));
|
||||
pauseButton.setToolTipText(text(paused ? DesktopUiMessages.PREGEN_RESUME_HINT : DesktopUiMessages.PREGEN_PAUSE_HINT));
|
||||
pauseButton.setEnabled(onPause != null && switch (snapshot.phase()) {
|
||||
@@ -361,7 +444,9 @@ public final class PregenRenderer extends JPanel {
|
||||
overallValue.setText(initializing ? pending() : rate(progress.overallChunksPerSecond()));
|
||||
thirtyValue.setText(initializing ? pending() : rate(progress.thirtySecondChunksPerSecond()));
|
||||
sixtyValue.setText(initializing ? pending() : rate(progress.sixtySecondChunksPerSecond()));
|
||||
etaValue.setText(initializing || paused || progress.eta() < 0
|
||||
etaValue.setText((snapshot.phase() != PregenRenderSnapshot.Phase.GENERATING
|
||||
&& snapshot.phase() != PregenRenderSnapshot.Phase.SAVING
|
||||
&& snapshot.phase() != PregenRenderSnapshot.Phase.COMPLETED) || progress.eta() < 0
|
||||
|| (progress.eta() == 0 && progress.chunksRemaining() > 0)
|
||||
? pending() : Form.duration(progress.eta(), 2));
|
||||
elapsedValue.setText(initializing ? pending() : Form.duration(progress.elapsed(), 2));
|
||||
@@ -375,12 +460,20 @@ public final class PregenRenderer extends JPanel {
|
||||
method = text(DesktopUiMessages.PREGEN_CACHED, MessageArgument.untrusted("method", method));
|
||||
}
|
||||
methodLabel.setText(text(DesktopUiMessages.PREGEN_METHOD, MessageArgument.untrusted("method", method)));
|
||||
failureLabel.setText(progress.failed() > 0 ? text(DesktopUiMessages.PREGEN_FAILED,
|
||||
MessageArgument.trusted("count", Form.f(progress.failed()))) : "");
|
||||
failureLabel.setToolTipText(snapshot.failure());
|
||||
methodLabel.setToolTipText(methodLabel.getText());
|
||||
String failureSummary = progress.failed() > 0 ? text(DesktopUiMessages.PREGEN_FAILED,
|
||||
MessageArgument.trusted("count", Form.f(progress.failed()))) : "";
|
||||
if (snapshot.failure() != null) {
|
||||
failureLabel.setText(text(DesktopUiMessages.PREGEN_ERROR_DETAILS));
|
||||
failureSummary += (failureSummary.isEmpty() ? "" : "\n")
|
||||
+ text(DesktopUiMessages.PREGEN_ERROR_DETAILS) + "\n" + snapshot.failure();
|
||||
}
|
||||
if (controlFailure != null) {
|
||||
failureSummary += (failureSummary.isEmpty() ? "" : "\n") + controlFailure;
|
||||
}
|
||||
if (!failureSummary.equals(failureLabel.getText())) {
|
||||
failureLabel.setText(failureSummary);
|
||||
}
|
||||
failureLabel.setVisible(!failureSummary.isEmpty());
|
||||
}
|
||||
|
||||
private void setRenderingEnabled(boolean enabled) {
|
||||
@@ -418,6 +511,19 @@ public final class PregenRenderer extends JPanel {
|
||||
return label;
|
||||
}
|
||||
|
||||
private static JTextArea wrappedText(String text, Font font, Color foreground, Color background) {
|
||||
JTextArea area = new JTextArea(text);
|
||||
area.setEditable(false);
|
||||
area.setFocusable(false);
|
||||
area.setLineWrap(true);
|
||||
area.setWrapStyleWord(true);
|
||||
area.setFont(font);
|
||||
area.setForeground(foreground);
|
||||
area.setBackground(background);
|
||||
area.setBorder(BorderFactory.createEmptyBorder());
|
||||
return area;
|
||||
}
|
||||
|
||||
private static String pending() {
|
||||
return text(DesktopUiMessages.PREGEN_METHOD_PENDING);
|
||||
}
|
||||
@@ -429,4 +535,35 @@ public final class PregenRenderer extends JPanel {
|
||||
private static String text(TextKey key, MessageArgument... arguments) {
|
||||
return IrisLanguage.plain(key, arguments);
|
||||
}
|
||||
|
||||
private static final class StatusPanel extends JPanel implements Scrollable {
|
||||
private StatusPanel() {
|
||||
super(new GridBagLayout());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getPreferredScrollableViewportSize() {
|
||||
return getPreferredSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getScrollableUnitIncrement(Rectangle visible, int orientation, int direction) {
|
||||
return 16;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getScrollableBlockIncrement(Rectangle visible, int orientation, int direction) {
|
||||
return Math.max(16, visible.height - 16);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getScrollableTracksViewportWidth() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getScrollableTracksViewportHeight() {
|
||||
return getParent() != null && getPreferredSize().height < getParent().getHeight();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,7 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
||||
private final List<Runnable> whenDone = new CopyOnWriteArrayList<>();
|
||||
private final IrisPregenerator pregenerator;
|
||||
private final PregenRenderSnapshot.Bounds bounds;
|
||||
private final PregenMapState mapState;
|
||||
private final Engine engine;
|
||||
private final ExecutorService service;
|
||||
private final Thread worker;
|
||||
@@ -98,6 +99,7 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
||||
this.pregenerator = new IrisPregenerator(task, method, this);
|
||||
int[] chunkBounds = task.chunkBounds();
|
||||
bounds = new PregenRenderSnapshot.Bounds(chunkBounds[0], chunkBounds[1], chunkBounds[2], chunkBounds[3]);
|
||||
mapState = new PregenMapState(bounds);
|
||||
lastTotalChunks = task.chunkCount();
|
||||
lastChunksRemaining = lastTotalChunks;
|
||||
publishView(PregenRenderSnapshot.Phase.INITIALIZING);
|
||||
@@ -371,23 +373,13 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
||||
}
|
||||
|
||||
public void drawRegion(int x, int z, Color color) {
|
||||
PregenRenderer activeRenderer = renderer;
|
||||
if (activeRenderer == null || closed) {
|
||||
return;
|
||||
if (!closed) {
|
||||
task.iterateChunks(x, z, (chunkX, chunkZ) -> mapState.submit(chunkX, chunkZ, color));
|
||||
}
|
||||
task.iterateChunks(x, z, (chunkX, chunkZ) -> activeRenderer.submit(chunkX, chunkZ, color));
|
||||
}
|
||||
|
||||
public void draw(int x, int z, Color color) {
|
||||
try {
|
||||
PregenRenderer activeRenderer = renderer;
|
||||
if (activeRenderer != null) {
|
||||
activeRenderer.submit(x, z, color);
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError(error);
|
||||
IrisLogging.error("Failed to draw pregen");
|
||||
}
|
||||
mapState.submit(x, z, color);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
@@ -588,6 +580,11 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
||||
return renderSnapshot;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PregenMapState renderMapState() {
|
||||
return mapState;
|
||||
}
|
||||
|
||||
private synchronized void publishView(PregenRenderSnapshot.Phase phase) {
|
||||
if (failure != null) {
|
||||
phase = PregenRenderSnapshot.Phase.ERROR;
|
||||
@@ -603,12 +600,9 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
||||
}
|
||||
|
||||
private void drawChunkPreview(int x, int z, Color statusColor) {
|
||||
PregenRenderer activeRenderer = renderer;
|
||||
if (activeRenderer == null) {
|
||||
return;
|
||||
}
|
||||
draw(x, z, statusColor);
|
||||
if (!activeRenderer.isVisibleFrame() || service.isShutdown()) {
|
||||
PregenRenderer activeRenderer = renderer;
|
||||
if (activeRenderer == null || !activeRenderer.isVisibleFrame() || service.isShutdown()) {
|
||||
return;
|
||||
}
|
||||
if (engine != null) {
|
||||
|
||||
@@ -20,6 +20,7 @@ public final class BoundaryColumnGeometry implements NativeBlockColumn {
|
||||
private final List<Voxel> palette;
|
||||
private final int[] runEnds;
|
||||
private final short[] paletteIndices;
|
||||
private final int highestSolidOffset;
|
||||
|
||||
public BoundaryColumnGeometry(int minimumY, List<Voxel> palette, int[] runEnds, short[] paletteIndices) {
|
||||
this.minimumY = minimumY;
|
||||
@@ -27,6 +28,7 @@ public final class BoundaryColumnGeometry implements NativeBlockColumn {
|
||||
this.runEnds = Objects.requireNonNull(runEnds, "Geometry run ends").clone();
|
||||
this.paletteIndices = Objects.requireNonNull(paletteIndices, "Geometry palette indices").clone();
|
||||
validate();
|
||||
highestSolidOffset = findHighestSolidOffset();
|
||||
}
|
||||
|
||||
public static BoundaryColumnGeometry empty() {
|
||||
@@ -112,20 +114,16 @@ public final class BoundaryColumnGeometry implements NativeBlockColumn {
|
||||
}
|
||||
|
||||
public boolean hasSolidAbove(int worldY) {
|
||||
long offset = (long) worldY - minimumY;
|
||||
if (offset >= height() - 1L) {
|
||||
return false;
|
||||
}
|
||||
int startRun = offset < 0 ? 0 : runAt((int) offset);
|
||||
for (int run = startRun; run < runEnds.length; run++) {
|
||||
if (runEnds[run] <= offset + 1L) {
|
||||
continue;
|
||||
}
|
||||
return highestSolidOffset >= 0 && (long) worldY - minimumY < highestSolidOffset;
|
||||
}
|
||||
|
||||
private int findHighestSolidOffset() {
|
||||
for (int run = runEnds.length - 1; run >= 0; run--) {
|
||||
if (palette.get(paletteIndices[run]).phase() == Phase.SOLID) {
|
||||
return true;
|
||||
return runEnds[run] - 1;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return -1;
|
||||
}
|
||||
|
||||
public List<Voxel> voxels() {
|
||||
|
||||
@@ -1869,6 +1869,8 @@ status = "{name} | X: {x} Z: {z} | Zoom: {zoom} | Wert: {value} | {fps}
|
||||
"category.other" = "Sonstige"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "Chunk X {x} · Z {z}"
|
||||
window_hint = "Beim Schließen dieses Fensters läuft die Generierung weiter."
|
||||
initializing = "Initialisierung..."
|
||||
title = "Vorgenerierungsansicht"
|
||||
method_pending = "Ausstehend"
|
||||
|
||||
@@ -1869,6 +1869,8 @@ status = "{name} | X: {x} Z: {z} | Zoom: {zoom} | Valor: {value} | {fps
|
||||
"category.other" = "Otros"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "Chunk X {x} · Z {z}"
|
||||
window_hint = "Cerrar esta ventana no detiene la generación."
|
||||
initializing = "Inicializando..."
|
||||
title = "Vista de pregen"
|
||||
method_pending = "Pendiente"
|
||||
|
||||
@@ -1874,6 +1874,8 @@ status = "{name} | X: {x} Z: {z} Zoom: {zoom} Arvo: {value} | {fps} FPS"
|
||||
"category.other" = "Muut"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "Chunk X {x} · Z {z}"
|
||||
window_hint = "Generointi jatkuu, vaikka suljet tämän ikkunan."
|
||||
initializing = "Alustetaan..."
|
||||
title = "Pregen- näkymä"
|
||||
method_pending = "Odotetaan"
|
||||
|
||||
@@ -1869,6 +1869,8 @@ status = "{name} | X: {x} Z: {z} | Zoom : {zoom} | Valeur : {value} | {
|
||||
"category.other" = "Autres"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "Chunk X {x} · Z {z}"
|
||||
window_hint = "La génération continue après la fermeture de cette fenêtre."
|
||||
initializing = "Initialisation..."
|
||||
title = "Vue de pregen"
|
||||
method_pending = "En attente"
|
||||
|
||||
@@ -1874,6 +1874,8 @@ status = "{name} | X: {x} Z: {z} זום: {zoom} ערך: {value} | {fps}
|
||||
"category.other" = "אחרים"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "צ'אנק X {x} · Z {z}"
|
||||
window_hint = "היצירה נמשכת גם לאחר סגירת החלון."
|
||||
initializing = "הדמיה..."
|
||||
title = "תצוגה מקדימה"
|
||||
method_pending = "עקבו"
|
||||
|
||||
@@ -1869,6 +1869,8 @@ status = "{name} | X: {x} Z: {z} | Zoom: {zoom} | Valore: {value} | {fps}
|
||||
"category.other" = "Altri"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "Chunk X {x} · Z {z}"
|
||||
window_hint = "La generazione continua dopo la chiusura di questa finestra."
|
||||
initializing = "Inizializzazione..."
|
||||
title = "Vista pregenerazione"
|
||||
method_pending = "In attesa"
|
||||
|
||||
@@ -1872,6 +1872,8 @@ status = "{name} | X: {x} Z: {z} | ズーム: {zoom} | 値: {value} | {
|
||||
"category.other" = "その他"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "チャンク X {x} · Z {z}"
|
||||
window_hint = "このウィンドウを閉じても生成は続行されます。"
|
||||
initializing = "初期化中..."
|
||||
title = "事前生成ビュー"
|
||||
method_pending = "保留中"
|
||||
|
||||
@@ -1872,6 +1872,8 @@ status = "{name} | X: {x} Z: {z} | 줌: {zoom} | 가치: {value} | {fps}
|
||||
"category.other" = "기타"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "청크 X {x} · Z {z}"
|
||||
window_hint = "이 창을 닫아도 생성은 계속됩니다."
|
||||
initializing = "초기화..."
|
||||
title = "Pregen 보기"
|
||||
method_pending = "대기 중"
|
||||
|
||||
@@ -1874,6 +1874,8 @@ status = "{name} | X: {x} Z: {z} 124; didinimas: {zoom} SmithKline 124; Ver
|
||||
"category.other" = "Kita"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "Chunk X {x} · Z {z}"
|
||||
window_hint = "Uždarius šį langą generavimas tęsiamas."
|
||||
initializing = "Inicijuojama..."
|
||||
title = "Comment"
|
||||
method_pending = "Laukiama"
|
||||
|
||||
@@ -1869,6 +1869,8 @@ status = "{name} | X: {x} Z: {z} Zoom: {zoom} Waarde: {value} | {fps} FP
|
||||
"category.other" = "Andere"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "Chunk X {x} · Z {z}"
|
||||
window_hint = "Het genereren gaat door wanneer je dit venster sluit."
|
||||
initializing = "Initialiseren..."
|
||||
title = "Beeld instellen"
|
||||
method_pending = "In afwachting"
|
||||
|
||||
@@ -1874,6 +1874,8 @@ status = "{name} | X: {x} Z: {z} | Zoom: {zoom} | Wartość: {value} |
|
||||
"category.other" = "Inne"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "Chunk X {x} · Z {z}"
|
||||
window_hint = "Generowanie trwa dalej po zamknięciu tego okna."
|
||||
initializing = "Inicjowanie..."
|
||||
title = "Widok z góry"
|
||||
method_pending = "Oczekiwanie"
|
||||
|
||||
@@ -1869,6 +1869,8 @@ status = "{name} | X: {x} Z: {z} □ Zoom: {zoom} Valor: {value} | {fps}
|
||||
"category.other" = "Outros"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "Chunk X {x} · Z {z}"
|
||||
window_hint = "A geração continua após fechar esta janela."
|
||||
initializing = "Inicializando..."
|
||||
title = "Visão Pregen"
|
||||
method_pending = "Pendente"
|
||||
|
||||
@@ -1874,6 +1874,8 @@ status = "{name} | X: {x} Z: {z} Зум: {zoom} Величина: {value}
|
||||
"category.other" = "другой"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "Чанк X {x} · Z {z}"
|
||||
window_hint = "После закрытия этого окна генерация продолжится."
|
||||
initializing = "инициировать..."
|
||||
title = "Прегеновский взгляд"
|
||||
method_pending = "В ожидании"
|
||||
|
||||
@@ -1874,6 +1874,8 @@ status = "{name} | X: {x} Z: {z} yakınlaştırma: {zoom} Değer: {value}
|
||||
"category.other" = "Diğer"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "Chunk X {x} · Z {z}"
|
||||
window_hint = "Bu pencere kapatılsa da oluşturma devam eder."
|
||||
initializing = "Başlangıç..."
|
||||
title = "Prejen View"
|
||||
method_pending = "Pending Ping"
|
||||
|
||||
@@ -1872,6 +1872,8 @@ status = "{name} | X: {x} Z: {z} Phóng to- thu nhỏ {zoom} Giá trị {va
|
||||
"category.other" = "Khác"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "Chunk X {x} · Z {z}"
|
||||
window_hint = "Quá trình tạo vẫn tiếp tục khi đóng cửa sổ này."
|
||||
initializing = "Đang sở khởi..."
|
||||
title = "Xem mẫu"
|
||||
method_pending = "Đang đợi"
|
||||
|
||||
@@ -1872,6 +1872,8 @@ status = "{name} | X: {x} Z: {z} 缩放 : {zoom} 数值 : {value} | {fps}
|
||||
"category.other" = "其他人员"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "区块 X {x} · Z {z}"
|
||||
window_hint = "关闭此窗口后仍会继续生成。"
|
||||
initializing = "初始化..."
|
||||
title = "Pregen 视图"
|
||||
method_pending = "待决"
|
||||
|
||||
@@ -1872,6 +1872,8 @@ status = "{name} | X: {x} Z: {z} 縮放 : {zoom} 數值 : {value} | {fps}
|
||||
"category.other" = "其他人員"
|
||||
|
||||
[iris.desktop.pregen]
|
||||
chunk_coordinates = "區塊 X {x} · Z {z}"
|
||||
window_hint = "關閉此視窗後仍會繼續生成。"
|
||||
initializing = "初始化..."
|
||||
title = "Pregen 檢視"
|
||||
method_pending = "待決"
|
||||
|
||||
Reference in new issue
Block a user