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:
Brian Neumann-Fopiano committed 2026-10-07 10:25:41 -04:00
1 parent e72189adc7
commit cfa274825f
56 files changed
+1656 -280

No files matched your search

@@ -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) {
@@ -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;
@@ -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)));
}
@@ -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)
@@ -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));
}
@@ -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;
}
@@ -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()) {
@@ -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 = "待決"